diff --git a/package-lock.json b/package-lock.json index 330d1b38f..9cb3da7c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "date-fns": "^2.30.0", "glob": "^10.3.10", "keycloak-js": "^22.0.4", + "keycloak-react-web": "^0.1.19", "ky": "^1.1.0", "material-ui-popup-state": "^5.0.9", "notistack": "^3.0.1", @@ -14281,6 +14282,23 @@ "js-sha256": "^0.9.0" } }, + "node_modules/keycloak-react-web": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/keycloak-react-web/-/keycloak-react-web-0.1.19.tgz", + "integrity": "sha512-ECpJ75rajWPB77zjUcigzpCofi6ynqACKNDyhzx7fUI17JlsCsoRGGGDrDGPlVh+sMvurQNcmZHGhSNWnzCZAw==", + "dependencies": { + "keycloak-js": "^19.0.2" + } + }, + "node_modules/keycloak-react-web/node_modules/keycloak-js": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-19.0.3.tgz", + "integrity": "sha512-mzCBxrzfl+vB551Q7MB+T9+40IHU4i0a6g1eTatzeEGrQMis5m/BqvPC3kxTsI+/LxHbB9XYQE3u9SlWKDHQCw==", + "dependencies": { + "base64-js": "^1.5.1", + "js-sha256": "^0.9.0" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -32645,6 +32663,25 @@ "js-sha256": "^0.9.0" } }, + "keycloak-react-web": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/keycloak-react-web/-/keycloak-react-web-0.1.19.tgz", + "integrity": "sha512-ECpJ75rajWPB77zjUcigzpCofi6ynqACKNDyhzx7fUI17JlsCsoRGGGDrDGPlVh+sMvurQNcmZHGhSNWnzCZAw==", + "requires": { + "keycloak-js": "^19.0.2" + }, + "dependencies": { + "keycloak-js": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-19.0.3.tgz", + "integrity": "sha512-mzCBxrzfl+vB551Q7MB+T9+40IHU4i0a6g1eTatzeEGrQMis5m/BqvPC3kxTsI+/LxHbB9XYQE3u9SlWKDHQCw==", + "requires": { + "base64-js": "^1.5.1", + "js-sha256": "^0.9.0" + } + } + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", diff --git a/package.json b/package.json index 5659410f6..be8e0f148 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "date-fns": "^2.30.0", "glob": "^10.3.10", "keycloak-js": "^22.0.4", + "keycloak-react-web": "^0.1.19", "ky": "^1.1.0", "material-ui-popup-state": "^5.0.9", "notistack": "^3.0.1", diff --git a/src/App.tsx b/src/App.tsx index cd85d7948..6d77afa1c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { ConfigProvider } from './config/ConfigService'; import { PythonConverterService } from './PythonConverter/PythonConverterService'; import { Auth } from './services/AuthService'; import { DialogProvider } from './services/DialogService'; +import { KeycloakAuth } from './services/KeycloakAuthService'; import { Loader } from './services/LoaderService'; import { ShSimulation } from './services/ShSimulatorService'; import { Store } from './services/StoreService'; @@ -84,11 +85,12 @@ function App() { , , , + , + , + , , , , - , - , ]}> diff --git a/src/ThreeEditor/components/Dialog/ClearHistoryDialog.tsx b/src/ThreeEditor/components/Dialog/ClearHistoryDialog.tsx index 6603bafb5..e5e594ee4 100644 --- a/src/ThreeEditor/components/Dialog/ClearHistoryDialog.tsx +++ b/src/ThreeEditor/components/Dialog/ClearHistoryDialog.tsx @@ -1,11 +1,12 @@ import { Button } from '@mui/material'; -import { useStore } from '../../../services/StoreService'; +import { StoreContext } from '../../../services/StoreService'; import { ConcreteDialogProps, CustomDialog } from './CustomDialog'; -export function ClearHistoryDialog({ onClose }: ConcreteDialogProps) { - const { yaptideEditor } = useStore(); - +export function ClearHistoryDialog({ + onClose, + yaptideEditor +}: ConcreteDialogProps>>) { return ( theme.palette.grey[500] + color: ({ palette }: Theme) => palette.grey[500] }}> diff --git a/src/ThreeEditor/components/Dialog/EditProjectInfoDialog.tsx b/src/ThreeEditor/components/Dialog/EditProjectInfoDialog.tsx index d4c13e4cd..8e5a2b9ca 100644 --- a/src/ThreeEditor/components/Dialog/EditProjectInfoDialog.tsx +++ b/src/ThreeEditor/components/Dialog/EditProjectInfoDialog.tsx @@ -1,11 +1,13 @@ import { Box, Button, TextField } from '@mui/material'; import { useState } from 'react'; -import { useStore } from '../../../services/StoreService'; +import { StoreContext } from '../../../services/StoreService'; import { ConcreteDialogProps, CustomDialog } from './CustomDialog'; -export function EditProjectInfoDialog({ onClose }: ConcreteDialogProps) { - const { yaptideEditor } = useStore(); +export function EditProjectInfoDialog({ + onClose, + yaptideEditor +}: ConcreteDialogProps>>) { const [title, setTitle] = useState(yaptideEditor?.config.getKey('project/title')); const [description, setDescription] = useState( yaptideEditor?.config.getKey('project/description') diff --git a/src/ThreeEditor/components/Dialog/LoadFileDialog.tsx b/src/ThreeEditor/components/Dialog/LoadFileDialog.tsx index 4c8ae48d0..9ae869a82 100644 --- a/src/ThreeEditor/components/Dialog/LoadFileDialog.tsx +++ b/src/ThreeEditor/components/Dialog/LoadFileDialog.tsx @@ -1,20 +1,21 @@ import { Button } from '@mui/material'; import Typography from '@mui/material/Typography'; -import { useStore } from '../../../services/StoreService'; +import { StoreContext } from '../../../services/StoreService'; import { EditorJson } from '../../js/EditorJson'; import { ConcreteDialogProps, CustomDialog } from './CustomDialog'; export function LoadFileDialog({ onClose, validVersion = true, - data -}: ConcreteDialogProps<{ - validVersion: boolean; - data: EditorJson; -}>) { - const { yaptideEditor } = useStore(); - + data, + yaptideEditor +}: ConcreteDialogProps< + { + validVersion: boolean; + data: EditorJson; + } & Required> +>) { return ( >>) { return ( ) { + return ( + + + + ); +} diff --git a/src/ThreeEditor/components/Dialog/RunSimulationDialog.tsx b/src/ThreeEditor/components/Dialog/RunSimulationDialog.tsx index 6ecb84c16..bfb86c323 100644 --- a/src/ThreeEditor/components/Dialog/RunSimulationDialog.tsx +++ b/src/ThreeEditor/components/Dialog/RunSimulationDialog.tsx @@ -2,8 +2,8 @@ import { Card, CardContent, Fade, Modal } from '@mui/material'; import { enqueueSnackbar } from 'notistack'; import { useState } from 'react'; -import { useShSimulation } from '../../../services/ShSimulatorService'; -import { useStore } from '../../../services/StoreService'; +import { RestSimulationContext } from '../../../services/ShSimulatorService'; +import { StoreContext } from '../../../services/StoreService'; import { SimulatorType } from '../../../types/RequestTypes'; import { SimulationInputFiles } from '../../../types/ResponseTypes'; import { @@ -19,15 +19,19 @@ export function RunSimulationDialog({ inputFiles = {}, simulator, onClose, - onSubmit = () => {} -}: ConcreteDialogProps<{ - onSubmit?: (jobId: string) => void; - inputFiles?: Record; - simulator: SimulatorType; -}>) { - const { yaptideEditor } = useStore(); + onSubmit = () => {}, + yaptideEditor, + postJobDirect, + postJobBatch +}: ConcreteDialogProps< + { + onSubmit?: (jobId: string) => void; + inputFiles?: Record; + simulator: SimulatorType; + } & Required> & + Pick +>) { const [controller] = useState(new AbortController()); - const { postJobDirect, postJobBatch } = useShSimulation(); const sendSimulationRequest = ( postJobFn: typeof postJobDirect, runType: SimulationRunType, @@ -46,16 +50,22 @@ export function RunSimulationDialog({ runType === 'batch' ? { ...batchOptions, - arrayOptions: batchOptions.arrayOptions?.reduce((acc, curr) => { - acc[curr.optionKey] = curr.optionValue; + arrayOptions: batchOptions.arrayOptions?.reduce( + (acc, curr) => { + acc[curr.optionKey] = curr.optionValue; - return acc; - }, {} as Record), - collectOptions: batchOptions.collectOptions?.reduce((acc, curr) => { - acc[curr.optionKey] = curr.optionValue; + return acc; + }, + {} as Record + ), + collectOptions: batchOptions.collectOptions?.reduce( + (acc, curr) => { + acc[curr.optionKey] = curr.optionValue; - return acc; - }, {} as Record) + return acc; + }, + {} as Record + ) } : undefined; diff --git a/src/ThreeEditor/components/Dialog/SaveFileDialog.tsx b/src/ThreeEditor/components/Dialog/SaveFileDialog.tsx index ebd6413dd..e1ed63d08 100644 --- a/src/ThreeEditor/components/Dialog/SaveFileDialog.tsx +++ b/src/ThreeEditor/components/Dialog/SaveFileDialog.tsx @@ -2,7 +2,7 @@ import { Button, Checkbox, FormControlLabel, TextField } from '@mui/material'; import { ChangeEvent, useCallback, useEffect, useState } from 'react'; import { FullSimulationData } from '../../../services/ShSimulatorService'; -import { useStore } from '../../../services/StoreService'; +import { StoreContext } from '../../../services/StoreService'; import { saveString } from '../../../util/File'; import { ConcreteDialogProps, CustomDialog } from './CustomDialog'; @@ -25,13 +25,15 @@ export function SaveFileDialog({ onClose, name: defaultName = 'editor', results: providedResults, - disableCheckbox = false -}: ConcreteDialogProps<{ - name?: string; - results?: FullSimulationData; - disableCheckbox?: boolean; -}>) { - const { yaptideEditor } = useStore(); + disableCheckbox = false, + yaptideEditor +}: ConcreteDialogProps< + { + name?: string; + results?: FullSimulationData; + disableCheckbox?: boolean; + } & Required> +>) { const results: FullSimulationData | undefined = providedResults ?? yaptideEditor?.getResults(); const [keepResults, setKeepResults] = useState(false); diff --git a/src/ThreeEditor/components/Editor/EditorAppBar/EditorAppBar.tsx b/src/ThreeEditor/components/Editor/EditorAppBar/EditorAppBar.tsx index 41e2c1295..99c492607 100644 --- a/src/ThreeEditor/components/Editor/EditorAppBar/EditorAppBar.tsx +++ b/src/ThreeEditor/components/Editor/EditorAppBar/EditorAppBar.tsx @@ -12,6 +12,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useDialog } from '../../../../services/DialogService'; import { useLoader } from '../../../../services/LoaderService'; +import { useStore } from '../../../../services/StoreService'; import { YaptideEditor } from '../../../js/YaptideEditor'; import { EditorTitleBar } from './components/EditorTitlebar'; import { EditorToolbar } from './components/EditorToolbar'; @@ -30,11 +31,12 @@ type AppBarOptions = { function EditorAppBar({ editor }: AppBarProps) { const { loadFromJson, loadFromFiles, loadFromUrl, loadFromJsonString } = useLoader(); - const [openTheOpenFileDialog] = useDialog('openFile'); - const [openTheSaveFileDialog] = useDialog('saveFile'); - const [openTheNewProjectDialog] = useDialog('newProject'); + const { open: openTheOpenFileDialog } = useDialog('openFile'); + const { open: openTheSaveFileDialog } = useDialog('saveFile'); + const { open: openTheNewProjectDialog } = useDialog('newProject'); const [canUndo, setCanUndo] = useState((editor?.history.undos.length ?? 0) > 0); const [canRedo, setCanRedo] = useState((editor?.history.redos.length ?? 0) > 0); + const { yaptideEditor } = useStore(); const updateHistoryButtons = useCallback(() => { setCanUndo((editor?.history.undos.length ?? 0) > 0); @@ -71,7 +73,7 @@ function EditorAppBar({ editor }: AppBarProps) { label: 'New', icon: , disabled: false, - onClick: () => openTheNewProjectDialog() + onClick: () => yaptideEditor && openTheNewProjectDialog({ yaptideEditor }) }, { label: 'Open', @@ -95,7 +97,7 @@ function EditorAppBar({ editor }: AppBarProps) { label: 'Save as', icon: , disabled: false, - onClick: () => openTheSaveFileDialog() + onClick: () => yaptideEditor && openTheSaveFileDialog({ yaptideEditor }) }, { label: 'Redo (ctrl+y)', @@ -113,6 +115,7 @@ function EditorAppBar({ editor }: AppBarProps) { [ canUndo, canRedo, + yaptideEditor, openTheNewProjectDialog, openTheOpenFileDialog, loadFromFiles, diff --git a/src/ThreeEditor/components/Editor/EditorAppBar/components/EditorTitlebar.tsx b/src/ThreeEditor/components/Editor/EditorAppBar/components/EditorTitlebar.tsx index aaeef64a9..4dbc2d526 100644 --- a/src/ThreeEditor/components/Editor/EditorAppBar/components/EditorTitlebar.tsx +++ b/src/ThreeEditor/components/Editor/EditorAppBar/components/EditorTitlebar.tsx @@ -15,7 +15,7 @@ import { useDialog } from '../../../../../services/DialogService'; import { useStore } from '../../../../../services/StoreService'; export function EditorTitleBar() { - const [open, , isOpen] = useDialog('editProject'); + const { open: openEditProjectDialog, isOpen } = useDialog('editProject'); const { yaptideEditor } = useStore(); const [saving, setSaving] = useState(false); const [editMode, setEditMode] = useState(false); @@ -60,7 +60,11 @@ export function EditorTitleBar() { key='edit project description' onClick={() => { popupState.close(); - open(); + + if (yaptideEditor) + openEditProjectDialog({ + yaptideEditor + }); }}> Edit Project Description diff --git a/src/ThreeEditor/components/Editor/EditorMenu/EditorMenu.tsx b/src/ThreeEditor/components/Editor/EditorMenu/EditorMenu.tsx index 54a1a4177..b9ec10cf0 100644 --- a/src/ThreeEditor/components/Editor/EditorMenu/EditorMenu.tsx +++ b/src/ThreeEditor/components/Editor/EditorMenu/EditorMenu.tsx @@ -3,6 +3,7 @@ import { MouseEvent, useCallback, useEffect, useState } from 'react'; import { Object3D } from 'three'; import { useDialog } from '../../../../services/DialogService'; +import { useStore } from '../../../../services/StoreService'; import { useSignal } from '../../../../util/hooks/signals'; import { toggleFullscreen } from '../../../../util/toggleFullscreen'; import { @@ -104,8 +105,9 @@ function MenuPosition({ label, idx, openIdx, setOpenIdx, options }: MenuPosition } export function EditorMenu({ editor }: EditorMenuProps) { - const [open] = useDialog('clearHistory'); + const { open: openClearHistory } = useDialog('clearHistory'); const [openIdx, setOpenIdx] = useState(-1); + const { yaptideEditor } = useStore(); const [, setSelectedObject] = useState(editor?.selected); const handleObjectUpdate = useCallback((o: Object3D) => { @@ -188,7 +190,7 @@ export function EditorMenu({ editor }: EditorMenuProps) { [ { label: 'Clear history', - onClick: () => open(), + onClick: () => yaptideEditor && openClearHistory({ yaptideEditor }), disabled: editor?.history.undos.length === 0 && editor?.history.redos.length === 0 diff --git a/src/ThreeEditor/components/ZoneManagerPanel/BooleanAlgebra/OperationToggle.tsx b/src/ThreeEditor/components/ZoneManagerPanel/BooleanAlgebra/OperationToggle.tsx index 05244c9b5..0adaa6ca5 100644 --- a/src/ThreeEditor/components/ZoneManagerPanel/BooleanAlgebra/OperationToggle.tsx +++ b/src/ThreeEditor/components/ZoneManagerPanel/BooleanAlgebra/OperationToggle.tsx @@ -6,6 +6,7 @@ import { Chip, IconButton, SvgIconProps, + Theme, ToggleButton, ToggleButtonGroup, ToggleButtonGroupProps, @@ -169,10 +170,8 @@ function OperationInput({ 'height': 55, 'width': 55, 'margin': '0 auto', - 'color': theme => - theme.palette.mode === 'dark' - ? 'text.primary' - : 'secondary.main', + 'color': ({ palette }: Theme) => + palette.mode === 'dark' ? 'text.primary' : 'secondary.main', '&:hover': { color: 'error.main' } @@ -217,8 +216,8 @@ function OperationInput({ 'padding': '4px 0', 'minWidth': '56px', 'flexDirection': 'column', - '&.Mui-selected': theme => - theme.palette.mode === 'dark' + '&.Mui-selected': ({ palette }: Theme) => + palette.mode === 'dark' ? {} : { backgroundColor: 'primary.main', diff --git a/src/ThreeEditor/components/ZoneManagerPanel/ZoneManagerPanel.tsx b/src/ThreeEditor/components/ZoneManagerPanel/ZoneManagerPanel.tsx index dbc91717b..1322e9a8c 100644 --- a/src/ThreeEditor/components/ZoneManagerPanel/ZoneManagerPanel.tsx +++ b/src/ThreeEditor/components/ZoneManagerPanel/ZoneManagerPanel.tsx @@ -8,6 +8,7 @@ import { SelectChangeEvent, Tab, Tabs, + Theme, Tooltip } from '@mui/material'; import { @@ -379,8 +380,8 @@ function ZoneManagerPanel(props: BooleanZoneManagerPanelProps) { 'width': '24px', 'height': '24px', 'borderRadius': 0, - 'color': theme => - theme.palette.mode === 'dark' + 'color': ({ palette }: Theme) => + palette.mode === 'dark' ? 'text.primary' : 'secondary.main', '&:hover': { @@ -398,7 +399,7 @@ function ZoneManagerPanel(props: BooleanZoneManagerPanelProps) { ref={backdropRef} sx={{ color: '#fff', - zIndex: theme => theme.zIndex.drawer + 1, + zIndex: ({ zIndex }: Theme) => zIndex.drawer + 1, position: 'absolute', left: 0, bottom: 0, diff --git a/src/ThreeEditor/js/Storage.js b/src/ThreeEditor/js/Storage.js index 22485481f..2af75efdb 100644 --- a/src/ThreeEditor/js/Storage.js +++ b/src/ThreeEditor/js/Storage.js @@ -20,6 +20,21 @@ function Storage() { let database; + const deleteDatabase = function () { + const req = indexedDB.deleteDatabase(name); + req.onsuccess = function () { + console.log('Deleted database successfully'); + }; + + req.onerror = function () { + console.log('Could not delete database'); + }; + + req.onblocked = function () { + console.log('Could not delete database due to the operation being blocked'); + }; + }; + return { init: function (callback) { const request = indexedDB.open(name, version); @@ -39,27 +54,42 @@ function Storage() { request.onerror = function (event) { console.error('IndexedDB', event); + // delete database + deleteDatabase(); }; }, get: function (callback) { - const transaction = database.transaction(['states'], 'readwrite'); - const objectStore = transaction.objectStore('states'); - const request = objectStore.get(0); - request.onsuccess = function (event) { - callback(event.target.result); - }; + try { + const transaction = database.transaction(['states'], 'readwrite'); + const objectStore = transaction.objectStore('states'); + const request = objectStore.get(0); + request.onsuccess = function (event) { + callback(event.target.result); + }; + } catch (error) { + console.error(error); + deleteDatabase(); + } }, set: function (data) { - const start = performance.now(); - - const transaction = database.transaction(['states'], 'readwrite'); - const objectStore = transaction.objectStore('states'); - const request = objectStore.put(data, 0); - request.onsuccess = function () { - devLog('Saved state to IndexedDB.', `${(performance.now() - start).toFixed(2)}ms`); - }; + try { + const start = performance.now(); + + const transaction = database.transaction(['states'], 'readwrite'); + const objectStore = transaction.objectStore('states'); + const request = objectStore.put(data, 0); + request.onsuccess = function () { + devLog( + 'Saved state to IndexedDB.', + `${(performance.now() - start).toFixed(2)}ms` + ); + }; + } catch (error) { + console.error(error); + deleteDatabase(); + } }, clear: function () { diff --git a/src/WrapperApp/components/InputEditor/InputFilesEditor.tsx b/src/WrapperApp/components/InputEditor/InputFilesEditor.tsx index e2ef43e65..ce35c7f07 100644 --- a/src/WrapperApp/components/InputEditor/InputFilesEditor.tsx +++ b/src/WrapperApp/components/InputEditor/InputFilesEditor.tsx @@ -5,6 +5,7 @@ import CodeEditor from '@uiw/react-textarea-code-editor'; import { useConfig } from '../../../config/ConfigService'; import { useAuth } from '../../../services/AuthService'; import { useDialog } from '../../../services/DialogService'; +import { useShSimulation } from '../../../services/ShSimulatorService'; import { useStore } from '../../../services/StoreService'; import { SimulatorType } from '../../../types/RequestTypes'; import { @@ -26,8 +27,9 @@ interface InputFilesEditorProps { } export function InputFilesEditor(props: InputFilesEditorProps) { - const [open] = useDialog('runSimulation'); - const { setTrackedId } = useStore(); + const { open: openRunSimulationDialog } = useDialog('runSimulation'); + const { postJobDirect, postJobBatch } = useShSimulation(); + const { yaptideEditor, setTrackedId } = useStore(); const { demoMode } = useConfig(); const { isAuthorized } = useAuth(); const inputFiles = props.inputFiles ?? _defaultShInputFiles; @@ -85,12 +87,16 @@ export function InputFilesEditor(props: InputFilesEditorProps) { variant='contained' disabled={demoMode || !isAuthorized} onClick={() => - open({ + yaptideEditor && + openRunSimulationDialog({ inputFiles: Object.fromEntries( Object.entries(inputFiles).filter(([, data]) => data.length > 0) ), simulator: props.simulator, - onSubmit: setTrackedId + onSubmit: setTrackedId, + postJobDirect, + postJobBatch, + yaptideEditor }) }> Run with these input files diff --git a/src/WrapperApp/components/Panels/LoginPanel.tsx b/src/WrapperApp/components/Panels/LoginPanel.tsx index 4451304cf..d246660dd 100644 --- a/src/WrapperApp/components/Panels/LoginPanel.tsx +++ b/src/WrapperApp/components/Panels/LoginPanel.tsx @@ -3,10 +3,12 @@ import { ChangeEvent, useCallback, useEffect, useState } from 'react'; import { useConfig } from '../../../config/ConfigService'; import { useAuth } from '../../../services/AuthService'; +import { useKeycloakAuth } from '../../../services/KeycloakAuthService'; export default function LoginPanel() { const { altAuth } = useConfig(); - const { login, tokenLogin } = useAuth(); + const { login } = useAuth(); + const { keycloak, initialized } = useKeycloakAuth(); const theme = useTheme(); const [username, setUsername] = useState(''); @@ -34,6 +36,10 @@ export default function LoginPanel() { return () => document.removeEventListener('keydown', handleEnter); }, [handleEnter]); + const keycloakLogin = useCallback(() => { + if (initialized && !keycloak.authenticated) keycloak.login(); + }, [initialized, keycloak]); + return ( + onClick={keycloakLogin}> Connect with PLGrid diff --git a/src/WrapperApp/components/Results/ResultsPanel.tsx b/src/WrapperApp/components/Results/ResultsPanel.tsx index 1ef1df045..d4a6aaacd 100644 --- a/src/WrapperApp/components/Results/ResultsPanel.tsx +++ b/src/WrapperApp/components/Results/ResultsPanel.tsx @@ -9,7 +9,7 @@ import { Tabs, Typography } from '@mui/material'; -import { SyntheticEvent, useEffect, useState } from 'react'; +import { ChangeEvent, SyntheticEvent, useEffect, useState } from 'react'; import { Estimator, generateGraphs, isPage0d, Page, Page0D } from '../../../JsRoot/GraphData'; import { useDialog } from '../../../services/DialogService'; @@ -25,8 +25,8 @@ export interface EstimatorResults extends Estimator { } function ResultsPanel() { - const [open] = useDialog('saveFile'); - const { resultsSimulationData: simulation } = useStore(); + const { open: openSaveFileDialog } = useDialog('saveFile'); + const { yaptideEditor, resultsSimulationData: simulation } = useStore(); const [tabsValue, setTabsValue] = useState(0); const [estimatorsResults, setEstimatorsResults] = useState([]); @@ -42,11 +42,13 @@ function ResultsPanel() { }; const onClickSaveToFile = () => { - open({ - name: `${titleToKebabCase(simulation?.title ?? 'simulation')}-result.json`, - results: simulation, - disableCheckbox: true - }); + if (yaptideEditor) + openSaveFileDialog({ + name: `${titleToKebabCase(simulation?.title ?? 'simulation')}-result.json`, + results: simulation, + disableCheckbox: true, + yaptideEditor + }); }; const parseEstimators = (estimators: Estimator[]) => { @@ -107,7 +109,9 @@ function ResultsPanel() { control={ setGroupQuantities(e.target.checked)} + onChange={(e: ChangeEvent) => + setGroupQuantities(e.target.checked) + } disabled={!resultsGeneratedFromProjectFile} /> } diff --git a/src/WrapperApp/components/Simulation/RunSimulationForm.tsx b/src/WrapperApp/components/Simulation/RunSimulationForm.tsx index 32cc852ca..36893c796 100644 --- a/src/WrapperApp/components/Simulation/RunSimulationForm.tsx +++ b/src/WrapperApp/components/Simulation/RunSimulationForm.tsx @@ -244,8 +244,9 @@ export function RunSimulationForm({ size='small' label='Simulation software' defaultValue={forwardedSimulator} - onChange={evn => setSelectedSimulator(evn.target.value as SimulatorType)} - > + onChange={evn => + setSelectedSimulator(evn.target.value as SimulatorType) + }> SHIELD-HIT12A Fluka diff --git a/src/WrapperApp/components/Simulation/SimulationCard.tsx b/src/WrapperApp/components/Simulation/SimulationCard.tsx index da567e69d..6ec257822 100644 --- a/src/WrapperApp/components/Simulation/SimulationCard.tsx +++ b/src/WrapperApp/components/Simulation/SimulationCard.tsx @@ -73,11 +73,11 @@ export default function SimulationCard({ showInputFiles, ...other }: SimulationCardProps) { - const { resultsSimulationData } = useStore(); + const { yaptideEditor, resultsSimulationData } = useStore(); const { loadFromJson } = useLoader(); const { getJobLogs, getJobInputs, getFullSimulationData } = useShSimulation(); const { enqueueSnackbar } = useSnackbar(); - const [open] = useDialog('saveFile'); + const { open: openSaveFileDialog } = useDialog('saveFile'); const [disableLoadJson, setDisableLoadJson] = useState(false); @@ -149,11 +149,13 @@ export default function SimulationCard({ const onClickSaveToFile = () => { getFullSimulationData(simulationStatus) .then((simulation: FullSimulationData | undefined) => { - open({ - name: `${titleToKebabCase(simulation?.title ?? 'simulation')}-result.json`, - results: simulation, - disableCheckbox: true - }); + if (yaptideEditor) + openSaveFileDialog({ + name: `${titleToKebabCase(simulation?.title ?? 'simulation')}-result.json`, + results: simulation, + disableCheckbox: true, + yaptideEditor + }); }) .catch(() => { enqueueSnackbar('Could not load simulation data', { variant: 'error' }); @@ -299,8 +301,8 @@ export default function SimulationCard({ component={Paper} sx={{ '& .MuiTableRow-root': { - backgroundColor: theme => - theme.palette.mode === 'dark' + backgroundColor: ({ palette }: Theme) => + palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)' } @@ -427,7 +429,9 @@ export default function SimulationCard({ color='info' size='small' onClick={onClickSaveToFile} - disabled={!Boolean(simulationStatus.jobState === StatusState.COMPLETED)}> + disabled={ + !(simulationStatus.jobState === StatusState.COMPLETED && yaptideEditor) + }> Save to file diff --git a/src/WrapperApp/components/Simulation/SimulationCardGrid.tsx b/src/WrapperApp/components/Simulation/SimulationCardGrid.tsx index b2206b93d..eb1f02cd3 100644 --- a/src/WrapperApp/components/Simulation/SimulationCardGrid.tsx +++ b/src/WrapperApp/components/Simulation/SimulationCardGrid.tsx @@ -9,11 +9,13 @@ import { CircularProgress, Grid, GridProps, + Theme, Typography } from '@mui/material'; import { FC, useState } from 'react'; import { useDialog } from '../../../services/DialogService'; +import { useShSimulation } from '../../../services/ShSimulatorService'; import { useStore } from '../../../services/StoreService'; import { SimulatorType } from '../../../types/RequestTypes'; import { JobStatusData, SimulationInputFiles } from '../../../types/ResponseTypes'; @@ -66,6 +68,10 @@ const stylesByLayout: Record { + return layout in stylesByLayout; +}; + export function SimulationCardGrid({ simulations, layout, @@ -79,7 +85,7 @@ export function SimulationCardGrid({ let gridContainerProps: GridProps = { container: true }; let gridItemProps: GridProps = { item: true }; - if (layout in stylesByLayout) { + if (validGriLayout(layout)) { gridContainerProps = { ...gridContainerProps, ...stylesByLayout[layout].gridContainerProps @@ -119,20 +125,20 @@ export function SimulationCardGrid({ ) : ( palette.text.disabled} + color={({ palette }: Theme) => palette.text.disabled} sx={{ textAlign: 'center', width: '100%', - p: ({ spacing }) => spacing(8, 4), + p: ({ spacing }: Theme) => spacing(8, 4), display: 'flex', alignItems: 'center', justifyContent: 'center' }}> spacing(0, 2), - pb: ({ spacing }) => spacing(0.5), - fontSize: ({ spacing }) => spacing(4) + m: ({ spacing }: Theme) => spacing(0, 2), + pb: ({ spacing }: Theme) => spacing(0.5), + fontSize: ({ spacing }: Theme) => spacing(4) }} /> No simulations found @@ -141,7 +147,7 @@ export function SimulationCardGrid({ ) : ( spacing(4) + p: ({ spacing }: Theme) => spacing(4) }} /> )} @@ -173,13 +179,13 @@ export function PaginatedSimulationCardGrid({ isAccordion={isAccordion} simulations={simulations} layout={layout} - header={accordion => + header={(accordion: SimulationAccordionProps) => SimulationBackendHeader({ title, subtitle, accordion, sx: { - mb: ({ spacing }) => spacing(0) + mb: ({ spacing }: Theme) => spacing(0) }, children, ...pageData @@ -190,8 +196,8 @@ export function PaginatedSimulationCardGrid({ ...pageData, stickTo: 'bottom', sx: { - mt: ({ spacing }) => spacing(0), - zIndex: ({ zIndex }) => zIndex.appBar + mt: ({ spacing }: Theme) => spacing(0), + zIndex: ({ zIndex }: Theme) => zIndex.appBar } }) } @@ -210,7 +216,9 @@ export function PaginatedSimulationsFromBackend({ ...other }: SimulationsFromBackendProps) { const { setTrackedId } = useStore(); - const [open] = useDialog('runSimulation'); + const { open: openRunSimulationDialog } = useDialog('runSimulation'); + const { yaptideEditor } = useStore(); + const { postJobDirect, postJobBatch } = useShSimulation(); return ( @@ -222,18 +230,22 @@ export function PaginatedSimulationsFromBackend({ variant='contained' color='info' startIcon={} - disabled={!isBackendAlive} + disabled={!(isBackendAlive && yaptideEditor)} onClick={() => - open({ + yaptideEditor && + openRunSimulationDialog({ onSubmit: setTrackedId, - simulator: SimulatorType.SHIELDHIT + simulator: SimulatorType.SHIELDHIT, + yaptideEditor, + postJobDirect, + postJobBatch }) }> Run new simulation spacing(2) + p: ({ spacing }: Theme) => spacing(2) }} isBackendAlive={isBackendAlive} /> @@ -271,16 +283,16 @@ export function AccordionCardGrid({ '&:before': { display: 'none' }, - 'p': ({ spacing }) => spacing(0) + 'p': ({ spacing }: Theme) => spacing(0) }}> spacing(0), - m: ({ spacing }) => spacing(0), + p: ({ spacing }: Theme) => spacing(0), + m: ({ spacing }: Theme) => spacing(0), position: 'sticky', - inset: ({ spacing }) => spacing(0, 0, 0, 0), - zIndex: ({ zIndex }) => zIndex.appBar + 1, - mb: ({ spacing }) => (expanded ? spacing(7) : spacing(0)) + inset: ({ spacing }: Theme) => spacing(0, 0, 0, 0), + zIndex: ({ zIndex }: Theme) => zIndex.appBar + 1, + mb: ({ spacing }: Theme) => (expanded ? spacing(7) : spacing(0)) }}> {header && header( @@ -291,7 +303,7 @@ export function AccordionCardGrid({ (expanded ? spacing(-14) : spacing(0)), + mt: ({ spacing }: Theme) => (expanded ? spacing(-14) : spacing(0)), p: 0, ...sx }}> @@ -322,12 +334,12 @@ export function DemoCardGrid({ isAccordion={isAccordion} simulations={simulations} layout={layout} - header={accordion => + header={(accordion: SimulationAccordionProps) => SimulationLabelBar({ title: title, accordion, sx: { - mb: ({ spacing }) => spacing(accordion.expanded ? 4 : -2) + mb: ({ spacing }: Theme) => spacing(accordion.expanded ? 4 : -2) } }) } diff --git a/src/services/AuthService.tsx b/src/services/AuthService.tsx index 7051a6819..e6e0f9355 100644 --- a/src/services/AuthService.tsx +++ b/src/services/AuthService.tsx @@ -1,5 +1,4 @@ -import { Backdrop, CircularProgress, Typography } from '@mui/material'; -import Keycloak from 'keycloak-js'; +import { Backdrop, CircularProgress, Theme, Typography } from '@mui/material'; import ky, { HTTPError } from 'ky'; import { KyInstance } from 'ky/distribution/types/ky'; import { useSnackbar } from 'notistack'; @@ -16,8 +15,9 @@ import { import { hasFields } from '../util/customGuards'; import useIntervalAsync from '../util/hooks/useIntervalAsync'; import { snakeToCamelCase } from '../util/Notation/Notation'; +import { useDialog } from './DialogService'; import { createGenericContext, GenericContextProviderProps } from './GenericContext'; -import { keycloakConfig } from './keycloakConfig'; +import { useKeycloakAuth } from './KeycloakAuthService'; type AuthUser = Pick; @@ -67,14 +67,12 @@ export interface AuthContext { isAuthorized: boolean; isServerReachable: boolean; login: (...args: RequestAuthLogin) => void; - tokenLogin: () => void; logout: (...args: RequestAuthLogout) => void; refresh: (...args: RequestAuthRefresh) => void; authKy: KyInstance; } const [useAuth, AuthContextProvider] = createGenericContext(); -const keycloak = new Keycloak(keycloakConfig); const ignored_messages = ['No token provided']; const Auth = ({ children }: GenericContextProviderProps) => { @@ -82,9 +80,13 @@ const Auth = ({ children }: GenericContextProviderProps) => { const [user, setUser] = useState(load(StorageKey.USER, isAuthUser)); const [reachInterval, setReachInterval] = useState(); const [refreshInterval, setRefreshInterval] = useState(180000); // 3 minutes in ms default interval for refresh token + const { keycloak, initialized } = useKeycloakAuth(); const [keyCloakInterval, setKeyCloakInterval] = useState(); const [isServerReachable, setIsServerReachable] = useState(null); const { enqueueSnackbar } = useSnackbar(); + const { open: openRejectKeycloakDialog } = useDialog('rejectKeycloak'); + + const isAuthorized = useMemo(() => user !== null || demoMode, [demoMode, user]); useEffect(() => { setReachInterval(isServerReachable ? 180000 : undefined); @@ -168,67 +170,82 @@ const Auth = ({ children }: GenericContextProviderProps) => { }, [reachServer]); useEffect(() => { - if (user !== null && user.source !== 'keycloak' && isServerReachable) + if (!demoMode && user?.source !== 'keycloak' && isServerReachable) setRefreshInterval(prev => (prev === undefined ? 3000 : prev)); // 3 seconds in ms default interval for refresh when logged in with username and password - else if (!isServerReachable || !user?.source) setRefreshInterval(undefined); - }, [isServerReachable, user]); + else setRefreshInterval(undefined); + }, [demoMode, isServerReachable, user]); const tokenVerification = useCallback(() => { - if (!keycloak.authenticated) return Promise.reject(); + if (!initialized || !keycloak.authenticated) return; const username = keycloak.tokenParsed?.preferred_username; - return kyRef - .post(`auth/keycloak`, { - headers: { - Authorization: `Bearer ${keycloak.token}` - }, - json: { - username - } - }) - .then(() => { - setUser({ - username, - source: 'keycloak' - }); - }) - .catch((_: HTTPError) => { - setUser(null); - setRefreshInterval(undefined); + /** + * TODO: Check if user is authorized to use this application (e.g. by checking if user is in a certain group) + */ + const validUser = true; + + if (!validUser) + openRejectKeycloakDialog({ + reason: 'You are not authorized to use this application.', + keycloakAuth: { keycloak, initialized } }); - }, [kyRef]); + else if (initialized) + kyRef + .post(`auth/keycloak`, { + headers: { + Authorization: `Bearer ${keycloak.token}` + }, + json: { + username + } + }) + .json() + .then(({ accessExp }) => { + setUser(prev => + prev?.username === username + ? prev + : { + username, + source: 'keycloak' + } + ); + setRefreshInterval(getRefreshDelay(accessExp)); + }) + .catch((err: HTTPError) => { + setUser(null); + setRefreshInterval(undefined); + openRejectKeycloakDialog({ + reason: + err.response?.status === 403 + ? 'You are not authorized to use this application.' + : err.message, + keycloakAuth: { keycloak, initialized } + }); + }); + }, [initialized, keycloak, kyRef, openRejectKeycloakDialog]); useEffect(() => { - keycloak - .init({ - pkceMethod: 'S256', - checkLoginIframe: false - }) - .then(auth => { - console.log('after init', auth); - - if (auth) { - setKeyCloakInterval( - keycloak.tokenParsed?.exp !== undefined - ? getRefreshDelay(keycloak.tokenParsed.exp * 1000) - : undefined - ); - tokenVerification(); - } - }); - }, [tokenVerification]); + if (initialized && keycloak.authenticated) + setKeyCloakInterval( + keycloak.tokenParsed?.exp !== undefined + ? getRefreshDelay(keycloak.tokenParsed.exp * 1000) + : undefined + ); + tokenVerification(); + }, [initialized, keycloak, tokenVerification]); const logout = useCallback(() => { setUser(null); setRefreshInterval(undefined); + setKeyCloakInterval(undefined); - if (user?.source === 'keycloak') keycloak.logout(); + if (initialized && keycloak.authenticated) keycloak.logout(); kyRef .delete(`auth/logout`) .json() .catch((_: HTTPError) => {}); - }, [kyRef, user?.source]); + }, [initialized, keycloak, kyRef]); const login = useCallback( (...[username, password]: RequestAuthLogin) => { @@ -252,6 +269,8 @@ const Auth = ({ children }: GenericContextProviderProps) => { ); const tokenRefresh = useCallback(() => { + if (!initialized) return Promise.resolve(); + return keycloak .updateToken(300) // 5 minutes in seconds minimum remaining lifetime for token before refresh is allowed .then(refreshed => { @@ -262,14 +281,15 @@ const Auth = ({ children }: GenericContextProviderProps) => { ); }) .catch(reason => {}); - }, [enqueueSnackbar]); + }, [enqueueSnackbar, keycloak, initialized]); - useIntervalAsync(tokenRefresh, keycloak.authenticated ? keyCloakInterval : undefined); - - const isAuthorized = useMemo(() => user !== null || demoMode, [demoMode, user]); + useIntervalAsync( + tokenRefresh, + initialized && keycloak.authenticated ? keyCloakInterval : undefined + ); const refresh = useCallback(async () => { - if (user?.source === 'keycloak' && isAuthorized) return tokenVerification(); + if (user?.source === 'keycloak' && isAuthorized) return await tokenVerification(); if (demoMode || !isServerReachable) { setRefreshInterval(undefined); @@ -285,7 +305,7 @@ const Auth = ({ children }: GenericContextProviderProps) => { return setRefreshInterval(getRefreshDelay(accessExp)); } catch (_) {} - }, [demoMode, isAuthorized, isServerReachable, kyIntervalRef, tokenVerification, user?.source]); + }, [demoMode, isAuthorized, isServerReachable, kyIntervalRef, tokenVerification, user]); const authKy = useMemo(() => kyRef, [kyRef]); @@ -302,13 +322,17 @@ const Auth = ({ children }: GenericContextProviderProps) => { isAuthorized, isServerReachable: Boolean(isServerReachable), login, - tokenLogin: keycloak.login, logout, authKy, refresh }}> {children} - + zIndex.drawer + 1, + color: '#fff' + }}> Waiting for verification... diff --git a/src/services/DialogService.tsx b/src/services/DialogService.tsx index ccbae1d5d..387449122 100644 --- a/src/services/DialogService.tsx +++ b/src/services/DialogService.tsx @@ -5,6 +5,7 @@ import { EditProjectInfoDialog } from '../ThreeEditor/components/Dialog/EditProj import { LoadFileDialog } from '../ThreeEditor/components/Dialog/LoadFileDialog'; import { NewProjectDialog } from '../ThreeEditor/components/Dialog/NewProjectDialog'; import { OpenFileDialog } from '../ThreeEditor/components/Dialog/OpenFileDialog'; +import { RejectKeycloakUserDialog } from '../ThreeEditor/components/Dialog/RejectKeycloakUserDialog'; import { RunSimulationDialog } from '../ThreeEditor/components/Dialog/RunSimulationDialog'; import { SaveFileDialog } from '../ThreeEditor/components/Dialog/SaveFileDialog'; import { @@ -35,7 +36,8 @@ export type DialogComponentTypeMap = EntriesToObj< ValidComponentTypes<['openFile', typeof OpenFileDialog]>, ValidComponentTypes<['runSimulation', typeof RunSimulationDialog]>, ValidComponentTypes<['saveFile', typeof SaveFileDialog]>, - ValidComponentTypes<['editProject', typeof EditProjectInfoDialog]> + ValidComponentTypes<['editProject', typeof EditProjectInfoDialog]>, + ValidComponentTypes<['rejectKeycloak', typeof RejectKeycloakUserDialog]> ] >; @@ -43,17 +45,21 @@ const [useDialogContext, DialogContextProvider] = createGenericContext( name: T -): readonly [(props: RestDialogPropsType[T]) => void, () => void, boolean]; +): { + open: (props: RestDialogPropsType[T]) => void; + close: () => void; + isOpen: boolean; +}; function useDialog( name: T -): readonly [(props?: RestDialogPropsType[T]) => void, () => void, boolean]; +): { open: (props?: RestDialogPropsType[T]) => void; close: () => void; isOpen: boolean }; function useDialog( name: T -): readonly [ - ((props?: RestDialogPropsType[T]) => void) | ((props: RestDialogPropsType[T]) => void), - () => void, - boolean -] { +): { + open: ((props?: RestDialogPropsType[T]) => void) | ((props: RestDialogPropsType[T]) => void); + close: () => void; + isOpen: boolean; +} { const { openDialog, closeDialog, getIsOpen } = useDialogContext(); const open = useCallback( (props: RestDialogPropsType[T] = {} as RestDialogPropsType[T]) => { @@ -67,7 +73,7 @@ function useDialog( }, [name, closeDialog]); const isOpen = useMemo(() => getIsOpen(name), [name, getIsOpen]); - return [open, close, isOpen] as const; + return { open, close, isOpen }; } const DialogProvider = ({ children }: GenericContextProviderProps) => { @@ -104,7 +110,8 @@ const DialogProvider = ({ children }: GenericContextProviderProps) => { openFile: OpenFileDialog, runSimulation: RunSimulationDialog, saveFile: SaveFileDialog, - editProject: EditProjectInfoDialog + editProject: EditProjectInfoDialog, + rejectKeycloak: RejectKeycloakUserDialog }), [] ); diff --git a/src/services/GenericContext.ts b/src/services/GenericContext.ts index 4190277a0..7821375ce 100644 --- a/src/services/GenericContext.ts +++ b/src/services/GenericContext.ts @@ -21,3 +21,25 @@ export const createGenericContext = () => { return [useGenericContext, genericContext.Provider] as const; }; + +export const createSubstituteContext = (useSubstituted: () => T) => { + // Create a context with a generic parameter or undefined + const genericContext = createContext(undefined); + + // Check if the value provided to the context is defined or call the substitute + const useSubstituteOrGenericContext = () => { + const contextIsDefined = useContext(genericContext); + + try { + return useSubstituted(); + } catch (e) { + if (!contextIsDefined) { + throw new Error('useSubstituteOrGenericContext must be used within a Provider'); + } + + return contextIsDefined; + } + }; + + return [useSubstituteOrGenericContext, genericContext.Provider] as const; +}; diff --git a/src/services/KeycloakAuthService.tsx b/src/services/KeycloakAuthService.tsx new file mode 100644 index 000000000..7e7da738d --- /dev/null +++ b/src/services/KeycloakAuthService.tsx @@ -0,0 +1,61 @@ +import Keycloak, { KeycloakInitOptions } from 'keycloak-js'; +import { KeycloakProvider, useKeycloak } from 'keycloak-react-web'; +import { ReactNode, useCallback } from 'react'; + +import { useConfig } from '../config/ConfigService'; +import { createSubstituteContext, GenericContextProviderProps } from './GenericContext'; + +const keycloakParams = { + url: `${process.env.REACT_APP_KEYCLOAK_BASE_URL ?? 'https://localhost:8080'}/auth/`, + realm: `${process.env.REACT_APP_KEYCLOAK_REALM ?? ''}`, + clientId: `${process.env.REACT_APP_KEYCLOAK_CLIENT_ID ?? ''}` +}; + +const authInstance = new Keycloak(keycloakParams) as any; + +const initOptions = { + pkceMethod: 'S256', + onLoad: 'check-sso', + checkLoginIframe: false, + enableLogging: false +} as const satisfies KeycloakInitOptions; + +export type KeycloakAuthContext = + | { + initialized: false; + keycloak?: Keycloak; + } + | { + initialized: true; + keycloak: Keycloak; + }; + +const [useKeycloakAuth, KeycloakAuthContextProvider] = + createSubstituteContext(useKeycloak); + +const KeycloakAuth = ({ children }: GenericContextProviderProps) => { + const { altAuth } = useConfig(); + const proxyContextProvider = useCallback( + (children: ReactNode) => ( + + {children} + + ), + [] + ); + + return altAuth ? ( + + {proxyContextProvider(children)} + + ) : ( + proxyContextProvider(children) + ); +}; + +export { KeycloakAuth, useKeycloakAuth }; diff --git a/src/services/LoaderService.tsx b/src/services/LoaderService.tsx index cc9658717..cfcf17b1d 100644 --- a/src/services/LoaderService.tsx +++ b/src/services/LoaderService.tsx @@ -43,7 +43,7 @@ const readFile = (file: File) => { }; const Loader = ({ children }: GenericContextProviderProps) => { - const [open] = useDialog('loadFile'); + const { open: openLoadFileDialog } = useDialog('loadFile'); const { yaptideEditor, setResultsSimulationData, setLocalResultsSimulationData } = useStore(); const [, setUrlInPath] = useState(); @@ -53,10 +53,12 @@ const Loader = ({ children }: GenericContextProviderProps) => { switch (type) { case 'Editor': - open({ - data: json, - validVersion: json.metadata.version === JSON_VERSION - }); + if (yaptideEditor) + openLoadFileDialog({ + data: json, + validVersion: json.metadata.version === JSON_VERSION, + yaptideEditor + }); break; default: @@ -65,7 +67,7 @@ const Loader = ({ children }: GenericContextProviderProps) => { break; } }, - [open] + [openLoadFileDialog, yaptideEditor] ); const loadData = useCallback( diff --git a/src/services/keycloakConfig.ts b/src/services/keycloakConfig.ts deleted file mode 100644 index 75f2b30f8..000000000 --- a/src/services/keycloakConfig.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const keycloakConfig = { - url: 'https://sso.pre.plgrid.pl/auth/', - realm: 'PLGrid', - clientId: 'yaptide-staging', - enableLogging: true -};