diff --git a/packages/threat-composer/src/components/generic/WindowExporter/index.tsx b/packages/threat-composer/src/components/generic/WindowExporter/index.tsx new file mode 100644 index 00000000..7c527b3b --- /dev/null +++ b/packages/threat-composer/src/components/generic/WindowExporter/index.tsx @@ -0,0 +1,105 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ +import { useCallback, FC, PropsWithChildren, useEffect } from 'react'; +import { useWorkspacesContext } from '../../../contexts'; +import { ThreatComposerNamespace } from '../../../customTypes/dataExchange'; +import useExportImport, { + PLACEHOLDER_EXCHANGE_DATA, + PLACEHOLDER_EXCHANGE_DATA_FOR_WORKSPACE, +} from '../../../hooks/useExportImport'; +import useRemoveData from '../../../hooks/useRemoveData'; + +declare global { + interface Window { + threatcomposer: ThreatComposerNamespace; + } +} + +const stringifyWorkspaceData = (data: any) => { + return JSON.stringify(data, null, 2); +}; + +window.threatcomposer = { + getWorkspaceList: () => [PLACEHOLDER_EXCHANGE_DATA_FOR_WORKSPACE], + getCurrentWorkspaceMetadata: () => PLACEHOLDER_EXCHANGE_DATA_FOR_WORKSPACE, + getCurrentWorkspaceData: () => PLACEHOLDER_EXCHANGE_DATA, + stringifyWorkspaceData, + setCurrentWorkspaceData: () => Promise.resolve(), + switchWorkspace: () => {}, + createWorkspace: () => + Promise.resolve(PLACEHOLDER_EXCHANGE_DATA_FOR_WORKSPACE), + deleteWorkspace: () => Promise.resolve(), + renameWorkspace: () => Promise.resolve(), +}; + +/** + * Export threat-composer functionalities via window object. + */ +const WindowExporter: FC> = ({ children }) => { + const { getWorkspaceData, parseImportedData, importData } = useExportImport(); + const { + currentWorkspace, + workspaceList, + addWorkspace, + switchWorkspace, + renameWorkspace, + } = useWorkspacesContext(); + const { deleteWorkspace } = useRemoveData(); + + const setWorkspaceData = useCallback( + async (data: any) => { + const parsedData = parseImportedData(data); + await importData(parsedData); + }, + [importData], + ); + + useEffect(() => { + window.threatcomposer.getWorkspaceList = () => workspaceList; + }, [workspaceList]); + + useEffect(() => { + window.threatcomposer.getCurrentWorkspaceMetadata = () => currentWorkspace; + }, [currentWorkspace]); + + useEffect(() => { + window.threatcomposer.getCurrentWorkspaceData = getWorkspaceData; + }, [getWorkspaceData]); + + useEffect(() => { + window.threatcomposer.setCurrentWorkspaceData = setWorkspaceData; + }, [setWorkspaceData]); + + useEffect(() => { + window.threatcomposer.createWorkspace = addWorkspace; + }, [addWorkspace]); + + useEffect(() => { + window.threatcomposer.deleteWorkspace = deleteWorkspace; + }, [deleteWorkspace]); + + useEffect(() => { + window.threatcomposer.switchWorkspace = switchWorkspace; + }, [switchWorkspace]); + + useEffect(() => { + window.threatcomposer.renameWorkspace = renameWorkspace; + }, [renameWorkspace]); + + return <>{children}; +}; + +export default WindowExporter; diff --git a/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx b/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx index b89c82c1..0fc9e3ba 100644 --- a/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx +++ b/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx @@ -27,7 +27,7 @@ import Input from '../../generic/Input'; export interface EditWorkspaceProps { visible: boolean; setVisible: React.Dispatch>; - onConfirm: (workspace: string) => void; + onConfirm: (workspace: string) => Promise; value?: string; editMode?: boolean; } @@ -42,8 +42,8 @@ const EditWorkspace: FC = ({ const inputRef= useRef(); const [value, setValue] = useState(props.value || ''); - const handleConfirm = useCallback(() => { - onConfirm(value); + const handleConfirm = useCallback(async () => { + await onConfirm(value); setVisible(false); }, [onConfirm, value]); diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx index f449a021..193d8bca 100644 --- a/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx +++ b/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx @@ -60,7 +60,7 @@ const WorkspaceSelector: FC> = ({ const { workspaceExamples } = useWorkspaceExamplesContext(); const { importData, exportAll, exportSelectedThreats } = useImportExport(); - const { removeData, deleteCurrentWorkspace } = useRemoveData(); + const { removeData, deleteWorkspace } = useRemoveData(); const { composerMode, onPreview, @@ -109,10 +109,7 @@ const WorkspaceSelector: FC> = ({ if (selectedItem.value === DEFAULT_WORKSPACE_ID) { switchWorkspace(null); } else { - selectedItem.value && selectedItem.label && switchWorkspace({ - id: selectedItem.value, - name: selectedItem.label, - }); + selectedItem.value && selectedItem.label && switchWorkspace(selectedItem.value); } }, [switchWorkspace]); @@ -147,7 +144,7 @@ const WorkspaceSelector: FC> = ({ exportAll, exportSelectedThreats, filteredThreats, - deleteCurrentWorkspace, + deleteWorkspace, currentWorkspace, setRemoveDataModalVisible, setRemoveWorkspaceModalVisible, @@ -168,7 +165,7 @@ const WorkspaceSelector: FC> = ({ setIsRemovingWorkspace(true); try { - await deleteCurrentWorkspace(toDeleteWorkspaceId); + await deleteWorkspace(toDeleteWorkspaceId); } catch (e) { console.log('Error in deleting workspace', e); } finally { @@ -177,8 +174,8 @@ const WorkspaceSelector: FC> = ({ } }, []); - const handleImport = useCallback((data: DataExchangeFormat) => { - importData(data); + const handleImport = useCallback(async (data: DataExchangeFormat) => { + await importData(data); onImported?.(); }, [importData, onImported]); @@ -248,7 +245,9 @@ const WorkspaceSelector: FC> = ({ {addWorkspaceModalVisible && { + await addWorkspace(workspaceName); + }} />} {editWorkspaceModalVisible && currentWorkspace && > = (props) => { - const { getWorkspaceExample } = useWorkspaceExamplesContext(); + const { storageType, value } = useWorkspaceStorage(props.workspaceId); - return isWorkspaceExample(props.workspaceId) ? + return storageType === STORAGE_LOCAL_STATE ? () : (); }; diff --git a/packages/threat-composer/src/contexts/ArchitectureContext/index.tsx b/packages/threat-composer/src/contexts/ArchitectureContext/index.tsx index 380f6cd9..faed9a7b 100644 --- a/packages/threat-composer/src/contexts/ArchitectureContext/index.tsx +++ b/packages/threat-composer/src/contexts/ArchitectureContext/index.tsx @@ -18,16 +18,16 @@ import ArchitectureLocalStateContextProvider from './components/LocalStateContex import ArchitectureLocalStorageContextProvider from './components/LocalStorageContextProvider'; import { useArchitectureInfoContext } from './context'; import { ArchitectureContextProviderProps } from './types'; -import isWorkspaceExample from '../../utils/isWorkspaceExample'; -import { useWorkspaceExamplesContext } from '../WorkspaceExamplesContext'; +import { STORAGE_LOCAL_STATE } from '../../configs'; +import useWorkspaceStorage from '../../hooks/useWorkspaceStorage'; const ArchitectureContextProvider: FC> = (props) => { - const { getWorkspaceExample } = useWorkspaceExamplesContext(); + const { storageType, value } = useWorkspaceStorage(props.workspaceId); - return isWorkspaceExample(props.workspaceId) ? + return storageType === STORAGE_LOCAL_STATE ? () : (); }; diff --git a/packages/threat-composer/src/contexts/AssumptionLinksContext/index.tsx b/packages/threat-composer/src/contexts/AssumptionLinksContext/index.tsx index 7b00486e..2ac86f86 100644 --- a/packages/threat-composer/src/contexts/AssumptionLinksContext/index.tsx +++ b/packages/threat-composer/src/contexts/AssumptionLinksContext/index.tsx @@ -18,16 +18,16 @@ import AssumptionLinksLocalStateContextProvider from './components/LocalStateCon import AssumptionLinksLocalStorageContextProvider from './components/LocalStorageContextProvider'; import { useAssumptionLinksContext } from './context'; import { AssumptionLinksContextProviderProps } from './types'; -import isWorkspaceExample from '../../utils/isWorkspaceExample'; -import { useWorkspaceExamplesContext } from '../WorkspaceExamplesContext'; +import { STORAGE_LOCAL_STATE } from '../../configs'; +import useWorkspaceStorage from '../../hooks/useWorkspaceStorage'; const AssumptionLinksContextProvider: FC> = (props) => { - const { getWorkspaceExample } = useWorkspaceExamplesContext(); + const { storageType, value } = useWorkspaceStorage(props.workspaceId); - return isWorkspaceExample(props.workspaceId) ? + return storageType === STORAGE_LOCAL_STATE ? () : (); }; diff --git a/packages/threat-composer/src/contexts/AssumptionsContext/index.tsx b/packages/threat-composer/src/contexts/AssumptionsContext/index.tsx index 1b31ae40..3738ebe8 100644 --- a/packages/threat-composer/src/contexts/AssumptionsContext/index.tsx +++ b/packages/threat-composer/src/contexts/AssumptionsContext/index.tsx @@ -18,16 +18,16 @@ import AssumptionsLocalStateContextProvider from './components/LocalStateContext import AssumptionsLocalStorageContextProvider from './components/LocalStorageContextProvider'; import { useAssumptionsContext } from './context'; import { AssumptionsContextProviderProps } from './types'; -import isWorkspaceExample from '../../utils/isWorkspaceExample'; -import { useWorkspaceExamplesContext } from '../WorkspaceExamplesContext'; +import { STORAGE_LOCAL_STATE } from '../../configs'; +import useWorkspaceStorage from '../../hooks/useWorkspaceStorage'; const AssumptionsContextProvider: FC> = (props) => { - const { getWorkspaceExample } = useWorkspaceExamplesContext(); + const { storageType, value } = useWorkspaceStorage(props.workspaceId); - return isWorkspaceExample(props.workspaceId) ? + return storageType === STORAGE_LOCAL_STATE ? () : (); }; diff --git a/packages/threat-composer/src/contexts/ContextAggregator/index.tsx b/packages/threat-composer/src/contexts/ContextAggregator/index.tsx index eaa39a21..6779b509 100644 --- a/packages/threat-composer/src/contexts/ContextAggregator/index.tsx +++ b/packages/threat-composer/src/contexts/ContextAggregator/index.tsx @@ -17,6 +17,7 @@ import { FC, PropsWithChildren } from 'react'; import { ComposerMode, DataExchangeFormat, ViewNavigationEvent } from '../../customTypes'; import GlobalSetupContextProvider from '../GlobalSetupContext'; import WorkspaceContextAggregator from '../WorkspaceContextAggregator'; +import WorkspaceExamplesContext from '../WorkspaceExamplesContext'; import WorkspacesContextProvider, { WorkspacesContextProviderProps } from '../WorkspacesContext'; export interface ContextAggregatorProps extends ViewNavigationEvent { @@ -48,16 +49,18 @@ const ContextAggregator: FC> = ({ features={features} onDefineWorkload={onDefineWorkload} composerMode={composerMode}> - - {(workspaceId) => ( - {children} - )} - + + + {(workspaceId) => ( + {children} + )} + + ); }; diff --git a/packages/threat-composer/src/contexts/DataflowContext/index.tsx b/packages/threat-composer/src/contexts/DataflowContext/index.tsx index 5a35f674..709b6a63 100644 --- a/packages/threat-composer/src/contexts/DataflowContext/index.tsx +++ b/packages/threat-composer/src/contexts/DataflowContext/index.tsx @@ -18,16 +18,16 @@ import DataflowLocalStateContextProvider from './components/LocalStateContextPro import DataflowLocalStorageContextProvider from './components/LocalStorageContextProvider'; import { useDataflowInfoContext } from './context'; import { DataflowContextProviderProps } from './types'; -import isWorkspaceExample from '../../utils/isWorkspaceExample'; -import { useWorkspaceExamplesContext } from '../WorkspaceExamplesContext'; +import { STORAGE_LOCAL_STATE } from '../../configs'; +import useWorkspaceStorage from '../../hooks/useWorkspaceStorage'; const DataflowContextProvider: FC> = (props) => { - const { getWorkspaceExample } = useWorkspaceExamplesContext(); + const { storageType, value } = useWorkspaceStorage(props.workspaceId); - return isWorkspaceExample(props.workspaceId) ? + return storageType === STORAGE_LOCAL_STATE ? () : (); }; diff --git a/packages/threat-composer/src/contexts/MitigationLinksContext/index.tsx b/packages/threat-composer/src/contexts/MitigationLinksContext/index.tsx index 412684f7..f27c8704 100644 --- a/packages/threat-composer/src/contexts/MitigationLinksContext/index.tsx +++ b/packages/threat-composer/src/contexts/MitigationLinksContext/index.tsx @@ -18,16 +18,16 @@ import MitigationLinksLocalStateContextProvider from './components/LocalStateCon import MitigationLinksLocalStorageContextProvider from './components/LocalStorageContextProvider'; import { useMitigationLinksContext } from './context'; import { MitigationLinksContextProviderProps } from './types'; -import isWorkspaceExample from '../../utils/isWorkspaceExample'; -import { useWorkspaceExamplesContext } from '../WorkspaceExamplesContext'; +import { STORAGE_LOCAL_STATE } from '../../configs'; +import useWorkspaceStorage from '../../hooks/useWorkspaceStorage'; const MitigationLinksContextProvider: FC> = (props) => { - const { getWorkspaceExample } = useWorkspaceExamplesContext(); + const { storageType, value } = useWorkspaceStorage(props.workspaceId); - return isWorkspaceExample(props.workspaceId) ? + return storageType === STORAGE_LOCAL_STATE ? () : (); }; diff --git a/packages/threat-composer/src/contexts/MitigationsContext/index.tsx b/packages/threat-composer/src/contexts/MitigationsContext/index.tsx index 7d30b787..ec0b1459 100644 --- a/packages/threat-composer/src/contexts/MitigationsContext/index.tsx +++ b/packages/threat-composer/src/contexts/MitigationsContext/index.tsx @@ -18,16 +18,16 @@ import MitigationsLocalStateContextProvider from './components/LocalStateContext import MitigationsLocalStorageContextProvider from './components/LocalStorageContextProvider'; import { useMitigationsContext } from './context'; import { MitigationsContextProviderProps } from './types'; -import isWorkspaceExample from '../../utils/isWorkspaceExample'; -import { useWorkspaceExamplesContext } from '../WorkspaceExamplesContext'; +import { STORAGE_LOCAL_STATE } from '../../configs'; +import useWorkspaceStorage from '../../hooks/useWorkspaceStorage'; const MitigationsContextProvider: FC> = (props) => { - const { getWorkspaceExample } = useWorkspaceExamplesContext(); + const { storageType, value } = useWorkspaceStorage(props.workspaceId); - return isWorkspaceExample(props.workspaceId) ? + return storageType === STORAGE_LOCAL_STATE ? () : + initialValue={value?.mitigations} {...props} />) : (); }; diff --git a/packages/threat-composer/src/contexts/ThreatsContext/index.tsx b/packages/threat-composer/src/contexts/ThreatsContext/index.tsx index 1f817b7d..f8864ce3 100644 --- a/packages/threat-composer/src/contexts/ThreatsContext/index.tsx +++ b/packages/threat-composer/src/contexts/ThreatsContext/index.tsx @@ -18,17 +18,17 @@ import ThreatsLocalStateContextProvider from './components/LocalStateContextProv import ThreatsLocalStorageContextProvider from './components/LocalStorageContextProvider'; import { useThreatsContext } from './context'; import { ThreatsContextProviderProps } from './types'; +import { STORAGE_LOCAL_STATE } from '../../configs'; +import useWorkspaceStorage from '../../hooks/useWorkspaceStorage'; import ThreatsMigration from '../../migrations/ThreatsMigration'; -import isWorkspaceExample from '../../utils/isWorkspaceExample'; -import { useWorkspaceExamplesContext } from '../WorkspaceExamplesContext'; const ThreatsContextProvider: FC> = ({ children, ...props }) => { - const { getWorkspaceExample } = useWorkspaceExamplesContext(); + const { storageType, value } = useWorkspaceStorage(props.workspaceId); - return isWorkspaceExample(props.workspaceId) ? + return storageType === STORAGE_LOCAL_STATE ? ( {children} ) : diff --git a/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx b/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx index 3fab81c8..b82f671e 100644 --- a/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx +++ b/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx @@ -14,6 +14,7 @@ limitations under the License. ******************************************************************************************************************** */ import { FC, PropsWithChildren } from 'react'; +import WindowExporter from '../../components/generic/WindowExporter'; import { ComposerMode, DataExchangeFormat, ViewNavigationEvent } from '../../customTypes'; import ApplicationInfoContextProvider from '../ApplicationContext'; import ArchitectureInfoContextProvider from '../ArchitectureContext'; @@ -26,8 +27,6 @@ import MitigationPacksContextProvider from '../MitigationPacksContext'; import MitigationsContextProvider from '../MitigationsContext'; import ThreatPacksContextProvider from '../ThreatPacksContext'; import ThreatsContextProvider from '../ThreatsContext'; -import WorkspaceExamplesContext from '../WorkspaceExamplesContext'; - export interface WorkspaceContextAggregatorProps extends ViewNavigationEvent { workspaceId: string | null; composerMode?: ComposerMode; @@ -44,33 +43,34 @@ const WorkspaceContextInnerAggregator: FC { return ( - - - - - - - - - - - + + + + + + + + + + + + {children} - - - - - - - - - - - + + + + + + + + + + + ); }; diff --git a/packages/threat-composer/src/contexts/WorkspaceExamplesContext/index.tsx b/packages/threat-composer/src/contexts/WorkspaceExamplesContext/index.tsx index 7b4f5367..a8f26e5e 100644 --- a/packages/threat-composer/src/contexts/WorkspaceExamplesContext/index.tsx +++ b/packages/threat-composer/src/contexts/WorkspaceExamplesContext/index.tsx @@ -16,7 +16,7 @@ /** @jsxImportSource @emotion/react */ import { FC, PropsWithChildren, useCallback, useMemo } from 'react'; import { WorkspaceExamplesContext, useWorkspaceExamplesContext } from './context'; -import { EXAMPLES_WORKSPACE_ID_PREFIX } from '../../configs'; +import { EXAMPLES_WORKSPACE_ID_PREFIX, STORAGE_LOCAL_STATE } from '../../configs'; import workspaceExamplesData from '../../data/workspaceExamples/workspaceExamples'; const WorkspaceExamplesContextProvider: FC> = ({ @@ -26,6 +26,7 @@ const WorkspaceExamplesContextProvider: FC> = ({ return workspaceExamplesData.map(x => ({ ...x, id: `${EXAMPLES_WORKSPACE_ID_PREFIX}${x.name.replace(/\s/g, '')}`, + storageType: STORAGE_LOCAL_STATE, })); }, [workspaceExamplesData]); diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/context.ts b/packages/threat-composer/src/contexts/WorkspacesContext/context.ts index cbcc62ba..0aba280b 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/context.ts +++ b/packages/threat-composer/src/contexts/WorkspacesContext/context.ts @@ -15,15 +15,16 @@ ******************************************************************************************************************** */ import { useContext, createContext } from 'react'; import { ViewNavigationEvent, Workspace } from '../../customTypes'; +import { PLACEHOLDER_EXCHANGE_DATA_FOR_WORKSPACE } from '../../hooks/useExportImport'; export interface WorkspacesContextApi extends ViewNavigationEvent { workspaceList: Workspace[]; setWorkspaceList: (workspace: Workspace[]) => void; currentWorkspace: Workspace | null; + switchWorkspace: (workspaceId: string | null) => void; removeWorkspace: (id: string) => Promise; - addWorkspace: (workspaceName: string) => void; - renameWorkspace: (id: string, newWorkspaceName: string) => void; - switchWorkspace: (workspace: Workspace | null) => void; + addWorkspace: (workspaceName: string, storageType?: Workspace['storageType'], metadata?: Workspace['metadata']) => Promise; + renameWorkspace: (id: string, newWorkspaceName: string) => Promise; } const initialState: WorkspacesContextApi = { @@ -31,9 +32,9 @@ const initialState: WorkspacesContextApi = { setWorkspaceList: () => { }, currentWorkspace: null, switchWorkspace: () => { }, - addWorkspace: () => { }, + addWorkspace: () => Promise.resolve(PLACEHOLDER_EXCHANGE_DATA_FOR_WORKSPACE), removeWorkspace: () => Promise.resolve(), - renameWorkspace: () => { }, + renameWorkspace: () => Promise.resolve(), }; export const WorkspacesContext = createContext(initialState); diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx b/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx index 3307aa98..38e740b6 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx +++ b/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx @@ -21,6 +21,8 @@ import { DEFAULT_WORKSPACE_ID } from '../../configs/constants'; import { LOCAL_STORAGE_KEY_CURRENT_WORKSPACE, LOCAL_STORAGE_KEY_WORKSPACE_LIST } from '../../configs/localStorageKeys'; import { ViewNavigationEvent, Workspace } from '../../customTypes'; import WorkspacesMigration from '../../migrations/WorkspacesMigration'; +import isWorkspaceExample from '../../utils/isWorkspaceExample'; +import { useWorkspaceExamplesContext } from '../WorkspaceExamplesContext'; export interface WorkspacesContextProviderProps extends ViewNavigationEvent { workspaceId?: string; @@ -42,6 +44,8 @@ const WorkspacesContextProvider: FC = ({ defaultValue: [], }); + const { workspaceExamples } = useWorkspaceExamplesContext(); + useEffect(() => { if (workspaceId) { if (workspaceId === DEFAULT_WORKSPACE_ID && currentWorkspace !== null) { @@ -57,29 +61,51 @@ const WorkspacesContextProvider: FC = ({ } }, [workspaceId, workspaceList, currentWorkspace]); - const handleSwitchWorkspace = useCallback((workspace: Workspace | null) => { + const getWorkspace = useCallback((toBeSwitchedWorkspaceId: string | null) => { + const isExample = isWorkspaceExample(toBeSwitchedWorkspaceId); + if (isExample) { + return workspaceExamples.find(w => w.id === toBeSwitchedWorkspaceId) || null; + } + + if (toBeSwitchedWorkspaceId && toBeSwitchedWorkspaceId !== DEFAULT_WORKSPACE_ID) { + return workspaceList.find(w => w.id === toBeSwitchedWorkspaceId) || null; + } + + return null; + }, [ + workspaceExamples, + workspaceList, + ]); + + const handleSwitchWorkspace = useCallback((toBeSwitchedWorkspaceId: string | null) => { + const workspace = getWorkspace(toBeSwitchedWorkspaceId); setCurrentWorkspace(workspace); onWorkspaceChanged?.(workspace?.id || DEFAULT_WORKSPACE_ID); - }, [onWorkspaceChanged]); + }, [onWorkspaceChanged, getWorkspace]); - const handleAddWorkspace = useCallback((workspaceName: string) => { + const handleAddWorkspace = useCallback(async (workspaceName: string, + storageType?: Workspace['storageType'], + metadata?: Workspace['metadata']) => { const newWorkspace = { id: uuidv4(), name: workspaceName, + storageType, + metadata, }; setWorkspaceList(prev => prev.find(p => p.name === workspaceName) ? [...prev] : [...prev, newWorkspace]); setCurrentWorkspace(newWorkspace); onWorkspaceChanged?.(newWorkspace.id); + return newWorkspace; }, [onWorkspaceChanged]); const handleRemoveWorkspace = useCallback(async (id: string) => { setWorkspaceList(prev => prev.filter(p => p.id !== id)); }, []); - const handleRenameWorkspace = useCallback((id: string, newWorkspaceName: string) => { + const handleRenameWorkspace = useCallback(async (id: string, newWorkspaceName: string) => { setWorkspaceList(prev => { const index = prev.findIndex(w => w.id === id); - const newList = [...prev.slice(0, index - 1), { + const newList = [... index <= 1 ? [] : prev.slice(0, index - 1), { id, name: newWorkspaceName, }, ...prev.slice(index + 1)]; diff --git a/packages/threat-composer/src/customTypes/dataExchange.ts b/packages/threat-composer/src/customTypes/dataExchange.ts index edcc1a11..8467fc0a 100644 --- a/packages/threat-composer/src/customTypes/dataExchange.ts +++ b/packages/threat-composer/src/customTypes/dataExchange.ts @@ -20,7 +20,7 @@ import { AssumptionSchema, AssumptionLinkSchema } from './assumptions'; import { DataflowInfoSchema } from './dataflow'; import { MitigationSchema, MitigationLinkSchema } from './mitigations'; import { TemplateThreatStatementSchema } from './threats'; -import { WorkspaceSchema } from './workspaces'; +import { WorkspaceSchema, Workspace } from './workspaces'; export const DataExchangeFormatSchema = z.object({ schema: z.number(), @@ -58,4 +58,18 @@ export interface HasContentDetails { assumptions: boolean; mitigations: boolean; threats: boolean; +} + +export interface ThreatComposerNamespace { + getWorkspaceList: () => Workspace[]; + getCurrentWorkspaceMetadata: () => Workspace | null; + getCurrentWorkspaceData: () => DataExchangeFormat; + stringifyWorkspaceData: (arg0: any) => string; + setCurrentWorkspaceData: (arg0: DataExchangeFormat) => Promise; + switchWorkspace: (id: string | null) => void; + createWorkspace: (workspaceName: string, + storageType?: Workspace['storageType'], + metadata?: Workspace['metadata']) => Promise; + deleteWorkspace: (id: string) => Promise; + renameWorkspace: (id: string, newWorkspaceName: string) => Promise; } \ No newline at end of file diff --git a/packages/threat-composer/src/customTypes/entities.ts b/packages/threat-composer/src/customTypes/entities.ts index cb82bffd..3dd55d12 100644 --- a/packages/threat-composer/src/customTypes/entities.ts +++ b/packages/threat-composer/src/customTypes/entities.ts @@ -55,6 +55,10 @@ export const MetadataSchema = z.object({ export type Metadata = z.infer; +export const MetadataNodeSchema = z.object({}); + +export type MetadataNode = z.infer; + export const EntityBaseSchema = z.object({ /** * The unique Id of the entity. diff --git a/packages/threat-composer/src/customTypes/workspaces.ts b/packages/threat-composer/src/customTypes/workspaces.ts index 39dc2ea1..6cda8788 100644 --- a/packages/threat-composer/src/customTypes/workspaces.ts +++ b/packages/threat-composer/src/customTypes/workspaces.ts @@ -14,11 +14,18 @@ limitations under the License. ******************************************************************************************************************** */ import { z } from 'zod'; -import { SINGLE_FIELD_INPUT_SMALL_MAX_LENGTH } from '../configs'; +import { MetadataNodeSchema } from './entities'; +import { + SINGLE_FIELD_INPUT_SMALL_MAX_LENGTH, + STORAGE_LOCAL_STATE, + STORAGE_LOCAL_STORAGE, +} from '../configs'; export const WorkspaceSchema = z.object({ id: z.string().length(36), name: z.string().max(SINGLE_FIELD_INPUT_SMALL_MAX_LENGTH), + storageType: z.enum([STORAGE_LOCAL_STATE, STORAGE_LOCAL_STORAGE]).optional(), + metadata: MetadataNodeSchema.optional(), }); export type Workspace = z.infer; \ No newline at end of file diff --git a/packages/threat-composer/src/hooks/useExportImport/index.ts b/packages/threat-composer/src/hooks/useExportImport/index.ts index bcd7cd62..fea164a1 100644 --- a/packages/threat-composer/src/hooks/useExportImport/index.ts +++ b/packages/threat-composer/src/hooks/useExportImport/index.ts @@ -32,8 +32,18 @@ import recalculateThreatData from '../../utils/recalculateThreatData'; import sanitizeHtml from '../../utils/sanitizeHtml'; import validateData from '../../utils/validateData'; +const PLACEHOLDER_SCHEMA_VERSION = 0; const SCHEMA_VERSION = 1.0; +export const PLACEHOLDER_EXCHANGE_DATA = { + schema: PLACEHOLDER_SCHEMA_VERSION, +}; + +export const PLACEHOLDER_EXCHANGE_DATA_FOR_WORKSPACE = { + id: '', + name: '', +}; + const useImportExport = () => { const { composerMode } = useGlobalSetupContext(); const { currentWorkspace } = useWorkspacesContext(); @@ -114,7 +124,7 @@ const useImportExport = () => { return importedData; }, []); - const importData = useCallback((data: DataExchangeFormat) => { + const importData = useCallback(async (data: DataExchangeFormat) => { const calculatedThreats = recalculateThreatData(data.threats || []); if (data.schema > 0) { diff --git a/packages/threat-composer/src/hooks/useRemoveData/index.ts b/packages/threat-composer/src/hooks/useRemoveData/index.ts index 20e3c9d3..7389e4a7 100644 --- a/packages/threat-composer/src/hooks/useRemoveData/index.ts +++ b/packages/threat-composer/src/hooks/useRemoveData/index.ts @@ -58,7 +58,7 @@ const useRemoveData = () => { removeAllStatements, removeAllAssumptionLinks, removeAllMitigationLinks]); - const deleteCurrentWorkspace = useCallback(async (toDeleteWorkspaceId: string) => { + const deleteWorkspace = useCallback(async (toDeleteWorkspaceId: string) => { if (toDeleteWorkspaceId) { await Promise.all([ removeWorkspace(toDeleteWorkspaceId), @@ -89,7 +89,7 @@ const useRemoveData = () => { return { removeData, - deleteCurrentWorkspace, + deleteWorkspace, }; }; diff --git a/packages/threat-composer/src/hooks/useWorkspaceStorage/index.ts b/packages/threat-composer/src/hooks/useWorkspaceStorage/index.ts new file mode 100644 index 00000000..dc2674fe --- /dev/null +++ b/packages/threat-composer/src/hooks/useWorkspaceStorage/index.ts @@ -0,0 +1,57 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ +import { useMemo } from 'react'; +import { STORAGE_LOCAL_STATE, STORAGE_LOCAL_STORAGE } from '../../configs'; +import { useWorkspaceExamplesContext } from '../../contexts/WorkspaceExamplesContext'; +import { useWorkspacesContext } from '../../contexts/WorkspacesContext'; +import { DataExchangeFormat } from '../../customTypes'; +import isWorkspaceExample from '../../utils/isWorkspaceExample'; + +type StorageType = typeof STORAGE_LOCAL_STATE | typeof STORAGE_LOCAL_STORAGE; + +const useWorkspaceStorage = (workspaceId: string | null): { + storageType: StorageType; + value?: DataExchangeFormat; +} => { + const { workspaceList } = useWorkspacesContext(); + const { getWorkspaceExample } = useWorkspaceExamplesContext(); + + return useMemo(() => { + if (workspaceId && isWorkspaceExample(workspaceId)) { + return { + storageType: STORAGE_LOCAL_STATE, + value: getWorkspaceExample(workspaceId)?.value, + }; + } + + if (workspaceId) { + const workspace = workspaceList.find(x => x.id === workspaceId); + if (workspace && workspace.storageType === STORAGE_LOCAL_STATE) { + return { + storageType: STORAGE_LOCAL_STATE, + value: undefined, + }; + } + } + + return { + storageType: STORAGE_LOCAL_STORAGE, + value: undefined, + }; + }, [workspaceId, workspaceList]); +}; + +export default useWorkspaceStorage; \ No newline at end of file diff --git a/packages/threat-composer/src/types.ts b/packages/threat-composer/src/types.ts index b0507378..db0e57e0 100644 --- a/packages/threat-composer/src/types.ts +++ b/packages/threat-composer/src/types.ts @@ -21,4 +21,5 @@ declare module '*.png' { declare module '*.gif' { const value: string; export default value; -} \ No newline at end of file +} +