diff --git a/.env b/.env index 8af5c88c1..007015d16 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ VITE_API_GATEWAY=api/gateway VITE_WS_GATEWAY=ws/gateway +VITE_DEBUG_REQUESTS=false diff --git a/package-lock.json b/package-lock.json index 87ed4c033..ebb37db17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@gridsuite/commons-ui": "0.63.4", + "@gridsuite/commons-ui": "file:../commons-ui/gridsuite-commons-ui-0.63.4.tgz", "@hookform/resolvers": "^3.3.4", "@mui/icons-material": "^5.15.14", "@mui/lab": "5.0.0-alpha.169", @@ -36,6 +36,7 @@ "react-window": "^1.8.10", "reconnecting-websocket": "^4.4.0", "redux": "^5.0.1", + "type-fest": "^4.24.0", "typeface-roboto": "^1.1.13", "uuid": "^9.0.1", "yup": "^1.4.0" @@ -2956,8 +2957,9 @@ }, "node_modules/@gridsuite/commons-ui": { "version": "0.63.4", - "resolved": "https://registry.npmjs.org/@gridsuite/commons-ui/-/commons-ui-0.63.4.tgz", - "integrity": "sha512-0o0pfC8uySLLaEJSdur4/K54jnV9TResXwBI7JD51mfCNq1woXkzlBWmpDi7ruLwGIvGSF/8SR9Tw0F60Eovjw==", + "resolved": "file:../commons-ui/gridsuite-commons-ui-0.63.4.tgz", + "integrity": "sha512-QChFJVffZ60q1JIbfvNcAOIN/bX/yN35CgJLvzYF8Ppg2NI6G8vu4khmxyvuQ6QFBnv8sqZqw+eMNU0MjjUQAQ==", + "license": "MPL-2.0", "dependencies": { "@react-querybuilder/dnd": "^7.2.0", "@react-querybuilder/material": "^7.2.0", @@ -2973,6 +2975,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-querybuilder": "^7.2.0", "react-virtualized": "^9.22.5", + "type-fest": "^4.21.0", "uuid": "^9.0.1" }, "engines": { @@ -2998,6 +3001,7 @@ "react-intl": "^6.6.4", "react-papaparse": "^4.1.0", "react-router-dom": "^6.22.3", + "reconnecting-websocket": "^4.4.0", "yup": "^1.4.0" } }, @@ -5821,6 +5825,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -14547,12 +14563,11 @@ } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.24.0.tgz", + "integrity": "sha512-spAaHzc6qre0TlZQQ2aA/nGMe+2Z/wyGk5Z+Ru2VUfdNwT6kWO6TjevOlpebsATEG1EIQ2sOiDszud3lO5mt/Q==", "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index e9ad72f3e..5f10092cc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@gridsuite/commons-ui": "0.63.4", + "@gridsuite/commons-ui": "file:../commons-ui/gridsuite-commons-ui-0.63.4.tgz", "@hookform/resolvers": "^3.3.4", "@mui/icons-material": "^5.15.14", "@mui/lab": "5.0.0-alpha.169", @@ -32,6 +32,7 @@ "react-window": "^1.8.10", "reconnecting-websocket": "^4.4.0", "redux": "^5.0.1", + "type-fest": "^4.24.0", "typeface-roboto": "^1.1.13", "uuid": "^9.0.1", "yup": "^1.4.0" diff --git a/src/components/app-top-bar.tsx b/src/components/app-top-bar.tsx index 8d10552ab..23f49ff34 100644 --- a/src/components/app-top-bar.tsx +++ b/src/components/app-top-bar.tsx @@ -4,20 +4,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useEffect, useRef } from 'react'; -import { - GridSuiteModule, - fetchAppsMetadata, - LIGHT_THEME, - logout, - TopBar, - UserManagerState, - GsTheme, - GsLang, -} from '@gridsuite/commons-ui'; -import { APP_NAME, PARAM_LANGUAGE, PARAM_THEME } from '../utils/config-params'; +import { useCallback, useEffect, useRef } from 'react'; +import { LIGHT_THEME, logout, PARAM_LANGUAGE, PARAM_THEME, TopBar, UserManagerState } from '@gridsuite/commons-ui'; +import { APP_NAME } from '../utils/config-params'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchVersion, getServersInfos } from '../utils/rest-api'; import { useNavigate } from 'react-router-dom'; import GridExploreLogoLight from '../images/GridExplore_logo_light.svg?react'; import GridExploreLogoDark from '../images/GridExplore_logo_dark.svg?react'; @@ -26,17 +16,22 @@ import AppPackage from '../../package.json'; import { SearchBar } from './search/search-bar'; import { AppState } from '../redux/reducer'; import { AppDispatch } from '../redux/store'; +import { appsMetadataSrv } from '../services'; import { useParameterState } from './dialogs/use-parameters-dialog'; type AppTopBarProps = { userManagerInstance: UserManagerState['instance']; }; -export default function AppTopBar({ userManagerInstance }: AppTopBarProps) { +export default function AppTopBar({ userManagerInstance }: Readonly) { const navigate = useNavigate(); + const onLogoClick = useCallback(() => navigate('/', { replace: true }), [navigate]); + const dispatch = useDispatch(); + const onLogoutClick = useCallback(() => logout(dispatch, userManagerInstance), [dispatch, userManagerInstance]); + const user = useSelector((state: AppState) => state.user); const appsAndUrls = useSelector((state: AppState) => state.appsAndUrls); @@ -50,8 +45,8 @@ export default function AppTopBar({ userManagerInstance }: AppTopBarProps) { const searchInputRef = useRef(null); useEffect(() => { - if (user !== null) { - fetchAppsMetadata().then((res) => { + if (user !== undefined) { + appsMetadataSrv.fetchAppsMetadata().then((res) => { dispatch(setAppsAndUrls(res)); }); } @@ -70,6 +65,11 @@ export default function AppTopBar({ userManagerInstance }: AppTopBarProps) { } }, [user]); + const globalVersionFetcher = useCallback( + () => appsMetadataSrv.fetchVersion().then((res) => res?.deployVersion ?? ''), + [] + ); + return ( : } appVersion={AppPackage.version} appLicense={AppPackage.license} - onLogoutClick={() => logout(dispatch, userManagerInstance)} - onLogoClick={() => navigate('/', { replace: true })} - user={user ?? undefined} + onLogoutClick={onLogoutClick} + onLogoClick={onLogoClick} + user={user} appsAndUrls={appsAndUrls} onThemeClick={handleChangeTheme} - theme={themeLocal as GsTheme} + theme={themeLocal} onLanguageClick={handleChangeLanguage} - language={languageLocal as GsLang} - globalVersionPromise={() => fetchVersion().then((res) => res?.deployVersion)} - additionalModulesPromise={getServersInfos as () => Promise} + language={languageLocal} + globalVersionPromise={globalVersionFetcher} + additionalModulesPromise={'explore'} > {user && } diff --git a/src/components/app-wrapper.tsx b/src/components/app-wrapper.tsx index bf9d0c939..a285f0e4d 100644 --- a/src/components/app-wrapper.tsx +++ b/src/components/app-wrapper.tsx @@ -34,6 +34,7 @@ import { login_fr, multiple_selection_dialog_en, multiple_selection_dialog_fr, + PARAM_THEME, SnackbarProvider, table_en, table_fr, @@ -57,7 +58,6 @@ import import_parameters_en from '../translations/external/import-parameters-en' import import_parameters_fr from '../translations/external/import-parameters-fr'; import { store } from '../redux/store'; import CssBaseline from '@mui/material/CssBaseline'; -import { PARAM_THEME } from '../utils/config-params'; import { AppState } from '../redux/reducer'; import { Theme } from '@mui/material'; diff --git a/src/components/app.tsx b/src/components/app.tsx index 6362f3915..a27153517 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -12,28 +12,24 @@ import { selectComputedLanguage, selectLanguage, selectTheme } from '../redux/ac import { AuthenticationRouter, CardErrorBoundary, + getComputedLanguage, getPreLoginPath, initializeAuthenticationProd, + PARAM_LANGUAGE, + PARAM_THEME, UserManagerState, useSnackMessage, } from '@gridsuite/commons-ui'; import { FormattedMessage } from 'react-intl'; -import { - connectNotificationsWsUpdateConfig, - fetchConfigParameter, - fetchConfigParameters, - fetchIdpSettings, - fetchValidateUser, -} from '../utils/rest-api'; -import { APP_NAME, COMMON_APP_NAME, PARAM_LANGUAGE, PARAM_THEME } from '../utils/config-params'; -import { getComputedLanguage } from '../utils/language'; +import { APP_NAME } from '../utils/config-params'; import AppTopBar from './app-top-bar'; -import Grid from '@mui/material/Grid'; +import { Grid } from '@mui/material'; import TreeViewsContainer from './tree-views-container'; import DirectoryContent from './directory-content'; import DirectoryBreadcrumbs from './directory-breadcrumbs'; import { AppState } from '../redux/reducer'; import { AppDispatch } from '../redux/store'; +import { appLocalSrv, configNotificationSrv, configSrv, userAdminSrv } from '../services'; const App = () => { const { snackError } = useSnackMessage(); @@ -86,14 +82,14 @@ const App = () => { }); const connectNotificationsUpdateConfig = useCallback(() => { - const ws = connectNotificationsWsUpdateConfig(); - + const ws = configNotificationSrv.connectNotificationsWsUpdateConfig(APP_NAME); ws.onmessage = function (event) { let eventData = JSON.parse(event.data); if (eventData.headers && eventData.headers['parameterName']) { - fetchConfigParameter(eventData.headers['parameterName']) - .then((param) => updateParams([param])) - .catch((error) => + configSrv + .fetchConfigParameter(eventData.headers['parameterName']) + .then((param: any) => updateParams([param])) + .catch((error: any) => snackError({ messageTxt: error.message, headerId: 'paramsRetrievingError', @@ -128,8 +124,8 @@ const App = () => { instance: await initializeAuthenticationProd( dispatch, initialMatchSilentRenewCallbackUrl != null, - fetchIdpSettings, - fetchValidateUser, + appLocalSrv.fetchIdpSettings, + userAdminSrv.fetchValidateUser, initialMatchSigninCallbackUrl != null ), error: null, @@ -142,8 +138,9 @@ const App = () => { }, [initialMatchSilentRenewCallbackUrl, dispatch, initialMatchSigninCallbackUrl]); useEffect(() => { - if (user !== null) { - fetchConfigParameters(COMMON_APP_NAME) + if (user !== undefined) { + configSrv + .fetchConfigParameters('common') .then((params) => updateParams(params)) .catch((error) => snackError({ @@ -152,7 +149,8 @@ const App = () => { }) ); - fetchConfigParameters(APP_NAME) + configSrv + .fetchConfigParameters(APP_NAME) .then((params) => updateParams(params)) .catch((error) => snackError({ @@ -188,7 +186,7 @@ const App = () => { marginTop: '20px', }} > - {user !== null ? ( + {user !== undefined ? ( = ({ label, name={name} label={label} elementType={elementType} - elementExists={elementExists} activeDirectory={activeDirectory} autoFocus={!caseFile} onManualChangeCallback={() => setModifiedByUser(true)} diff --git a/src/components/dialogs/commons/upload-new-case.tsx b/src/components/dialogs/commons/upload-new-case.tsx index da5005934..fdd5dde51 100644 --- a/src/components/dialogs/commons/upload-new-case.tsx +++ b/src/components/dialogs/commons/upload-new-case.tsx @@ -5,25 +5,24 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React, { useState } from 'react'; +import { ChangeEvent, FunctionComponent, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import Button from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; -import { Grid, Input } from '@mui/material'; +import { Button, CircularProgress, Grid, Input } from '@mui/material'; import { useController, useFormContext } from 'react-hook-form'; -import { createCaseWithoutDirectoryElementCreation, deleteCase } from '../../../utils/rest-api'; import { FieldConstants } from '@gridsuite/commons-ui'; +import { UUID } from 'crypto'; +import { caseSrv } from '../../../services'; interface UploadNewCaseProps { isNewStudyCreation?: boolean; - getCurrentCaseImportParams?: (uuid: string) => void; + getCurrentCaseImportParams?: (uuid: UUID) => void; handleApiCallError?: ErrorCallback; } const MAX_FILE_SIZE_IN_MO = 100; const MAX_FILE_SIZE_IN_BYTES = MAX_FILE_SIZE_IN_MO * 1024 * 1024; -const UploadNewCase: React.FunctionComponent = ({ +const UploadNewCase: FunctionComponent = ({ isNewStudyCreation = false, getCurrentCaseImportParams, handleApiCallError, @@ -49,7 +48,7 @@ const UploadNewCase: React.FunctionComponent = ({ const caseFile = value as File; const { name: caseFileName } = caseFile || {}; - const onChange = (event: React.ChangeEvent) => { + const onChange = (event: ChangeEvent) => { event.preventDefault(); clearErrors(FieldConstants.CASE_FILE); @@ -68,12 +67,13 @@ const UploadNewCase: React.FunctionComponent = ({ if (isNewStudyCreation) { // Create new case setCaseFileLoading(true); - createCaseWithoutDirectoryElementCreation(currentFile) + caseSrv + .createCaseWithoutDirectoryElementCreation(currentFile) .then((newCaseUuid) => { const prevCaseUuid = getValues(FieldConstants.CASE_UUID); if (prevCaseUuid && prevCaseUuid !== newCaseUuid) { - deleteCase(prevCaseUuid).catch(handleApiCallError); + caseSrv.deleteCase(prevCaseUuid).catch(handleApiCallError); } onCaseUuidChange(newCaseUuid); diff --git a/src/components/dialogs/contingency-list/creation/contingency-list-creation-dialog.jsx b/src/components/dialogs/contingency-list/creation/contingency-list-creation-dialog.jsx index db4f09f50..eadac5c01 100644 --- a/src/components/dialogs/contingency-list/creation/contingency-list-creation-dialog.jsx +++ b/src/components/dialogs/contingency-list/creation/contingency-list-creation-dialog.jsx @@ -6,18 +6,22 @@ */ import { useSelector } from 'react-redux'; -import { useSnackMessage, CustomMuiDialog, getCriteriaBasedSchema, FieldConstants } from '@gridsuite/commons-ui'; +import { + CustomMuiDialog, + FieldConstants, + getCriteriaBasedSchema, + PARAM_LANGUAGE, + useSnackMessage, + yup, +} from '@gridsuite/commons-ui'; import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; -import { createContingencyList } from '../../../../utils/rest-api'; -import React from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; import ContingencyListCreationForm from './contingency-list-creation-form'; import { getContingencyListEmptyFormData, getFormContent } from '../contingency-list-utils'; -import yup from '../../../utils/yup-config'; import { getExplicitNamingSchema } from '../explicit-naming/explicit-naming-form'; import { ContingencyListType } from '../../../../utils/elementType'; import { useParameterState } from '../../use-parameters-dialog'; -import { PARAM_LANGUAGE } from '../../../../utils/config-params'; +import { exploreSrv } from '../../../../services'; const schema = yup.object().shape({ [FieldConstants.NAME]: yup.string().trim().required('nameEmpty'), @@ -61,13 +65,14 @@ const ContingencyListCreationDialog = ({ onClose, open, titleId }) => { const onSubmit = (data) => { const formContent = getFormContent(null, data); - createContingencyList( - data[FieldConstants.CONTINGENCY_LIST_TYPE], - data[FieldConstants.NAME], - data[FieldConstants.DESCRIPTION], - formContent, - activeDirectory - ) + exploreSrv + .createContingencyList( + data[FieldConstants.CONTINGENCY_LIST_TYPE], + data[FieldConstants.NAME], + data[FieldConstants.DESCRIPTION], + formContent, + activeDirectory + ) .then(() => closeAndClear()) .catch((error) => { snackError({ diff --git a/src/components/dialogs/contingency-list/creation/contingency-list-creation-form.jsx b/src/components/dialogs/contingency-list/creation/contingency-list-creation-form.jsx index 81d53cb10..f83eae08f 100644 --- a/src/components/dialogs/contingency-list/creation/contingency-list-creation-form.jsx +++ b/src/components/dialogs/contingency-list/creation/contingency-list-creation-form.jsx @@ -6,24 +6,22 @@ */ import { - RadioInput, - getCriteriaBasedFormData, CONTINGENCY_LIST_EQUIPMENTS, CriteriaBasedForm, - FieldConstants, - UniqueNameInput, - ExpandingTextField, ElementType, + ExpandingTextField, + FieldConstants, + getCriteriaBasedFormData, gridItem, + RadioInput, + UniqueNameInput, } from '@gridsuite/commons-ui'; import { ContingencyListType } from '../../../../utils/elementType'; import { Box, Grid } from '@mui/material'; -import React from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import ExplicitNamingForm from '../explicit-naming/explicit-naming-form'; import ScriptInputForm from '../script/script-input-form'; import { useSelector } from 'react-redux'; -import { elementExists } from '../../../../utils/rest-api'; const ContingencyListCreationForm = () => { const { setValue } = useFormContext(); @@ -57,7 +55,6 @@ const ContingencyListCreationForm = () => { elementType={ElementType.CONTINGENCY_LIST} autoFocus activeDirectory={activeDirectory} - elementExists={elementExists} /> diff --git a/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-dialog.jsx b/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-dialog.jsx index 4ce1b53bb..7793ce534 100644 --- a/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-dialog.jsx +++ b/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-dialog.jsx @@ -5,23 +5,27 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useSnackMessage, CustomMuiDialog, getCriteriaBasedSchema, FieldConstants } from '@gridsuite/commons-ui'; +import { + CustomMuiDialog, + FieldConstants, + getCriteriaBasedSchema, + PARAM_LANGUAGE, + useSnackMessage, + yup, +} from '@gridsuite/commons-ui'; import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; - -import React, { useEffect, useState } from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useEffect, useState } from 'react'; import { getContingencyListEmptyFormData, getCriteriaBasedFormDataFromFetchedElement, } from '../../contingency-list-utils'; -import { getContingencyList, saveCriteriaBasedContingencyList } from 'utils/rest-api'; -import yup from 'components/utils/yup-config'; import CriteriaBasedEditionForm from './criteria-based-edition-form'; import { useDispatch, useSelector } from 'react-redux'; import { noSelectionForCopy } from 'utils/constants'; import { setSelectionForCopy } from '../../../../../redux/actions'; import { useParameterState } from '../../../use-parameters-dialog'; -import { PARAM_LANGUAGE } from '../../../../../utils/config-params'; +import { actionsSrv, exploreSrv } from '../../../../../services'; const schema = yup.object().shape({ [FieldConstants.NAME]: yup.string().trim().required('nameEmpty'), @@ -61,7 +65,8 @@ const CriteriaBasedEditionDialog = ({ useEffect(() => { if (contingencyListId) { setIsFetching(true); - getContingencyList(contingencyListType, contingencyListId) + actionsSrv + .getContingencyList(contingencyListType, contingencyListId) .then((response) => { if (response) { const formData = getCriteriaBasedFormDataFromFetchedElement(response, name); @@ -84,7 +89,7 @@ const CriteriaBasedEditionDialog = ({ }; const editContingencyList = (contingencyListId, contingencyList) => { - return saveCriteriaBasedContingencyList(contingencyListId, contingencyList); + return exploreSrv.saveCriteriaBasedContingencyList(contingencyListId, contingencyList); }; const onSubmit = (contingencyList) => { diff --git a/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-form.jsx b/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-form.jsx index f51a0d018..1bfaf4d2c 100644 --- a/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-form.jsx +++ b/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-form.jsx @@ -6,16 +6,14 @@ */ import { Grid } from '@mui/material'; -import React from 'react'; import { - UniqueNameInput, - ElementType, - CriteriaBasedForm, - getCriteriaBasedFormData, CONTINGENCY_LIST_EQUIPMENTS, + CriteriaBasedForm, + ElementType, FieldConstants, + getCriteriaBasedFormData, + UniqueNameInput, } from '@gridsuite/commons-ui'; -import { elementExists } from 'utils/rest-api'; import { useSelector } from 'react-redux'; const CriteriaBasedEditionForm = () => { @@ -29,7 +27,6 @@ const CriteriaBasedEditionForm = () => { label={'nameProperty'} elementType={ElementType.CONTINGENCY_LIST} activeDirectory={activeDirectory} - elementExists={elementExists} /> { if (contingencyListId) { setIsFetching(true); - getContingencyList(contingencyListType, contingencyListId) + actionsSrv + .getContingencyList(contingencyListType, contingencyListId) .then((response) => { if (response) { const formData = getExplicitNamingFormDataFromFetchedElement(response); @@ -85,7 +84,7 @@ const ExplicitNamingEditionDialog = ({ const editContingencyList = (contingencyListId, contingencyList) => { const equipments = prepareContingencyListForBackend(contingencyListId, contingencyList); - return saveExplicitNamingContingencyList(equipments, contingencyList[FieldConstants.NAME]); + return exploreSrv.saveExplicitNamingContingencyList(equipments, contingencyList[FieldConstants.NAME]); }; const onSubmit = (contingencyList) => { diff --git a/src/components/dialogs/contingency-list/edition/explicit-naming/explicit-naming-edition-form.jsx b/src/components/dialogs/contingency-list/edition/explicit-naming/explicit-naming-edition-form.jsx index ca4464b38..0afd77b35 100644 --- a/src/components/dialogs/contingency-list/edition/explicit-naming/explicit-naming-edition-form.jsx +++ b/src/components/dialogs/contingency-list/edition/explicit-naming/explicit-naming-edition-form.jsx @@ -6,11 +6,9 @@ */ import { Grid } from '@mui/material'; -import React from 'react'; import ExplicitNamingForm from '../../explicit-naming/explicit-naming-form'; -import { ElementType, UniqueNameInput, FieldConstants } from '@gridsuite/commons-ui'; +import { ElementType, FieldConstants, UniqueNameInput } from '@gridsuite/commons-ui'; import { useSelector } from 'react-redux'; -import { elementExists } from 'utils/rest-api'; const ExplicitNamingEditionForm = () => { const activeDirectory = useSelector((state) => state.activeDirectory); @@ -22,7 +20,6 @@ const ExplicitNamingEditionForm = () => { label={'nameProperty'} elementType={ElementType.CONTINGENCY_LIST} activeDirectory={activeDirectory} - elementExists={elementExists} /> diff --git a/src/components/dialogs/contingency-list/edition/script/script-edition-dialog.jsx b/src/components/dialogs/contingency-list/edition/script/script-edition-dialog.jsx index fd3933fa0..fcc788232 100644 --- a/src/components/dialogs/contingency-list/edition/script/script-edition-dialog.jsx +++ b/src/components/dialogs/contingency-list/edition/script/script-edition-dialog.jsx @@ -5,18 +5,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useSnackMessage, CustomMuiDialog, FieldConstants } from '@gridsuite/commons-ui'; +import { CustomMuiDialog, FieldConstants, useSnackMessage, yup } from '@gridsuite/commons-ui'; import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; - -import React, { useEffect, useState } from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useEffect, useState } from 'react'; import { getContingencyListEmptyFormData, getScriptFormDataFromFetchedElement } from '../../contingency-list-utils'; -import { getContingencyList, saveScriptContingencyList } from 'utils/rest-api'; -import yup from 'components/utils/yup-config'; import ScriptEditionForm from './script-edition-form'; import { useDispatch, useSelector } from 'react-redux'; import { noSelectionForCopy } from 'utils/constants'; import { setSelectionForCopy } from '../../../../../redux/actions'; +import { actionsSrv, exploreSrv } from '../../../../../services'; const schema = yup.object().shape({ [FieldConstants.NAME]: yup.string().trim().required('nameEmpty'), @@ -56,7 +54,8 @@ const ScriptEditionDialog = ({ useEffect(() => { if (contingencyListId) { setIsFetching(true); - getContingencyList(contingencyListType, contingencyListId) + actionsSrv + .getContingencyList(contingencyListType, contingencyListId) .then((response) => { if (response) { const formData = getScriptFormDataFromFetchedElement(response); @@ -83,7 +82,7 @@ const ScriptEditionDialog = ({ id: contingencyListId, script: contingencyList[FieldConstants.SCRIPT], }; - return saveScriptContingencyList(newScript, contingencyList[FieldConstants.NAME]); + return exploreSrv.saveScriptContingencyList(newScript, contingencyList[FieldConstants.NAME]); }; const onSubmit = (contingencyList) => { editContingencyList(contingencyListId, contingencyList) diff --git a/src/components/dialogs/contingency-list/edition/script/script-edition-form.jsx b/src/components/dialogs/contingency-list/edition/script/script-edition-form.jsx index b14a2a95d..9163d9a3c 100644 --- a/src/components/dialogs/contingency-list/edition/script/script-edition-form.jsx +++ b/src/components/dialogs/contingency-list/edition/script/script-edition-form.jsx @@ -6,10 +6,8 @@ */ import { Grid } from '@mui/material'; -import React from 'react'; import ScriptInputForm from '../../script/script-input-form'; -import { UniqueNameInput, ElementType, FieldConstants } from '@gridsuite/commons-ui'; -import { elementExists } from 'utils/rest-api'; +import { ElementType, FieldConstants, UniqueNameInput } from '@gridsuite/commons-ui'; import { useSelector } from 'react-redux'; const ScriptEditionForm = () => { @@ -22,7 +20,6 @@ const ScriptEditionForm = () => { label={'nameProperty'} elementType={ElementType.CONTINGENCY_LIST} activeDirectory={activeDirectory} - elementExists={elementExists} /> diff --git a/src/components/dialogs/contingency-list/explicit-naming/explicit-naming-form.jsx b/src/components/dialogs/contingency-list/explicit-naming/explicit-naming-form.jsx index 0852d8faa..cf6e6b071 100644 --- a/src/components/dialogs/contingency-list/explicit-naming/explicit-naming-form.jsx +++ b/src/components/dialogs/contingency-list/explicit-naming/explicit-naming-form.jsx @@ -7,12 +7,17 @@ import { useIntl } from 'react-intl'; import React, { useCallback, useMemo } from 'react'; -import yup from '../../../utils/yup-config'; import { makeDefaultRowData } from '../contingency-list-utils'; import ChipsArrayEditor from '../../../utils/rhf-inputs/ag-grid-table-rhf/cell-editors/chips-array-editor'; import { ContingencyListType } from 'utils/elementType'; import { v4 as uuid4 } from 'uuid'; -import { FieldConstants, gridItem, CustomAgGridTable, ROW_DRAGGING_SELECTION_COLUMN_DEF } from '@gridsuite/commons-ui'; +import { + CustomAgGridTable, + FieldConstants, + gridItem, + ROW_DRAGGING_SELECTION_COLUMN_DEF, + yup, +} from '@gridsuite/commons-ui'; export const getExplicitNamingSchema = (id) => { return { diff --git a/src/components/dialogs/copy-to-script-dialog.tsx b/src/components/dialogs/copy-to-script-dialog.tsx index cd23abd1c..8440c7744 100644 --- a/src/components/dialogs/copy-to-script-dialog.tsx +++ b/src/components/dialogs/copy-to-script-dialog.tsx @@ -4,16 +4,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React, { useCallback, useEffect, useState } from 'react'; +import { FunctionComponent, useCallback, useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; -import yup from 'components/utils/yup-config'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { CircularProgress, Grid } from '@mui/material'; -import { UniqueNameInput, ElementType, CustomMuiDialog, FieldConstants } from '@gridsuite/commons-ui'; -import { elementExists, getNameCandidate } from 'utils/rest-api'; +import { CustomMuiDialog, ElementType, FieldConstants, UniqueNameInput, yup } from '@gridsuite/commons-ui'; import { useSelector } from 'react-redux'; -import { AppState } from 'redux/reducer'; +import { UUID } from 'crypto'; +import { AppState } from '../../redux/reducer'; +import { directorySrv } from '../../services'; const schema = yup.object().shape({ [FieldConstants.NAME]: yup.string().trim().required('nameEmpty'), @@ -30,7 +30,7 @@ interface CopyToScriptDialogProps { onValidate: (...args: any[]) => void; currentName: string; title: string; - directoryUuid: string; + directoryUuid: UUID; elementType: ElementType; handleError: (...args: any[]) => void; } @@ -51,7 +51,7 @@ interface FormData { * @param elementType Type of the element to copy * @param handleError Function to call to handle error */ -const CopyToScriptDialog: React.FunctionComponent = ({ +const CopyToScriptDialog: FunctionComponent = ({ id, open, onClose, @@ -100,7 +100,8 @@ const CopyToScriptDialog: React.FunctionComponent = ({ useEffect(() => { setLoading(true); - getNameCandidate(directoryUuid, currentName, elementType) + directorySrv + .getNameCandidate(directoryUuid, currentName, elementType) .then((newName) => { let generatedName: string = newName || ''; setValue(FieldConstants.NAME, generatedName, { @@ -137,7 +138,6 @@ const CopyToScriptDialog: React.FunctionComponent = ({ elementType={ElementType.CONTINGENCY_LIST} autoFocus activeDirectory={activeDirectory} - elementExists={elementExists} /> )} diff --git a/src/components/dialogs/create-case-dialog/create-case-dialog-utils.ts b/src/components/dialogs/create-case-dialog/create-case-dialog-utils.ts index 78f01fc60..a1d50b57a 100644 --- a/src/components/dialogs/create-case-dialog/create-case-dialog-utils.ts +++ b/src/components/dialogs/create-case-dialog/create-case-dialog-utils.ts @@ -5,8 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import yup from '../../utils/yup-config'; -import { FieldConstants } from '@gridsuite/commons-ui'; +import { FieldConstants, yup } from '@gridsuite/commons-ui'; export const getCreateCaseDialogFormValidationDefaultValues = () => ({ [FieldConstants.CASE_NAME]: '', diff --git a/src/components/dialogs/create-case-dialog/create-case-dialog.tsx b/src/components/dialogs/create-case-dialog/create-case-dialog.tsx index 5f12ac744..33fcd87c9 100644 --- a/src/components/dialogs/create-case-dialog/create-case-dialog.tsx +++ b/src/components/dialogs/create-case-dialog/create-case-dialog.tsx @@ -5,15 +5,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { createCase } from '../../../utils/rest-api'; +import { exploreSrv } from '../../../services'; import { HTTP_UNPROCESSABLE_ENTITY_STATUS } from '../../../utils/UIconstants'; import { Grid } from '@mui/material'; import { addUploadingElement, removeUploadingElement } from '../../../redux/actions'; import UploadNewCase from '../commons/upload-new-case'; import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; +import { yupResolver } from '@hookform/resolvers/yup'; import { createCaseDialogFormValidationSchema, getCreateCaseDialogFormValidationDefaultValues, @@ -33,6 +32,7 @@ import { } from '@gridsuite/commons-ui'; import { handleMaxElementsExceededError } from '../../utils/rest-errors'; import { AppDispatch } from '../../../redux/store'; +import { FunctionComponent } from 'react'; interface IFormData { [FieldConstants.CASE_NAME]: string; @@ -45,7 +45,7 @@ interface CreateCaseDialogProps { open: boolean; } -const CreateCaseDialog: React.FunctionComponent = ({ onClose, open }) => { +const CreateCaseDialog: FunctionComponent = ({ onClose, open }) => { const dispatch = useDispatch(); const { snackError } = useSnackMessage(); @@ -75,14 +75,11 @@ const CreateCaseDialog: React.FunctionComponent = ({ onCl uploading: true, }; - createCase({ - name: caseName, - description, - file: caseFile, - parentDirectoryUuid: activeDirectory, - }) + exploreSrv + // @ts-expect-error TODO: description can't be null with rest call + .createCase(caseName, description, caseFile, activeDirectory) .then(onClose) - .catch((err) => { + .catch((err: any) => { if (handleMaxElementsExceededError(err, snackError)) { return; } else if (err?.status === HTTP_UNPROCESSABLE_ENTITY_STATUS) { diff --git a/src/components/dialogs/create-directory-dialog.tsx b/src/components/dialogs/create-directory-dialog.tsx index 4860df1a2..287b10ba3 100644 --- a/src/components/dialogs/create-directory-dialog.tsx +++ b/src/components/dialogs/create-directory-dialog.tsx @@ -5,24 +5,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import Dialog from '@mui/material/Dialog'; -import DialogTitle from '@mui/material/DialogTitle'; +import { FunctionComponent } from 'react'; +import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; import { FormattedMessage } from 'react-intl'; -import DialogContent from '@mui/material/DialogContent'; -import DialogActions from '@mui/material/DialogActions'; -import Button from '@mui/material/Button'; -import Alert from '@mui/material/Alert'; -import { ElementType } from '@gridsuite/commons-ui'; +import { CancelButton, ElementType } from '@gridsuite/commons-ui'; +import { UUID } from 'crypto'; import { useNameField } from './field-hook'; -import { CancelButton } from '@gridsuite/commons-ui'; -import { FunctionComponent } from 'react'; interface CreateDirectoryDialogProps { open: boolean; onClose: () => void; onClick: (newName: string) => void; title: string; - parentDirectory: string; + parentDirectory: UUID; error: string; } diff --git a/src/components/dialogs/create-study-dialog/create-study-dialog-utils.ts b/src/components/dialogs/create-study-dialog/create-study-dialog-utils.ts index fd1fb3bfb..4889ca5c7 100644 --- a/src/components/dialogs/create-study-dialog/create-study-dialog-utils.ts +++ b/src/components/dialogs/create-study-dialog/create-study-dialog-utils.ts @@ -5,25 +5,25 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import yup from '../../utils/yup-config'; -import { ElementAttributes, FieldConstants, Parameter } from '@gridsuite/commons-ui'; +import { ElementAttributes, FieldConstants, Parameter, yup } from '@gridsuite/commons-ui'; +import { UUID } from 'crypto'; export const getCreateStudyDialogFormDefaultValues = ({ directory = '', studyName = '', - caseFile = null, - caseUuid = '', + caseFile, + caseUuid, }: { directory?: string; studyName?: string; caseFile?: ElementAttributes | null; - caseUuid?: string; + caseUuid?: UUID; }): CreateStudyDialogFormValues => { return { [FieldConstants.STUDY_NAME]: studyName, [FieldConstants.DESCRIPTION]: '', - [FieldConstants.CASE_FILE]: caseFile, - [FieldConstants.CASE_UUID]: caseUuid, + [FieldConstants.CASE_FILE]: caseFile ?? null, + [FieldConstants.CASE_UUID]: caseUuid ?? null, [FieldConstants.FORMATTED_CASE_PARAMETERS]: [], [FieldConstants.CURRENT_PARAMETERS]: {}, [FieldConstants.DIRECTORY]: directory, @@ -37,7 +37,7 @@ export const createStudyDialogFormValidationSchema = yup.object().shape({ [FieldConstants.FORMATTED_CASE_PARAMETERS]: yup.mixed().required(), [FieldConstants.DESCRIPTION]: yup.string().max(500, 'descriptionLimitError'), [FieldConstants.CURRENT_PARAMETERS]: yup.mixed>().required(), - [FieldConstants.CASE_UUID]: yup.string().nullable().required(), + [FieldConstants.CASE_UUID]: yup.string().nullable().required(), [FieldConstants.CASE_FILE]: yup.mixed().nullable().required(), [FieldConstants.DIRECTORY]: yup.string().required(), [FieldConstants.CASE_FORMAT]: yup.string().optional(), @@ -48,7 +48,7 @@ export interface CreateStudyDialogFormValues { [FieldConstants.STUDY_NAME]: string; [FieldConstants.DESCRIPTION]?: string; [FieldConstants.CASE_FILE]: ElementAttributes | null; - [FieldConstants.CASE_UUID]: string | null; + [FieldConstants.CASE_UUID]: UUID | null; [FieldConstants.FORMATTED_CASE_PARAMETERS]: Parameter[]; [FieldConstants.CURRENT_PARAMETERS]: Record; [FieldConstants.DIRECTORY]: string; diff --git a/src/components/dialogs/create-study-dialog/create-study-dialog.tsx b/src/components/dialogs/create-study-dialog/create-study-dialog.tsx index 7e7a07b7f..b42139c7f 100644 --- a/src/components/dialogs/create-study-dialog/create-study-dialog.tsx +++ b/src/components/dialogs/create-study-dialog/create-study-dialog.tsx @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2023, RTE (http://www.rte-france.com) +/* + * Copyright © 2024, RTE (http://www.rte-france.com) * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. @@ -9,35 +9,35 @@ import { Box, Grid } from '@mui/material'; import { useIntl } from 'react-intl'; import { FunctionComponent, useCallback, useEffect } from 'react'; import UploadNewCase from '../commons/upload-new-case'; -import { createStudy, deleteCase, getCaseImportParameters } from '../../../utils/rest-api'; import { HTTP_CONNECTION_FAILED_MESSAGE, HTTP_UNPROCESSABLE_ENTITY_STATUS } from '../../../utils/UIconstants'; import { - useSnackMessage, + CustomMuiDialog, + ElementAttributes, + ElementType, ErrorInput, - FieldErrorAlert, ExpandingTextField, - ElementType, - CustomMuiDialog, FieldConstants, + FieldErrorAlert, isObjectEmpty, keyGenerator, ModifyElementSelection, - ElementAttributes, Parameter, + useSnackMessage, } from '@gridsuite/commons-ui'; import { useDispatch, useSelector } from 'react-redux'; import ImportParametersSection from './importParametersSection'; import { addUploadingElement, removeUploadingElement, setActiveDirectory } from '../../../redux/actions'; import { - CreateStudyDialogFormValues, createStudyDialogFormValidationSchema, + CreateStudyDialogFormValues, getCreateStudyDialogFormDefaultValues, } from './create-study-dialog-utils'; -import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; +import { yupResolver } from '@hookform/resolvers/yup'; import PrefilledNameInput from '../commons/prefilled-name-input'; import { handleMaxElementsExceededError } from '../../utils/rest-errors'; import { AppState, UploadingElement } from 'redux/reducer'; import { UUID } from 'crypto'; +import { caseSrv, exploreSrv, networkConversionSrv } from '../../../services'; const STRING_LIST = 'STRING_LIST'; @@ -123,8 +123,9 @@ const CreateStudyDialog: FunctionComponent = ({ open, on ); const getCurrentCaseImportParams = useCallback( - (uuid: string) => { - getCaseImportParameters(uuid) + (uuid: UUID) => { + networkConversionSrv + .getCaseImportParameters(uuid) .then((result) => { const formattedParams = formatCaseImportParameters(result.parameters); setValue(FieldConstants.CURRENT_PARAMETERS, customizeCurrentParameters(formattedParams)); @@ -153,7 +154,7 @@ const CreateStudyDialog: FunctionComponent = ({ open, on const caseUuid = getValues(FieldConstants.CASE_UUID); // if we cancel case creation, we need to delete the associated newly created case (if we created one) if (caseUuid && !providedExistingCase) { - deleteCase(caseUuid).catch(handleApiCallError); + caseSrv.deleteCase(caseUuid).catch(handleApiCallError); } }; @@ -191,15 +192,17 @@ const CreateStudyDialog: FunctionComponent = ({ open, on caseFormat, }; - createStudy( - studyName, - description, - caseUuid, - !!providedExistingCase, - directory, - currentParameters ? JSON.stringify(currentParameters) : '', - caseFormat - ) + exploreSrv + .createStudy( + studyName, + // @ts-expect-error TODO: manage nullable description case + description, + caseUuid, + !!providedExistingCase, + directory, + currentParameters ? JSON.stringify(currentParameters) : '', + caseFormat + ) .then(() => { dispatch(setActiveDirectory(selectedDirectory?.elementUuid)); onClose(); diff --git a/src/components/dialogs/export-case-dialog.tsx b/src/components/dialogs/export-case-dialog.tsx index e2f806cbe..53e1fee21 100644 --- a/src/components/dialogs/export-case-dialog.tsx +++ b/src/components/dialogs/export-case-dialog.tsx @@ -22,24 +22,10 @@ import { Typography, } from '@mui/material'; import { useCallback, useEffect, useState } from 'react'; -import { getExportFormats } from '../../utils/rest-api'; import { FormattedMessage, useIntl } from 'react-intl'; import { ExpandLess, ExpandMore } from '@mui/icons-material'; import { CancelButton, FlatParameters } from '@gridsuite/commons-ui'; - -type ExportFormats = - | { - [formatName: string]: { - formatName: string; - parameters: { - name: string; - type: string; - defaultValue: any; - possibleValues: any; - }[]; - }; - } - | []; +import { ExportFormats, networkConversionSrv } from '../../services'; type FormatParameters = { [parameterName: string]: any; @@ -60,7 +46,7 @@ const ExportCaseDialog = (props: ExportCaseDialogProps) => { const intl = useIntl(); useEffect(() => { - getExportFormats().then((fetchedFormats: ExportFormats) => { + networkConversionSrv.getExportFormats().then((fetchedFormats: ExportFormats) => { // we check if the param is for extension, if it is, we select all possible values by default. // the only way for the moment to check if the param is for extension, is by checking his type is name. //TODO to be removed when extensions param default value corrected in backend to include all possible values diff --git a/src/components/dialogs/field-hook.tsx b/src/components/dialogs/field-hook.tsx index ee6d406fb..cc991559c 100644 --- a/src/components/dialogs/field-hook.tsx +++ b/src/components/dialogs/field-hook.tsx @@ -1,16 +1,17 @@ /* - * Copyright (c) 2024, RTE (http://www.rte-france.com) + * Copyright © 2024, RTE (http://www.rte-france.com) * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ChangeEvent, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { elementExists, rootDirectoryExists } from '../../utils/rest-api'; import { CircularProgress, InputAdornment, TextField, TextFieldProps } from '@mui/material'; import CheckIcon from '@mui/icons-material/Check'; import { ElementType, useDebounce } from '@gridsuite/commons-ui'; +import { UUID } from 'crypto'; +import { directorySrv } from '../../services'; const styles = { helperText: { @@ -23,7 +24,7 @@ interface UseTextValueProps extends Omit void, boolean] => { +}: UseTextValueProps): [string, ReactNode, (value: string) => void, boolean] => { const [value, setValue] = useState(defaultValue); const [hasChanged, setHasChanged] = useState(false); - const handleChangeValue = useCallback((event: React.ChangeEvent) => { + const handleChangeValue = useCallback((event: ChangeEvent) => { setValue(event.target.value); setHasChanged(true); }, []); @@ -68,8 +69,19 @@ export const useTextValue = ({ return [value, field, setValue, hasChanged]; }; +async function doesElementInDirExist(elementType: ElementType, parentDirectoryId: UUID | undefined, name: string) { + // if element is a root directory, we need to make a specific api rest call (elementType is directory, and no parent element) + if (elementType === ElementType.DIRECTORY && parentDirectoryId === undefined) { + return directorySrv.rootDirectoryExists(name); + } else if (parentDirectoryId !== undefined) { + return directorySrv.elementExists(parentDirectoryId, name, elementType); + } else { + return false; + } +} + interface UseNameFieldProps extends UseTextValueProps { - parentDirectoryId?: string; + parentDirectoryId?: UUID; elementType: ElementType; active: boolean; alreadyExistingErrorMessage?: string; @@ -81,17 +93,13 @@ export const useNameField = ({ active, alreadyExistingErrorMessage, ...props -}: UseNameFieldProps): [string, React.ReactNode, string | undefined, boolean, (value: string) => void, boolean] => { +}: Readonly): [string, ReactNode, string | undefined, boolean, (value: string) => void, boolean] => { const [error, setError] = useState(); const intl = useIntl(); const [checking, setChecking] = useState(undefined); - // if element is a root directory, we need to make a specific api rest call (elementType is directory, and no parent element) const doesElementExist = useCallback( - (name: string) => - elementType === ElementType.DIRECTORY && !parentDirectoryId - ? rootDirectoryExists(name) - : elementExists(parentDirectoryId, name, elementType), + (name: string) => doesElementInDirExist(elementType, parentDirectoryId, name), [elementType, parentDirectoryId] ); @@ -110,11 +118,10 @@ export const useNameField = ({ if (nameFormatted !== '' && name === props.defaultValue) { setError( - alreadyExistingErrorMessage - ? alreadyExistingErrorMessage - : intl.formatMessage({ - id: 'nameAlreadyUsed', - }) + alreadyExistingErrorMessage ?? + intl.formatMessage({ + id: 'nameAlreadyUsed', + }) ); setChecking(false); } @@ -124,9 +131,8 @@ export const useNameField = ({ .then((data) => { setError( data - ? alreadyExistingErrorMessage - ? alreadyExistingErrorMessage - : intl.formatMessage({ + ? alreadyExistingErrorMessage ?? + intl.formatMessage({ id: 'nameAlreadyUsed', }) : '' diff --git a/src/components/dialogs/rename-dialog.tsx b/src/components/dialogs/rename-dialog.tsx index 9f06e47ff..f3e14e5f7 100644 --- a/src/components/dialogs/rename-dialog.tsx +++ b/src/components/dialogs/rename-dialog.tsx @@ -13,10 +13,10 @@ import Button from '@mui/material/Button'; import { FormattedMessage, useIntl } from 'react-intl'; import { useNameField } from './field-hook'; import { useSelector } from 'react-redux'; -import { ElementType } from '@gridsuite/commons-ui'; -import { CancelButton } from '@gridsuite/commons-ui'; +import { CancelButton, ElementType } from '@gridsuite/commons-ui'; import { FunctionComponent } from 'react'; import { AppState } from 'redux/reducer'; +import { UUID } from 'crypto'; interface RenameDialogProps { open: boolean; @@ -27,7 +27,7 @@ interface RenameDialogProps { currentName: string; type: ElementType; error?: string; - parentDirectory?: string; + parentDirectory?: UUID; } /** diff --git a/src/components/dialogs/use-parameters-dialog.tsx b/src/components/dialogs/use-parameters-dialog.tsx index 12bd41610..6def22815 100644 --- a/src/components/dialogs/use-parameters-dialog.tsx +++ b/src/components/dialogs/use-parameters-dialog.tsx @@ -6,31 +6,29 @@ */ import { useCallback, useEffect, useState } from 'react'; - import { useSelector } from 'react-redux'; - -import { updateConfigParameter } from '../../utils/rest-api'; -import { useSnackMessage } from '@gridsuite/commons-ui'; -import { PARAM_LANGUAGE, PARAM_THEME } from '../../utils/config-params'; +import { PARAM_LANGUAGE, PARAM_THEME, useSnackMessage } from '@gridsuite/commons-ui'; import { AppState } from 'redux/reducer'; +import { configSrv } from '../../services'; type ParamName = typeof PARAM_THEME | typeof PARAM_LANGUAGE; -export function useParameterState(paramName: ParamName): [string, (value: string) => void] { +export function useParameterState( + paramName: TParam +): [AppState[TParam], (value: AppState[TParam]) => void] { const { snackError } = useSnackMessage(); const paramGlobalState = useSelector((state: AppState) => state[paramName]); - - const [paramLocalState, setParamLocalState] = useState(paramGlobalState); + const [paramLocalState, setParamLocalState] = useState(paramGlobalState); useEffect(() => { setParamLocalState(paramGlobalState); }, [paramGlobalState]); const handleChangeParamLocalState = useCallback( - (value: string) => { + (value: AppState[TParam]) => { setParamLocalState(value); - updateConfigParameter(paramName, value).catch((error) => { + configSrv.updateConfigParameter(paramName, value).catch((error) => { setParamLocalState(paramGlobalState); snackError({ messageTxt: error.message, diff --git a/src/components/directory-content.jsx b/src/components/directory-content.jsx index b1cb51f32..0f42dfc42 100644 --- a/src/components/directory-content.jsx +++ b/src/components/directory-content.jsx @@ -9,25 +9,20 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { setActiveDirectory, setSelectionForCopy } from '../redux/actions'; import { FormattedMessage, useIntl } from 'react-intl'; - import * as constants from '../utils/UIconstants'; -import CircularProgress from '@mui/material/CircularProgress'; -import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded'; - +import { Box, CircularProgress, Grid } from '@mui/material'; +import { FolderOpenRounded as FolderOpenRoundedIcon } from '@mui/icons-material'; import { ContingencyListType, FilterType } from '../utils/elementType'; import { - ElementType, - useSnackMessage, - ExplicitNamingFilterEditionDialog, - ExpertFilterEditionDialog, CriteriaBasedFilterEditionDialog, DescriptionModificationDialog, + ElementType, + ExpertFilterEditionDialog, + ExplicitNamingFilterEditionDialog, noSelectionForCopy, + PARAM_LANGUAGE, + useSnackMessage, } from '@gridsuite/commons-ui'; -import { Box } from '@mui/material'; - -import { elementExists, getFilterById, updateElement } from '../utils/rest-api'; - import ContentContextualMenu from './menus/content-contextual-menu'; import ContentToolbar from './toolbars/content-toolbar'; import DirectoryTreeContextualMenu from './menus/directory-tree-contextual-menu'; @@ -35,17 +30,15 @@ import CriteriaBasedEditionDialog from './dialogs/contingency-list/edition/crite import ExplicitNamingEditionDialog from './dialogs/contingency-list/edition/explicit-naming/explicit-naming-edition-dialog'; import ScriptEditionDialog from './dialogs/contingency-list/edition/script/script-edition-dialog'; import { useParameterState } from './dialogs/use-parameters-dialog'; -import { PARAM_LANGUAGE } from '../utils/config-params'; -import Grid from '@mui/material/Grid'; import { useDirectoryContent } from '../hooks/useDirectoryContent'; import { - getColumnsDefinition, computeCheckedElements, formatMetadata, + getColumnsDefinition, isRowUnchecked, } from './utils/directory-content-utils'; import NoContentDirectory from './no-content-directory'; -import { DirectoryContentTable, CUSTOM_ROW_CLASS } from './directory-content-table'; +import { CUSTOM_ROW_CLASS, DirectoryContentTable } from './directory-content-table'; import { useHighlightSearchedElement } from './search/use-highlight-searched-element'; const circularProgressSize = '70px'; @@ -485,7 +478,6 @@ const DirectoryContent = () => { setActiveElement(null); setOpenDescModificationDialog(false); }} - updateElement={updateElement} /> ); } @@ -537,9 +529,7 @@ const DirectoryContent = () => { titleId={'editFilter'} name={name} broadcastChannel={broadcastChannel} - getFilterById={getFilterById} activeDirectory={activeDirectory} - elementExists={elementExists} language={languageLocal} /> ); @@ -552,10 +542,8 @@ const DirectoryContent = () => { titleId={'editFilter'} name={name} broadcastChannel={broadcastChannel} - getFilterById={getFilterById} selectionForCopy={selectionForCopy} activeDirectory={activeDirectory} - elementExists={elementExists} language={languageLocal} /> ); @@ -569,9 +557,7 @@ const DirectoryContent = () => { name={name} broadcastChannel={broadcastChannel} selectionForCopy={selectionForCopy} - getFilterById={getFilterById} activeDirectory={activeDirectory} - elementExists={elementExists} language={languageLocal} /> ); diff --git a/src/components/menus/content-contextual-menu.jsx b/src/components/menus/content-contextual-menu.jsx index 7224bf439..8c4ce3081 100644 --- a/src/components/menus/content-contextual-menu.jsx +++ b/src/components/menus/content-contextual-menu.jsx @@ -5,53 +5,40 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React, { useCallback, useState } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import { useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; - -import FileCopyIcon from '@mui/icons-material/FileCopy'; -import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; -import DeleteIcon from '@mui/icons-material/Delete'; -import DriveFileMoveIcon from '@mui/icons-material/DriveFileMove'; -import PhotoLibrary from '@mui/icons-material/PhotoLibrary'; -import ContentCopy from '@mui/icons-material/ContentCopy'; -import CopyAllIcon from '@mui/icons-material/CopyAll'; -import DoNotDisturbAltIcon from '@mui/icons-material/DoNotDisturbAlt'; - +import { + ContentCopy, + CopyAll as CopyAllIcon, + Delete as DeleteIcon, + DoNotDisturbAlt as DoNotDisturbAltIcon, + DownloadForOffline, + DriveFileMove as DriveFileMoveIcon, + FileCopy as FileCopyIcon, + FileDownload, + InsertDriveFile as InsertDriveFileIcon, + PhotoLibrary, +} from '@mui/icons-material'; import RenameDialog from '../dialogs/rename-dialog'; import DeleteDialog from '../dialogs/delete-dialog'; import ReplaceWithScriptDialog from '../dialogs/replace-with-script-dialog'; import CopyToScriptDialog from '../dialogs/copy-to-script-dialog'; import CreateStudyDialog from '../dialogs/create-study-dialog/create-study-dialog'; - import { DialogsId } from '../../utils/UIconstants'; - -import { - deleteElements, - duplicateElement, - elementExists, - moveElementsToDirectory, - newScriptFromFilter, - newScriptFromFiltersContingencyList, - renameElement, - replaceFiltersWithScript, - replaceFormContingencyListWithScript, -} from '../../utils/rest-api'; - import { ContingencyListType, FilterType } from '../../utils/elementType'; -import { ElementType, useSnackMessage, FilterCreationDialog } from '@gridsuite/commons-ui'; - +import { ElementType, FilterCreationDialog, PARAM_LANGUAGE, useSnackMessage } from '@gridsuite/commons-ui'; import CommonContextualMenu from './common-contextual-menu'; -import { useDeferredFetch, useMultipleDeferredFetch } from '../../utils/custom-hooks'; +import useDeferredFetch from '../../hooks/useDeferredFetch'; +import useMultipleDeferredFetch from '../../hooks/useMultipleDeferredFetch'; import MoveDialog from '../dialogs/move-dialog'; -import { DownloadForOffline, FileDownload } from '@mui/icons-material'; import { useDownloadUtils } from '../utils/caseUtils'; import ExportCaseDialog from '../dialogs/export-case-dialog'; import { setSelectionForCopy } from '../../redux/actions'; import { useParameterState } from '../dialogs/use-parameters-dialog'; -import { PARAM_LANGUAGE } from '../../utils/config-params'; import { handleMaxElementsExceededError } from '../utils/rest-errors'; +import { exploreSrv } from '../../services'; const ContentContextualMenu = (props) => { const { @@ -200,36 +187,42 @@ const ContentContextualMenu = (props) => { case ElementType.STUDY: case ElementType.FILTER: case ElementType.MODIFICATION: - duplicateElement(activeElement.elementUuid, undefined, activeElement.type).catch((error) => { - if (handleMaxElementsExceededError(error, snackError)) { - return; - } - handleDuplicateError(error.message); - }); + exploreSrv + .duplicateElement(activeElement.elementUuid, undefined, activeElement.type) + .catch((error) => { + if (handleMaxElementsExceededError(error, snackError)) { + return; + } + handleDuplicateError(error.message); + }); break; case ElementType.CONTINGENCY_LIST: - duplicateElement( - activeElement.elementUuid, - undefined, - activeElement.type, - selectedElements[0].specificMetadata.type - ).catch((error) => { - handleDuplicateError(error.message); - }); + exploreSrv + .duplicateElement( + activeElement.elementUuid, + undefined, + activeElement.type, + selectedElements[0].specificMetadata.type + ) + .catch((error) => { + handleDuplicateError(error.message); + }); break; case ElementType.VOLTAGE_INIT_PARAMETERS: case ElementType.SENSITIVITY_PARAMETERS: case ElementType.SECURITY_ANALYSIS_PARAMETERS: case ElementType.LOADFLOW_PARAMETERS: case ElementType.SHORT_CIRCUIT_PARAMETERS: - duplicateElement( - activeElement.elementUuid, - undefined, - ElementType.PARAMETERS, - activeElement.type - ).catch((error) => { - handleDuplicateError(error.message); - }); + exploreSrv + .duplicateElement( + activeElement.elementUuid, + undefined, + ElementType.PARAMETERS, + activeElement.type + ) + .catch((error) => { + handleDuplicateError(error.message); + }); break; default: { handleLastError( @@ -259,8 +252,9 @@ const ContentContextualMenu = (props) => { const handleDeleteElements = useCallback( (elementsUuids) => { setDeleteError(''); - deleteElements(elementsUuids, selectedDirectory.elementUuid) - .then(() => handleCloseDialog()) + exploreSrv + .deleteElements(elementsUuids, selectedDirectory.elementUuid) + .then(handleCloseDialog) //show the error message and don't close the dialog .catch((error) => { setDeleteError(error.message); @@ -300,7 +294,7 @@ const ContentContextualMenu = (props) => { ); const [moveCB] = useMultipleDeferredFetch( - moveElementsToDirectory, + exploreSrv.moveElementsToDirectory, undefined, moveElementErrorToString, moveElementOnError, @@ -308,7 +302,7 @@ const ContentContextualMenu = (props) => { ); const [renameCB, renameState] = useDeferredFetch( - renameElement, + exploreSrv.renameElement, (elementUuid, renamedElement) => { // if copied element is renamed if (selectionForCopy.sourceItemUuid === renamedElement[0]) { @@ -349,7 +343,7 @@ const ContentContextualMenu = (props) => { ); const [FiltersReplaceWithScriptCB] = useDeferredFetch( - replaceFiltersWithScript, + exploreSrv.replaceFiltersWithScript, handleCloseDialog, undefined, handleLastError, @@ -357,7 +351,7 @@ const ContentContextualMenu = (props) => { ); const [newScriptFromFiltersContingencyListCB] = useDeferredFetch( - newScriptFromFiltersContingencyList, + exploreSrv.newScriptFromFiltersContingencyList, handleCloseDialog, undefined, handleLastError, @@ -365,7 +359,7 @@ const ContentContextualMenu = (props) => { ); const [replaceFormContingencyListWithScriptCB] = useDeferredFetch( - replaceFormContingencyListWithScript, + exploreSrv.replaceFormContingencyListWithScript, handleCloseDialog, undefined, handleLastError, @@ -373,7 +367,7 @@ const ContentContextualMenu = (props) => { ); const [newScriptFromFilterCB] = useDeferredFetch( - newScriptFromFilter, + exploreSrv.newScriptFromFilter, handleCloseDialog, undefined, handleLastError, @@ -696,7 +690,6 @@ const ContentContextualMenu = (props) => { equipmentType: activeElement.specificMetadata.equipmentType, }} activeDirectory={activeDirectory} - elementExists={elementExists} language={languageLocal} /> ); diff --git a/src/components/menus/directory-tree-contextual-menu.jsx b/src/components/menus/directory-tree-contextual-menu.jsx index 3d698377a..28607e575 100644 --- a/src/components/menus/directory-tree-contextual-menu.jsx +++ b/src/components/menus/directory-tree-contextual-menu.jsx @@ -5,41 +5,31 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React, { useCallback, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; - -import DeleteIcon from '@mui/icons-material/Delete'; -import FolderSpecialIcon from '@mui/icons-material/FolderSpecial'; -import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; -import AddIcon from '@mui/icons-material/Add'; -import CreateIcon from '@mui/icons-material/Create'; -import ContentPasteIcon from '@mui/icons-material/ContentPaste'; +import { + Add as AddIcon, + ContentPaste as ContentPasteIcon, + Create as CreateIcon, + CreateNewFolder as CreateNewFolderIcon, + Delete as DeleteIcon, + FolderSpecial as FolderSpecialIcon, +} from '@mui/icons-material'; import CreateStudyForm from '../dialogs/create-study-dialog/create-study-dialog'; import CreateDirectoryDialog from '../dialogs/create-directory-dialog'; import RenameDialog from '../dialogs/rename-dialog'; import DeleteDialog from '../dialogs/delete-dialog'; - import { DialogsId } from '../../utils/UIconstants'; - -import { - deleteElement, - duplicateElement, - insertDirectory, - insertRootDirectory, - renameElement, - elementExists, -} from '../../utils/rest-api'; - import CommonContextualMenu from './common-contextual-menu'; -import { useDeferredFetch } from '../../utils/custom-hooks'; -import { ElementType, FilterCreationDialog, useSnackMessage } from '@gridsuite/commons-ui'; +import useDeferredFetch from '../../hooks/useDeferredFetch'; +import { ElementType, FilterCreationDialog, PARAM_LANGUAGE, useSnackMessage } from '@gridsuite/commons-ui'; import ContingencyListCreationDialog from '../dialogs/contingency-list/creation/contingency-list-creation-dialog'; import CreateCaseDialog from '../dialogs/create-case-dialog/create-case-dialog'; import { useParameterState } from '../dialogs/use-parameters-dialog'; -import { PARAM_LANGUAGE } from '../../utils/config-params'; import { handleMaxElementsExceededError } from '../utils/rest-errors'; +import { directorySrv, exploreSrv } from '../../services'; const DirectoryTreeContextualMenu = (props) => { const { directory, open, onClose, openDialog, setOpenDialog, ...others } = props; @@ -69,7 +59,7 @@ const DirectoryTreeContextualMenu = (props) => { ); const [renameCB, renameState] = useDeferredFetch( - renameElement, + exploreSrv.renameElement, () => handleCloseDialog(null, null), (HTTPStatusCode) => { if (HTTPStatusCode === 403) { @@ -80,12 +70,13 @@ const DirectoryTreeContextualMenu = (props) => { false ); - const [insertDirectoryCB, insertDirectoryState] = useDeferredFetch(insertDirectory, (response) => + const [insertDirectoryCB, insertDirectoryState] = useDeferredFetch(directorySrv.insertDirectory, (response) => handleCloseDialog(null, response?.elementUuid) ); - const [insertRootDirectoryCB, insertRootDirectoryState] = useDeferredFetch(insertRootDirectory, (response) => - handleCloseDialog(null, response?.elementUuid) + const [insertRootDirectoryCB, insertRootDirectoryState] = useDeferredFetch( + directorySrv.insertRootDirectory, + (response) => handleCloseDialog(null, response?.elementUuid) ); const selectionForCopy = useSelector((state) => state.selectionForCopy); @@ -123,41 +114,47 @@ const DirectoryTreeContextualMenu = (props) => { case ElementType.STUDY: case ElementType.FILTER: case ElementType.MODIFICATION: - duplicateElement( - selectionForCopy.sourceItemUuid, - directoryUuid, - selectionForCopy.typeItem, - undefined - ).catch((error) => { - if (handleMaxElementsExceededError(error, snackError)) { - return; - } - handlePasteError(error); - }); + exploreSrv + .duplicateElement( + selectionForCopy.sourceItemUuid, + directoryUuid, + selectionForCopy.typeItem, + undefined + ) + .catch((error) => { + if (handleMaxElementsExceededError(error, snackError)) { + return; + } + handlePasteError(error); + }); break; case ElementType.VOLTAGE_INIT_PARAMETERS: case ElementType.SECURITY_ANALYSIS_PARAMETERS: case ElementType.SENSITIVITY_PARAMETERS: case ElementType.LOADFLOW_PARAMETERS: case ElementType.SHORT_CIRCUIT_PARAMETERS: - duplicateElement( - selectionForCopy.sourceItemUuid, - directoryUuid, - ElementType.PARAMETERS, - selectionForCopy.typeItem - ).catch((error) => { - handlePasteError(error); - }); + exploreSrv + .duplicateElement( + selectionForCopy.sourceItemUuid, + directoryUuid, + ElementType.PARAMETERS, + selectionForCopy.typeItem + ) + .catch((error) => { + handlePasteError(error); + }); break; case ElementType.CONTINGENCY_LIST: - duplicateElement( - selectionForCopy.sourceItemUuid, - directoryUuid, - selectionForCopy.typeItem, - selectionForCopy.specificType - ).catch((error) => { - handlePasteError(error); - }); + exploreSrv + .duplicateElement( + selectionForCopy.sourceItemUuid, + directoryUuid, + selectionForCopy.typeItem, + selectionForCopy.specificType + ) + .catch((error) => { + handlePasteError(error); + }); break; default: handleError( @@ -175,7 +172,8 @@ const DirectoryTreeContextualMenu = (props) => { const handleDeleteElement = useCallback( (elementsUuid) => { setDeleteError(''); - deleteElement(elementsUuid) + exploreSrv + .deleteElement(elementsUuid) .then(() => handleCloseDialog(null, directory?.parentUuid)) .catch((error) => { //show the error message and don't close the dialog @@ -354,7 +352,6 @@ const DirectoryTreeContextualMenu = (props) => { open={true} onClose={handleCloseDialog} activeDirectory={activeDirectory} - elementExists={elementExists} language={languageLocal} /> ); diff --git a/src/components/search/search-bar.tsx b/src/components/search/search-bar.tsx index df1acfc73..a3e2d863d 100644 --- a/src/components/search/search-bar.tsx +++ b/src/components/search/search-bar.tsx @@ -5,15 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { FunctionComponent, RefObject, useCallback, useRef } from 'react'; -import { searchElementsInfos } from '../../utils/rest-api'; -import { - ElementSearchInput, - ElementType, - fetchDirectoryContent, - Paginated, - useElementSearch, - useSnackMessage, -} from '@gridsuite/commons-ui'; +import { ElementSearchInput, ElementType, Paginated, useElementSearch, useSnackMessage } from '@gridsuite/commons-ui'; import { useDispatch, useSelector } from 'react-redux'; import { setSearchedElement, setSelectedDirectory, setTreeData } from '../../redux/actions'; import { updatedTree } from '../tree-views-container'; @@ -23,6 +15,7 @@ import { TextFieldProps } from '@mui/material'; import { SearchBarRenderInput } from './search-bar-render-input'; import { AppDispatch } from '../../redux/store'; import { SearchBarPaperDisplayedElementWarning } from './search-bar-displayed-element-warning'; +import { directorySrv } from '../../services'; export const SEARCH_FETCH_TIMEOUT_MILLIS = 1000; // 1 second @@ -42,7 +35,8 @@ export const SearchBar: FunctionComponent = ({ inputRef }) => { treeDataRef.current = treeData; const fetchElementsPageable: (newSearchTerm: string) => Promise> = useCallback( - (newSearchTerm) => searchElementsInfos(newSearchTerm, selectedDirectory?.elementUuid), + // @ts-expect-error TODO: manage null elementUuid case + (newSearchTerm) => directorySrv.searchElementsInfos(newSearchTerm, selectedDirectory?.elementUuid), [selectedDirectory?.elementUuid] ); const { elementsFound, isLoading, searchTerm, updateSearchTerm, totalElements } = useElementSearch({ @@ -99,7 +93,7 @@ export const SearchBar: FunctionComponent = ({ inputRef }) => { const elementUuidPath = matchingElement?.pathUuid; try { for (const uuid of elementUuidPath) { - const res = await fetchDirectoryContent(uuid); + const res = await directorySrv.fetchDirectoryContent(uuid); updateMapData(uuid, res.filter((res) => res.type === ElementType.DIRECTORY) as IDirectory[]); } } catch (error: any) { diff --git a/src/components/toolbars/content-toolbar.jsx b/src/components/toolbars/content-toolbar.jsx index ed9f9da7b..87b2dd8b4 100644 --- a/src/components/toolbars/content-toolbar.jsx +++ b/src/components/toolbars/content-toolbar.jsx @@ -9,20 +9,21 @@ import { useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; - -import { deleteElements, moveElementsToDirectory } from '../../utils/rest-api'; -import DeleteIcon from '@mui/icons-material/Delete'; -import DriveFileMoveIcon from '@mui/icons-material/DriveFileMove'; +import { + Delete as DeleteIcon, + DriveFileMove as DriveFileMoveIcon, + DownloadForOffline, + FileDownload, +} from '@mui/icons-material'; import DeleteDialog from '../dialogs/delete-dialog'; import CommonToolbar from './common-toolbar'; -import { useMultipleDeferredFetch } from '../../utils/custom-hooks'; -import { useSnackMessage } from '@gridsuite/commons-ui'; +import useMultipleDeferredFetch from '../../hooks/useMultipleDeferredFetch'; +import { ElementType, useSnackMessage } from '@gridsuite/commons-ui'; import MoveDialog from '../dialogs/move-dialog'; -import { ElementType } from '@gridsuite/commons-ui'; -import { DownloadForOffline, FileDownload } from '@mui/icons-material'; import { useDownloadUtils } from '../utils/caseUtils'; import ExportCaseDialog from '../dialogs/export-case-dialog'; import { DialogsId } from '../../utils/UIconstants'; +import { exploreSrv } from '../../services'; const ContentToolbar = (props) => { const { selectedElements, ...others } = props; @@ -87,7 +88,7 @@ const ContentToolbar = (props) => { ); const [moveCB] = useMultipleDeferredFetch( - moveElementsToDirectory, + exploreSrv.moveElementsToDirectory, undefined, moveElementErrorToString, moveElementOnError, @@ -121,7 +122,8 @@ const ContentToolbar = (props) => { const handleDeleteElements = useCallback( (elementsUuids) => { setDeleteError(''); - deleteElements(elementsUuids, selectedDirectory.elementUuid) + exploreSrv + .deleteElements(elementsUuids, selectedDirectory.elementUuid) .then(handleCloseDialog) .catch((error) => { //show the error message and don't close the dialog diff --git a/src/components/tree-views-container.jsx b/src/components/tree-views-container.jsx index b80d5246a..72107424b 100644 --- a/src/components/tree-views-container.jsx +++ b/src/components/tree-views-container.jsx @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { directoryUpdated, @@ -15,15 +15,12 @@ import { setSelectedDirectory, setTreeData, } from '../redux/actions'; - -import { connectNotificationsWsUpdateDirectories } from '../utils/rest-api'; +import { ElementType, useSnackMessage } from '@gridsuite/commons-ui'; import DirectoryTreeView from './directory-tree-view'; -import { useSnackMessage, fetchDirectoryContent, fetchRootFolders, ElementType } from '@gridsuite/commons-ui'; import { notificationType } from '../utils/notificationType'; - import * as constants from '../utils/UIconstants'; -// Menu import DirectoryTreeContextualMenu from './menus/directory-tree-contextual-menu'; +import { directoryNotificationSrv, directorySrv } from '../services'; const initialMousePosition = { mouseX: null, @@ -181,7 +178,7 @@ const TreeViewsContainer = () => { /** * Contextual Menus */ - const [openDirectoryMenu, setOpenDirectoryMenu] = React.useState(false); + const [openDirectoryMenu, setOpenDirectoryMenu] = useState(false); const treeData = useSelector((state) => state.treeData); @@ -209,7 +206,7 @@ const TreeViewsContainer = () => { }; /* Menu states */ - const [mousePosition, setMousePosition] = React.useState(initialMousePosition); + const [mousePosition, setMousePosition] = useState(initialMousePosition); /* User interactions */ const onContextMenu = useCallback( @@ -231,7 +228,8 @@ const TreeViewsContainer = () => { /* RootDirectories management */ const updateRootDirectories = useCallback(() => { - fetchRootFolders() + directorySrv + .fetchRootFolders() .then((data) => { let [nrs, mdr] = updatedTree( treeDataRef.current.rootDirectories, @@ -254,7 +252,7 @@ const TreeViewsContainer = () => { /* rootDirectories initialization */ useEffect(() => { - if (user != null) { + if (user !== undefined) { updateRootDirectories(); } }, [user, updateRootDirectories]); @@ -357,7 +355,8 @@ const TreeViewsContainer = () => { const updateDirectoryTreeAndContent = useCallback( (nodeId) => { - fetchDirectoryContent(nodeId) + directorySrv + .fetchDirectoryContent(nodeId) .then((childrenToBeInserted) => { // update directory Content updateCurrentChildren(childrenToBeInserted); @@ -394,7 +393,8 @@ const TreeViewsContainer = () => { return; } - fetchDirectoryContent(nodeId) + directorySrv + .fetchDirectoryContent(nodeId) .then((childrenToBeInserted) => { // Update Tree Map data updateMapData(nodeId, childrenToBeInserted); @@ -423,7 +423,7 @@ const TreeViewsContainer = () => { useEffect(() => { // create ws at mount event - wsRef.current = connectNotificationsWsUpdateDirectories(); + wsRef.current = directoryNotificationSrv.connectNotificationsWsUpdateDirectories(); wsRef.current.onclose = function () { console.error('Unexpected Notification WebSocket closed'); diff --git a/src/components/utils/caseUtils.ts b/src/components/utils/caseUtils.ts index 50b9d93cb..8c3d624e4 100644 --- a/src/components/utils/caseUtils.ts +++ b/src/components/utils/caseUtils.ts @@ -5,19 +5,18 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { downloadCase, fetchConvertedCase, getCaseOriginalName } from '../../utils/rest-api'; import { useIntl } from 'react-intl'; import { ElementAttributes, ElementType, useSnackMessage } from '@gridsuite/commons-ui'; import { useCallback, useState } from 'react'; +import { caseSrv } from '../../services'; const downloadCases = async (selectedCases: ElementAttributes[]) => { for (const selectedCase of selectedCases) { - const result = await downloadCase(selectedCase.elementUuid); - let caseOriginalName = await getCaseOriginalName(selectedCase.elementUuid); + let caseOriginalName = await caseSrv.getCaseOriginalName(selectedCase.elementUuid); let caseFormat = typeof caseOriginalName === 'string' ? caseOriginalName.split('.').pop() : 'xiidm'; let caseName = selectedCase.elementName; const filename = `${caseName}.${caseFormat}`; - const blob = await result.blob(); + const blob = await caseSrv.downloadCase(selectedCase.elementUuid); const href = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = href; @@ -50,17 +49,17 @@ export function useDownloadUtils() { abortController: AbortController ): Promise => { try { - const result = await fetchConvertedCase( + const result = await caseSrv.fetchConvertedCase( caseElement.elementUuid, caseElement.elementName, format, formatParameters, abortController ); + // @ts-expect-error TODO: manage nullable field case let filename = result.headers.get('Content-Disposition').split('filename=')[1]; filename = filename.substring(1, filename.length - 1); // We remove quotes const blob = await result.blob(); - const href = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = href; diff --git a/src/components/utils/yup-config.js b/src/components/utils/yup-config.js deleted file mode 100644 index 4209b421b..000000000 --- a/src/components/utils/yup-config.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2023, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import * as yup from 'yup'; - -yup.setLocale({ - mixed: { - required: 'YupRequired', - notType: ({ type }) => { - if (type === 'number') { - return 'YupNotTypeNumber'; - } else { - return 'YupNotTypeDefault'; - } - }, - }, -}); - -export default yup; diff --git a/src/hooks/useDeferredFetch.ts b/src/hooks/useDeferredFetch.ts new file mode 100644 index 000000000..44b3d57c5 --- /dev/null +++ b/src/hooks/useDeferredFetch.ts @@ -0,0 +1,160 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { useCallback, useReducer } from 'react'; +import { FetchStatus, FetchStatusType } from '@gridsuite/commons-ui'; + +export enum ActionType { + START = 'START', + ERROR = 'ERROR', + SUCCESS = 'SUCCESS', + ADD_ERROR = 'ADD_ERROR', // Use by multipleDeferredFetch when one request respond with error + ADD_SUCCESS = 'ADD_SUCCESS', // Use by multipleDeferredFetch when one request respond with success +} + +type FetchAction = + | { + type: ActionType.START; + } + | { + type: ActionType.ERROR; + payload: unknown; + } + | { + type: ActionType.SUCCESS; + payload: unknown; + } + | { + type: ActionType.ADD_ERROR; + } + | { + type: ActionType.ADD_SUCCESS; + }; + +type FetchState = { + status: FetchStatusType; + data: unknown; + errorMessage: unknown; +}; + +const initialState: FetchState = { + status: FetchStatus.IDLE, + errorMessage: '', + data: null, +}; + +/** + * This custom hook manage a fetch workflow and return a unique callback to defer process execution when needed. + * It also returns a unique state which contains fetch status, results and error message if it failed. + * @param {function} fetchFunction the fetch function to call + * @param {Object} params Params of the fetch function. WARNING: Must respect order here + * @param {function} onSuccess callback to call on request success + * @param {function} errorToString callback to translate HTTPCode to string error messages + * @param {function} onError callback to call if request failed + * @param {boolean} hasResult Configure if fetchFunction return results or only HTTP request response + * @returns {function} fetchCallback The callback to call to execute the request. + * It accepts params as argument which must follow fetch function params. + * @returns {state} state complete state of the request + * {Enum} state.status Status of the request + * {String} state.errorMessage error message of the request + * {Object} state.data The JSON results of the request (see hasResult) + */ +export default function useDeferredFetch( + fetchFunction: (...args: TArgs) => Promise, + onSuccess: ((data: unknown | null, args: TArgs) => void) | undefined, + errorToString: ((status: unknown) => string) | undefined = undefined, + onError: ((errorMessage: unknown | null, paramsOnError: TArgs) => void) | undefined = undefined, + hasResult: boolean = true +): [(...args: TArgs) => void, FetchState] { + const [state, dispatch] = useReducer((lastState: FetchState, action: FetchAction) => { + switch (action.type) { + case ActionType.START: + return { ...initialState, status: FetchStatus.FETCHING }; + case ActionType.SUCCESS: + return { + ...initialState, + status: FetchStatus.FETCH_SUCCESS, + data: action.payload, + }; + case ActionType.ERROR: + return { + ...initialState, + status: FetchStatus.FETCH_ERROR, + errorMessage: action.payload, + }; + default: + return lastState; + } + }, initialState); + + const handleError = useCallback( + (error: any, paramsOnError: TArgs) => { + const defaultErrorMessage = error.message; + let errorMessage = defaultErrorMessage; + if (error && errorToString) { + const providedErrorMessage = errorToString(error.status); + if (providedErrorMessage && providedErrorMessage !== '') { + errorMessage = providedErrorMessage; + } + } + dispatch({ + type: ActionType.ERROR, + payload: errorMessage, + }); + if (onError) { + onError(errorMessage, paramsOnError); + } + }, + [errorToString, onError] + ); + + const fetchData = useCallback( + async (...args: TArgs) => { + dispatch({ type: ActionType.START }); + try { + // Params resolution + const response = await fetchFunction(...args); + + if (hasResult) { + dispatch({ + type: ActionType.SUCCESS, + payload: response, + }); + if (onSuccess) { + onSuccess(response, args); + } + } else { + dispatch({ + type: ActionType.SUCCESS, + payload: null, + }); + if (onSuccess) { + onSuccess(null, args); + } + } + } catch (error: any) { + if (!error.status) { + // an http error + handleError(null, args); + throw error; + } else { + handleError(error, args); + } + } + }, + [fetchFunction, onSuccess, handleError, hasResult] + ); + + const fetchCallback = useCallback( + (...args: TArgs) => { + fetchData(...args); + }, + [fetchData] + ); + + return [fetchCallback, state]; +} diff --git a/src/hooks/useDirectoryContent.ts b/src/hooks/useDirectoryContent.ts index 62190ff4b..a11dff426 100644 --- a/src/hooks/useDirectoryContent.ts +++ b/src/hooks/useDirectoryContent.ts @@ -6,12 +6,13 @@ */ import { useSelector } from 'react-redux'; -import React, { useRef, useEffect, useCallback, useState } from 'react'; -import { ElementAttributes, fetchElementsInfos, useSnackMessage } from '@gridsuite/commons-ui'; +import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; +import { ElementAttributes, useSnackMessage } from '@gridsuite/commons-ui'; import { UUID } from 'crypto'; import { AppState } from '../redux/reducer'; +import { exploreSrv } from '../services'; -export const useDirectoryContent = (setIsMissingDataAfterDirChange: React.Dispatch>) => { +export const useDirectoryContent = (setIsMissingDataAfterDirChange: Dispatch>) => { const currentChildren = useSelector((state: AppState) => state.currentChildren); const [childrenMetadata, setChildrenMetadata] = useState>({}); const { snackError } = useSnackMessage(); @@ -39,7 +40,8 @@ export const useDirectoryContent = (setIsMissingDataAfterDirChange: React.Dispat .filter((e) => !e.uploading) .map((e) => e.elementUuid); if (childrenToFetchElementsInfos.length > 0) { - fetchElementsInfos(childrenToFetchElementsInfos) + exploreSrv + .fetchElementsInfos(childrenToFetchElementsInfos) .then((res) => { res.forEach((e) => { metadata[e.elementUuid] = e; diff --git a/src/hooks/useMultipleDeferredFetch.ts b/src/hooks/useMultipleDeferredFetch.ts new file mode 100644 index 000000000..ee19d06f0 --- /dev/null +++ b/src/hooks/useMultipleDeferredFetch.ts @@ -0,0 +1,184 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { useCallback, useEffect, useReducer, useState } from 'react'; +import { UnknownArray } from 'type-fest'; +import { FetchStatus, FetchStatusType } from '@gridsuite/commons-ui'; +import useDeferredFetch, { ActionType } from './useDeferredFetch'; + +type MultipleFetchAction = + | { + type: ActionType.START; + } + | { + type: ActionType.ERROR; + } + | { + type: ActionType.SUCCESS; + } + | { + type: ActionType.ADD_ERROR; + payload: unknown; + context: TArgs; + } + | { + type: ActionType.ADD_SUCCESS; + payload: unknown; + context: TArgs; + }; + +type MultipleFetchState = { + public: { + status: FetchStatusType; + data: UnknownArray; + errorMessage: UnknownArray; + paramsOnError: TArgs[]; + paramsOnSuccess: TArgs[]; + }; + counter: number; +}; + +const initialState: MultipleFetchState = { + public: { + status: FetchStatus.IDLE, + errorMessage: [], + paramsOnError: [], + data: [], + paramsOnSuccess: [], + }, + counter: 0, +}; + +/** + * This custom hook manage multiple fetchs workflows and return a unique callback to defer process execution when needed. + * It also return a unique state which concatenate all fetch results independently. + * @param {function} fetchFunction the fetch function to call for each request + * @param {function} onSuccess callback to call on all request success + * @param {function} errorToString callback to translate HTTPCode to string error messages + * @param {function} onError callback to call if one or more requests failed + * @param {boolean} hasResult Configure if fetchFunction return results or only HTTP request response + * @returns {function} fetchCallback The callback to call to execute the requests collection. + * It accepts params array as arguments which define the number of fetch to execute. + * @returns {state} state complete states of the requests collection + * {Enum} state.status Status of the requests set + * {Array} state.errorMessage error message of the requests set + * {Array} state.paramsOnError The parameters used when requests set have failed + * {Array} state.data The results array of each request (see hasResult) + */ +export default function useMultipleDeferredFetch( + fetchFunction: (...args: TArgs) => Promise, + onSuccess: ((data: UnknownArray) => void) | undefined, + errorToString: ((status: unknown) => string) | undefined = undefined, + onError: ((errorMessage: UnknownArray, paramList: TArgs, paramsOnError: TArgs[]) => void) | undefined = undefined, + hasResult = true +) { + const [state, dispatch] = useReducer((lastState: MultipleFetchState, action: MultipleFetchAction) => { + switch (action.type) { + case ActionType.START: + return { + ...initialState, + public: { + ...initialState.public, + status: FetchStatus.FETCHING, + }, + }; + case ActionType.ADD_SUCCESS: + return { + public: { + ...lastState.public, + data: lastState.public.data.concat([action.payload]), + paramsOnSuccess: lastState.public.paramsOnSuccess.concat([action.context]), + }, + counter: lastState.counter + 1, + }; + case ActionType.ADD_ERROR: + return { + public: { + ...lastState.public, + errorMessage: lastState.public.errorMessage.concat([action.payload]), + paramsOnError: lastState.public.paramsOnError.concat([action.context]), + }, + counter: lastState.counter + 1, + }; + case ActionType.SUCCESS: + return { + ...lastState, + public: { + ...lastState.public, + status: FetchStatus.FETCH_SUCCESS, + }, + counter: 0, + }; + case ActionType.ERROR: + return { + ...lastState, + public: { + ...lastState.public, + status: FetchStatus.FETCH_ERROR, + }, + counter: 0, + }; + default: + return lastState; + } + }, initialState); + + const [paramList, setParamList] = useState([] as unknown as TArgs); + + const onInstanceSuccess = useCallback((data: unknown, paramsOnSuccess: TArgs) => { + dispatch({ + type: ActionType.ADD_SUCCESS, + payload: data, + context: paramsOnSuccess, + }); + }, []); + + const onInstanceError = useCallback((errorMessage: unknown, paramsOnError: TArgs) => { + // counter now stored in reducer to avoid counter and state being updated not simultaneously, + // causing useEffect to be triggered once for each change, which would cause an expected behaviour + dispatch({ + type: ActionType.ADD_ERROR, + payload: errorMessage, + context: paramsOnError, + }); + }, []); + + const [fetchCB] = useDeferredFetch(fetchFunction, onInstanceSuccess, errorToString, onInstanceError, hasResult); + + const fetchCallback = useCallback( + (cbParamsList: TArgs) => { + dispatch({ type: ActionType.START }); + setParamList(cbParamsList); + for (let params of cbParamsList) { + fetchCB(...params); + } + }, + [fetchCB] + ); + + useEffect(() => { + if (paramList.length !== 0 && paramList.length === state.counter) { + if (state.public.errorMessage.length > 0) { + dispatch({ + type: ActionType.ERROR, + }); + if (onError) { + onError(state.public.errorMessage, paramList, state.public.paramsOnError); + } + } else { + dispatch({ + type: ActionType.SUCCESS, + }); + if (onSuccess) { + onSuccess(state.public.data); + } + } + } + }, [paramList, onError, onSuccess, state]); + + return [fetchCallback]; +} diff --git a/src/redux/actions.ts b/src/redux/actions.ts index 8fa889d74..ff76697b8 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -6,7 +6,7 @@ */ import { Action } from 'redux'; -import { PARAM_LANGUAGE } from '../utils/config-params'; +import { PARAM_LANGUAGE } from '@gridsuite/commons-ui'; import { AppState } from './reducer'; export type AppActions = diff --git a/src/redux/local-storage.ts b/src/redux/local-storage.ts deleted file mode 100644 index 60fd2df08..000000000 --- a/src/redux/local-storage.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2021, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { DARK_THEME, GsLang, GsTheme, LANG_SYSTEM } from '@gridsuite/commons-ui'; -import { getComputedLanguage } from '../utils/language'; -import { APP_NAME } from '../utils/config-params'; - -const LOCAL_STORAGE_THEME_KEY = (APP_NAME + '_THEME').toUpperCase(); -const LOCAL_STORAGE_LANGUAGE_KEY = (APP_NAME + '_LANGUAGE').toUpperCase(); - -export function getLocalStorageTheme() { - return (localStorage.getItem(LOCAL_STORAGE_THEME_KEY) as GsTheme) || DARK_THEME; -} - -export function saveLocalStorageTheme(theme: GsTheme) { - localStorage.setItem(LOCAL_STORAGE_THEME_KEY, theme); -} - -export function getLocalStorageLanguage() { - return (localStorage.getItem(LOCAL_STORAGE_LANGUAGE_KEY) as GsLang) || LANG_SYSTEM; -} - -export function saveLocalStorageLanguage(language: GsLang) { - localStorage.setItem(LOCAL_STORAGE_LANGUAGE_KEY, language); -} - -export function getLocalStorageComputedLanguage() { - return getComputedLanguage(getLocalStorageLanguage()); -} diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index f89f3a69d..d09294caf 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -6,15 +6,6 @@ */ import { createReducer } from '@reduxjs/toolkit'; - -import { - getLocalStorageComputedLanguage, - getLocalStorageLanguage, - getLocalStorageTheme, - saveLocalStorageLanguage, - saveLocalStorageTheme, -} from './local-storage'; - import { ACTIVE_DIRECTORY, ActiveDirectoryAction, @@ -46,21 +37,26 @@ import { TREE_DATA, TreeDataAction, } from './actions'; - import { + AppMetadata, AuthenticationActions, AuthenticationRouterErrorAction, AuthenticationRouterErrorState, - CommonMetadata, - CommonStoreState, ElementAttributes, ElementType, + getLocalStorageComputedLanguage, + getLocalStorageLanguage, + getLocalStorageTheme, GsLang, GsLangUser, GsTheme, LOGOUT_ERROR, LogoutErrorAction, + PARAM_LANGUAGE, + PARAM_THEME, RESET_AUTHENTICATION_ROUTER_ERROR, + saveLocalStorageLanguage, + saveLocalStorageTheme, SHOW_AUTH_INFO_LOGIN, ShowAuthenticationRouterLoginAction, SIGNIN_CALLBACK_ERROR, @@ -72,8 +68,9 @@ import { UserAction, UserValidationErrorAction, } from '@gridsuite/commons-ui'; -import { PARAM_LANGUAGE, PARAM_THEME } from '../utils/config-params'; +import { APP_NAME } from '../utils/config-params'; import { UUID } from 'crypto'; +import { User } from 'oidc-client'; // IDirectory is exactly an IElement, with a specific type value export type IDirectory = ElementAttributes & { @@ -109,7 +106,8 @@ export type UploadingElement = { caseFormat?: string; }; -export interface AppState extends CommonStoreState { +export interface AppState { + user: User | undefined; [PARAM_THEME]: GsTheme; [PARAM_LANGUAGE]: GsLang; computedLanguage: GsLangUser; @@ -119,7 +117,7 @@ export interface AppState extends CommonStoreState { authenticationRouterError: AuthenticationRouterErrorState | null; showAuthenticationRouterLogin: boolean; - appsAndUrls: CommonMetadata[]; + appsAndUrls: AppMetadata[]; activeDirectory?: UUID; currentChildren?: ElementAttributes[]; selectedDirectory: ElementAttributes | null; @@ -141,15 +139,15 @@ export interface AppState extends CommonStoreState { const initialState: AppState = { // authentication - user: null, + user: undefined, signInCallbackError: null, authenticationRouterError: null, showAuthenticationRouterLogin: false, // params - computedLanguage: getLocalStorageComputedLanguage(), - [PARAM_THEME]: getLocalStorageTheme(), - [PARAM_LANGUAGE]: getLocalStorageLanguage(), + computedLanguage: getLocalStorageComputedLanguage(APP_NAME), + [PARAM_THEME]: getLocalStorageTheme(APP_NAME), + [PARAM_LANGUAGE]: getLocalStorageLanguage(APP_NAME), currentChildren: undefined, selectedDirectory: null, @@ -184,12 +182,12 @@ function filterFromObject( export const reducer = createReducer(initialState, (builder) => { builder.addCase(SELECT_THEME, (state, action: ThemeAction) => { state.theme = action.theme; - saveLocalStorageTheme(state.theme); + saveLocalStorageTheme(APP_NAME, state.theme); }); builder.addCase(SELECT_LANGUAGE, (state, action: LanguageAction) => { state.language = action.language; - saveLocalStorageLanguage(state.language); + saveLocalStorageLanguage(APP_NAME, state.language); }); builder.addCase(USER, (state, action: UserAction) => { diff --git a/src/redux/store.ts b/src/redux/store.ts index 2be460a5b..178cf1e1f 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -6,10 +6,8 @@ */ import { legacy_createStore as createStore, Store } from 'redux'; import { Actions, AppState, reducer } from './reducer'; -import { setCommonStore } from '@gridsuite/commons-ui'; export const store: Store = createStore(reducer); -setCommonStore(store); export type AppDispatch = typeof store.dispatch; // to avoid to reset the state with HMR @@ -17,3 +15,7 @@ export type AppDispatch = typeof store.dispatch; if (import.meta.env.DEV && import.meta.hot) { import.meta.hot.accept('./reducer', () => store.replaceReducer(reducer)); } + +export function getUser() { + return store.getState().user; +} diff --git a/src/services/actions.ts b/src/services/actions.ts new file mode 100644 index 000000000..9c025cf0d --- /dev/null +++ b/src/services/actions.ts @@ -0,0 +1,25 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { ApiService } from '@gridsuite/commons-ui'; +import { getUser } from '../redux/store'; +import { getContingencyUriParamType } from './utils'; +import { ContingencyListTypeIds } from '../utils/elementType'; + +export default class ActionsSvc extends ApiService { + public constructor() { + super(getUser, 'actions'); + } + + /** + * Get contingency list by type and id + * @returns {Promise} + */ + public async getContingencyList(type: ContingencyListTypeIds, id: string) { + return this.backendFetchJson(`${this.getPrefix(1)}/${getContingencyUriParamType(type)}/${id}`); + } +} diff --git a/src/services/app-local.ts b/src/services/app-local.ts new file mode 100644 index 000000000..d983687ae --- /dev/null +++ b/src/services/app-local.ts @@ -0,0 +1,20 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { AppLocalComSvc, Env } from '@gridsuite/commons-ui'; + +export type EnvJson = Env & typeof import('../../public/env.json'); + +export default class AppLocalSvc extends AppLocalComSvc { + public constructor() { + super(); + } + + public async fetchEnv() { + return (await super.fetchEnv()) as EnvJson; + } +} diff --git a/src/services/case.ts b/src/services/case.ts new file mode 100644 index 000000000..03fb774cd --- /dev/null +++ b/src/services/case.ts @@ -0,0 +1,65 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { UUID } from 'crypto'; +import { ApiService } from '@gridsuite/commons-ui'; +import { getUser } from '../redux/store'; + +export default class CaseSvc extends ApiService { + public constructor() { + super(getUser, 'case'); + } + + public async createCaseWithoutDirectoryElementCreation(selectedFile: File) { + const formData = new FormData(); + formData.append('file', selectedFile); + formData.append('withExpiration', encodeURIComponent(true)); + return this.backendSendFetchJson(`${this.getPrefix(1)}/cases`, 'POST', formData); + } + + public async deleteCase(caseUuid: UUID) { + await this.backendFetch(`${this.getPrefix(1)}/cases/${caseUuid}`, 'DELETE'); + } + + public async fetchConvertedCase( + caseUuid: UUID, + fileName: string, + format: string, + formatParameters: Record, + abortController: AbortController + ) { + return this.backendSendFetch( + `${this.getPrefix(1)}/cases/${caseUuid}?format=${format}&fileName=${fileName}`, + { + method: 'POST', + signal: abortController.signal, + }, + JSON.stringify(formatParameters) + ); + } + + public async downloadCase(caseUuid: UUID) { + return this.backendFetchFile(`${this.getPrefix(1)}/cases/${caseUuid}`, 'GET'); + } + + /** + * Retrieves the original name of a case using its UUID. + * @param {string} caseUuid - The UUID of the element. + * @returns {Promise} - A promise that resolves to the original name of the case if found, or false if not found. + */ + public async getCaseOriginalName(caseUuid: UUID) { + try { + return await this.backendFetchText(`/${this.getPrefix(1)}/cases/${caseUuid}/name`); + } catch (error: any) { + if (error.status === 404) { + return false; + } else { + throw error; + } + } + } +} diff --git a/src/services/directory-notification.ts b/src/services/directory-notification.ts new file mode 100644 index 000000000..aad334226 --- /dev/null +++ b/src/services/directory-notification.ts @@ -0,0 +1,31 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import ReconnectingWebSocket from 'reconnecting-websocket'; +import { WsService } from '@gridsuite/commons-ui'; +import { getUser } from '../redux/store'; + +export default class DirectoryNotificationSvc extends WsService { + public constructor() { + super(getUser, 'directory-notification'); + } + + /** + * Function will be called to connect with notification websocket to update directories list + * @returns {ReconnectingWebSocket} + */ + public connectNotificationsWsUpdateDirectories() { + const webSocketUrl = `${this.queryPrefix}/notify?updateType=directories`; + const reconnectingWebSocket = new ReconnectingWebSocket(() => this.getUrlWithToken(webSocketUrl), undefined, { + debug: `${import.meta.env.VITE_DEBUG_REQUESTS}` === 'true', + }); + reconnectingWebSocket.onopen = function () { + console.debug('Connected Websocket update studies:', webSocketUrl); + }; + return reconnectingWebSocket; + } +} diff --git a/src/services/directory.ts b/src/services/directory.ts new file mode 100644 index 000000000..b79cf9f05 --- /dev/null +++ b/src/services/directory.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { UUID } from 'crypto'; +import { DirectoryComSvc, Paginated } from '@gridsuite/commons-ui'; +import { getUser } from '../redux/store'; +import { ElementAttributesES } from '../redux/reducer'; + +export default class DirectorySvc extends DirectoryComSvc { + public constructor() { + super(getUser); + } + + public async insertDirectory(directoryName: string, parentUuid: UUID, owner: unknown) { + console.debug("Inserting a new folder '%s'", directoryName); + return this.backendSendFetchJson( + `${this.getPrefix(1)}/directories/${parentUuid}/elements`, + 'POST', + JSON.stringify({ + elementUuid: null, + elementName: directoryName, + type: 'DIRECTORY', + owner: owner, + }) + ); + } + + public async insertRootDirectory(directoryName: string, owner: unknown) { + console.debug("Inserting a new root folder '%s'", directoryName); + return this.backendSendFetchJson( + `${this.getPrefix(1)}/root-directories`, + 'POST', + JSON.stringify({ + elementName: directoryName, + owner: owner, + }) + ); + } + + public async getNameCandidate(directoryUuid: UUID, elementName: string, type: unknown) { + return this.backendFetchText( + `${this.getPrefix(1)}/directories/${directoryUuid}/${elementName}/newNameCandidate?type=${type}` + ); + } + + public async rootDirectoryExists(directoryName: string) { + const response = await this.backendFetch( + `${this.getPrefix(1)}/root-directories?${new URLSearchParams({ directoryName: directoryName })}`, + 'HEAD' + ); + return response.status !== 204; // HTTP 204 : No-content + } + + public async searchElementsInfos(searchTerm: string, currentDirectoryUuid: UUID) { + console.debug("Fetching elements infos matching with '%s' term ... ", searchTerm); + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('userInput', searchTerm); + urlSearchParams.append('directoryUuid', currentDirectoryUuid); + return this.backendFetchJson>( + `${this.getPrefix(1)}/elements/indexation-infos?${urlSearchParams}` + ); + } +} diff --git a/src/services/explore.ts b/src/services/explore.ts new file mode 100644 index 000000000..ea63ab805 --- /dev/null +++ b/src/services/explore.ts @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { ElementType, ExploreComSvc, getRequestParam } from '@gridsuite/commons-ui'; +import { UUID } from 'crypto'; +import { getUser } from '../redux/store'; +import { ContingencyListType } from '../utils/elementType'; +import { EquipmentType } from '../utils/equipment-types-for-predefined-properties-mapper'; +import { FORM_CONTINGENCY_LISTS, getContingencyUriParamType } from './utils'; + +//TODO: temporary type, check if already defined elsewhere +type saveCriteriaBasedContingencyListForm = { + name: string; + equipmentType: unknown; + criteriaBased: any; +}; + +export default class ExploreSvc extends ExploreComSvc { + public constructor() { + super(getUser); + } + + public async deleteElement(elementUuid: UUID) { + console.debug("Deleting element %s'", elementUuid); + return this.backendFetchJson(`${this.getPrefix(1)}/explore/elements/${elementUuid}`, 'DELETE'); + } + + public async deleteElements(elementUuids: UUID[], activeDirectory: unknown) { + console.debug('Deleting elements : %s', elementUuids); + await this.backendFetch( + `${this.getPrefix(1)}/explore/elements/${activeDirectory}?${getRequestParam('ids', elementUuids)}`, + 'DELETE' + ); + } + + public async moveElementsToDirectory(elementsUuids: UUID[], targetDirectoryUuid: UUID) { + console.debug('Moving elements to directory %s', targetDirectoryUuid); + return this.backendSendFetchJson( + `${this.getPrefix(1)}/explore/elements?targetDirectoryUuid=${targetDirectoryUuid}`, + 'PUT', + JSON.stringify(elementsUuids) + ); + } + + public async renameElement(elementUuid: UUID, newElementName: string) { + console.debug('Renaming element ' + elementUuid); + return this.backendSendFetchJson( + `${this.getPrefix(1)}/explore/elements/${elementUuid}`, + 'PUT', + JSON.stringify({ + elementName: newElementName, + }) + ); + } + + public async createStudy( + studyName: string, + studyDescription: string, + caseUuid: UUID, + duplicateCase: string, + parentDirectoryUuid: UUID, + importParameters: BodyInit, + caseFormat: string + ) { + console.debug('Creating a new study...'); + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('duplicateCase', duplicateCase); + urlSearchParams.append('description', studyDescription); + urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); + urlSearchParams.append('caseFormat', caseFormat); + await this.backendSend( + `${this.getPrefix(1)}/explore/studies/${encodeURIComponent(studyName)}/cases/${encodeURIComponent( + caseUuid + )}?${urlSearchParams}`, + 'POST', + importParameters + ); + } + + public async createCase(name: string, description: string, file: File, parentDirectoryUuid: UUID) { + console.debug('Creating a new case...'); + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('description', description); + urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); + const formData = new FormData(); + formData.append('caseFile', file); + await this.backendSend( + `${this.getPrefix(1)}/explore/cases/${encodeURIComponent(name)}?${urlSearchParams}`, + 'POST', + formData + ); + } + + public async duplicateElement( + sourceCaseUuid: UUID, + parentDirectoryUuid: UUID, + type: ElementType, + specificType?: string + ) { + console.debug('Duplicating an element of type ' + type + ' ...'); + const queryParams = new URLSearchParams(); + queryParams.append('duplicateFrom', sourceCaseUuid); + if (parentDirectoryUuid) { + queryParams.append('parentDirectoryUuid', parentDirectoryUuid); + } + if (specificType) { + queryParams.append('type', specificType); + } + await this.backendFetch( + `${this.getPrefix(1)}/explore${ExploreSvc.getDuplicateEndpoint(type)}?${queryParams}`, + 'POST' + ); + } + + private static getDuplicateEndpoint(type: EquipmentType) { + switch (type) { + case ElementType.CASE: + return '/cases'; + case ElementType.STUDY: + return '/studies'; + case ElementType.FILTER: + return '/filters'; + case ElementType.CONTINGENCY_LIST: + return '/contingency-lists'; + //TODO: not existing type, wasn't check because was in js file... + // case ElementType.PARAMETERS: return '/parameters'; + case ElementType.MODIFICATION: + return '/modifications'; + default: + break; + } + } + + public async createContingencyList( + contingencyListType: unknown, + contingencyListName: string, + description: string, + formContent: unknown, + parentDirectoryUuid: UUID + ) { + console.debug('Creating a new contingency list...'); + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('description', description); + urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); + const typeUriParam = getContingencyUriParamType(contingencyListType); + await this.backendSend( + `${this.getPrefix(1)}/explore${typeUriParam}/${encodeURIComponent(contingencyListName)}?${urlSearchParams}`, + 'POST', + JSON.stringify(formContent) + ); + } + + /** + * Saves a Filter contingency list + * @returns {Promise} + */ + public async saveCriteriaBasedContingencyList(id: string, form: saveCriteriaBasedContingencyListForm) { + const { name, equipmentType, criteriaBased } = form; + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('name', name); + urlSearchParams.append('contingencyListType', ContingencyListType.CRITERIA_BASED.id); + await this.backendSend( + `${this.getPrefix(1)}/explore/contingency-lists/${id}?${urlSearchParams}`, + 'PUT', + JSON.stringify({ + ...criteriaBased, + equipmentType, + nominalVoltage1: criteriaBased.nominalVoltage1 === '' ? -1 : criteriaBased.nominalVoltage1, + }) + ); + } + + /** + * Saves a script contingency list + * @returns {Promise} + */ + public async saveScriptContingencyList(scriptContingencyList: any, name: string) { + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('name', name); + urlSearchParams.append('contingencyListType', ContingencyListType.SCRIPT.id); + await this.backendSend( + `${this.getPrefix(1)}/explore/contingency-lists/${scriptContingencyList.id}?${urlSearchParams}`, + 'PUT', + JSON.stringify(scriptContingencyList) + ); + } + + /** + * Saves an explicit naming contingency list + * @returns {Promise} + */ + public async saveExplicitNamingContingencyList(explicitNamingContingencyList: any, name: string) { + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('name', name); + urlSearchParams.append('contingencyListType', ContingencyListType.EXPLICIT_NAMING.id); + await this.backendSend( + `${this.getPrefix(1)}/explore/contingency-lists/${explicitNamingContingencyList.id}?${urlSearchParams}`, + 'PUT', + JSON.stringify(explicitNamingContingencyList) + ); + } + + /** + * Replace form contingency list with script contingency list + * @returns {Promise} + */ + public async replaceFormContingencyListWithScript(id: string, parentDirectoryUuid: UUID) { + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); + await this.backendFetch( + `${this.getPrefix(1)}/explore${FORM_CONTINGENCY_LISTS}/${encodeURIComponent( + id + )}/replace-with-script?${urlSearchParams}`, + 'POST' + ); + } + + /** + * Save new script contingency list from form contingency list + * @returns {Promise} + */ + public async newScriptFromFiltersContingencyList(id: string, newName: string, parentDirectoryUuid: UUID) { + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); + await this.backendFetch( + `${this.getPrefix(1)}/explore${FORM_CONTINGENCY_LISTS}/${encodeURIComponent( + id + )}/new-script/${encodeURIComponent(newName)}?${urlSearchParams}`, + 'POST' + ); + } + + /** + * Replace filter with script filter + * @returns {Promise} + */ + public async replaceFiltersWithScript(id: string, parentDirectoryUuid: UUID) { + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); + await this.backendFetch( + `${this.getPrefix(1)}/explore/filters/${encodeURIComponent(id)}/replace-with-script?${urlSearchParams}`, + 'POST' + ); + } + + /** + * Save new script from filters + * @returns {Promise} + */ + public async newScriptFromFilter(id: string, newName: string, parentDirectoryUuid: UUID) { + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); + await this.backendFetch( + `${this.getPrefix(1)}/explore/filters/${encodeURIComponent(id)}/new-script/${encodeURIComponent( + newName + )}?${urlSearchParams}`, + 'POST' + ); + } +} diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 000000000..90703be41 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,54 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { getUser } from '../redux/store'; +import { + AppsMetadataComSvc, + ConfigComSvc, + ConfigNotificationComSvc, + FilterComSvc, + setCommonServices, + StudyComSvc, + UserAdminComSvc, +} from '@gridsuite/commons-ui'; +import AppLocalSvc from './app-local'; +import DirectorySvc from './directory'; +import { APP_NAME } from '../utils/config-params'; +import CaseSvc from './case'; +import DirectoryNotificationSvc from './directory-notification'; +import ActionsSvc from './actions'; +import NetworkConversionSvc from './network-conversion'; +import ExploreSvc from './explore'; + +export type { EnvJson } from './app-local'; +export type { ExportFormats } from './network-conversion'; + +export const actionsSrv = new ActionsSvc(), + appLocalSrv = new AppLocalSvc(), + appsMetadataSrv = new AppsMetadataComSvc(appLocalSrv), + caseSrv = new CaseSvc(), + configSrv = new ConfigComSvc(APP_NAME, getUser), + configNotificationSrv = new ConfigNotificationComSvc(getUser), + directorySrv = new DirectorySvc(), + directoryNotificationSrv = new DirectoryNotificationSvc(), + exploreSrv = new ExploreSvc(), + filterSrv = new FilterComSvc(getUser), + networkConversionSrv = new NetworkConversionSvc(), + studySrv = new StudyComSvc(getUser), + userAdminSrv = new UserAdminComSvc(getUser); + +setCommonServices( + appLocalSrv, + appsMetadataSrv, + configSrv, + configNotificationSrv, + directorySrv, + exploreSrv, + filterSrv, + studySrv, + userAdminSrv +); diff --git a/src/services/network-conversion.ts b/src/services/network-conversion.ts new file mode 100644 index 000000000..468dd5815 --- /dev/null +++ b/src/services/network-conversion.ts @@ -0,0 +1,44 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { ApiService, Parameter } from '@gridsuite/commons-ui'; +import { UUID } from 'crypto'; +import { getUser } from '../redux/store'; + +export type CaseImportParameters = { + formatName: string; + parameters: Parameter[]; +}; +export type ExportFormats = + | Record< + string, + { + formatName: string; + parameters: { + name: string; + type: string; + defaultValue: any; + possibleValues: any; + }[]; + } + > + | []; + +export default class NetworkConversionSvc extends ApiService { + public constructor() { + super(getUser, 'network-conversion'); + } + + public async getCaseImportParameters(caseUuid: UUID) { + console.debug(`get import parameters for case '${caseUuid}' ...`); + return this.backendFetchJson(`${this.getPrefix(1)}/cases/${caseUuid}/import-parameters`); + } + + public async getExportFormats() { + return this.backendFetchJson(`${this.getPrefix(1)}/export/formats`); + } +} diff --git a/src/services/utils.ts b/src/services/utils.ts new file mode 100644 index 000000000..4c4c11152 --- /dev/null +++ b/src/services/utils.ts @@ -0,0 +1,25 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { ContingencyListType, ContingencyListTypeIds } from '../utils/elementType'; + +export const SCRIPT_CONTINGENCY_LISTS = '/script-contingency-lists'; +export const FORM_CONTINGENCY_LISTS = '/form-contingency-lists'; +export const IDENTIFIER_CONTINGENCY_LISTS = '/identifier-contingency-lists'; + +export function getContingencyUriParamType(contingencyListType: ContingencyListTypeIds | unknown) { + switch (contingencyListType) { + case ContingencyListType.SCRIPT.id: + return SCRIPT_CONTINGENCY_LISTS; + case ContingencyListType.CRITERIA_BASED.id: + return FORM_CONTINGENCY_LISTS; + case ContingencyListType.EXPLICIT_NAMING.id: + return IDENTIFIER_CONTINGENCY_LISTS; + default: + return null; + } +} diff --git a/src/utils/UIconstants.js b/src/utils/UIconstants.js deleted file mode 100644 index a1d4efdfa..000000000 --- a/src/utils/UIconstants.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2021, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -export const HORIZONTAL_SHIFT = 16; -export const VERTICAL_SHIFT = -4; -export const MOUSE_EVENT_RIGHT_BUTTON = 2; - -export const DialogsId = { - RENAME: 'rename', - DELETE: 'delete', - MOVE: 'move', - EXPORT: 'export', - ADD_NEW_STUDY_FROM_CASE: 'create_study_from_case', - ADD_NEW_STUDY: 'create_study', - SCRIPT: 'script', - REPLACE_FILTER_BY_SCRIPT_CONTINGENCY: 'replace_filter_by_script_contingency', - COPY_FILTER_TO_SCRIPT_CONTINGENCY: 'copy_filter_to_script_contingency', - REPLACE_FILTER_BY_SCRIPT: 'replace_filter_by_script', - COPY_FILTER_TO_SCRIPT: 'copy_filter_to_script', - CONVERT_TO_EXPLICIT_NAMING_FILTER: 'convert_to_explicit_naming_filter', - GENERIC_FILTER: 'generic_filter', - ADD_ROOT_DIRECTORY: 'add_root_directory', - ADD_DIRECTORY: 'add_directory', - ADD_NEW_CONTINGENCY_LIST: 'add_new_contingency_list', - ADD_NEW_FILTER: 'add_new_filter', - ADD_NEW_CASE: 'add_new_case', - RENAME_DIRECTORY: 'rename_directory', - DELETE_DIRECTORY: 'delete_directory', - NONE: 'none', -}; -export const HTTP_UNPROCESSABLE_ENTITY_STATUS = 422; -export const HTTP_CONNECTION_FAILED_MESSAGE = 'failed: Connection refused'; -export const HTTP_MAX_ELEMENTS_EXCEEDED_MESSAGE = 'MAX_ELEMENTS_EXCEEDED'; diff --git a/src/utils/UIconstants.ts b/src/utils/UIconstants.ts new file mode 100644 index 000000000..40f83f6fd --- /dev/null +++ b/src/utils/UIconstants.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +export const HORIZONTAL_SHIFT = 16; +export const VERTICAL_SHIFT = -4; +export const MOUSE_EVENT_RIGHT_BUTTON = 2; + +export enum DialogsId { + RENAME = 'rename', + DELETE = 'delete', + MOVE = 'move', + EXPORT = 'export', + ADD_NEW_STUDY_FROM_CASE = 'create_study_from_case', + ADD_NEW_STUDY = 'create_study', + SCRIPT = 'script', + REPLACE_FILTER_BY_SCRIPT_CONTINGENCY = 'replace_filter_by_script_contingency', + COPY_FILTER_TO_SCRIPT_CONTINGENCY = 'copy_filter_to_script_contingency', + REPLACE_FILTER_BY_SCRIPT = 'replace_filter_by_script', + COPY_FILTER_TO_SCRIPT = 'copy_filter_to_script', + CONVERT_TO_EXPLICIT_NAMING_FILTER = 'convert_to_explicit_naming_filter', + GENERIC_FILTER = 'generic_filter', + ADD_ROOT_DIRECTORY = 'add_root_directory', + ADD_DIRECTORY = 'add_directory', + ADD_NEW_CONTINGENCY_LIST = 'add_new_contingency_list', + ADD_NEW_FILTER = 'add_new_filter', + ADD_NEW_CASE = 'add_new_case', + RENAME_DIRECTORY = 'rename_directory', + DELETE_DIRECTORY = 'delete_directory', + NONE = 'none', +} + +export const HTTP_UNPROCESSABLE_ENTITY_STATUS = 422; +export const HTTP_CONNECTION_FAILED_MESSAGE = 'failed: Connection refused'; +export const HTTP_MAX_ELEMENTS_EXCEEDED_MESSAGE = 'MAX_ELEMENTS_EXCEEDED'; diff --git a/src/utils/config-params.js b/src/utils/config-params.js deleted file mode 100644 index 57f1befd6..000000000 --- a/src/utils/config-params.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2021, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -export const COMMON_APP_NAME = 'common'; -export const APP_NAME = 'Explore'; - -export const PARAM_THEME = 'theme'; -export const PARAM_LANGUAGE = 'language'; - -const COMMON_CONFIG_PARAMS_NAMES = new Set([PARAM_THEME, PARAM_LANGUAGE]); - -export function getAppName(paramName) { - return COMMON_CONFIG_PARAMS_NAMES.has(paramName) ? COMMON_APP_NAME : APP_NAME; -} diff --git a/src/utils/notificationType.js b/src/utils/config-params.ts similarity index 62% rename from src/utils/notificationType.js rename to src/utils/config-params.ts index 243e770e3..1244d6c05 100644 --- a/src/utils/notificationType.js +++ b/src/utils/config-params.ts @@ -5,8 +5,4 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -export const notificationType = { - DELETE_DIRECTORY: 'DELETE_DIRECTORY', - ADD_DIRECTORY: 'ADD_DIRECTORY', - UPDATE_DIRECTORY: 'UPDATE_DIRECTORY', -}; +export const APP_NAME = 'Explore'; diff --git a/src/utils/constants-endpoints.ts b/src/utils/constants-endpoints.ts deleted file mode 100644 index a2439dd1f..000000000 --- a/src/utils/constants-endpoints.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) 2024, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -export const CONTINGENCY_ENDPOINTS = { - SCRIPT_CONTINGENCY_LISTS: '/script-contingency-lists', - FORM_CONTINGENCY_LISTS: '/form-contingency-lists', - IDENTIFIER_CONTINGENCY_LISTS: '/identifier-contingency-lists', -}; diff --git a/src/utils/constants.js b/src/utils/constants.ts similarity index 96% rename from src/utils/constants.js rename to src/utils/constants.ts index 7d1cd27e7..f477f9b36 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.ts @@ -4,9 +4,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + export const noSelectionForCopy = { sourceCaseUuid: null, name: null, description: null, parentDirectoryUuid: null, -}; +} as const; diff --git a/src/utils/custom-hooks.js b/src/utils/custom-hooks.js deleted file mode 100644 index edeff35ea..000000000 --- a/src/utils/custom-hooks.js +++ /dev/null @@ -1,284 +0,0 @@ -/** - * Copyright (c) 2022, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { useEffect, useCallback, useReducer, useState } from 'react'; - -export const FetchStatus = { - IDLE: 'IDLE', - FETCHING: 'FETCHING', - FETCH_SUCCESS: 'FETCH_SUCCESS', - FETCH_ERROR: 'FETCH_ERROR', -}; - -export const ActionType = { - START: 'START', - ERROR: 'ERROR', - SUCCESS: 'SUCCESS', - ADD_ERROR: 'ADD_ERROR', // Use by multipleDeferredFetch when one request respond with error - ADD_SUCCESS: 'ADD_SUCCESS', // Use by multipleDeferredFetch when one request respond with success -}; - -/** - * This custom hook manage a fetch workflow and return a unique callback to defer process execution when needed. - * It also returns a unique state which contains fetch status, results and error message if it failed. - * @param {function} fetchFunction the fetch function to call - * @param {Object} params Params of the fetch function. WARNING: Must respect order here - * @param {function} onSuccess callback to call on request success - * @param {function} errorToString callback to translate HTTPCode to string error messages - * @param {function} onError callback to call if request failed - * @param {boolean} hasResult Configure if fetchFunction return results or only HTTP request response - * @returns {function} fetchCallback The callback to call to execute the request. - * It accepts params as argument which must follow fetch function params. - * @returns {state} state complete state of the request - * {Enum} state.status Status of the request - * {String} state.errorMessage error message of the request - * {Object} state.data The JSON results of the request (see hasResult) - */ -export const useDeferredFetch = ( - fetchFunction, - onSuccess, - errorToString = undefined, - onError = undefined, - hasResult = true -) => { - const initialState = { - status: FetchStatus.IDLE, - errorMessage: '', - data: null, - }; - - const [state, dispatch] = useReducer((lastState, action) => { - switch (action.type) { - case ActionType.START: - return { ...initialState, status: FetchStatus.FETCHING }; - case ActionType.SUCCESS: - return { - ...initialState, - status: FetchStatus.FETCH_SUCCESS, - data: action.payload, - }; - case ActionType.ERROR: - return { - ...initialState, - status: FetchStatus.FETCH_ERROR, - errorMessage: action.payload, - }; - default: - return lastState; - } - }, initialState); - - const handleError = useCallback( - (error, paramsOnError) => { - const defaultErrorMessage = error.message; - let errorMessage = defaultErrorMessage; - if (error && errorToString) { - const providedErrorMessage = errorToString(error.status); - if (providedErrorMessage && providedErrorMessage !== '') { - errorMessage = providedErrorMessage; - } - } - dispatch({ - type: ActionType.ERROR, - payload: errorMessage, - }); - if (onError) { - onError(errorMessage, paramsOnError); - } - }, - [errorToString, onError] - ); - - const fetchData = useCallback( - async (...args) => { - dispatch({ type: ActionType.START }); - try { - // Params resolution - const response = await fetchFunction.apply(null, args); - - if (hasResult) { - const data = response; - dispatch({ - type: ActionType.SUCCESS, - payload: data, - }); - if (onSuccess) { - onSuccess(data, args); - } - } else { - dispatch({ - type: ActionType.SUCCESS, - }); - if (onSuccess) { - onSuccess(null, args); - } - } - } catch (error) { - if (!error.status) { - // an http error - handleError(null, args); - throw error; - } else { - handleError(error, args); - } - } - }, - [fetchFunction, onSuccess, handleError, hasResult] - ); - - const fetchCallback = useCallback( - (...args) => { - fetchData(...args); - }, - [fetchData] - ); - - return [fetchCallback, state]; -}; - -/////////////////////////////////////////////////////////////////: - -/** - * This custom hook manage multiple fetchs workflows and return a unique callback to defer process execution when needed. - * It also return a unique state which concatenate all fetch results independently. - * @param {function} fetchFunction the fetch function to call for each request - * @param {function} onSuccess callback to call on all request success - * @param {function} errorToString callback to translate HTTPCode to string error messages - * @param {function} onError callback to call if one or more requests failed - * @param {boolean} hasResult Configure if fetchFunction return results or only HTTP request response - * @returns {function} fetchCallback The callback to call to execute the requests collection. - * It accepts params array as arguments which define the number of fetch to execute. - * @returns {state} state complete states of the requests collection - * {Enum} state.status Status of the requests set - * {Array} state.errorMessage error message of the requests set - * {Array} state.paramsOnError The parameters used when requests set have failed - * {Array} state.data The results array of each request (see hasResult) - */ -export const useMultipleDeferredFetch = ( - fetchFunction, - onSuccess, - errorToString = undefined, - onError = undefined, - hasResult = true -) => { - const initialState = { - public: { - status: FetchStatus.IDLE, - errorMessage: [], - paramsOnError: [], - data: [], - paramsOnSuccess: [], - }, - counter: 0, - }; - - const [state, dispatch] = useReducer((lastState, action) => { - switch (action.type) { - case ActionType.START: - return { - ...initialState, - public: { - ...initialState.public, - status: FetchStatus.FETCHING, - }, - }; - case ActionType.ADD_SUCCESS: - return { - public: { - ...lastState.public, - data: lastState.public.data.concat([action.payload]), - paramsOnSuccess: lastState.public.paramsOnSuccess.concat([action.context]), - }, - counter: lastState.counter + 1, - }; - case ActionType.ADD_ERROR: - return { - public: { - ...lastState.public, - errorMessage: lastState.public.errorMessage.concat([action.payload]), - paramsOnError: lastState.public.paramsOnError.concat([action.context]), - }, - counter: lastState.counter + 1, - }; - case ActionType.SUCCESS: - return { - ...lastState, - public: { - ...lastState.public, - status: FetchStatus.FETCH_SUCCESS, - }, - counter: 0, - }; - case ActionType.ERROR: - return { - ...lastState, - public: { - ...lastState.public, - status: FetchStatus.FETCH_ERROR, - }, - counter: 0, - }; - default: - return lastState; - } - }, initialState); - - const [paramList, setParamList] = useState([]); - - const onInstanceSuccess = useCallback((data, paramsOnSuccess) => { - dispatch({ - type: ActionType.ADD_SUCCESS, - payload: data, - context: paramsOnSuccess, - }); - }, []); - - const onInstanceError = useCallback((errorMessage, paramsOnError) => { - // counter now stored in reducer to avoid counter and state being updated not simultenaously, - // causing useEffect to be triggered once for each change, which would cause an expected behaviour - dispatch({ - type: ActionType.ADD_ERROR, - payload: errorMessage, - context: paramsOnError, - }); - }, []); - - const [fetchCB] = useDeferredFetch(fetchFunction, onInstanceSuccess, errorToString, onInstanceError, hasResult); - - const fetchCallback = useCallback( - (cbParamsList) => { - dispatch({ type: ActionType.START }); - setParamList(cbParamsList); - for (let params of cbParamsList) { - fetchCB(...params); - } - }, - [fetchCB] - ); - - useEffect(() => { - if (paramList.length !== 0 && paramList.length === state.counter) { - if (state.public.errorMessage.length > 0) { - dispatch({ - type: ActionType.ERROR, - }); - if (onError) { - onError(state.public.errorMessage, paramList, state.public.paramsOnError); - } - } else { - dispatch({ - type: ActionType.SUCCESS, - }); - if (onSuccess) { - onSuccess(state.public.data); - } - } - } - }, [paramList, onError, onSuccess, state]); - - return [fetchCallback]; -}; diff --git a/src/utils/elementType.ts b/src/utils/elementType.ts index c2edcce0f..896d87eef 100644 --- a/src/utils/elementType.ts +++ b/src/utils/elementType.ts @@ -5,11 +5,18 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +type IdLabel = { + id: string; + label: string; +}; + export const FilterType = { CRITERIA_BASED: { id: 'CRITERIA', label: 'filter.criteriaBased' }, EXPLICIT_NAMING: { id: 'IDENTIFIER_LIST', label: 'filter.explicitNaming' }, EXPERT: { id: 'EXPERT', label: 'filter.expert' }, -}; +} as const satisfies Record; +export type FilterTypeKeys = keyof typeof FilterType; +export type FilterTypeIds = (typeof FilterType)[FilterTypeKeys]['id']; export const ContingencyListType = { CRITERIA_BASED: { id: 'FORM', label: 'contingencyList.criteriaBased' }, @@ -18,4 +25,6 @@ export const ContingencyListType = { label: 'contingencyList.explicitNaming', }, SCRIPT: { id: 'SCRIPT', label: 'contingencyList.script' }, -}; +} as const satisfies Record; +export type ContingencyListTypeKeys = keyof typeof ContingencyListType; +export type ContingencyListTypeIds = (typeof ContingencyListType)[ContingencyListTypeKeys]['id']; diff --git a/src/utils/language.js b/src/utils/language.js deleted file mode 100644 index d8d3cad3a..000000000 --- a/src/utils/language.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2021, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { LANG_ENGLISH, LANG_FRENCH, LANG_SYSTEM } from '@gridsuite/commons-ui'; - -const supportedLanguages = [LANG_FRENCH, LANG_ENGLISH]; - -export const getSystemLanguage = () => { - const systemLanguage = navigator.language.split(/[-_]/)[0]; - return supportedLanguages.includes(systemLanguage) ? systemLanguage : LANG_ENGLISH; -}; - -export const getComputedLanguage = (language) => { - return language === LANG_SYSTEM ? getSystemLanguage() : language; -}; diff --git a/src/utils/notificationType.ts b/src/utils/notificationType.ts new file mode 100644 index 000000000..695693fbd --- /dev/null +++ b/src/utils/notificationType.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2021, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +export enum notificationType { + DELETE_DIRECTORY = 'DELETE_DIRECTORY', + ADD_DIRECTORY = 'ADD_DIRECTORY', + UPDATE_DIRECTORY = 'UPDATE_DIRECTORY', +} diff --git a/src/utils/rest-api.js b/src/utils/rest-api.js deleted file mode 100644 index aa17c22fc..000000000 --- a/src/utils/rest-api.js +++ /dev/null @@ -1,698 +0,0 @@ -/** - * Copyright (c) 2021, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { APP_NAME, getAppName } from './config-params'; -import { store } from '../redux/store'; -import ReconnectingWebSocket from 'reconnecting-websocket'; -import { ContingencyListType } from './elementType'; -import { CONTINGENCY_ENDPOINTS } from './constants-endpoints'; -import { ElementType, getRequestParamFromList, fetchEnv, backendFetchJson, backendFetch } from '@gridsuite/commons-ui'; - -const PREFIX_USER_ADMIN_SERVER_QUERIES = import.meta.env.VITE_API_GATEWAY + '/user-admin'; -const PREFIX_CONFIG_NOTIFICATION_WS = import.meta.env.VITE_WS_GATEWAY + '/config-notification'; -const PREFIX_CONFIG_QUERIES = import.meta.env.VITE_API_GATEWAY + '/config'; -const PREFIX_DIRECTORY_SERVER_QUERIES = import.meta.env.VITE_API_GATEWAY + '/directory'; -const PREFIX_EXPLORE_SERVER_QUERIES = import.meta.env.VITE_API_GATEWAY + '/explore'; -const PREFIX_ACTIONS_QUERIES = import.meta.env.VITE_API_GATEWAY + '/actions'; -const PREFIX_CASE_QUERIES = import.meta.env.VITE_API_GATEWAY + '/case'; -const PREFIX_NETWORK_CONVERSION_SERVER_QUERIES = import.meta.env.VITE_API_GATEWAY + '/network-conversion'; -const PREFIX_NOTIFICATION_WS = import.meta.env.VITE_WS_GATEWAY + '/directory-notification'; -const PREFIX_FILTERS_QUERIES = import.meta.env.VITE_API_GATEWAY + '/filter/v1/filters'; -const PREFIX_STUDY_QUERIES = import.meta.env.VITE_API_GATEWAY + '/study'; - -function getToken() { - const state = store.getState(); - return state.user.id_token; -} - -export function connectNotificationsWsUpdateConfig() { - const webSocketBaseUrl = document.baseURI.replace(/^http:\/\//, 'ws://').replace(/^https:\/\//, 'wss://'); - const webSocketUrl = webSocketBaseUrl + PREFIX_CONFIG_NOTIFICATION_WS + '/notify?appName=' + APP_NAME; - - const reconnectingWebSocket = new ReconnectingWebSocket(() => webSocketUrl + '&access_token=' + getToken()); - reconnectingWebSocket.onopen = function () { - console.info('Connected Websocket update config ui ' + webSocketUrl + ' ...'); - }; - return reconnectingWebSocket; -} - -function parseError(text) { - try { - return JSON.parse(text); - } catch (err) { - return null; - } -} - -function handleError(response) { - return response.text().then((text) => { - const errorName = 'HttpResponseError : '; - let error; - const errorJson = parseError(text); - if (errorJson && errorJson.status && errorJson.error && errorJson.message) { - error = new Error( - errorName + errorJson.status + ' ' + errorJson.error + ', message : ' + errorJson.message - ); - error.status = errorJson.status; - } else { - error = new Error(errorName + response.status + ' ' + response.statusText); - error.status = response.status; - } - throw error; - }); -} - -function prepareRequest(init, token) { - if (!(typeof init == 'undefined' || typeof init == 'object')) { - throw new TypeError('Argument 2 of backendFetch is not an object' + typeof init); - } - const initCopy = Object.assign({}, init); - initCopy.headers = new Headers(initCopy.headers || {}); - const tokenCopy = token ? token : getToken(); - initCopy.headers.append('Authorization', 'Bearer ' + tokenCopy); - return initCopy; -} - -function safeFetch(url, initCopy) { - return fetch(url, initCopy).then((response) => (response.ok ? response : handleError(response))); -} - -export function backendFetchText(url, init, token) { - const initCopy = prepareRequest(init, token); - return safeFetch(url, initCopy).then((safeResponse) => safeResponse.text()); -} - -const getContingencyUriParamType = (contingencyListType) => { - switch (contingencyListType) { - case ContingencyListType.SCRIPT.id: - return CONTINGENCY_ENDPOINTS.SCRIPT_CONTINGENCY_LISTS; - case ContingencyListType.CRITERIA_BASED.id: - return CONTINGENCY_ENDPOINTS.FORM_CONTINGENCY_LISTS; - case ContingencyListType.EXPLICIT_NAMING.id: - return CONTINGENCY_ENDPOINTS.IDENTIFIER_CONTINGENCY_LISTS; - default: - return null; - } -}; - -export function fetchValidateUser(user) { - const sub = user?.profile?.sub; - if (!sub) { - return Promise.reject(new Error('Error : Fetching access for missing user.profile.sub : ' + user)); - } - - console.info(`Fetching access for user...`); - const CheckAccessUrl = PREFIX_USER_ADMIN_SERVER_QUERIES + `/v1/users/${sub}`; - console.debug(CheckAccessUrl); - - return backendFetch( - CheckAccessUrl, - { - method: 'head', - }, - user?.id_token - ) - .then((response) => { - //if the response is ok, the responseCode will be either 200 or 204 otherwise it's a Http error and it will be caught - return response.status === 200; - }) - .catch((error) => { - if (error.status === 403) { - return false; - } else { - throw error; - } - }); -} - -export function fetchIdpSettings() { - return fetch('idpSettings.json').then((res) => res.json()); -} - -export function fetchVersion() { - console.info(`Fetching global metadata...`); - return fetchEnv() - .then((env) => fetch(env.appsMetadataServerUrl + '/version.json')) - .then((response) => response.json()) - .catch((reason) => { - console.error('Error while fetching the version : ' + reason); - return reason; - }); -} - -export function fetchConfigParameters(appName) { - console.info('Fetching UI configuration params for app : ' + appName); - const fetchParams = PREFIX_CONFIG_QUERIES + `/v1/applications/${appName}/parameters`; - return backendFetchJson(fetchParams); -} - -export function fetchConfigParameter(name) { - const appName = getAppName(name); - console.info("Fetching UI config parameter '%s' for app '%s' ", name, appName); - const fetchParams = PREFIX_CONFIG_QUERIES + `/v1/applications/${appName}/parameters/${name}`; - return backendFetchJson(fetchParams); -} - -export function deleteElement(elementUuid) { - console.info("Deleting element %s'", elementUuid); - const fetchParams = PREFIX_EXPLORE_SERVER_QUERIES + `/v1/explore/elements/${elementUuid}`; - return backendFetch(fetchParams, { - method: 'delete', - }); -} - -export function deleteElements(elementUuids, activeDirectory) { - console.info('Deleting elements : %s', elementUuids); - const idsParams = getRequestParamFromList('ids', elementUuids).toString(); - return backendFetch(PREFIX_EXPLORE_SERVER_QUERIES + `/v1/explore/elements/` + activeDirectory + '?' + idsParams, { - method: 'delete', - }); -} - -export function moveElementsToDirectory(elementsUuids, targetDirectoryUuid) { - console.info('Moving elements to directory %s', targetDirectoryUuid); - - const fetchParams = - PREFIX_EXPLORE_SERVER_QUERIES + `/v1/explore/elements?targetDirectoryUuid=${targetDirectoryUuid}`; - return backendFetch(fetchParams, { - method: 'put', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(elementsUuids), - }); -} - -export function updateElement(elementUuid, element) { - console.info('Updating element info for ' + elementUuid); - const updateElementUrl = PREFIX_EXPLORE_SERVER_QUERIES + `/v1/explore/elements/${elementUuid}`; - return backendFetch(updateElementUrl, { - method: 'put', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(element), - }); -} - -export function insertDirectory(directoryName, parentUuid, owner) { - console.info("Inserting a new folder '%s'", directoryName); - const insertDirectoryUrl = PREFIX_DIRECTORY_SERVER_QUERIES + `/v1/directories/${parentUuid}/elements`; - return backendFetchJson(insertDirectoryUrl, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - elementUuid: null, - elementName: directoryName, - type: 'DIRECTORY', - owner: owner, - }), - }); -} - -export function insertRootDirectory(directoryName, owner) { - console.info("Inserting a new root folder '%s'", directoryName); - const insertRootDirectoryUrl = PREFIX_DIRECTORY_SERVER_QUERIES + `/v1/root-directories`; - return backendFetchJson(insertRootDirectoryUrl, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - elementName: directoryName, - owner: owner, - }), - }); -} - -export function renameElement(elementUuid, newElementName) { - console.info('Renaming element ' + elementUuid); - const renameElementUrl = PREFIX_EXPLORE_SERVER_QUERIES + `/v1/explore/elements/${elementUuid}`; - console.debug(renameElementUrl); - return backendFetch(renameElementUrl, { - method: 'put', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - elementName: newElementName, - }), - }); -} - -export function updateConfigParameter(name, value) { - const appName = getAppName(name); - console.info("Updating config parameter '%s=%s' for app '%s' ", name, value, appName); - const updateParams = - PREFIX_CONFIG_QUERIES + `/v1/applications/${appName}/parameters/${name}?value=` + encodeURIComponent(value); - return backendFetch(updateParams, { method: 'put' }); -} - -export function createStudy( - studyName, - studyDescription, - caseUuid, - duplicateCase, - parentDirectoryUuid, - importParameters, - caseFormat -) { - console.info('Creating a new study...'); - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('duplicateCase', duplicateCase); - urlSearchParams.append('description', studyDescription); - urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); - urlSearchParams.append('caseFormat', caseFormat); - - const createStudyUrl = - PREFIX_EXPLORE_SERVER_QUERIES + - '/v1/explore/studies/' + - encodeURIComponent(studyName) + - '/cases/' + - encodeURIComponent(caseUuid) + - '?' + - urlSearchParams.toString(); - console.debug(createStudyUrl); - return backendFetch(createStudyUrl, { - method: 'post', - body: importParameters, - headers: { 'Content-Type': 'application/json' }, - }); -} - -export function createCase({ name, description, file, parentDirectoryUuid }) { - console.info('Creating a new case...'); - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('description', description); - urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); - - const url = - PREFIX_EXPLORE_SERVER_QUERIES + - '/v1/explore/cases/' + - encodeURIComponent(name) + - '?' + - urlSearchParams.toString(); - const formData = new FormData(); - formData.append('caseFile', file); - console.debug(url); - - return backendFetch(url, { - method: 'post', - body: formData, - }); -} - -const getDuplicateEndpoint = (type) => { - switch (type) { - case ElementType.CASE: - return '/cases'; - case ElementType.STUDY: - return '/studies'; - case ElementType.FILTER: - return '/filters'; - case ElementType.CONTINGENCY_LIST: - return '/contingency-lists'; - case ElementType.PARAMETERS: - return '/parameters'; - case ElementType.MODIFICATION: - return '/modifications'; - default: - break; - } -}; - -export function duplicateElement(sourceCaseUuid, parentDirectoryUuid, type, specificType) { - console.info('Duplicating an element of type ' + type + ' ...'); - let queryParams = new URLSearchParams(); - queryParams.append('duplicateFrom', sourceCaseUuid); - if (parentDirectoryUuid) { - queryParams.append('parentDirectoryUuid', parentDirectoryUuid); - } - if (specificType) { - queryParams.append('type', specificType); - } - const url = `${PREFIX_EXPLORE_SERVER_QUERIES}/v1/explore${getDuplicateEndpoint(type)}?` + queryParams.toString(); - - console.debug(url); - - return backendFetch(url, { - method: 'post', - }); -} - -export function elementExists(directoryUuid, elementName, type) { - const elementNameEncoded = encodeURIComponent(elementName); - const existsElementUrl = - PREFIX_DIRECTORY_SERVER_QUERIES + - `/v1/directories/${directoryUuid}/elements/${elementNameEncoded}/types/${type}`; - console.debug(existsElementUrl); - return backendFetch(existsElementUrl, { method: 'head' }).then((response) => { - return response.status !== 204; // HTTP 204 : No-content - }); -} - -export function getNameCandidate(directoryUuid, elementName, type) { - const nameCandidateUrl = - PREFIX_DIRECTORY_SERVER_QUERIES + - `/v1/directories/${directoryUuid}/${elementName}/newNameCandidate?type=${type}`; - console.debug(nameCandidateUrl); - return backendFetchText(nameCandidateUrl); -} - -export function rootDirectoryExists(directoryName) { - const existsRootDirectoryUrl = - PREFIX_DIRECTORY_SERVER_QUERIES + - `/v1/root-directories?` + - new URLSearchParams({ - directoryName: directoryName, - }).toString(); - - console.debug(existsRootDirectoryUrl); - - return backendFetch(existsRootDirectoryUrl, { method: 'head' }).then((response) => { - return response.status !== 204; // HTTP 204 : No-content - }); -} - -export function createContingencyList( - contingencyListType, - contingencyListName, - description, - formContent, - parentDirectoryUuid -) { - console.info('Creating a new contingency list...'); - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('description', description); - urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); - - let typeUriParam = getContingencyUriParamType(contingencyListType); - - const createContingencyListUrl = - PREFIX_EXPLORE_SERVER_QUERIES + - '/v1/explore' + - typeUriParam + - '/' + - encodeURIComponent(contingencyListName) + - '?' + - urlSearchParams.toString(); - console.debug(createContingencyListUrl); - - return backendFetch(createContingencyListUrl, { - method: 'post', - body: JSON.stringify(formContent), - }); -} - -/** - * Get contingency list by type and id - * @returns {Promise} - */ -export function getContingencyList(type, id) { - let url = PREFIX_ACTIONS_QUERIES + '/v1' + getContingencyUriParamType(type) + '/' + id; - - return backendFetchJson(url); -} - -/** - * Saves a Filter contingency list - * @returns {Promise} - */ - -export function saveCriteriaBasedContingencyList(id, form) { - const { name, equipmentType, criteriaBased } = form; - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('name', name); - urlSearchParams.append('contingencyListType', ContingencyListType.CRITERIA_BASED.id); - - const url = - PREFIX_EXPLORE_SERVER_QUERIES + '/v1/explore/contingency-lists/' + id + '?' + urlSearchParams.toString(); - - return backendFetch(url, { - method: 'put', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - ...criteriaBased, - equipmentType, - nominalVoltage1: criteriaBased.nominalVoltage1 === '' ? -1 : criteriaBased.nominalVoltage1, - }), - }); -} - -/** - * Saves a script contingency list - * @returns {Promise} - */ -export function saveScriptContingencyList(scriptContingencyList, name) { - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('name', name); - urlSearchParams.append('contingencyListType', ContingencyListType.SCRIPT.id); - const url = - PREFIX_EXPLORE_SERVER_QUERIES + - '/v1/explore/contingency-lists/' + - scriptContingencyList.id + - '?' + - urlSearchParams.toString(); - return backendFetch(url, { - method: 'put', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(scriptContingencyList), - }); -} - -/** - * Saves an explicit naming contingency list - * @returns {Promise} - */ -export function saveExplicitNamingContingencyList(explicitNamingContingencyList, name) { - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('name', name); - urlSearchParams.append('contingencyListType', ContingencyListType.EXPLICIT_NAMING.id); - const url = - PREFIX_EXPLORE_SERVER_QUERIES + - '/v1/explore/contingency-lists/' + - explicitNamingContingencyList.id + - '?' + - urlSearchParams.toString(); - return backendFetch(url, { - method: 'put', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(explicitNamingContingencyList), - }); -} - -/** - * Replace form contingency list with script contingency list - * @returns {Promise} - */ -export function replaceFormContingencyListWithScript(id, parentDirectoryUuid) { - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); - - const url = - PREFIX_EXPLORE_SERVER_QUERIES + - '/v1/explore' + - CONTINGENCY_ENDPOINTS.FORM_CONTINGENCY_LISTS + - '/' + - encodeURIComponent(id) + - '/replace-with-script' + - '?' + - urlSearchParams.toString(); - - return backendFetch(url, { - method: 'post', - }); -} - -/** - * Save new script contingency list from form contingency list - * @returns {Promise} - */ -export function newScriptFromFiltersContingencyList(id, newName, parentDirectoryUuid) { - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); - - const url = - PREFIX_EXPLORE_SERVER_QUERIES + - '/v1/explore' + - CONTINGENCY_ENDPOINTS.FORM_CONTINGENCY_LISTS + - '/' + - encodeURIComponent(id) + - '/new-script/' + - encodeURIComponent(newName) + - '?' + - urlSearchParams.toString(); - - return backendFetch(url, { - method: 'post', - }); -} - -/** - * Function will be called to connect with notification websocket to update directories list - * @returns {ReconnectingWebSocket} - */ -export function connectNotificationsWsUpdateDirectories() { - const webSocketBaseUrl = document.baseURI.replace(/^http:\/\//, 'ws://').replace(/^https:\/\//, 'wss://'); - const webSocketUrl = webSocketBaseUrl + PREFIX_NOTIFICATION_WS + '/notify?updateType=directories'; - - const reconnectingWebSocket = new ReconnectingWebSocket(() => webSocketUrl + '&access_token=' + getToken()); - reconnectingWebSocket.onopen = function () { - console.info('Connected Websocket update studies ' + webSocketUrl + ' ...'); - }; - return reconnectingWebSocket; -} - -/** - * Get all filters (name & type) - * @returns {Promise} - */ -export function getFilters() { - return backendFetchJson(PREFIX_FILTERS_QUERIES).then((res) => res.sort((a, b) => a.name.localeCompare(b.name))); -} - -/** - * Get filter by id - * @returns {Promise} - */ -export function getFilterById(id) { - const url = PREFIX_FILTERS_QUERIES + '/' + id; - return backendFetchJson(url); -} - -/** - * Replace filter with script filter - * @returns {Promise} - */ -export function replaceFiltersWithScript(id, parentDirectoryUuid) { - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); - - const url = - PREFIX_EXPLORE_SERVER_QUERIES + - '/v1/explore/filters/' + - encodeURIComponent(id) + - '/replace-with-script' + - '?' + - urlSearchParams.toString(); - - return backendFetch(url, { - method: 'post', - }); -} - -/** - * Save new script from filters - * @returns {Promise} - */ -export function newScriptFromFilter(id, newName, parentDirectoryUuid) { - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('parentDirectoryUuid', parentDirectoryUuid); - const url = - PREFIX_EXPLORE_SERVER_QUERIES + - '/v1/explore/filters/' + - encodeURIComponent(id) + - '/new-script/' + - encodeURIComponent(newName) + - '?' + - urlSearchParams.toString(); - - return backendFetch(url, { - method: 'post', - }); -} - -export function getCaseImportParameters(caseUuid) { - console.info(`get import parameters for case '${caseUuid}' ...`); - const getExportFormatsUrl = - PREFIX_NETWORK_CONVERSION_SERVER_QUERIES + '/v1/cases/' + caseUuid + '/import-parameters'; - console.debug(getExportFormatsUrl); - return backendFetchJson(getExportFormatsUrl); -} - -export function createCaseWithoutDirectoryElementCreation(selectedFile) { - const createCaseUrl = PREFIX_CASE_QUERIES + '/v1/cases'; - const formData = new FormData(); - formData.append('file', selectedFile); - formData.append('withExpiration', encodeURIComponent(true)); - console.debug(createCaseUrl); - - return backendFetchJson(createCaseUrl, { - method: 'post', - body: formData, - }); -} - -export function deleteCase(caseUuid) { - const deleteCaseUrl = PREFIX_CASE_QUERIES + '/v1/cases/' + caseUuid; - return backendFetch(deleteCaseUrl, { - method: 'delete', - }); -} - -export const fetchConvertedCase = (caseUuid, fileName, format, formatParameters, abortController) => - backendFetch(`${PREFIX_CASE_QUERIES}/v1/cases/${caseUuid}?format=${format}&fileName=${fileName}`, { - method: 'post', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(formatParameters), - signal: abortController.signal, - }); - -export const downloadCase = (caseUuid) => - backendFetch(`${PREFIX_CASE_QUERIES}/v1/cases/${caseUuid}`, { - method: 'get', - headers: { 'Content-Type': 'application/json' }, - }); - -/** - * Retrieves the original name of a case using its UUID. - * @param {string} caseUuid - The UUID of the element. - * @returns {Promise} - A promise that resolves to the original name of the case if found, or false if not found. - */ -export function getCaseOriginalName(caseUuid) { - const caseNameUrl = PREFIX_CASE_QUERIES + `/v1/cases/${caseUuid}/name`; - console.debug(caseNameUrl); - return backendFetchText(caseNameUrl).catch((error) => { - if (error.status === 404) { - return false; - } else { - throw error; - } - }); -} - -export function getServersInfos() { - console.info('get backend servers informations'); - return backendFetchJson(PREFIX_STUDY_QUERIES + '/v1/servers/about?view=explore').catch((reason) => { - console.error('Error while fetching the servers infos : ' + reason); - return reason; - }); -} - -export const getExportFormats = () => { - console.info('get export formats'); - const url = PREFIX_NETWORK_CONVERSION_SERVER_QUERIES + '/v1/export/formats'; - console.debug(url); - return backendFetchJson(url); -}; - -export function searchElementsInfos(searchTerm, currentDirectoryUuid) { - console.info("Fetching elements infos matching with '%s' term ... ", searchTerm); - const urlSearchParams = new URLSearchParams(); - urlSearchParams.append('userInput', searchTerm); - urlSearchParams.append('directoryUuid', currentDirectoryUuid); - return backendFetchJson( - PREFIX_DIRECTORY_SERVER_QUERIES + '/v1/elements/indexation-infos?' + urlSearchParams.toString() - ); -} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index db95603f7..69106770c 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -7,3 +7,22 @@ /// /// + +/* Don't know why but seem that TypeScript merge definitions of these two interfaces with existing ones. + * https://vitejs.dev/guide/env-and-mode#intellisense-for-typescript + */ +import { UrlString } from '@gridsuite/commons-ui'; + +interface ImportMetaEnv { + /* From @gridsuite/commons-ui */ + readonly VITE_API_GATEWAY: UrlString; + readonly VITE_WS_GATEWAY: UrlString; + // readonly VITE_DEBUG_REQUESTS?: boolean; + readonly VITE_DEBUG_HOOK_RENDER?: boolean; + /* From this app */ + readonly VITE_DEBUG_REQUESTS: boolean; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}