diff --git a/.github/images/2-configuration.png b/.github/images/2-configuration.png index 367b007..ded8dff 100644 Binary files a/.github/images/2-configuration.png and b/.github/images/2-configuration.png differ diff --git a/.github/images/3-logs.png b/.github/images/3-logs.png index f004b46..cf2d80c 100644 Binary files a/.github/images/3-logs.png and b/.github/images/3-logs.png differ diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 6026872..d90bedf 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; +import React, { ReactElement, useState } from 'react'; import { createStyles, makeStyles } from '@mui/styles'; -import { ControlledTabPanels, SystemStatus, Header, Logs, StartConfigs } from './components'; +import { ControlledTabPanels, Header, Logs, OnBoarding, StartConfigs, SystemStatus } from './components'; +import { useMountPoint } from './services/hooks'; const useStyles = makeStyles(() => createStyles({ sticky: { @@ -10,12 +11,15 @@ const useStyles = makeStyles(() => createStyles({ }, })); -export function App() { +export const App = (): ReactElement => { const [selected, setSelected] = useState(0); + const { data: mountPoint } = useMountPoint(); const classes = useStyles(); + const shouldDialogOpen = !mountPoint || mountPoint === ''; return ( <> + { shouldDialogOpen && }
@@ -39,4 +43,4 @@ export function App() { /> ); -} +}; diff --git a/ui/src/components/Configs/ConfirmableButton.tsx b/ui/src/components/Configs/ConfirmableButton.tsx index 2279590..5d16658 100644 --- a/ui/src/components/Configs/ConfirmableButton.tsx +++ b/ui/src/components/Configs/ConfirmableButton.tsx @@ -23,6 +23,8 @@ export type BaseProps = { title: string; text?: string | JSX.Element; okText?: string; + okColor?: ButtonProps['color']; + cancelColor?: ButtonProps['color']; cancelText?: string; children?: ReactNode; } @@ -39,6 +41,8 @@ export const ConfirmableButton = ({ text, component = 'Button', okText, + okColor, + cancelColor, cancelText, children, ...rest @@ -66,6 +70,7 @@ export const ConfirmableButton = ({ )} + + + ); +}; diff --git a/ui/src/components/Configs/StartConfigs.tsx b/ui/src/components/Configs/StartConfigs.tsx index 6e37d06..bec15c6 100644 --- a/ui/src/components/Configs/StartConfigs.tsx +++ b/ui/src/components/Configs/StartConfigs.tsx @@ -1,10 +1,10 @@ import { Add as AddIcon, Delete, Edit } from '@mui/icons-material'; -import { Box, Button, IconButton, Theme } from '@mui/material'; +import { Box, Button, ButtonGroup, IconButton, Theme } from '@mui/material'; import React, { ReactElement, useState } from 'react'; import { createStyles, makeStyles } from '@mui/styles'; import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { v4 as uuid } from 'uuid'; -import { useRunConfig } from '../../services/hooks'; +import { useMountPoint, useRunConfig } from '../../services/hooks'; import { UpsertConfig } from './UpsertConfig'; import { Optional, RunConfig } from '../../types'; import { DEFAULT_CONFIGURATION_ID } from '../../constants'; @@ -18,8 +18,8 @@ const useStyles = makeStyles((theme: Theme) => createStyles({ export const StartConfigs = (): ReactElement => { - const { deleteConfig } = useRunConfig(); - const { runConfig } = useRunConfig(); + const { runConfig, deleteConfig } = useRunConfig(); + const { setMountPointUser } = useMountPoint(); const [openModal, setOpenModal] = useState(false); const [targetConfig, setTargetConfig] = useState(null); @@ -75,14 +75,18 @@ export const StartConfigs = (): ReactElement => { ]; return ( - + + + + }; const handleSaveButtonPress = () => { - updateConfig({ name: configName, id: newConfig.id, vars: newConfig.vars }); + updateConfig({ + name: configName, id: newConfig.id, vars: + newVar && newValue ? [...newConfig.vars, { variable: newVar, value: newValue, id: uuid() }] : newConfig.vars, + }); onClose(); }; @@ -123,6 +126,13 @@ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement => + + } diff --git a/ui/src/components/Header/Header.tsx b/ui/src/components/Header/Header.tsx index dcb5ace..f5435bb 100644 --- a/ui/src/components/Header/Header.tsx +++ b/ui/src/components/Header/Header.tsx @@ -22,7 +22,7 @@ export const Header = (): ReactElement => { } endIcon={} > - Web App + LocalStack Web Application diff --git a/ui/src/components/Header/Menu.tsx b/ui/src/components/Header/Menu.tsx index bb606d0..c475a56 100644 --- a/ui/src/components/Header/Menu.tsx +++ b/ui/src/components/Header/Menu.tsx @@ -1,13 +1,18 @@ import { MoreVert } from '@mui/icons-material'; -import { IconButton, Menu, MenuItem } from '@mui/material'; -import React, { useState } from 'react'; -import { UpdateDialog } from '../UpdateDialog'; +import { IconButton, Menu } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { useDDClient } from '../../services/hooks'; +import { DockerImage } from '../../types'; +import { ConfirmableButton } from '../Configs/ConfirmableButton'; +import { UpdateDialog } from '../Dialog/UpdateDialog'; const ITEM_HEIGHT = 80; export const LongMenu = () => { const [anchorEl, setAnchorEl] = useState(null); const [openModal, setOpenModal] = useState(false); + const [images, setImages] = useState('Loading...'); + const ddClient = useDDClient(); const open = Boolean(anchorEl); @@ -24,21 +29,28 @@ export const LongMenu = () => { setOpenModal(true); }; + useEffect(() => { + setImages('a'); + (Promise.resolve(ddClient.docker.listImages()) as Promise<[DockerImage]>).then(images => + setImages(images.filter(image => image.RepoTags?.at(0).startsWith('localstack/')) + .map(image => image.RepoTags?.at(0).split('localstack/').at(-1)).join(', '))); + }, []); + return (
{openModal && setOpenModal(false)} />} { }, }} > - - Update - + handleUpdateClick()} + text={`Following images will be updated: ${images}`} + > + Update Images +
diff --git a/ui/src/components/Logs/Logs.tsx b/ui/src/components/Logs/Logs.tsx index 4238eb4..27175af 100644 --- a/ui/src/components/Logs/Logs.tsx +++ b/ui/src/components/Logs/Logs.tsx @@ -1,4 +1,4 @@ -import { Box, Typography } from '@mui/material'; +import { Box, Card, Typography } from '@mui/material'; import React, { ReactElement, useEffect, useState } from 'react'; import { useDDClient, useLocalStack } from '../../services/hooks'; @@ -32,16 +32,16 @@ export const Logs = (): ReactElement => { } }, [data]); + return ( - <> - {!data && - - - No instance is running - Start LocalStack to see it's logs - - - } - + !data ? + + + No instance is running - Start LocalStack to see its logs + + + : + {logs.map(log => ( <> @@ -50,7 +50,7 @@ export const Logs = (): ReactElement => {
))} -
- + + ); }; diff --git a/ui/src/components/ControlledTabPanel.tsx b/ui/src/components/TabPanel/ControlledTabPanel.tsx similarity index 100% rename from ui/src/components/ControlledTabPanel.tsx rename to ui/src/components/TabPanel/ControlledTabPanel.tsx diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index ea28756..f944154 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -1,6 +1,6 @@ export * from './Configs'; -export * from './ControlledTabPanel'; +export * from './TabPanel/ControlledTabPanel'; export * from './Header'; export * from './Logs/Logs'; export * from './SystemStatus'; -export * from './UpdateDialog'; +export * from './Dialog/UpdateDialog'; diff --git a/ui/src/constants/docker.ts b/ui/src/constants/docker.ts index 5eb3102..abf6fa8 100644 --- a/ui/src/constants/docker.ts +++ b/ui/src/constants/docker.ts @@ -9,10 +9,6 @@ const COMMON_ARGS = [ ]; export const START_ARGS = [ - '-e', - 'LOCALSTACK_VOLUME_DIR=/home', - '-e', - 'EXTRA_CORS_ALLOWED_ORIGINS=http://localhost:3000', ...COMMON_ARGS, 'start', '-d', diff --git a/ui/src/constants/keys.ts b/ui/src/constants/keys.ts index 9c6a046..4818c57 100644 --- a/ui/src/constants/keys.ts +++ b/ui/src/constants/keys.ts @@ -4,3 +4,4 @@ export enum SwrCacheKey { export const STORAGE_KEY_ENVVARS = 'envVars'; export const STORAGE_KEY_LOCALSTACK = 'localstack'; +export const STORAGE_KEY_MOUNT = 'mountPoint'; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 5b62a47..03156db 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -4,7 +4,7 @@ import CssBaseline from '@mui/material/CssBaseline'; import { DockerMuiThemeProvider } from '@docker/docker-mui-theme'; import { App } from './App'; -import { GlobalDDProvider } from './components/provider'; +import { GlobalDDProvider } from './services/context/provider'; ReactDOM.render( diff --git a/ui/src/components/provider.tsx b/ui/src/services/context/provider.tsx similarity index 88% rename from ui/src/components/provider.tsx rename to ui/src/services/context/provider.tsx index a6815c9..8b4cbba 100644 --- a/ui/src/components/provider.tsx +++ b/ui/src/services/context/provider.tsx @@ -1,7 +1,7 @@ // eslint-disable-next-line import/no-unresolved import { createDockerDesktopClient } from '@docker/extension-api-client'; import React, { ReactNode, useMemo } from 'react'; -import { GlobalDDContext } from '../services/context/GlobalDDContext'; +import { GlobalDDContext } from './GlobalDDContext'; interface Props { children: ReactNode; diff --git a/ui/src/services/hooks/api.ts b/ui/src/services/hooks/api.ts index d8597eb..ee3f0f4 100644 --- a/ui/src/services/hooks/api.ts +++ b/ui/src/services/hooks/api.ts @@ -1,5 +1,5 @@ import useSWR from 'swr'; -import { STORAGE_KEY_ENVVARS, STORAGE_KEY_LOCALSTACK } from '../../constants'; +import { STORAGE_KEY_ENVVARS, STORAGE_KEY_LOCALSTACK, STORAGE_KEY_MOUNT } from '../../constants'; import { DockerContainer, RunConfig } from '../../types'; import { useDDClient } from './utils'; @@ -47,6 +47,33 @@ export const useRunConfig = (): useRunConfigReturn => { }; }; +interface useMountPointReturn { + data: string | null, + isLoading: boolean, + setMountPointUser: (data: string) => unknown; +} + +export const useMountPoint = (): useMountPointReturn => { + const ddClient = useDDClient(); + const cacheKey = STORAGE_KEY_MOUNT; + + const { data, mutate, isValidating, error } = useSWR( + cacheKey, + async () => (ddClient.extension.vm.service.get('/mount') as Promise), + ); + + const setMountPointUser = async (user: string) => { + await ddClient.extension.vm.service.post('/mount',{ Data: user }); + mutate(); + }; + + return { + data: (!error && data ) ? data.Message : null, + isLoading: isValidating || (!error && !data), + setMountPointUser, + }; +}; + interface useLocalStackReturn { data: DockerContainer | null, mutate: () => void; @@ -62,7 +89,7 @@ export const useLocalStack = (): useLocalStackReturn => { .find(container => container.Image === 'localstack/localstack' && container.Command !== 'bin/localstack update docker-images', - ), + ), { refreshInterval: 2000, compare: (a, b) => a?.Id === b?.Id }, ); return { diff --git a/vm/main.go b/vm/main.go index 387320d..8ef3356 100644 --- a/vm/main.go +++ b/vm/main.go @@ -16,7 +16,8 @@ import ( var ERRORS = [...]string{"Bad format", "Errors while saving data", "Failed retrieving data", "Configuration already present"} -const FILE_NAME = "data.json" +const CONFIG_FILE = "data.json" +const MOUNT_POINT_FILE = "mountPoint.txt" func main() { var socketPath string @@ -41,6 +42,8 @@ func main() { router.POST("/configs", createSetting) router.PUT("/configs", updateSetting) router.DELETE("/configs/:id", deleteSetting) + router.GET("/mount", getMount) + router.POST("/mount", setMount) log.Fatal(router.Start(startURL)) } @@ -67,8 +70,27 @@ type Payload struct { Data string `json:"data"` } +func getMount(ctx echo.Context) error { + content, err := readData(MOUNT_POINT_FILE) + if err != nil { + return ctx.JSON(http.StatusConflict, HTTPMessageBody{Message: ERRORS[2]}) + } + return ctx.JSON(http.StatusOK, HTTPMessageBody{Message: string(content[:])}) +} + +func setMount(ctx echo.Context) error { + var payload Payload + + ctx.Bind(&payload) + err := os.WriteFile(MOUNT_POINT_FILE, []byte(payload.Data), 0644) + if err != nil { + return ctx.JSON(http.StatusConflict, HTTPMessageBody{Message: ERRORS[2]}) + } + return ctx.NoContent(http.StatusOK) +} + func getSettings(ctx echo.Context) error { - content, err := readData() + content, err := readData(CONFIG_FILE) if err != nil { return ctx.JSON(http.StatusConflict, HTTPMessageBody{Message: ERRORS[2]}) } @@ -175,29 +197,29 @@ func createSetting(ctx echo.Context) error { } -func readData() ([]byte, error) { - _, err := os.Stat(FILE_NAME) +func readData(fileName string) ([]byte, error) { + _, err := os.Stat(fileName) var content []byte if errors.Is(err, os.ErrNotExist) { - content, file, err := createFile() + content, file, err := createFile(fileName) file.Close() return content, err } - content, err = os.ReadFile(FILE_NAME) + content, err = os.ReadFile(fileName) return content, err } func readDataKeepOpen() ([]byte, *os.File, error) { - _, err := os.Stat(FILE_NAME) + _, err := os.Stat(CONFIG_FILE) var content []byte if errors.Is(err, os.ErrNotExist) { - return createFile() + return createFile(CONFIG_FILE) } - file, err := os.OpenFile(FILE_NAME, os.O_RDWR, 0755) + file, err := os.OpenFile(CONFIG_FILE, os.O_RDWR, 0755) var buff = make([]byte, 1024) if err == nil { for { @@ -220,16 +242,24 @@ func writeData(data []Configuration, file *os.File) error { return err } -func createFile() ([]byte, *os.File, error) { +func createFile(filename string) ([]byte, *os.File, error) { logrus.New().Infof("File not exists, creating") - file, err := os.OpenFile(FILE_NAME, os.O_CREATE|os.O_RDWR, 0755) + file, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, 0755) if err != nil { logrus.New().Infof("Errors while creating file") logrus.New().Infof(err.Error()) } - _, err = file.Write([]byte("[]")) + st, err := file.Stat() + if err != nil { + logrus.New().Infof(err.Error()) + } else { + if st.Size() == 0 { + _, err = file.Write([]byte("[]")) + } + } + var toReturn []byte return toReturn, file, err }