Skip to content

Notifications section #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const express = require('express')
const bodyParser = require('body-parser')

const chartsRouter = require('./routes/charts')
const notificationsRouter = require('./routes/notifications')
const userRouter = require('./routes/user')

const app = express()
Expand All @@ -11,6 +12,7 @@ app.use(bodyParser.urlencoded({ extended: true }))

app.use(express.json())
app.use('/charts', chartsRouter)
app.use('/notifications', notificationsRouter)
app.use('/user', userRouter)

module.exports = app
55 changes: 55 additions & 0 deletions backend/clients/notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const fs = require('fs')

const notificationsStore = '/root/data/.notifications.json'

function deleteNotification(id) {
const currentNotifications = readNotifications()

const targetNotification = currentNotifications.find(n => n.id === id)
if (!targetNotification) {
throw new Error(`Notification does not exist [notificationId=${id}]`)
}

const finalNotifications = currentNotifications.filter(n => n.id !== id)
const stringNotifications = JSON.stringify(finalNotifications)
fs.writeFileSync(notificationsStore, stringNotifications)
}

function readNotifications() {
if (fs.existsSync(notificationsStore)) {
const rawFile = fs.readFileSync(notificationsStore)
return JSON.parse(rawFile)
}

return []
}

function saveNotification(notification) {
const currentNotifications = readNotifications()
if (currentNotifications.find(n => n.id === notification.id)) {
throw new Error(`Notification with the same id already exists [notificationId=${notification.id}]`)
}

currentNotifications.push(notification)

const stringNotifications = JSON.stringify(currentNotifications.sort((a, b) => b.timestamp - a.timestamp))
fs.writeFileSync(notificationsStore, stringNotifications)
}

function updateNotification(id, update) {
const currentNotifications = readNotifications()

const targetNotification = currentNotifications.find(n => n.id === id)
if (!targetNotification) {
throw new Error(`Notification does not exist [notificationId=${id}]`)
}

const updatedNotifications = currentNotifications.map(n => {
if (n.id !== id) return n
return Object.assign(n, update)
})
const stringNotifications = JSON.stringify(updatedNotifications)
fs.writeFileSync(notificationsStore, stringNotifications)
}

module.exports = { deleteNotification, readNotifications, saveNotification, updateNotification }
57 changes: 57 additions & 0 deletions backend/routes/notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const express = require('express')
const { readNotifications, saveNotification, deleteNotification, updateNotification } = require('../clients/notifications')
var router = express.Router()

router.delete('/:id', function (req, res) {
const { id } = req.params
try {
deleteNotification(id)
res.status(200)
} catch (err) {
console.error('Unexpected error deleting notification [notificationId=' + id + ']', err)
res.status(500)
}

res.end()
})

router.get('/', function (req, res) {
try {
const notifications = readNotifications()
res.json(notifications.sort((a, b) => b.timestamp - a.timestamp))
} catch (err) {
console.error('Unexpected error getting user notifications', err)
res.status(500)
}

res.end()
})

router.post('/', function (req, res) {
try {
const notification = req.body
saveNotification(notification)
res.status(200)
} catch (err) {
console.error('Unexpected error saving notification', err)
res.status(500)
}

res.end()
})

router.put('/:id', function (req, res) {
const { id } = req.params
try {
const update = req.body
updateNotification(id, update)
res.status(200)
} catch (err) {
console.error('Unexpected error updating notification [notificationId=' + id + ']', err)
res.status(500)
}

res.end()
})

module.exports = router
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
volumes:
helm:
data:

services:
docker-desktop-extension:
image: ${DESKTOP_PLUGIN_IMAGE}
volumes:
- helm:/root/.config/helm
- data:/root/data
54 changes: 14 additions & 40 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
"buffer": "^6.0.3",
"flat": "^6.0.1",
"moment": "^2.30.1",
"notistack": "^3.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.27.0",
"semver": "^7.6.3",
"uuid": "^11.1.0",
"yaml": "^2.7.0"
},
"scripts": {
Expand Down
18 changes: 6 additions & 12 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createTheme, styled, ThemeProvider, useMediaQuery } from '@mui/material'
import { createTheme, ThemeProvider, useMediaQuery } from '@mui/material'
import { createHashRouter, RouterProvider } from 'react-router-dom'
import ApplicationsPage from './pages/ApplicationsPage'
import ApplicationDetailsPage, { loader as ApplicationDetailsLoader } from './pages/ApplicationDetailsPage'
Expand All @@ -7,7 +7,7 @@ import WorkloadsPage from './pages/WorkloadsPage'
import { AuthProvider } from './AuthContext'
import { Layout } from './Layout'
import { useMemo } from 'react'
import { MaterialDesignContent, SnackbarProvider } from 'notistack'
import NotificationsProvider from './components/NotificationsCenter/NotificationsContext'
import WorkloadDetailsPage, { loader as WorkloadDetailsLoader } from './pages/WorkloadDetailsPage'

