Skip to content

Commit

Permalink
feat: Allows clone of a workspace (#144)
Browse files Browse the repository at this point in the history
* feat: Allows clone of a workspace

* chore: Update the menu label wording

* chore: Fix the typo
  • Loading branch information
jessieweiyi authored Oct 6, 2024
1 parent d59151f commit c273356
Show file tree
Hide file tree
Showing 25 changed files with 533 additions and 155 deletions.
2 changes: 1 addition & 1 deletion .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 54 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,19 @@ Reference or example threat models are available directly in the Workspace selec
},
] as WorkspaceExample[];
```
1. Build the project.
1. Build the project
#### Dynamically inject example threat models in build time
1. Follow steps 1-2 above to author your example threat models
1. Store your example threat models within a folder in a seperate location or repository
1. Copy the file folder containing example threat models under the path `packages/threat-composer/src/data/workspaceExamples` in your build
1. Run the script below in your build from the project root to inject the example threat models entry to configuration file `packages/threat-composer/src/data/workspaceExamples/workspaceExamples.ts`:
```
npx ts-node ./scripts/data/injectData.ts WorkspaceExample <SourceDir-relative path to the workspaceExamples folder>
```
1. Build the project
### Threat packs
Expand Down Expand Up @@ -424,7 +435,27 @@ Threat packs allow you to quickly find and add bulk or selected threat statement
GenAIChatbot,
] as ThreatPack[];
```
1. Build the project.
1. Build the project
#### Dynamically inject example threat packs in build time
1. Follow steps 1-2 above to author your threat packs
1. Store your threat packs within a folder in a seperate location or repository
1. Follow steps 4-8 above to author your threat pack metadata files
1. Store your threat pack metadata files within a folder in a seperate location or repository (can be the same folder of threat pack files)
1. Copy the file folder(s) containing threat pack files and metadata files under the path `packages/threat-composer/src/data/threatPacks` in your build
1. Run the script below in your build from the project root to build the threat packs
```
npx ts-node ./scripts/data/buildPacks.ts ThreatPack <SourceDir-the relative path to the threatPacks folder for the folder containing metadata files> <DestDir-the relative path to the threatPacks folder for output threat packs files>
```
1. Run the script below in your build from the project root to inject the generated threat packs entry to configuration file `packages/threat-composer/src/data/threatPacks/threatPacks.ts`:
```
npx ts-node ./scripts/data/injectData.ts ThreatPack <SourceDir-the value DestDir from the previous step>
```
1. Build the project
### Mitigation packs
Expand Down Expand Up @@ -459,7 +490,27 @@ Mitigation packs allow you to quickly find and add bulk or selected mitigation c
GenAIChatbot,
] as MitigationPack[];
```
1. Build the project.
1. Build the project
#### Dynamically inject example mitigation packs in build time
1. Follow steps 1-2 above to author your mitigation packs
1. Store your mitigation packs within a folder in a seperate location or repository
1. Follow steps 4-8 above to author your mitigation pack metadata files
1. Store your mitigation pack metadata files within a folder in a seperate location or repository (can be the same folder of mitigation pack files)
1. Copy the file folder(s) containing mitigation pack files and metadata files under the path `packages/threat-composer/src/data/mitigationPacks` in your build
1. Run the script below in your build from the project root to build the mitigation packs
```
npx ts-node ./scripts/data/buildPacks.ts MitigationPack <SourceDir-the relative path to the mitigationPacks folder for the folder containing metadata files> <DestDir-the relative path to the the mitigationtPacks folder for output mitigation packs files>
```
1. Run the script below in your build from the project root to inject the generated mitigation packs entry to configuration file `packages/threat-composer/src/data/mitigationPacks/mitigationPacks.ts`:
```
npx ts-node ./scripts/data/injectData.ts MitigationPack <SourceDir-the value DestDir from the previous step>
```
1. Build the project
### Threat examples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,29 @@ export interface EditWorkspaceProps {
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
onConfirm: (workspace: string) => Promise<void>;
value?: string;
editMode?: boolean;
editMode: 'add' | 'update' | 'clone';
currentWorkspace?: Workspace;
workspaceList: Workspace[];
exampleWorkspaceList: Workspace[];
}

const BUTTON_LABEL = {
add: 'Add',
update: 'Update',
clone: 'Clone',
};

const HEADER_TEXT = {
add: 'Add new workspace',
update: 'Update workspace',
clone: 'Clone workspace',
};

const EditWorkspace: FC<EditWorkspaceProps> = ({
visible,
setVisible,
onConfirm,
editMode = false,
editMode = 'add',
workspaceList,
exampleWorkspaceList,
currentWorkspace,
Expand Down Expand Up @@ -74,21 +86,21 @@ const EditWorkspace: FC<EditWorkspaceProps> = ({
<SpaceBetween direction="horizontal" size="xs">
<Button variant="link" onClick={() => setVisible(false)}>Cancel</Button>
<Button variant="primary" disabled={value.length < 3} onClick={handleConfirm}>{
editMode ? 'Update' : 'Add'
BUTTON_LABEL[editMode as 'add' | 'update' | 'clone'] || 'Add'
}</Button>
</SpaceBetween>
</Box>);
}, [setVisible, handleConfirm, value, editMode]);

return <Modal
header={<Header>{editMode ? 'Update workspace' : 'Add new workspace'}</Header>}
header={<Header>{HEADER_TEXT[editMode as 'add' | 'update' | 'clone'] || 'Add'}</Header>}
visible={visible}
footer={footer}
onDismiss={() => setVisible(false)}
>
<SpaceBetween direction="vertical" size="m">
<FormField
label="Workspace name"
label="New workspace name"
errorText={errorText}
>
<Input ref={inputRef as RefObject<InputProps.Ref>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
DataExchangeFormat,
TemplateThreatStatement,
} from '../../../customTypes';
import useCloneWorkspace from '../../../hooks/useCloneWorkspace';
import useImportExport from '../../../hooks/useExportImport';
import useRemoveData from '../../../hooks/useRemoveData';
import getMobileMediaQuery from '../../../utils/getMobileMediaQuery';
Expand Down Expand Up @@ -76,6 +77,7 @@ export interface WorkspaceSelectorProps {
onPreview?: (data: DataExchangeFormat) => void;
onPreviewClose?: () => void;
onImported?: () => void;
onClone?: () => Promise<void>;
}

const WorkspaceSelector: FC<PropsWithChildren<WorkspaceSelectorProps>> = ({
Expand All @@ -95,6 +97,8 @@ const WorkspaceSelector: FC<PropsWithChildren<WorkspaceSelectorProps>> = ({
useState(false);
const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] =
useState(false);
const [cloneWorkspaceModalVisible, setCloneWorkspaceModalVisible] =
useState(false);
const [removeDataModalVisible, setRemoveDataModalVisible] = useState(false);
const [removeWorkspaceModalVisible, setRemoveWorkspaceModalVisible] =
useState(false);
Expand All @@ -120,6 +124,8 @@ const WorkspaceSelector: FC<PropsWithChildren<WorkspaceSelectorProps>> = ({
switchWorkspace,
} = useWorkspacesContext();

const cloneWorkspace = useCloneWorkspace();

const workspacesOptions = useMemo(() => {
const options: (SelectProps.Option | SelectProps.OptionGroup)[] = [
{
Expand Down Expand Up @@ -188,6 +194,9 @@ const WorkspaceSelector: FC<PropsWithChildren<WorkspaceSelectorProps>> = ({
case 'import':
setFileImportModalVisible(true);
break;
case 'clone':
setCloneWorkspaceModalVisible(true);
break;
case 'exportAll':
exportAll();
break;
Expand Down Expand Up @@ -217,6 +226,7 @@ const WorkspaceSelector: FC<PropsWithChildren<WorkspaceSelectorProps>> = ({
setRemoveDataModalVisible,
setRemoveWorkspaceModalVisible,
setAddWorkspaceModalVisible,
setCloneWorkspaceModalVisible,
setEditWorkspaceModalVisible,
handleSingletonPrimaryButtonClick,
],
Expand Down Expand Up @@ -272,9 +282,13 @@ const WorkspaceSelector: FC<PropsWithChildren<WorkspaceSelectorProps>> = ({
{ id: 'add', text: 'Add new workspace' },
{
id: 'import',
text: 'Import',
text: 'Import into current workspace',
disabled: isWorkspaceExample(currentWorkspace?.id),
},
{
id: 'clone',
text: 'Clone current workspace',
},
{
id: 'exportAll',
text: embededMode
Expand Down Expand Up @@ -390,6 +404,7 @@ const WorkspaceSelector: FC<PropsWithChildren<WorkspaceSelectorProps>> = ({
)}
{addWorkspaceModalVisible && (
<EditWorkspace
editMode='add'
visible={addWorkspaceModalVisible}
setVisible={setAddWorkspaceModalVisible}
onConfirm={async (workspaceName: string) => {
Expand All @@ -399,11 +414,23 @@ const WorkspaceSelector: FC<PropsWithChildren<WorkspaceSelectorProps>> = ({
exampleWorkspaceList={workspaceExamples}
/>
)}
{cloneWorkspaceModalVisible && (
<EditWorkspace
editMode='clone'
visible={cloneWorkspaceModalVisible}
setVisible={setCloneWorkspaceModalVisible}
onConfirm={async (workspaceName: string) => {
await cloneWorkspace(workspaceName);
}}
workspaceList={workspaceList}
exampleWorkspaceList={workspaceExamples}
/>
)}
{editWorkspaceModalVisible && currentWorkspace && (
<EditWorkspace
editMode='update'
visible={editWorkspaceModalVisible}
setVisible={setEditWorkspaceModalVisible}
editMode
value={currentWorkspace.name}
onConfirm={(newWorkspaceName) =>
renameWorkspace(currentWorkspace.id, newWorkspaceName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { INFO_DEFAULT_VALUE } from '../../../constants';
import { ApplicationInfoContext } from '../../context';
import { ApplicationContextProviderProps } from '../../types';

const getLocalStorageKey = (workspaceId: string | null) => {
export const getLocalStorageKey = (workspaceId: string | null) => {
if (workspaceId) {
return `${LOCAL_STORAGE_KEY_APPLICATION_INFO}_${workspaceId}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { INFO_DEFAULT_VALUE } from '../../../constants';
import { ArchitectureInfoContext } from '../../context';
import { ArchitectureContextProviderProps } from '../../types';

