diff --git a/ui/src/components/Configs/ConfigOptions.tsx b/ui/src/components/Configs/ConfigOptions.tsx deleted file mode 100644 index 139d5f4..0000000 --- a/ui/src/components/Configs/ConfigOptions.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Box, Card, IconButton, List, ListItem, TextField } from '@mui/material'; -import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material'; -import React, { ReactElement, useState } from 'react'; -import { v4 as uuid } from 'uuid'; -import { useRunConfig } from '../../services/hooks'; -import { RunConfig } from '../../types'; - -const DEFAULT_COLUMN_WIDTH = 2000; - -type Props = { - config: RunConfig; -}; - -export const ConfigOptions = ({ config }: Props): ReactElement => { - - const { runConfig, setRunConfig } = useRunConfig(); - const [newVar, setNewVar] = useState(''); - const [newValue, setNewValue] = useState(''); - - const handleAddButtonPress = () => { - setRunConfig([...runConfig, - { - name: config.name, id: config.id, vars: [...config.vars, - { variable: newVar, value: newValue, id: uuid() }], - }]); - setNewVar(''); - setNewValue(''); - }; - - const handleRemoveButtonPress = (id: string) => { - setRunConfig(runConfig.map(config1 => config1.id !== config.id ? - config1 : { - id: config1.id, name: config1.name, - vars: config1.vars?.filter(item => item?.id !== id) || [], - } as RunConfig)); - }; - - return ( - - - {config.vars.map(item => ( - - - - - handleRemoveButtonPress(item.id)} > - - - - - ))} - - - setNewVar(e.target.value)} - value={newVar} - /> - setNewValue(e.target.value)} - value={newValue} - /> - - - - - - - - ); -}; diff --git a/ui/src/components/Configs/UpsertConfig.tsx b/ui/src/components/Configs/UpsertConfig.tsx index 93298ec..8be2bd7 100644 --- a/ui/src/components/Configs/UpsertConfig.tsx +++ b/ui/src/components/Configs/UpsertConfig.tsx @@ -38,7 +38,7 @@ const useStyles = makeStyles((theme: Theme) => createStyles({ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement => { - const { runConfig, setRunConfig } = useRunConfig(); + const { updateConfig, deleteConfig } = useRunConfig(); const [newVar, setNewVar] = useState(''); const [newValue, setNewValue] = useState(''); const [configName, setConfigName] = useState(config?.name || ''); @@ -55,10 +55,7 @@ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement => }; const handleSaveButtonPress = () => { - setRunConfig([...runConfig.filter(config1 => config1.id !== newConfig.id), - { - name: configName, id: newConfig.id, vars: newConfig.vars, - }]); + updateConfig({ name: configName, id: newConfig.id, vars: newConfig.vars }); onClose(); }; @@ -70,7 +67,7 @@ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement => const handleDeleteButtonPress = () => { if (newConfig.id) { - setRunConfig(runConfig.filter(config1 => config1.id !== newConfig.id)); + deleteConfig(newConfig.id); } onClose(); }; diff --git a/ui/src/components/Configs/index.tsx b/ui/src/components/Configs/index.tsx index 16509cd..c2710ce 100644 --- a/ui/src/components/Configs/index.tsx +++ b/ui/src/components/Configs/index.tsx @@ -1,3 +1,2 @@ -export * from './ConfigOptions'; export * from './StartConfigs'; export * from './UpsertConfig'; diff --git a/ui/src/components/Header/Controller.tsx b/ui/src/components/Header/Controller.tsx index 038b5f4..473cc0a 100644 --- a/ui/src/components/Header/Controller.tsx +++ b/ui/src/components/Header/Controller.tsx @@ -15,7 +15,7 @@ const useStyles = makeStyles(() => createStyles({ export const Controller = (): ReactElement => { const ddClient = useDDClient(); - const { runConfig, isLoading, setRunConfig } = useRunConfig(); + const { runConfig, isLoading, createConfig } = useRunConfig(); const { data, mutate } = useLocalStack(); const [runningConfig, setRunningConfig] = useState('Default'); const isRunning = data && data.State === 'running'; @@ -23,16 +23,14 @@ export const Controller = (): ReactElement => { const classes = useStyles(); useEffect(() => { - if (!isLoading && !runConfig.find(item => item.name === 'Default')) { - setRunConfig([...runConfig, - { - name: 'Default', id: DEFAULT_CONFIGURATION_ID, vars: + if (!isLoading && (!runConfig || !runConfig.find(item => item.name === 'Default'))) { + createConfig({ + name: 'Default', id: DEFAULT_CONFIGURATION_ID, vars: [{ variable: 'EXTRA_CORS_ALLOWED_ORIGINS', value: 'http://localhost:3000', id: uuid() }], - }, - ]); + }, + ); } - }); - + },[isLoading]); const start = async () => { const images = await ddClient.docker.listImages() as [DockerImage]; @@ -70,7 +68,7 @@ export const Controller = (): ReactElement => { onChange={({ target }) => setRunningConfig(target.value)} > { - runConfig.map(config => ( + runConfig?.map(config => ( {config.name} )) } diff --git a/ui/src/services/hooks/api.ts b/ui/src/services/hooks/api.ts index d420f89..8f3b183 100644 --- a/ui/src/services/hooks/api.ts +++ b/ui/src/services/hooks/api.ts @@ -6,7 +6,9 @@ import { useDDClient } from './utils'; interface useRunConfigReturn { runConfig: RunConfig[], isLoading: boolean, - setRunConfig: (data: RunConfig[]) => unknown; + createConfig: (data: RunConfig) => unknown; + updateConfig: (data: RunConfig) => unknown; + deleteConfig: (data: string) => unknown; } interface HTTPMessageBody { @@ -18,17 +20,30 @@ export const useRunConfig = (): useRunConfigReturn => { const ddClient = useDDClient(); const { data, mutate, isValidating, error } = useSWR( cacheKey, - () => (ddClient.extension.vm.service.get('/getConfig') as Promise), + () => (ddClient.extension.vm.service.get('/configs') as Promise), ); - const mutateRunConfig = async (newData: RunConfig[]) => { - await ddClient.extension.vm.service.post('/setConfig', { Data: JSON.stringify(newData) }); + + const updateConfig = async (newData: RunConfig) => { + await ddClient.extension.vm.service.put('/configs', { Data: JSON.stringify(newData) }); + mutate(); + }; + + const createConfig = async (newData: RunConfig) => { + await ddClient.extension.vm.service.post('/configs', { Data: JSON.stringify(newData) }); + mutate(); + }; + + const deleteConfig = async (configId: string) => { + await ddClient.extension.vm.service.delete(`/configs/${configId}`); mutate(); }; return { - runConfig: (!data || data?.Message === '' || data?.Message as string === 'Failed') ? [] : JSON.parse(data?.Message), + runConfig: (!data || data?.Message === '' || error) ? [] : JSON.parse(data?.Message), isLoading: isValidating || (!error && !data), - setRunConfig: mutateRunConfig, + createConfig, + updateConfig, + deleteConfig, }; }; diff --git a/vm/main.go b/vm/main.go index 347443a..387320d 100644 --- a/vm/main.go +++ b/vm/main.go @@ -1,8 +1,10 @@ package main import ( + "encoding/json" "errors" "flag" + "io" "log" "net" "net/http" @@ -12,6 +14,10 @@ import ( "github.com/sirupsen/logrus" ) +var ERRORS = [...]string{"Bad format", "Errors while saving data", "Failed retrieving data", "Configuration already present"} + +const FILE_NAME = "data.json" + func main() { var socketPath string flag.StringVar(&socketPath, "socket", "/run/guest/volumes-service.sock", "Unix domain socket to listen on") @@ -31,8 +37,10 @@ func main() { } router.Listener = ln os.Chdir("/saved_config") - router.GET("/getConfig", getSettings) - router.POST("/setConfig", setSettings) + router.GET("/configs", getSettings) + router.POST("/configs", createSetting) + router.PUT("/configs", updateSetting) + router.DELETE("/configs/:id", deleteSetting) log.Fatal(router.Start(startURL)) } @@ -41,43 +49,187 @@ func listen(path string) (net.Listener, error) { return net.Listen("unix", path) } +type HTTPMessageBody struct { + Message string +} + +type Configuration struct { + Name string `json:"name"` + ID string `json:"id"` + Vars []struct { + Variable string `json:"variable"` + Value string `json:"value"` + ID string `json:"id"` + } `json:"vars"` +} + +type Payload struct { + Data string `json:"data"` +} + func getSettings(ctx echo.Context) error { + content, err := readData() + if err != nil { + return ctx.JSON(http.StatusConflict, HTTPMessageBody{Message: ERRORS[2]}) + } + return ctx.JSON(http.StatusOK, HTTPMessageBody{Message: string(content[:])}) +} - _, err := os.Stat("data.json") +func updateSetting(ctx echo.Context) error { + var payload Payload + var reqContent Configuration + var parsedContent []Configuration + var indexToChange int = -1 - if errors.Is(err, os.ErrNotExist) { - logrus.New().Infof("File not exist, creating") - file, err := os.Create("data.json") - file.Close() - if err != nil { - logrus.New().Infof("Errors while creating file") - logrus.New().Infof(err.Error()) + ctx.Bind(&payload) + json.Unmarshal([]byte(payload.Data), &reqContent) + savedData, file, _ := readDataKeepOpen() + defer file.Close() + + err := json.Unmarshal(savedData, &parsedContent) + + if err != nil { + return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[0]}) + } + + for index, item := range parsedContent { + if item.ID == reqContent.ID { + indexToChange = index } } - content, err := os.ReadFile("data.json") - if err != nil { - return ctx.JSON(http.StatusConflict, HTTPMessageBody{Message: "Failed"}) + if indexToChange == -1 { //no config with that id found + parsedContent = append(parsedContent, reqContent) + } else { + parsedContent[indexToChange] = reqContent } - return ctx.JSON(http.StatusOK, HTTPMessageBody{Message: string(content[:])}) + + err = writeData(parsedContent, file) + if err == nil { + return ctx.NoContent(http.StatusOK) + } + return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[1]}) } -type Payload struct { - Data string `json:"data"` +func deleteSetting(ctx echo.Context) error { + + var idToDelete string + var parsedContent []Configuration + var indexToDelete int = -1 + + idToDelete = ctx.Param("id") + savedData, file, _ := readDataKeepOpen() + defer file.Close() + err := json.Unmarshal(savedData, &parsedContent) + + if err != nil { + return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[0]}) + } + + for index, item := range parsedContent { + if item.ID == idToDelete { + indexToDelete = index + } + } + + if indexToDelete != -1 { + if indexToDelete != len(parsedContent)-1 { + parsedContent[indexToDelete] = parsedContent[len(parsedContent)-1] + } + parsedContent = parsedContent[:len(parsedContent)-1] + } + + err = writeData(parsedContent, file) + if err != nil { + return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[1]}) + } + + return ctx.NoContent(http.StatusOK) + } -func setSettings(ctx echo.Context) error { +func createSetting(ctx echo.Context) error { + var payload Payload + var reqContent Configuration + var parsedContent []Configuration + ctx.Bind(&payload) - err := os.WriteFile("data.json", []byte(payload.Data), 0644) + json.Unmarshal([]byte(payload.Data), &reqContent) + savedData, file, _ := readDataKeepOpen() + defer file.Close() + err := json.Unmarshal(savedData, &parsedContent) + if err != nil { - logrus.New().Infof(err.Error()) - return ctx.NoContent(http.StatusInternalServerError) + return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[0]}) + } + + updatedArray := append(parsedContent, reqContent) + err = writeData(updatedArray, file) + + if err != nil { + return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[1]}) } return ctx.NoContent(http.StatusOK) + } -type HTTPMessageBody struct { - Message string +func readData() ([]byte, error) { + _, err := os.Stat(FILE_NAME) + var content []byte + + if errors.Is(err, os.ErrNotExist) { + content, file, err := createFile() + file.Close() + return content, err + } + + content, err = os.ReadFile(FILE_NAME) + return content, err +} + +func readDataKeepOpen() ([]byte, *os.File, error) { + _, err := os.Stat(FILE_NAME) + var content []byte + + if errors.Is(err, os.ErrNotExist) { + return createFile() + } + + file, err := os.OpenFile(FILE_NAME, os.O_RDWR, 0755) + var buff = make([]byte, 1024) + if err == nil { + for { + n, err := file.Read(buff) + if err == io.EOF { + break + } + content = append(content, buff[:n]...) + } + } + return content, file, err +} + +func writeData(data []Configuration, file *os.File) error { + jsonData, err := json.Marshal(data) + if err == nil { + file.Truncate(0) + _, err = file.WriteAt(jsonData, 0) + } + return err +} + +func createFile() ([]byte, *os.File, error) { + + logrus.New().Infof("File not exists, creating") + file, err := os.OpenFile(FILE_NAME, 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("[]")) + var toReturn []byte + return toReturn, file, err }