const themeOptions = (mode: 'light' | 'dark' = 'light') => {
Expand Down Expand Up @@ -165,12 +165,6 @@ declare module '@mui/material/Typography' {
}
}

const StyledMaterialDesignContent = styled(MaterialDesignContent)(() => ({
'&.notistack-MuiContent': {
fontFamily: 'Poppins',
},
}))

const router = createHashRouter([
{
path: '/',
Expand Down Expand Up @@ -217,11 +211,11 @@ export function App() {

return (
<ThemeProvider theme={ theme }>
<SnackbarProvider maxSnack={ 3 } Components={ { default: StyledMaterialDesignContent } }>
<AuthProvider>
<AuthProvider>
<NotificationsProvider>
<RouterProvider router={ router } />
</AuthProvider>
</SnackbarProvider>
</NotificationsProvider>
</AuthProvider>
</ThemeProvider>
)
}
70 changes: 33 additions & 37 deletions ui/src/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,57 @@
import { createContext, useContext, useReducer } from 'react'
import { createDockerDesktopClient } from '@docker/extension-api-client'
import { createContext, useContext, useEffect, useReducer } from 'react'
import { ServiceError } from '@docker/extension-api-client-types/dist/v1'

type AuthContextPayload = {
auth?: string | null,
errors?: {
dismissed: boolean,
message: string
}[]
}

const AuthContext = createContext<AuthContextPayload | undefined>(undefined)
const AuthDispatchContext = createContext<(action: ReducerAction) => any>(() => undefined)
type Auth = string | null | undefined

type ReducerAction = {
type: 'set' | 'update' | 'delete' | 'dismiss_errors',
payload?: AuthContextPayload
payload?: Auth
}

const TOKEN_KEY: string = 'token'

function authReducer(state: AuthContextPayload | undefined, action: ReducerAction): AuthContextPayload | undefined {
function authReducer(auth: Auth, action: ReducerAction): Auth | undefined {
switch (action.type) {
case 'set':
case 'update':
if (action.payload?.auth) {
localStorage.setItem(TOKEN_KEY, action.payload.auth)
if (action.payload) {
localStorage.setItem(TOKEN_KEY, action.payload)
}

return {
auth: action.payload?.auth ? action.payload.auth : state?.auth,
errors: action.payload?.errors ? action.payload.errors : state?.errors
}
case 'dismiss_errors':
return {
auth: state?.auth,
errors: state?.errors?.map(e => {
if (action.payload?.errors?.find(dismissed => dismissed.message === e.message)) {
return {
dismissed: true,
message: e.message
}
}

return e
} )
}
return action.payload
case 'delete':
localStorage.removeItem(TOKEN_KEY)
return {
auth: null
}
return null
default:
return undefined
}
}

const AuthContext = createContext<Auth | undefined>(undefined)
const AuthDispatchContext = createContext<(action: ReducerAction) => any>(() => undefined)

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [ auth, dispatch ] = useReducer(authReducer, { auth: localStorage.getItem(TOKEN_KEY) || undefined })
const ddClient = createDockerDesktopClient()
const [ auth, dispatch ] = useReducer(authReducer, localStorage.getItem(TOKEN_KEY) || undefined)

useEffect(() => {
async function getCredentialsFromBackend(currentAttempt: number, maxAttempts: number, intervalWaitMillis: number) {
try {
const base64Auth = await ddClient.extension.vm?.service?.get('/user/auth') as string
const auth = atob(base64Auth)

dispatch({ type: 'set', payload: auth })
} catch (e) {
console.error(e)
if (currentAttempt < maxAttempts && (!(e as ServiceError).statusCode || (e as ServiceError).statusCode >= 500)) {
setTimeout(() => getCredentialsFromBackend(currentAttempt + 1, maxAttempts, intervalWaitMillis), intervalWaitMillis)
}
}
}

getCredentialsFromBackend(0, 3, 500)
}, [])

return (
<AuthContext.Provider value={ auth }>
Expand Down
Loading