const getLocalStorageKey = (workspaceId: string | null) => {
export const getLocalStorageKey = (workspaceId: string | null) => {
if (workspaceId) {
return `${LOCAL_STORAGE_KEY_ARCHIECTURE_INFO}_${workspaceId}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AssumptionLinksContext } from '../../context';
import { AssumptionLinksContextProviderProps } from '../../types';
import useAssumptionLinks from '../../useAssumptionLinks';

const getLocalStorageKey = (workspaceId: string | null) => {
export const getLocalStorageKey = (workspaceId: string | null) => {
if (workspaceId) {
return `${LOCAL_STORAGE_KEY_ASSUMPTION_LINK_LIST}_${workspaceId}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AssumptionsContext } from '../../context';
import { AssumptionsContextProviderProps } from '../../types';
import useAssumptions from '../../useAssumptions';

const getLocalStorageKey = (workspaceId: string | null) => {
export const getLocalStorageKey = (workspaceId: string | null) => {
if (workspaceId) {
return `${LOCAL_STORAGE_KEY_ASSUMPTION_LIST}_${workspaceId}`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/** *******************************************************************************************************************
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 { FC, PropsWithChildren, useCallback } from 'react';
import { DataExchangeFormat } from '../../../../customTypes';
import setLocalStorageKey from '../../../../utils/setLocalStorageKey';
import { getLocalStorageKey as getApplicationInfoLocalStorageKey } from '../../../ApplicationContext/components/LocalStorageContextProvider';
import { getLocalStorageKey as getArchitectureInfoLocalStorageKey } from '../../../ArchitectureContext/components/LocalStorageContextProvider';
import { getLocalStorageKey as getAssumptionLinksLocalStorageKey } from '../../../AssumptionLinksContext/components/LocalStorageContextProvider';
import { getLocalStorageKey as getAssumptionsListLocalStorageKey } from '../../../AssumptionsContext/components/LocalStorageContextProvider';
import { getLocalStorageKey as getDataflowInfoLocalStorageKey } from '../../../DataflowContext/components/LocalStorageContextProvider';
import { getLocalStorageKey as getMitigationLinksLocalStorageKey } from '../../../MitigationLinksContext/components/LocalStorageContextProvider';
import { getLocalStorageKey as getMitigationsLocalStorageKey } from '../../../MitigationsContext/components/LocalStorageContextProvider';
import { getLocalStorageKey as getThreatsLocalStorageKey } from '../../../ThreatsContext/components/LocalStorageContextProvider';
import { CrossWorkspaceContext } from '../../context';


const CrossWorkspaceLocalStorageContextProvider: FC<PropsWithChildren<{}>> = ({
children,
}) => {
const handleCloneWorkspaceData = useCallback(async (targetWorkspaceId: string, data: DataExchangeFormat) => {
data.applicationInfo && setLocalStorageKey(getApplicationInfoLocalStorageKey(targetWorkspaceId), data.applicationInfo);
data.architecture && setLocalStorageKey(getArchitectureInfoLocalStorageKey(targetWorkspaceId), data.architecture);
data.dataflow && setLocalStorageKey(getDataflowInfoLocalStorageKey(targetWorkspaceId), data.dataflow);
data.assumptions && setLocalStorageKey(getAssumptionsListLocalStorageKey(targetWorkspaceId), data.assumptions);
data.threats && setLocalStorageKey(getThreatsLocalStorageKey(targetWorkspaceId), data.threats);
data.mitigations && setLocalStorageKey(getMitigationsLocalStorageKey(targetWorkspaceId), data.mitigations);
data.assumptionLinks && setLocalStorageKey(getAssumptionLinksLocalStorageKey(targetWorkspaceId), data.assumptionLinks);
data.mitigationLinks && setLocalStorageKey(getMitigationLinksLocalStorageKey(targetWorkspaceId), data.mitigationLinks);
}, []);

return (<CrossWorkspaceContext.Provider value={{
cloneWorkspaceData: handleCloneWorkspaceData,
}}>
{children}
</CrossWorkspaceContext.Provider>);
};

export default CrossWorkspaceLocalStorageContextProvider;

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/** *******************************************************************************************************************
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 { useContext, createContext } from 'react';
import { DataExchangeFormat } from '../../customTypes';

export interface CrossWorkspaceContextApi {
cloneWorkspaceData: (targetWorkspaceId: string, data: DataExchangeFormat) => Promise<void>;
}

const initialState: CrossWorkspaceContextApi = {
cloneWorkspaceData: () => Promise.resolve(),
};

export const CrossWorkspaceContext = createContext<CrossWorkspaceContextApi>(initialState);

export const useCrossWorkspaceContext = () => useContext(CrossWorkspaceContext);
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** *******************************************************************************************************************
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 { FC, PropsWithChildren } from 'react';
import CrossWorkspaceLocalStorageContextProvider from './components/LocalStorageContextProvider';
import { useCrossWorkspaceContext } from './context';

const CrossWorkspaceContextProvider: FC<PropsWithChildren<{}>> = (props) => {
return (<CrossWorkspaceLocalStorageContextProvider {...props} />);
};

export default CrossWorkspaceContextProvider;

export {
useCrossWorkspaceContext,
};
Loading

0 comments on commit c273356

Please sign in to comment.