diff --git a/.codeclimate.yml b/.codeclimate.yml index 31c8d76d7..83df27e82 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -33,15 +33,17 @@ plugins: enabled: false exclude_patterns: - - "**.txt" - "**.json" + - "**.txt" + - ".pylintrc" + - servers/lib/src/types.ts - "**/api/" - "**/build/" - "**/coverage/" - "**/dist/" - "**/node_modules/" - "**/public/" - - client/config/ - - servers/lib/src/types.ts + - "**/script/" + - "client/config/" - "deploy/config/client/env*.js" - - .pylintrc + - "files/" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d74ca64c3..1d64a7f9c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,14 +3,16 @@ { "name": "Digital Twin as a Service", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", + "image": "mcr.microsoft.com/devcontainers/typescript-node:22-bullseye", "features": { - "ghcr.io/devcontainers-contrib/features/apt-get-packages:1": { + "ghcr.io/devcontainers-extra/features/apt-get-packages:1": { + "version": "latest", "clean_ppas": true, "preserve_apt_list": true, "packages": "curl graphviz htop net-tools powerline" }, "ghcr.io/devcontainers/features/common-utils:2": { + "version": "latest", "installZsh": true, "configureZshAsDefaultShell": true, "installOhMyZsh": true, @@ -19,40 +21,45 @@ "userUid": "1001", "userGid": "automatic" }, - "ghcr.io/devcontainers-contrib/features/exa:1": { + "ghcr.io/devcontainers-extra/features/exa:1": { "version": "latest" }, "ghcr.io/devcontainers/features/git:1": { "version": "os-provided" }, - "ghcr.io/devcontainers-contrib/features/markdownlint-cli:1": { + "ghcr.io/devcontainers-extra/features/markdownlint-cli:1": { "version": "latest" }, - "ghcr.io/devcontainers-contrib/features/mkdocs:2": { + "ghcr.io/devcontainers-extra/features/mkdocs:2": { "version": "latest", "plugins": "mkdocs-material pymdown-extensions mkdocstrings[crystal,python] mkdocs-monorepo-plugin mkdocs-pdf-export-plugin mkdocs-awesome-pages-plugin python-markdown-math mkdocs-open-in-new-tab mkdocs-with-pdf qrcode" }, - "ghcr.io/devcontainers-contrib/features/nestjs-cli:2": { + "ghcr.io/devcontainers-extra/features/nestjs-cli:2": { "version": "latest" }, - "ghcr.io/devcontainers-contrib/features/npm-package:1": { + "ghcr.io/devcontainers-extra/features/npm-package:1": { "package": "madge" }, - "ghcr.io/devcontainers-contrib/features/pipx-package:1": {}, - "ghcr.io/devcontainers-contrib/features/poetry:2": { + "ghcr.io/devcontainers-extra/features/pipx-package:1": { + "version": "latest" + }, + "ghcr.io/devcontainers-extra/features/poetry:2": { "version": "latest" }, - "ghcr.io/devcontainers-contrib/features/pre-commit:2": { + "ghcr.io/devcontainers-extra/features/pre-commit:2": { "version": "latest" }, - "ghcr.io/devcontainers-contrib/features/tmux-apt-get:1": {}, - "ghcr.io/devcontainers-contrib/features/typescript:2": { + "ghcr.io/devcontainers-extra/features/tmux-apt-get:1": { "version": "latest" }, - "ghcr.io/devcontainers-contrib/features/vercel-serve:1": { + "ghcr.io/devcontainers-extra/features/typescript:2": { "version": "latest" }, - "ghcr.io/devcontainers-contrib/features/zsh-plugins:0": { + "ghcr.io/devcontainers-extra/features/vercel-serve:1": { + "version": "latest" + }, + "ghcr.io/devcontainers-extra/features/zsh-plugins:0": { + "version": "latest", "plugins": "ssh-agent npm zsh-autosuggestions", "omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions", "username": "node" @@ -105,4 +112,4 @@ ] } // Execute after login: -// source /usr/share/powerline/bindings/zsh/powerline.zsh +// source /usr/share/powerline/bindings/zsh/powerline.zsh \ No newline at end of file diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 7766822de..000000000 --- a/.eslintignore +++ /dev/null @@ -1,13 +0,0 @@ -**/api/ -**/build/ -**/config/ -**/coverage/ -**/node_modules/ -**/public/ -**/script/ -**/dist/ -deploy/ -docs/ -files/ -script/ -servers/lib/src/types.ts \ No newline at end of file diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 3f9ce5d32..3d78aa1fb 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -50,6 +50,7 @@ jobs: yarn test:unit yarn test:preview:unit yarn test:preview:int + yarn test:coverage:int-unit - name: Upload unit and integration test coverage to Codecov uses: codecov/codecov-action@v3 diff --git a/.gitignore b/.gitignore index 549ca9c49..3dc53e059 100644 --- a/.gitignore +++ b/.gitignore @@ -167,4 +167,9 @@ runner.yml servers/execution/runner/lifecycle* # certs -*.pem \ No newline at end of file +*.pem + +# gitlab service data +deploy/services/gitlab/logs/ +deploy/services/gitlab/data/ +deploy/services/gitlab/config/ diff --git a/client/DEVELOPER.md b/client/DEVELOPER.md index 7d4c9061c..45cbc0504 100644 --- a/client/DEVELOPER.md +++ b/client/DEVELOPER.md @@ -218,5 +218,5 @@ and tested using the following yarn commands. ```bash yarn test:preview:int yarn test:preview:unit +yarn test:coverage:int-unit ``` - diff --git a/client/config/test.js b/client/config/test.js index 5d4dfbd71..3f4e27d26 100644 --- a/client/config/test.js +++ b/client/config/test.js @@ -9,6 +9,7 @@ if (typeof window !== 'undefined') { REACT_APP_WORKBENCHLINK_VSCODE: '/tools/vscode/', REACT_APP_WORKBENCHLINK_JUPYTERLAB: '/lab', REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK: '', + REACT_APP_WORKBENCHLINK_DT_PREVIEW: '/preview/digitaltwins', REACT_APP_CLIENT_ID: '1be55736756190b3ace4c2c4fb19bde386d1dcc748d20b47ea8cfb5935b8446c', REACT_APP_AUTH_AUTHORITY: 'https://gitlab.com/', diff --git a/client/jest.config.json b/client/jest.config.json index 84534d9e5..7db0ff83f 100644 --- a/client/jest.config.json +++ b/client/jest.config.json @@ -33,14 +33,6 @@ "config" ], "coverageDirectory": "/coverage/", - "coverageThreshold": { - "global": { - "branches": 20, - "functions": 30, - "lines": 50, - "statements": 50 - } - }, "globals": { "window.ENV.SERVER_HOSTNAME": "localhost", "window.ENV.SERVER_PORT": 3500 diff --git a/client/package.json b/client/package.json index f4405c247..66489c896 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@into-cps-association/dtaas-web", - "version": "0.6.1", + "version": "0.7.0", "description": "Web client for Digital Twin as a Service (DTaaS)", "main": "index.tsx", "author": "prasadtalasila (http://prasad.talasila.in/)", @@ -31,10 +31,11 @@ "test:all": "yarn test:unit && yarn test:int && yarn test:e2e", "test:e2e:ext": "cross-env ext=true yarn test:e2e", "test:e2e": "yarn config:test && playwright test -c ./playwright.config.ts", - "test:int": "jest -c ./jest.config.json ../test/integration --setupFilesAfterEnv ./test/integration/jest.setup.ts", - "test:unit": "jest -c ./jest.config.json ../test/unit --setupFilesAfterEnv ./test/unit/jest.setup.ts", - "test:preview:int": "jest -c ./jest.config.json ../test/preview/integration --setupFilesAfterEnv ./test/preview/integration/jest.setup.ts", - "test:preview:unit": "jest -c ./jest.config.json ../test/preview/unit --setupFilesAfterEnv ./test/preview/unit/jest.setup.ts" + "test:coverage:int-unit": "npx istanbul-combine -d coverage/all -r lcov -r json -r text coverage/unit/coverage-final.json coverage/int/coverage-final.json coverage/preview/unit/coverage-final.json coverage/preview/int/coverage-final.json", + "test:int": "jest -c ./jest.config.json jest --coverage --coverageDirectory=coverage/int ../test/integration --setupFilesAfterEnv ./test/integration/jest.setup.ts", + "test:unit": "jest -c ./jest.config.json --coverageDirectory=coverage/unit ../test/unit --setupFilesAfterEnv ./test/unit/jest.setup.ts", + "test:preview:int": "jest -c ./jest.config.json --coverageDirectory=coverage/preview/int ../test/preview/integration --setupFilesAfterEnv ./test/preview/integration/jest.setup.ts", + "test:preview:unit": "jest -c ./jest.config.json --coverageDirectory=coverage/preview/unit ../test/preview/unit --setupFilesAfterEnv ./test/preview/unit/jest.setup.ts" }, "eslintConfig": { "extends": [ diff --git a/client/src/preview/components/asset/AssetBoard.tsx b/client/src/preview/components/asset/AssetBoard.tsx index 31d0b947e..c315bbb9a 100644 --- a/client/src/preview/components/asset/AssetBoard.tsx +++ b/client/src/preview/components/asset/AssetBoard.tsx @@ -3,8 +3,9 @@ import { Grid } from '@mui/material'; import { useSelector, useDispatch } from 'react-redux'; import { RootState } from 'store/store'; import { deleteAsset } from 'preview/store/assets.slice'; -import { AssetCardExecute, AssetCardManage } from './AssetCard'; +import { fetchAssets } from 'preview/util/init'; import { Asset } from './Asset'; +import { AssetCardExecute, AssetCardManage } from './AssetCard'; const outerGridContainerProps = { container: true, @@ -19,7 +20,6 @@ const outerGridContainerProps = { interface AssetBoardProps { tab: string; - error: string | null; } const AssetGridItem: React.FC<{ @@ -44,10 +44,18 @@ const AssetGridItem: React.FC<{ ); -const AssetBoard: React.FC = ({ tab, error }) => { +const AssetBoard: React.FC = ({ tab }) => { const assets = useSelector((state: RootState) => state.assets.items); + const [error, setError] = React.useState(null); const dispatch = useDispatch(); + React.useEffect(() => { + const fetchData = async () => { + await fetchAssets(dispatch, setError); + }; + fetchData(); + }, [dispatch]); + const handleDelete = (deletedAssetPath: string) => { dispatch(deleteAsset(deletedAssetPath)); }; diff --git a/client/src/preview/components/asset/AssetCard.tsx b/client/src/preview/components/asset/AssetCard.tsx index 1d0df1937..5c93a8de6 100644 --- a/client/src/preview/components/asset/AssetCard.tsx +++ b/client/src/preview/components/asset/AssetCard.tsx @@ -5,7 +5,7 @@ import CardContent from '@mui/material/CardContent'; import Typography from '@mui/material/Typography'; import { AlertColor, CardActions, Grid } from '@mui/material'; import styled from '@emotion/styled'; -import { formatName } from 'preview/util/gitlabDigitalTwin'; +import { formatName } from 'preview/util/digitalTwin'; import CustomSnackbar from 'preview/route/digitaltwins/Snackbar'; import { useSelector } from 'react-redux'; import { selectDigitalTwinByName } from 'preview/store/digitalTwin.slice'; diff --git a/client/src/preview/components/asset/DetailsButton.tsx b/client/src/preview/components/asset/DetailsButton.tsx index d2ac213f5..cf1862989 100644 --- a/client/src/preview/components/asset/DetailsButton.tsx +++ b/client/src/preview/components/asset/DetailsButton.tsx @@ -3,7 +3,7 @@ import { Dispatch, SetStateAction } from 'react'; import { Button } from '@mui/material'; import { useSelector } from 'react-redux'; import { selectDigitalTwinByName } from '../../store/digitalTwin.slice'; -import DigitalTwin from '../../util/gitlabDigitalTwin'; +import DigitalTwin from '../../util/digitalTwin'; interface DialogButtonProps { assetName: string; diff --git a/client/src/preview/route/digitaltwins/DigitalTwinTabDataPreview.ts b/client/src/preview/route/digitaltwins/DigitalTwinTabDataPreview.ts index 2c1dd08d0..db5bda480 100644 --- a/client/src/preview/route/digitaltwins/DigitalTwinTabDataPreview.ts +++ b/client/src/preview/route/digitaltwins/DigitalTwinTabDataPreview.ts @@ -3,15 +3,15 @@ import { ITabs } from 'route/IData'; const tabs: ITabs[] = [ { label: 'Create', - body: `Create digital twins from tools provided within user workspaces. Each digital twin will have one directory. It is suggested that user provide one bash shell script to run their digital twin. Users can create the required scripts and other files from tools provided in Workbench page.`, + body: `Create and save new digital twins. The new digital twins are saved in the linked gitlab repository. Remember to add valid '.gitlab-ci.yml' configuration as it is used for execution of digital twin.`, }, { label: 'Manage', - body: `Read the complete description of digital twins. If necessary, users can delete a digital twin, removing it from the workspace with all its associated data. Users can also reconfigure the digital twin.`, + body: `Explore, edit and delete existing digital twins. The changes get saved in the linked gitlab repository.`, }, { label: 'Execute', - body: 'Execute the Digital Twins using Gitlab CI/CD workflows.', + body: 'Execute existing digital twins using CI/CD pipelines of the linked gitlab repository. Availability of gitlab runners is required for execution of digital twins.', }, { label: 'Analyze', diff --git a/client/src/preview/route/digitaltwins/DigitalTwinsPreview.tsx b/client/src/preview/route/digitaltwins/DigitalTwinsPreview.tsx index f49bec310..575b2e3a3 100644 --- a/client/src/preview/route/digitaltwins/DigitalTwinsPreview.tsx +++ b/client/src/preview/route/digitaltwins/DigitalTwinsPreview.tsx @@ -1,96 +1,79 @@ import * as React from 'react'; -import { useState, useEffect } from 'react'; +import { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { Typography } from '@mui/material'; import Layout from 'page/Layout'; import TabComponent from 'components/tab/TabComponent'; import { TabData } from 'components/tab/subcomponents/TabRender'; import AssetBoard from 'preview/components/asset/AssetBoard'; -import GitlabInstance from 'preview/util/gitlab'; -import { getAuthority } from 'util/envUtil'; -import { setAssets } from 'preview/store/assets.slice'; -import { Asset } from 'preview/components/asset/Asset'; -import DigitalTwin from 'preview/util/gitlabDigitalTwin'; -import { setDigitalTwin } from 'preview/store/digitalTwin.slice'; +import { defaultFiles } from 'preview/util/fileUtils'; +import { addOrUpdateFile } from 'preview/store/file.slice'; import tabs from './DigitalTwinTabDataPreview'; +import CreatePage from './create/CreatePage'; -export const createDTTab = (error: string | null): TabData[] => +interface DTTabProps { + newDigitalTwinName: string; + setNewDigitalTwinName: React.Dispatch>; +} + +export const createDTTab = ({ + newDigitalTwinName, + setNewDigitalTwinName, +}: DTTabProps): TabData[] => tabs - .filter((tab) => tab.label === 'Manage' || tab.label === 'Execute') + .filter( + (tab) => + tab.label === 'Manage' || + tab.label === 'Execute' || + tab.label === 'Create', + ) .map((tab) => ({ label: tab.label, - body: ( - <> - {tab.body} - - - ), + body: + tab.label === 'Create' ? ( + <> + {tab.body} + + + ) : ( + <> + {tab.body} + + + ), })); -export const fetchSubfolders = async ( - gitlabInstance: GitlabInstance, - dispatch: ReturnType, - setError: React.Dispatch>, -) => { - try { - await gitlabInstance.init(); - if (gitlabInstance.projectId) { - const subfolders = await gitlabInstance.getDTSubfolders( - gitlabInstance.projectId, - ); - dispatch(setAssets(subfolders)); - return subfolders; - } - dispatch(setAssets([])); - return []; - } catch (_error) { - setError(`An error occurred`); - return []; - } -}; - -export const createDigitalTwinsForAssets = async ( - assets: Asset[], - dispatch: ReturnType, -) => { - assets.forEach(async (asset) => { - const gitlabInstance = new GitlabInstance( - sessionStorage.getItem('username') || '', - getAuthority(), - sessionStorage.getItem('access_token') || '', - ); - await gitlabInstance.init(); - const digitalTwin = new DigitalTwin(asset.name, gitlabInstance); - await digitalTwin.getDescription(); - dispatch(setDigitalTwin({ assetName: asset.name, digitalTwin })); - }); -}; - export const DTContent = () => { - const [error, setError] = useState(null); + const [newDigitalTwinName, setNewDigitalTwinName] = React.useState(''); const dispatch = useDispatch(); useEffect(() => { - const gitlabInstance = new GitlabInstance( - sessionStorage.getItem('username') || '', - getAuthority(), - sessionStorage.getItem('access_token') || '', - ); - fetchSubfolders(gitlabInstance, dispatch, setError).then((assets) => { - if (assets) { - createDigitalTwinsForAssets(assets, dispatch); - } + defaultFiles.forEach((file) => { + dispatch( + addOrUpdateFile({ + name: file.name, + content: '', + isNew: true, + isModified: false, + }), + ); }); }, [dispatch]); return ( - - This page demonstrates integration of DTaaS with gitlab CI/CD workflows. - The feature is experimental and requires certain gitlab setup in order + + This page demonstrates integration of DTaaS with GitLab CI/CD workflows. + The feature is experimental and requires certain GitLab setup in order for it to work. - + ); }; diff --git a/client/src/preview/route/digitaltwins/create/ChangeFileNameDialog.tsx b/client/src/preview/route/digitaltwins/create/ChangeFileNameDialog.tsx new file mode 100644 index 000000000..f5a14372f --- /dev/null +++ b/client/src/preview/route/digitaltwins/create/ChangeFileNameDialog.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField, + Button, + Typography, +} from '@mui/material'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { handleChangeFileName } from 'preview/util/fileUtils'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from 'store/store'; + +interface ChangeFileNameDialogProps { + open: boolean; + setOpenChangeFileNameDialog: Dispatch>; + fileName: string; + setFileName: Dispatch>; + setFileType: Dispatch>; +} + +const ChangeFileNameDialog: React.FC = ({ + open, + setOpenChangeFileNameDialog, + fileName, + setFileName, + setFileType, +}) => { + const [modifiedFileName, setModifiedFileName] = useState(fileName); + const [errorChangeMessage, setErrorChangeMessage] = useState(''); + + const files = useSelector((state: RootState) => state.files); + const dispatch = useDispatch(); + + useEffect(() => { + setModifiedFileName(fileName); + }, [fileName]); + + const handleCloseChangeFileNameDialog = () => { + setOpenChangeFileNameDialog(false); + setErrorChangeMessage(''); + setModifiedFileName(fileName); + }; + + return ( + + Change the file name + + setModifiedFileName(e.target.value)} + /> + {errorChangeMessage} + + + + + + + ); +}; + +export default ChangeFileNameDialog; diff --git a/client/src/preview/route/digitaltwins/create/ConfirmDeleteDialog.tsx b/client/src/preview/route/digitaltwins/create/ConfirmDeleteDialog.tsx new file mode 100644 index 000000000..025ba07fe --- /dev/null +++ b/client/src/preview/route/digitaltwins/create/ConfirmDeleteDialog.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import { Dispatch, SetStateAction } from 'react'; +import { Dialog, DialogActions, DialogContent, Button } from '@mui/material'; +import { + removeAllCreationFiles, + addOrUpdateFile, +} from 'preview/store/file.slice'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from 'store/store'; +import { defaultFiles } from 'preview/util/fileUtils'; + +interface ConfirmDeleteDialogProps { + open: boolean; + setOpenConfirmDeleteDialog: Dispatch>; + setFileName: Dispatch>; + setFileContent: Dispatch>; + setFileType: Dispatch>; + setNewDigitalTwinName: Dispatch>; +} + +const ConfirmDeleteDialog: React.FC = ({ + open, + setOpenConfirmDeleteDialog, + setFileName, + setFileContent, + setFileType, + setNewDigitalTwinName, +}) => { + const dispatch = useDispatch(); + + const files = useSelector((state: RootState) => state.files); + + const handleConfirmCancel = () => { + setFileName(''); + setFileContent(''); + setFileType(''); + setNewDigitalTwinName(''); + dispatch(removeAllCreationFiles()); + + defaultFiles.forEach((file) => { + const fileExists = files.some( + (f) => f.name === file.name && f.isNew === true, + ); + if (!fileExists) { + dispatch( + addOrUpdateFile({ + name: file.name, + content: '', + isNew: true, + isModified: false, + }), + ); + } + }); + + setOpenConfirmDeleteDialog(false); + }; + + return ( + + + Are you sure you want to delete the inserted files and their content? + + + + + + + ); +}; + +export default ConfirmDeleteDialog; diff --git a/client/src/preview/route/digitaltwins/create/CreateDTDialog.tsx b/client/src/preview/route/digitaltwins/create/CreateDTDialog.tsx new file mode 100644 index 000000000..3890624fb --- /dev/null +++ b/client/src/preview/route/digitaltwins/create/CreateDTDialog.tsx @@ -0,0 +1,161 @@ +import * as React from 'react'; +import { Dispatch, SetStateAction } from 'react'; +import { + Dialog, + DialogActions, + DialogContent, + Typography, + Button, +} from '@mui/material'; +import { FileState, removeAllCreationFiles } from 'preview/store/file.slice'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from 'store/store'; +import DigitalTwin from 'preview/util/digitalTwin'; +import { showSnackbar } from 'preview/store/snackbar.slice'; +import { setDigitalTwin } from 'preview/store/digitalTwin.slice'; +import { + addDefaultFiles, + defaultFiles, + validateFiles, +} from 'preview/util/fileUtils'; +import { initDigitalTwin } from 'preview/util/init'; + +interface CreateDTDialogProps { + open: boolean; + setOpenCreateDTDialog: Dispatch>; + newDigitalTwinName: string; + setNewDigitalTwinName: Dispatch>; + errorMessage: string; + setErrorMessage: Dispatch>; + setFileName: Dispatch>; + setFileContent: Dispatch>; + setFileType: Dispatch>; +} + +const handleError = ( + message: string, + dispatch: ReturnType, +) => { + dispatch(showSnackbar({ message, severity: 'error' })); +}; + +const handleSuccess = ( + digitalTwin: DigitalTwin, + dispatch: ReturnType, + newDigitalTwinName: string, + files: FileState[], +) => { + dispatch( + showSnackbar({ + message: `Digital twin ${newDigitalTwinName} created successfully`, + severity: 'success', + }), + ); + dispatch(setDigitalTwin({ assetName: newDigitalTwinName, digitalTwin })); + dispatch(removeAllCreationFiles()); + + addDefaultFiles(defaultFiles, files, dispatch); +}; + +const resetDialogAndForm = ( + setOpenCreateDTDialog: Dispatch>, + setFileName: Dispatch>, + setFileContent: Dispatch>, + setFileType: Dispatch>, +) => { + setOpenCreateDTDialog(false); + setFileName(''); + setFileContent(''); + setFileType(''); +}; + +const handleConfirm = async ( + files: FileState[], + setErrorMessage: Dispatch>, + newDigitalTwinName: string, + dispatch: ReturnType, + setOpenCreateDTDialog: Dispatch>, + setFileName: Dispatch>, + setFileContent: Dispatch>, + setFileType: Dispatch>, + setNewDigitalTwinName: Dispatch>, +) => { + if (validateFiles(files, setErrorMessage)) return; + + const digitalTwin = await initDigitalTwin(newDigitalTwinName); + const result = await digitalTwin.create(files); + + if (result.startsWith('Error')) { + handleError(result, dispatch); + } else { + handleSuccess(digitalTwin, dispatch, newDigitalTwinName, files); + } + + resetDialogAndForm( + setOpenCreateDTDialog, + setFileName, + setFileContent, + setFileType, + ); + setNewDigitalTwinName(''); +}; + +const CreateDTDialog: React.FC = ({ + open, + setOpenCreateDTDialog, + newDigitalTwinName, + setNewDigitalTwinName, + errorMessage, + setErrorMessage, + setFileName, + setFileContent, + setFileType, +}) => { + const files: FileState[] = useSelector((state: RootState) => state.files); + const dispatch = useDispatch(); + + return ( + + + + Are you sure you want to create the{' '} + {newDigitalTwinName} digital twin? + + {errorMessage} + + + + + + + ); +}; + +export default CreateDTDialog; diff --git a/client/src/preview/route/digitaltwins/create/CreateDialogs.tsx b/client/src/preview/route/digitaltwins/create/CreateDialogs.tsx new file mode 100644 index 000000000..0753f4e73 --- /dev/null +++ b/client/src/preview/route/digitaltwins/create/CreateDialogs.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { Dispatch, SetStateAction } from 'react'; +import ChangeFileNameDialog from './ChangeFileNameDialog'; +import DeleteFileDialog from './DeleteFileDialog'; +import ConfirmDeleteDialog from './ConfirmDeleteDialog'; +import CreateDTDialog from './CreateDTDialog'; + +interface CreateDialogsProps { + openChangeFileNameDialog: boolean; + setOpenChangeFileNameDialog: Dispatch>; + fileName: string; + setFileName: Dispatch>; + setFileContent: Dispatch>; + setFileType: Dispatch>; + openDeleteFileDialog: boolean; + setOpenDeleteFileDialog: Dispatch>; + openConfirmDeleteDialog: boolean; + setOpenConfirmDeleteDialog: Dispatch>; + openCreateDTDialog: boolean; + setOpenCreateDTDialog: Dispatch>; + newDigitalTwinName: string; + setNewDigitalTwinName: Dispatch>; + errorMessage: string; + setErrorMessage: Dispatch>; +} + +const CreateDialogs: React.FC = (props) => ( + <> + + + + + + + + +); + +export default CreateDialogs; diff --git a/client/src/preview/route/digitaltwins/create/CreatePage.tsx b/client/src/preview/route/digitaltwins/create/CreatePage.tsx new file mode 100644 index 000000000..fddeda661 --- /dev/null +++ b/client/src/preview/route/digitaltwins/create/CreatePage.tsx @@ -0,0 +1,154 @@ +import * as React from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; +import { Box, Button, TextField } from '@mui/material'; +import Editor from 'preview/route/digitaltwins/editor/Editor'; +import CreateDialogs from './CreateDialogs'; +import CustomSnackbar from '../Snackbar'; +import FileActionButtons from './FileActionButtons'; + +interface CreatePageProps { + newDigitalTwinName: string; + setNewDigitalTwinName: Dispatch>; +} + +function DigitalTwinNameInput({ + value, + onChange, +}: { + value: string; + onChange: (e: React.ChangeEvent) => void; +}) { + return ( + + + + ); +} + +function ActionButtons({ + onCancel, + onSave, + isSaveDisabled, +}: { + onCancel: () => void; + onSave: () => void; + isSaveDisabled: boolean; +}) { + return ( + + + + + ); +} + +function CreatePage({ + newDigitalTwinName, + setNewDigitalTwinName, +}: CreatePageProps) { + const [fileName, setFileName] = useState(''); + const [fileContent, setFileContent] = useState(''); + const [fileType, setFileType] = useState(''); + const [openChangeFileNameDialog, setOpenChangeFileNameDialog] = + useState(false); + const [openDeleteFileDialog, setOpenDeleteFileDialog] = useState(false); + const [openConfirmDeleteDialog, setOpenConfirmDeleteDialog] = useState(false); + const [openCreateDTDialog, setOpenCreateDTDialog] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const confirmCancel = () => { + setOpenConfirmDeleteDialog(true); + }; + + const confirmSave = () => { + setErrorMessage(''); + setOpenCreateDTDialog(true); + }; + + return ( + <> + + + setNewDigitalTwinName(e.target.value)} + /> + + + + + + + + + + + + ); +} + +export default CreatePage; diff --git a/client/src/preview/route/digitaltwins/create/DeleteFileDialog.tsx b/client/src/preview/route/digitaltwins/create/DeleteFileDialog.tsx new file mode 100644 index 000000000..8550836ca --- /dev/null +++ b/client/src/preview/route/digitaltwins/create/DeleteFileDialog.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { Dispatch, SetStateAction } from 'react'; +import { Dialog, DialogActions, DialogContent, Button } from '@mui/material'; +import { deleteFile } from 'preview/store/file.slice'; +import { useDispatch } from 'react-redux'; + +interface DeleteFileDialogProps { + open: boolean; + setOpenDeleteFileDialog: Dispatch>; + fileName: string; + setFileName: Dispatch>; + setFileContent: Dispatch>; +} + +const DeleteFileDialog: React.FC = ({ + open, + setOpenDeleteFileDialog, + fileName, + setFileName, + setFileContent, +}) => { + const dispatch = useDispatch(); + + const handleDeleteFile = () => { + dispatch(deleteFile(fileName)); + setFileName(''); + setFileContent(''); + setOpenDeleteFileDialog(false); + }; + + return ( + + + Are you sure you want to delete the {fileName} file and + its content? + + + + + + + ); +}; + +export default DeleteFileDialog; diff --git a/client/src/preview/route/digitaltwins/create/FileActionButtons.tsx b/client/src/preview/route/digitaltwins/create/FileActionButtons.tsx new file mode 100644 index 000000000..0d023a8bb --- /dev/null +++ b/client/src/preview/route/digitaltwins/create/FileActionButtons.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import { isFileDeletable, isFileModifiable } from 'preview/util/fileUtils'; + +function FileActionButtons({ + fileName, + setOpenDeleteFileDialog, + setOpenChangeFileNameDialog, +}: { + fileName: string; + setOpenDeleteFileDialog: React.Dispatch>; + setOpenChangeFileNameDialog: React.Dispatch>; +}) { + return ( + + {isFileDeletable(fileName) && fileName && ( + + )} + {isFileModifiable(fileName) && fileName && ( + + )} + + ); +} + +export default FileActionButtons; diff --git a/client/src/preview/route/digitaltwins/editor/Editor.tsx b/client/src/preview/route/digitaltwins/editor/Editor.tsx index 30dcda0f7..affbc767d 100644 --- a/client/src/preview/route/digitaltwins/editor/Editor.tsx +++ b/client/src/preview/route/digitaltwins/editor/Editor.tsx @@ -1,65 +1,99 @@ import * as React from 'react'; import { useState } from 'react'; import { Box, Grid, Tabs, Tab } from '@mui/material'; -// import { Resizable } from 'react-resizable'; import EditorTab from './EditorTab'; import PreviewTab from './PreviewTab'; import Sidebar from './Sidebar'; interface EditorProps { - DTName: string; + DTName?: string; + tab: string; + fileName: string; + setFileName: React.Dispatch>; + fileContent: string; + setFileContent: React.Dispatch>; + fileType: string; + setFileType: React.Dispatch>; } -function Editor({ DTName }: EditorProps) { +function Editor({ + DTName, + tab, + fileName, + setFileName, + fileContent, + setFileContent, + fileType, + setFileType, +}: EditorProps) { const [activeTab, setActiveTab] = useState(0); - const [fileName, setFileName] = useState(''); - const [fileContent, setFileContent] = useState(''); - const [fileType, setFileType] = useState(''); const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { setActiveTab(newValue); }; return ( - - + + + - - - - - + + + + + + + - - {activeTab === 0 && ( - - )} - {activeTab === 1 && ( - - )} - - + + {activeTab === 0 && ( + + )} + {activeTab === 1 && ( + + )} + + + ); } diff --git a/client/src/preview/route/digitaltwins/editor/EditorTab.tsx b/client/src/preview/route/digitaltwins/editor/EditorTab.tsx index 885367f6d..5201f5eb5 100644 --- a/client/src/preview/route/digitaltwins/editor/EditorTab.tsx +++ b/client/src/preview/route/digitaltwins/editor/EditorTab.tsx @@ -5,12 +5,14 @@ import { useDispatch } from 'react-redux'; import { addOrUpdateFile } from '../../../store/file.slice'; interface EditorTabProps { + tab: string; fileName: string; fileContent: string; setFileContent: Dispatch>; } const handleEditorChange = ( + tab: string, value: string | undefined, setEditorValue: Dispatch>, setFileContent: Dispatch>, @@ -21,16 +23,33 @@ const handleEditorChange = ( setEditorValue(updatedValue); setFileContent(updatedValue); - dispatch( - addOrUpdateFile({ - name: fileName, - content: updatedValue, - isModified: true, - }), - ); + if (tab === 'create') { + dispatch( + addOrUpdateFile({ + name: fileName, + content: updatedValue, + isNew: true, + isModified: true, + }), + ); + } else { + dispatch( + addOrUpdateFile({ + name: fileName, + content: updatedValue, + isNew: false, + isModified: true, + }), + ); + } }; -function EditorTab({ fileName, fileContent, setFileContent }: EditorTabProps) { +function EditorTab({ + tab, + fileName, + fileContent, + setFileContent, +}: EditorTabProps) { const [editorValue, setEditorValue] = useState(fileContent); const dispatch = useDispatch(); @@ -39,13 +58,14 @@ function EditorTab({ fileName, fileContent, setFileContent }: EditorTabProps) { }, [fileContent]); return ( -
+
handleEditorChange( + tab, value, setEditorValue, setFileContent, @@ -53,7 +73,32 @@ function EditorTab({ fileName, fileContent, setFileContent }: EditorTabProps) { dispatch, ) } + options={{ + readOnly: fileName === '', + }} /> + {fileName === '' && ( +
+ Please select a file to edit. +
+ )}
); } diff --git a/client/src/preview/route/digitaltwins/editor/Sidebar.tsx b/client/src/preview/route/digitaltwins/editor/Sidebar.tsx index 9a8977a1b..f10336885 100644 --- a/client/src/preview/route/digitaltwins/editor/Sidebar.tsx +++ b/client/src/preview/route/digitaltwins/editor/Sidebar.tsx @@ -1,113 +1,55 @@ import * as React from 'react'; import { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { Grid, CircularProgress } from '@mui/material'; +import { Grid, CircularProgress, Button } from '@mui/material'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; -import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { RootState } from 'store/store'; import { FileState } from '../../../store/file.slice'; import { selectDigitalTwinByName } from '../../../store/digitalTwin.slice'; -import DigitalTwin from '../../../util/gitlabDigitalTwin'; +import { + fetchData, + getFilteredFileNames, + handleAddFileClick, + renderFileSection, + renderFileTreeItems, +} from './sidebarFunctions'; +import SidebarDialog from './SidebarDialog'; interface SidebarProps { - name: string; + name?: string; setFileName: Dispatch>; setFileContent: Dispatch>; setFileType: Dispatch>; + tab: string; } -const fetchData = async (digitalTwin: DigitalTwin) => { - await digitalTwin.getDescriptionFiles(); - await digitalTwin.getLifecycleFiles(); - await digitalTwin.getConfigFiles(); -}; - -export const handleFileClick = async ( - fileName: string, - digitalTwin: DigitalTwin, - setFileName: Dispatch>, - setFileContent: Dispatch>, - setFileType: Dispatch>, - modifiedFiles: FileState[], -) => { - const modifiedFile = modifiedFiles.find((file) => file.name === fileName); - - if (modifiedFile) { - updateFileState( - modifiedFile.name, - modifiedFile.content, - setFileName, - setFileContent, - setFileType, - ); - } else { - const fileContent = await digitalTwin.getFileContent(fileName); - if (fileContent) { - updateFileState( - fileName, - fileContent, - setFileName, - setFileContent, - setFileType, - ); - } else { - setFileContent(`Error fetching ${fileName} content`); - } - } -}; - -const updateFileState = ( - fileName: string, - fileContent: string, - setFileName: Dispatch>, - setFileContent: Dispatch>, - setFileType: Dispatch>, -) => { - setFileName(fileName); - setFileContent(fileContent); - setFileType(fileName.split('.').pop()!); -}; - const Sidebar = ({ name, setFileName, setFileContent, setFileType, + tab, }: SidebarProps) => { - const digitalTwin = useSelector(selectDigitalTwinByName(name)); - const modifiedFiles = useSelector((state: RootState) => state.files); - const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState(!!name); + const [newFileName, setNewFileName] = useState(''); + const [isFileNameDialogOpen, setIsFileNameDialogOpen] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); - useEffect(() => { - const loadData = async () => { - await fetchData(digitalTwin); - setIsLoading(false); - }; - - loadData(); - }, [digitalTwin]); - - const renderFileTreeItems = (label: string, files: string[]) => ( - - {files.map((item, id) => ( - - handleFileClick( - item, - digitalTwin, - setFileName, - setFileContent, - setFileType, - modifiedFiles, - ) - } - /> - ))} - + const digitalTwin = useSelector((state: RootState) => + name ? selectDigitalTwinByName(name)(state) : null, ); + const files: FileState[] = useSelector((state: RootState) => state.files); + const dispatch = useDispatch(); + + useEffect(() => { + if (name && digitalTwin) { + const loadData = async () => { + await fetchData(digitalTwin); + setIsLoading(false); + }; + loadData(); + } + }, [name, digitalTwin]); if (isLoading) { return ( @@ -145,10 +87,98 @@ const Sidebar = ({ overflowY: 'auto', }} > + {tab === 'create' && ( + + )} + + + - {renderFileTreeItems('Description', digitalTwin.descriptionFiles)} - {renderFileTreeItems('Configuration', digitalTwin.configFiles)} - {renderFileTreeItems('Lifecycle', digitalTwin.lifecycleFiles)} + {name ? ( + <> + {renderFileTreeItems( + 'Description', + digitalTwin!.descriptionFiles, + digitalTwin!, + setFileName, + setFileContent, + setFileType, + files, + tab, + )} + {renderFileTreeItems( + 'Configuration', + digitalTwin!.configFiles, + digitalTwin!, + setFileName, + setFileContent, + setFileType, + files, + tab, + )} + {renderFileTreeItems( + 'Lifecycle', + digitalTwin!.lifecycleFiles, + digitalTwin!, + setFileName, + setFileContent, + setFileType, + files, + tab, + )} + + ) : ( + <> + {renderFileSection( + 'Description', + 'description', + getFilteredFileNames('description', files), + digitalTwin!, + setFileName, + setFileContent, + setFileType, + files, + tab, + )} + {renderFileSection( + 'Configuration', + 'config', + getFilteredFileNames('config', files), + digitalTwin!, + setFileName, + setFileContent, + setFileType, + files, + tab, + )} + {renderFileSection( + 'Lifecycle', + 'lifecycle', + getFilteredFileNames('lifecycle', files), + digitalTwin!, + setFileName, + setFileContent, + setFileType, + files, + tab, + )} + + )} ); diff --git a/client/src/preview/route/digitaltwins/editor/SidebarDialog.tsx b/client/src/preview/route/digitaltwins/editor/SidebarDialog.tsx new file mode 100644 index 000000000..b3794e987 --- /dev/null +++ b/client/src/preview/route/digitaltwins/editor/SidebarDialog.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { Dispatch, SetStateAction } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + TextField, + DialogActions, + Button, +} from '@mui/material'; +import { useDispatch } from 'react-redux'; +import { FileState } from '../../../store/file.slice'; +import { + handleCloseFileNameDialog, + handleFileSubmit, +} from './sidebarFunctions'; + +interface SidebarDialogProps { + isOpen: boolean; + newFileName: string; + setNewFileName: Dispatch>; + setIsFileNameDialogOpen: Dispatch>; + errorMessage: string; + setErrorMessage: Dispatch>; + files: FileState[]; + dispatch: ReturnType; +} + +const SidebarDialog = ({ + isOpen, + newFileName, + setNewFileName, + setIsFileNameDialogOpen, + errorMessage, + setErrorMessage, + files, + dispatch, +}: SidebarDialogProps) => ( + + Enter the file name + + setNewFileName(e.target.value)} + /> + {errorMessage &&

{errorMessage}

} +
+ + + + +
+); + +export default SidebarDialog; diff --git a/client/src/preview/route/digitaltwins/editor/sidebarFunctions.tsx b/client/src/preview/route/digitaltwins/editor/sidebarFunctions.tsx new file mode 100644 index 000000000..badacec99 --- /dev/null +++ b/client/src/preview/route/digitaltwins/editor/sidebarFunctions.tsx @@ -0,0 +1,263 @@ +import { addOrUpdateFile, FileState } from 'preview/store/file.slice'; +import DigitalTwin from 'preview/util/digitalTwin'; +import { Dispatch, SetStateAction } from 'react'; +import { useDispatch } from 'react-redux'; +import { TreeItem, TreeItemProps } from '@mui/x-tree-view/TreeItem'; +import * as React from 'react'; + +export const getFileTypeFromExtension = (fileName: string): string => { + const extension = fileName.split('.').pop()?.toLowerCase(); + if (extension === 'md') return 'description'; + if (extension === 'json' || extension === 'yaml' || extension === 'yml') + return 'config'; + return 'lifecycle'; +}; + +export const fetchData = async (digitalTwin: DigitalTwin) => { + await digitalTwin.getDescriptionFiles(); + await digitalTwin.getLifecycleFiles(); + await digitalTwin.getConfigFiles(); +}; + +export const handleFileClick = ( + fileName: string, + digitalTwin: DigitalTwin | null, + setFileName: Dispatch>, + setFileContent: Dispatch>, + setFileType: Dispatch>, + files: FileState[], + tab: string, +) => { + if (tab === 'create') { + handleCreateFileClick( + fileName, + files, + setFileName, + setFileContent, + setFileType, + ); + } else if (tab === 'reconfigure') { + handleReconfigureFileClick( + fileName, + digitalTwin, + files, + setFileName, + setFileContent, + setFileType, + ); + } +}; + +export const renderFileTreeItems = ( + label: string, + filesToRender: string[], + digitalTwin: DigitalTwin, + setFileName: Dispatch>, + setFileContent: Dispatch>, + setFileType: Dispatch>, + files: FileState[], + tab: string, +) => ( + + {filesToRender.map((item) => ( + + handleFileClick( + item, + digitalTwin!, + setFileName, + setFileContent, + setFileType, + files, + tab, + ) + } + /> + ))} + +); + +export const getFilteredFileNames = (type: string, files: FileState[]) => + files + .filter( + (file) => file.isNew && getFileTypeFromExtension(file.name) === type, + ) + .map((file) => file.name); + +export const renderFileSection = ( + label: string, + type: string, + filesToRender: string[], + digitalTwin: DigitalTwin, + setFileName: Dispatch>, + setFileContent: Dispatch>, + setFileType: Dispatch>, + files: FileState[], + tab: string, +) => ( + + {filesToRender.map((item) => ( + + handleFileClick( + item, + digitalTwin!, + setFileName, + setFileContent, + setFileType, + files, + tab, + ) + } + /> + ))} + +); + +export const handleCreateFileClick = ( + fileName: string, + files: FileState[], + setFileName: Dispatch>, + setFileContent: Dispatch>, + setFileType: Dispatch>, +) => { + const newFile = files.find((file) => file.name === fileName && file.isNew); + if (newFile) { + updateFileState( + newFile.name, + newFile.content, + setFileName, + setFileContent, + setFileType, + ); + } +}; + +export const handleReconfigureFileClick = ( + fileName: string, + digitalTwin: DigitalTwin | null, + files: FileState[], + setFileName: Dispatch>, + setFileContent: Dispatch>, + setFileType: Dispatch>, +) => { + const modifiedFile = files.find( + (file) => file.name === fileName && file.isModified && !file.isNew, + ); + if (modifiedFile) { + updateFileState( + modifiedFile.name, + modifiedFile.content, + setFileName, + setFileContent, + setFileType, + ); + } else { + fetchAndSetFileContent( + fileName, + digitalTwin, + setFileName, + setFileContent, + setFileType, + ); + } +}; + +export const fetchAndSetFileContent = async ( + fileName: string, + digitalTwin: DigitalTwin | null, + setFileName: Dispatch>, + setFileContent: Dispatch>, + setFileType: Dispatch>, +) => { + try { + const fileContent = await digitalTwin!.DTAssets.getFileContent(fileName); + if (fileContent) { + updateFileState( + fileName, + fileContent, + setFileName, + setFileContent, + setFileType, + ); + } + } catch { + setFileContent(`Error fetching ${fileName} content`); + } +}; + +export const updateFileState = ( + fileName: string, + fileContent: string, + setFileName: Dispatch>, + setFileContent: Dispatch>, + setFileType: Dispatch>, +) => { + setFileName(fileName); + setFileContent(fileContent); + setFileType(fileName.split('.').pop()!); +}; + +export const handleAddFileClick = ( + setIsFileNameDialogOpen: Dispatch>, +) => { + setIsFileNameDialogOpen(true); +}; + +export const handleCloseFileNameDialog = ( + setIsFileNameDialogOpen: Dispatch>, + setNewFileName: Dispatch>, + setErrorMessage: Dispatch>, +) => { + setIsFileNameDialogOpen(false); + setNewFileName(''); + setErrorMessage(''); +}; + +export const handleFileSubmit = ( + files: FileState[], + newFileName: string, + setErrorMessage: Dispatch>, + dispatch: ReturnType, + setIsFileNameDialogOpen: Dispatch>, + setNewFileName: Dispatch>, +) => { + const fileExists = files.some( + (fileStore: { name: string }) => fileStore.name === newFileName, + ); + + if (fileExists) { + setErrorMessage('A file with this name already exists.'); + return; + } + + if (newFileName === '') { + setErrorMessage("File name can't be empty."); + return; + } + + setErrorMessage(''); + const type = getFileTypeFromExtension(newFileName); + + dispatch( + addOrUpdateFile({ + name: newFileName, + content: '', + isNew: true, + isModified: false, + type, + }), + ); + + setIsFileNameDialogOpen(false); + setNewFileName(''); +}; diff --git a/client/src/preview/route/digitaltwins/execute/LogDialog.tsx b/client/src/preview/route/digitaltwins/execute/LogDialog.tsx index d4fcbbd00..f1474ce94 100644 --- a/client/src/preview/route/digitaltwins/execute/LogDialog.tsx +++ b/client/src/preview/route/digitaltwins/execute/LogDialog.tsx @@ -10,7 +10,7 @@ import { } from '@mui/material'; import { useSelector } from 'react-redux'; import { selectDigitalTwinByName } from 'preview/store/digitalTwin.slice'; -import { formatName } from 'preview/util/gitlabDigitalTwin'; +import { formatName } from 'preview/util/digitalTwin'; interface LogDialogProps { showLog: boolean; diff --git a/client/src/preview/route/digitaltwins/execute/pipelineChecks.ts b/client/src/preview/route/digitaltwins/execute/pipelineChecks.ts index 49ead6de5..250136be6 100644 --- a/client/src/preview/route/digitaltwins/execute/pipelineChecks.ts +++ b/client/src/preview/route/digitaltwins/execute/pipelineChecks.ts @@ -1,6 +1,6 @@ import { Dispatch, SetStateAction } from 'react'; import { useDispatch } from 'react-redux'; -import DigitalTwin, { formatName } from 'preview/util/gitlabDigitalTwin'; +import DigitalTwin, { formatName } from 'preview/util/digitalTwin'; import { fetchJobLogs, updatePipelineStateOnCompletion, diff --git a/client/src/preview/route/digitaltwins/execute/pipelineHandler.ts b/client/src/preview/route/digitaltwins/execute/pipelineHandler.ts index 4a17e2d3e..f0e5dbee4 100644 --- a/client/src/preview/route/digitaltwins/execute/pipelineHandler.ts +++ b/client/src/preview/route/digitaltwins/execute/pipelineHandler.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from 'react'; -import DigitalTwin, { formatName } from 'preview/util/gitlabDigitalTwin'; +import DigitalTwin, { formatName } from 'preview/util/digitalTwin'; import { useDispatch } from 'react-redux'; import { showSnackbar } from 'preview/store/snackbar.slice'; import { diff --git a/client/src/preview/route/digitaltwins/execute/pipelineUtils.ts b/client/src/preview/route/digitaltwins/execute/pipelineUtils.ts index 50b901d3a..9e2958962 100644 --- a/client/src/preview/route/digitaltwins/execute/pipelineUtils.ts +++ b/client/src/preview/route/digitaltwins/execute/pipelineUtils.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from 'react'; -import DigitalTwin, { formatName } from 'preview/util/gitlabDigitalTwin'; +import DigitalTwin, { formatName } from 'preview/util/digitalTwin'; import GitlabInstance from 'preview/util/gitlab'; import { setJobLogs, diff --git a/client/src/preview/route/digitaltwins/manage/DeleteDialog.tsx b/client/src/preview/route/digitaltwins/manage/DeleteDialog.tsx index a3f2c0887..40cc549ab 100644 --- a/client/src/preview/route/digitaltwins/manage/DeleteDialog.tsx +++ b/client/src/preview/route/digitaltwins/manage/DeleteDialog.tsx @@ -9,7 +9,7 @@ import { } from '@mui/material'; import { useDispatch, useSelector } from 'react-redux'; import { selectDigitalTwinByName } from '../../../store/digitalTwin.slice'; -import DigitalTwin, { formatName } from '../../../util/gitlabDigitalTwin'; +import DigitalTwin, { formatName } from '../../../util/digitalTwin'; import { showSnackbar } from '../../../store/snackbar.slice'; interface DeleteDialogProps { diff --git a/client/src/preview/route/digitaltwins/manage/ReconfigureDialog.tsx b/client/src/preview/route/digitaltwins/manage/ReconfigureDialog.tsx index 411564282..41e63ff5d 100644 --- a/client/src/preview/route/digitaltwins/manage/ReconfigureDialog.tsx +++ b/client/src/preview/route/digitaltwins/manage/ReconfigureDialog.tsx @@ -12,14 +12,17 @@ import { AlertColor, } from '@mui/material'; import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from 'store/store'; -import { FileState, saveAllFiles } from '../../../store/file.slice'; +import { + FileState, + removeAllModifiedFiles, + selectModifiedFiles, +} from '../../../store/file.slice'; import { selectDigitalTwinByName, updateDescription, } from '../../../store/digitalTwin.slice'; import { showSnackbar } from '../../../store/snackbar.slice'; -import DigitalTwin, { formatName } from '../../../util/gitlabDigitalTwin'; +import DigitalTwin, { formatName } from '../../../util/digitalTwin'; import Editor from '../editor/Editor'; interface ReconfigureDialogProps { @@ -39,10 +42,13 @@ function ReconfigureDialog({ setShowDialog, name, }: ReconfigureDialogProps) { + const [fileName, setFileName] = useState(''); + const [fileContent, setFileContent] = useState(''); + const [fileType, setFileType] = useState(''); const [openSaveDialog, setOpenSaveDialog] = useState(false); const [openCancelDialog, setOpenCancelDialog] = useState(false); const digitalTwin = useSelector(selectDigitalTwinByName(name)); - const modifiedFiles = useSelector((state: RootState) => state.files); + const modifiedFiles = useSelector(selectModifiedFiles); const dispatch = useDispatch(); const handleSave = () => setOpenSaveDialog(true); @@ -57,6 +63,7 @@ function ReconfigureDialog({ }; const handleConfirmCancel = () => { + dispatch(removeAllModifiedFiles()); setOpenCancelDialog(false); setShowDialog(false); }; @@ -69,6 +76,12 @@ function ReconfigureDialog({ name={name} handleCancel={handleCancel} handleSave={handleSave} + fileName={fileName} + setFileName={setFileName} + fileContent={fileContent} + setFileContent={setFileContent} + fileType={fileType} + setFileType={setFileType} /> , ) => { try { - await digitalTwin.updateFileContent(file.name, file.content); + await digitalTwin.DTAssets.updateFileContent(file.name, file.content); if (file.name === 'description.md') { dispatch( @@ -146,12 +159,24 @@ const ReconfigureMainDialog = ({ name, handleCancel, handleSave, + fileName, + setFileName, + fileContent, + setFileContent, + fileType, + setFileType, }: { showDialog: boolean; setShowDialog: Dispatch>; name: string; handleCancel: () => void; handleSave: () => void; + fileName: string; + setFileName: Dispatch>; + fileContent: string; + setFileContent: Dispatch>; + fileType: string; + setFileType: Dispatch>; }) => ( {formatName(name)} - +