Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allows clone of a workspace #144

Merged
merged 3 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
dboyd13 marked this conversation as resolved.
Show resolved Hide resolved
};

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
Loading