From 5a58b86f621c5877f9731df7cba6e4a6dc09e266 Mon Sep 17 00:00:00 2001 From: Nikita Karetnikov Date: Mon, 29 Apr 2024 06:15:08 +0200 Subject: [PATCH 01/10] Support creating lockfile envs --- .../components/EnvironmentCreate.tsx | 28 +++-- .../Specification/SpecificationCreate.tsx | 102 ++++++++++++++---- .../Specification/SpecificationReadOnly.tsx | 3 +- .../Specification/SpecificationReadOnly.tsx | 3 +- .../requestedPackagesSlice.ts | 10 +- src/features/yamlEditor/components/editor.tsx | 6 +- 6 files changed, 113 insertions(+), 39 deletions(-) diff --git a/src/features/environmentCreate/components/EnvironmentCreate.tsx b/src/features/environmentCreate/components/EnvironmentCreate.tsx index f369202c..f84fd149 100644 --- a/src/features/environmentCreate/components/EnvironmentCreate.tsx +++ b/src/features/environmentCreate/components/EnvironmentCreate.tsx @@ -53,14 +53,28 @@ export const EnvironmentCreate = ({ environmentNotification }: IEnvCreate) => { dispatch(descriptionChanged(value)); }, 300); - const createEnvironment = async (code: ICreateEnvironmentArgs) => { + const createEnvironment = async ( + code: ICreateEnvironmentArgs, + is_lockfile: boolean + ) => { const namespace = newEnvironment?.namespace; - const environmentInfo = { - namespace, - specification: `${stringify( - code - )}\ndescription: '${description}'\nname: ${name}\nprefix: null` - }; + let environmentInfo; + if (is_lockfile) { + environmentInfo = { + namespace, + specification: stringify(code), + environment_name: name, + environment_description: description, + is_lockfile: true + }; + } else { + environmentInfo = { + namespace, + specification: `${stringify( + code + )}\ndescription: '${description}'\nname: ${name}\nprefix: null` + }; + } try { const { data } = await createOrUpdate(environmentInfo).unwrap(); diff --git a/src/features/environmentCreate/components/Specification/SpecificationCreate.tsx b/src/features/environmentCreate/components/Specification/SpecificationCreate.tsx index 7ca76b74..209c84a2 100644 --- a/src/features/environmentCreate/components/Specification/SpecificationCreate.tsx +++ b/src/features/environmentCreate/components/Specification/SpecificationCreate.tsx @@ -13,6 +13,9 @@ import { environmentCreateStateCleared } from "../../environmentCreateSlice"; import { getStylesForStyleType } from "../../../../utils/helpers"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import FormControl from "@mui/material/FormControl"; export const SpecificationCreate = ({ onCreateEnvironment }: any) => { const dispatch = useAppDispatch(); @@ -20,17 +23,24 @@ export const SpecificationCreate = ({ onCreateEnvironment }: any) => { state => state.environmentCreate ); const [show, setShow] = useState(false); + const [specificationType, setSpecificationType] = + React.useState("specification"); const [editorContent, setEditorContent] = useState<{ channels: string[]; dependencies: string[]; variables: Record; }>({ channels: [], dependencies: [], variables: {} }); + const [editorContentLockfile, setEditorContentLockfile] = useState({}); const buttonStyles = getStylesForStyleType( { padding: "5px 60px" }, { padding: "5px 48px" } ); + const onUpdateSpecificationType = (event: SelectChangeEvent) => { + setSpecificationType(event.target.value as string); + }; + const onUpdateChannels = useCallback((channels: string[]) => { dispatch(channelsChanged(channels)); }, []); @@ -71,15 +81,22 @@ export const SpecificationCreate = ({ onCreateEnvironment }: any) => { setEditorContent(code); }; + const onUpdateEditorLockfile = (lockfile: any) => { + setEditorContentLockfile(lockfile); + }; + const onToggleEditorView = (value: boolean) => { if (show) { - dispatch( - editorCodeUpdated({ - channels: editorContent.channels, - dependencies: editorContent.dependencies, - variables: editorContent.variables - }) - ); + if (specificationType === "specification") { + dispatch( + editorCodeUpdated({ + channels: editorContent.channels, + dependencies: editorContent.dependencies, + variables: editorContent.variables + }) + ); + } + // Do nothing when specificationType === lockfile } else { setEditorContent({ dependencies: requestedPackages, @@ -103,16 +120,36 @@ export const SpecificationCreate = ({ onCreateEnvironment }: any) => { return stringify({ channels, dependencies, variables }); }; + const formatCodeLockfile = () => { + return stringify({ + version: 1, + metadata: {}, + package: [] + }); + }; + const handleSubmit = () => { - const code = show - ? editorContent - : { - dependencies: requestedPackages, - variables: environmentVariables, - channels - }; - - onCreateEnvironment(code); + let code; + let is_lockfile; + + if (show) { + if (specificationType === "specification") { + code = editorContent; + is_lockfile = false; + } else { + code = editorContentLockfile; + is_lockfile = true; + } + } else { + code = { + dependencies: requestedPackages, + variables: environmentVariables, + channels + }; + is_lockfile = false; + } + + onCreateEnvironment(code, is_lockfile); }; useEffect(() => { @@ -129,10 +166,33 @@ export const SpecificationCreate = ({ onCreateEnvironment }: any) => { > {show ? ( - + <> + + + + {specificationType === "specification" ? ( + + ) : ( + + )} + ) : ( <> @@ -140,7 +200,7 @@ export const SpecificationCreate = ({ onCreateEnvironment }: any) => { requestedPackages={requestedPackages} /> - + { /> - + {/* TODO: make sure channels are never undefined in the state */} + ); diff --git a/src/features/environmentDetails/components/Specification/SpecificationReadOnly.tsx b/src/features/environmentDetails/components/Specification/SpecificationReadOnly.tsx index 534b001a..5268200c 100644 --- a/src/features/environmentDetails/components/Specification/SpecificationReadOnly.tsx +++ b/src/features/environmentDetails/components/Specification/SpecificationReadOnly.tsx @@ -33,7 +33,8 @@ export const SpecificationReadOnly = () => { /> - + {/* TODO: make sure channels are never undefined in the state */} + ); diff --git a/src/features/requestedPackages/requestedPackagesSlice.ts b/src/features/requestedPackages/requestedPackagesSlice.ts index 998022cf..9d855000 100644 --- a/src/features/requestedPackages/requestedPackagesSlice.ts +++ b/src/features/requestedPackages/requestedPackagesSlice.ts @@ -8,7 +8,7 @@ import { import { requestedPackageParser } from "../../utils/helpers"; import { dependenciesApiSlice } from "../dependencies"; import { environmentDetailsApiSlice } from "../environmentDetails"; - +import { stringify } from "yaml"; export interface IRequestedPackagesState { requestedPackages: (string | CondaSpecificationPip)[]; versionsWithoutConstraints: { [key: string]: string }; @@ -79,13 +79,15 @@ export const requestedPackagesSlice = createSlice({ { payload: { data: { - specification: { - spec: { dependencies } - } + specification: { spec } } } } ) => { + // dependencies can be undefined if a lockfile specification is provided + // TODO: parse the lockfile and populate the dependencies + const dependencies = spec?.dependencies ?? []; + state.requestedPackages = dependencies; state.packagesWithLatestVersions = {}; state.versionsWithConstraints = {}; diff --git a/src/features/yamlEditor/components/editor.tsx b/src/features/yamlEditor/components/editor.tsx index 77f015cd..64ea571f 100644 --- a/src/features/yamlEditor/components/editor.tsx +++ b/src/features/yamlEditor/components/editor.tsx @@ -9,11 +9,7 @@ import { PrefContext } from "../../../preferences"; export interface ICodeEditor { code: any; - onChangeEditor: (code: { - channels: string[]; - dependencies: string[]; - variables: Record; - }) => void; + onChangeEditor: (code: any) => void; } export const CodeEditor = ({ code, onChangeEditor }: ICodeEditor) => { From 823df49fbed645347d90b007e7fcbdfc3c50f90e Mon Sep 17 00:00:00 2001 From: Nikita Karetnikov Date: Mon, 29 Apr 2024 06:57:40 +0200 Subject: [PATCH 02/10] Remove redundant import --- src/features/requestedPackages/requestedPackagesSlice.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/requestedPackages/requestedPackagesSlice.ts b/src/features/requestedPackages/requestedPackagesSlice.ts index 9d855000..9a39e3e9 100644 --- a/src/features/requestedPackages/requestedPackagesSlice.ts +++ b/src/features/requestedPackages/requestedPackagesSlice.ts @@ -8,7 +8,6 @@ import { import { requestedPackageParser } from "../../utils/helpers"; import { dependenciesApiSlice } from "../dependencies"; import { environmentDetailsApiSlice } from "../environmentDetails"; -import { stringify } from "yaml"; export interface IRequestedPackagesState { requestedPackages: (string | CondaSpecificationPip)[]; versionsWithoutConstraints: { [key: string]: string }; From 0df29c4b369057815322678043029680ab91a015 Mon Sep 17 00:00:00 2001 From: Nikita Karetnikov Date: Tue, 30 Apr 2024 01:30:18 +0200 Subject: [PATCH 03/10] Update state --- src/features/channels/channelsSlice.ts | 8 +++++--- .../environmentVariables/environmentVariablesSlice.ts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/features/channels/channelsSlice.ts b/src/features/channels/channelsSlice.ts index 4833bd1c..24c63bd8 100644 --- a/src/features/channels/channelsSlice.ts +++ b/src/features/channels/channelsSlice.ts @@ -24,13 +24,15 @@ export const channelsSlice = createSlice({ { payload: { data: { - specification: { - spec: { channels } - } + specification: { spec } } } } ) => { + // channels can be undefined if a lockfile specification is provided + // TODO: parse the lockfile and populate the channels + const channels = spec?.channels ?? []; + state.channels = channels; } ); diff --git a/src/features/environmentVariables/environmentVariablesSlice.ts b/src/features/environmentVariables/environmentVariablesSlice.ts index 5c040ffa..229b68d3 100644 --- a/src/features/environmentVariables/environmentVariablesSlice.ts +++ b/src/features/environmentVariables/environmentVariablesSlice.ts @@ -24,13 +24,15 @@ export const environmentVariablesSlice = createSlice({ { payload: { data: { - specification: { - spec: { variables: environmentVariables } - } + specification: { spec } } } } ) => { + // variables can be undefined if a lockfile specification is provided + // TODO: parse the lockfile and populate the variables + const environmentVariables = spec?.variables ?? []; + state.environmentVariables = environmentVariables; } ); From 4929d0aca8ad302782347b0cd598a4930a75a18c Mon Sep 17 00:00:00 2001 From: Nikita Karetnikov Date: Tue, 30 Apr 2024 03:15:44 +0200 Subject: [PATCH 04/10] Remove workarounds that are no longer needed --- .../components/Specification/SpecificationReadOnly.tsx | 3 +-- .../components/Specification/SpecificationReadOnly.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/features/environmentCreate/components/Specification/SpecificationReadOnly.tsx b/src/features/environmentCreate/components/Specification/SpecificationReadOnly.tsx index a9186426..d5e96f0d 100644 --- a/src/features/environmentCreate/components/Specification/SpecificationReadOnly.tsx +++ b/src/features/environmentCreate/components/Specification/SpecificationReadOnly.tsx @@ -34,8 +34,7 @@ export const SpecificationReadOnly = () => { /> - {/* TODO: make sure channels are never undefined in the state */} - + ); diff --git a/src/features/environmentDetails/components/Specification/SpecificationReadOnly.tsx b/src/features/environmentDetails/components/Specification/SpecificationReadOnly.tsx index 5268200c..534b001a 100644 --- a/src/features/environmentDetails/components/Specification/SpecificationReadOnly.tsx +++ b/src/features/environmentDetails/components/Specification/SpecificationReadOnly.tsx @@ -33,8 +33,7 @@ export const SpecificationReadOnly = () => { /> - {/* TODO: make sure channels are never undefined in the state */} - + ); From ab086691caa9ab2261a69b864959072cfa3ac55e Mon Sep 17 00:00:00 2001 From: Nikita Karetnikov Date: Tue, 30 Apr 2024 03:49:53 +0200 Subject: [PATCH 05/10] Handle `is_lockfile` in test --- .../environmentCreate/SpecificationCreate.test.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/environmentCreate/SpecificationCreate.test.tsx b/test/environmentCreate/SpecificationCreate.test.tsx index a0b92cd5..21a303e2 100644 --- a/test/environmentCreate/SpecificationCreate.test.tsx +++ b/test/environmentCreate/SpecificationCreate.test.tsx @@ -49,11 +49,15 @@ describe("", () => { fireEvent.click(switchButton); fireEvent.click(createButton); - expect(mockOnCreateEnvironment).toHaveBeenCalledWith({ - channels: [], - dependencies: [], - variables: {} - }); + const is_lockfile = false; + expect(mockOnCreateEnvironment).toHaveBeenCalledWith( + { + channels: [], + dependencies: [], + variables: {} + }, + is_lockfile + ); }); it("should update channels and dependencies", async () => { From 37efaa7a4f3fe69c4cb9e4d40c003b9e9d8807f6 Mon Sep 17 00:00:00 2001 From: Nikita Karetnikov Date: Tue, 30 Apr 2024 06:31:37 +0200 Subject: [PATCH 06/10] Initial edit spec support --- src/common/models/CondaSpecification.ts | 1 + .../Specification/SpecificationEdit.tsx | 120 ++++++++++++++---- src/features/lockfile/index.tsx | 1 + src/features/lockfile/lockfileSlice.ts | 41 ++++++ src/store/rootReducer.ts | 2 + 5 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 src/features/lockfile/index.tsx create mode 100644 src/features/lockfile/lockfileSlice.ts diff --git a/src/common/models/CondaSpecification.ts b/src/common/models/CondaSpecification.ts index 0512aeb0..fe52cb8e 100644 --- a/src/common/models/CondaSpecification.ts +++ b/src/common/models/CondaSpecification.ts @@ -6,4 +6,5 @@ export type CondaSpecification = { dependencies: (string | CondaSpecificationPip)[]; variables: Record; prefix?: string | null; + lockfile?: any | null; }; diff --git a/src/features/environmentDetails/components/Specification/SpecificationEdit.tsx b/src/features/environmentDetails/components/Specification/SpecificationEdit.tsx index f4afd065..c1429594 100644 --- a/src/features/environmentDetails/components/Specification/SpecificationEdit.tsx +++ b/src/features/environmentDetails/components/Specification/SpecificationEdit.tsx @@ -12,6 +12,7 @@ import { stringify } from "yaml"; import { BlockContainerEditMode } from "../../../../components"; import { ChannelsEdit, updateChannels } from "../../../../features/channels"; import { updateEnvironmentVariables } from "../../../../features/environmentVariables"; +import { updateLockfile } from "../../../../features/lockfile"; import { Dependencies, pageChanged } from "../../../../features/dependencies"; import { modeChanged, @@ -25,12 +26,16 @@ import { CodeEditor } from "../../../../features/yamlEditor"; import { useAppDispatch, useAppSelector } from "../../../../hooks"; import { StyledButtonPrimary } from "../../../../styles"; import { CondaSpecificationPip } from "../../../../common/models"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import FormControl from "@mui/material/FormControl"; + interface ISpecificationEdit { descriptionUpdated: boolean; defaultEnvVersIsChanged: boolean; onSpecificationIsChanged: (specificationIsChanged: boolean) => void; onDefaultEnvIsChanged: (defaultEnvVersIsChanged: boolean) => void; - onUpdateEnvironment: (specification: any) => void; + onUpdateEnvironment: (specification: any, is_lockfile: boolean) => void; onShowDialogAlert: (showDialog: boolean) => void; } export const SpecificationEdit = ({ @@ -51,10 +56,14 @@ export const SpecificationEdit = ({ const { dependencies, size, count, page } = useAppSelector( state => state.dependencies ); + const { lockfile } = useAppSelector(state => state.lockfile); const hasMore = size * page <= count; const dispatch = useAppDispatch(); const [show, setShow] = useState(false); + const [specificationType, setSpecificationType] = React.useState( + Object.keys(lockfile).length === 0 ? "specification" : "lockfile" + ); const [code, setCode] = useState<{ dependencies: (string | CondaSpecificationPip)[]; channels: string[]; @@ -64,11 +73,13 @@ export const SpecificationEdit = ({ variables: environmentVariables, channels }); + const [codeLockfile, setCodeLockfile] = useState(lockfile); const [envIsUpdated, setEnvIsUpdated] = useState(false); const initialChannels = useRef(cloneDeep(channels)); const initialPackages = useRef(cloneDeep(requestedPackages)); const initialEnvironmentVariables = useRef(cloneDeep(environmentVariables)); + const initialLockfile = useRef(cloneDeep(lockfile)); const stringifiedInitialChannels = useMemo(() => { return JSON.stringify(initialChannels.current); @@ -82,6 +93,14 @@ export const SpecificationEdit = ({ return JSON.stringify(initialEnvironmentVariables.current); }, [initialEnvironmentVariables.current]); + const stringifiedInitialLockfile = useMemo(() => { + return JSON.stringify(initialLockfile.current); + }, [initialLockfile.current]); + + const onUpdateSpecificationType = (event: SelectChangeEvent) => { + setSpecificationType(event.target.value as string); + }; + const onUpdateChannels = useCallback((channels: string[]) => { dispatch(updateChannels(channels)); onDefaultEnvIsChanged(false); @@ -138,11 +157,27 @@ export const SpecificationEdit = ({ 200 ); + const onUpdateEditorLockfile = debounce((lockfile: any) => { + const isDifferentLockfile = + JSON.stringify(lockfile) !== stringifiedInitialLockfile; + + if (isDifferentLockfile) { + setEnvIsUpdated(true); + onUpdateDefaultEnvironment(false); + onSpecificationIsChanged(true); + } + + setCodeLockfile(lockfile); + }, 200); + const onToggleEditorView = (value: boolean) => { if (show) { - dispatch(updatePackages(code.dependencies)); - dispatch(updateChannels(code.channels)); - dispatch(updateEnvironmentVariables(code.variables)); + if (specificationType === "specification") { + dispatch(updatePackages(code.dependencies)); + dispatch(updateChannels(code.channels)); + dispatch(updateEnvironmentVariables(code.variables)); + } + // Do nothing when specificationType === lockfile } else { setCode({ dependencies: requestedPackages, @@ -155,15 +190,27 @@ export const SpecificationEdit = ({ }; const onEditEnvironment = () => { - const envContent = show - ? code - : { - dependencies: requestedPackages, - variables: environmentVariables, - channels - }; + let envContent; + let is_lockfile; - onUpdateEnvironment(envContent); + if (show) { + if (specificationType === "specification") { + envContent = code; + is_lockfile = false; + } else { + envContent = codeLockfile; + is_lockfile = true; + } + } else { + envContent = { + dependencies: requestedPackages, + variables: environmentVariables, + channels + }; + is_lockfile = false; + } + + onUpdateEnvironment(envContent, is_lockfile); }; const onCancelEdition = () => { @@ -173,6 +220,7 @@ export const SpecificationEdit = ({ dispatch(updatePackages(initialPackages.current)); dispatch(updateChannels(initialChannels.current)); dispatch(updateEnvironmentVariables(initialEnvironmentVariables.current)); + dispatch(updateLockfile(initialLockfile.current)); }; useEffect(() => { @@ -187,17 +235,26 @@ export const SpecificationEdit = ({ const isDifferentEnvironmentVariables = JSON.stringify(environmentVariables) !== stringifiedInitialEnvironmentVariables; + const isDifferentLockfile = + JSON.stringify(lockfile) !== stringifiedInitialLockfile; if (defaultEnvVersIsChanged) { setEnvIsUpdated(false); } else if ( isDifferentChannels || isDifferentPackages || - isDifferentEnvironmentVariables + isDifferentEnvironmentVariables || + isDifferentLockfile ) { setEnvIsUpdated(true); } - }, [channels, requestedPackages, environmentVariables, descriptionUpdated]); + }, [ + channels, + requestedPackages, + environmentVariables, + lockfile, + descriptionUpdated + ]); return ( {show ? ( - + <> + + + + {specificationType === "specification" ? ( + + ) : ( + + )} + ) : ( <> diff --git a/src/features/lockfile/index.tsx b/src/features/lockfile/index.tsx new file mode 100644 index 00000000..4ab472c0 --- /dev/null +++ b/src/features/lockfile/index.tsx @@ -0,0 +1 @@ +export * from "./lockfileSlice"; diff --git a/src/features/lockfile/lockfileSlice.ts b/src/features/lockfile/lockfileSlice.ts new file mode 100644 index 00000000..49c309f7 --- /dev/null +++ b/src/features/lockfile/lockfileSlice.ts @@ -0,0 +1,41 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { environmentDetailsApiSlice } from "../environmentDetails"; + +export interface ILockfileState { + lockfile: any; +} + +const initialState: ILockfileState = { lockfile: {} }; + +export const lockfileSlice = createSlice({ + name: "lockfile", + initialState, + reducers: { + updateLockfile: (state, action) => { + const lockfile = action.payload; + state.lockfile = lockfile; + } + }, + extraReducers: builder => { + builder.addMatcher( + environmentDetailsApiSlice.endpoints.getBuild.matchFulfilled, + ( + state, + { + payload: { + data: { + specification: { spec } + } + } + } + ) => { + // checks if this is a lockfile specification + const lockfile = spec?.lockfile ?? {}; + + state.lockfile = lockfile; + } + ); + } +}); + +export const { updateLockfile } = lockfileSlice.actions; diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index d0a427ef..b2e374de 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -4,6 +4,7 @@ import { dependenciesSlice } from "../features/dependencies"; import { environmentDetailsSlice } from "../features/environmentDetails"; import { requestedPackagesSlice } from "../features/requestedPackages"; import { environmentVariablesSlice } from "../features/environmentVariables"; +import { lockfileSlice } from "../features/lockfile"; import { tabsSlice } from "../features/tabs"; import { enviromentsSlice } from "../features/metadata"; import { environmentCreateSlice } from "../features/environmentCreate/environmentCreateSlice"; @@ -13,6 +14,7 @@ export const rootReducer = { channels: channelsSlice.reducer, requestedPackages: requestedPackagesSlice.reducer, environmentVariables: environmentVariablesSlice.reducer, + lockfile: lockfileSlice.reducer, tabs: tabsSlice.reducer, enviroments: enviromentsSlice.reducer, environmentDetails: environmentDetailsSlice.reducer, From 78da64de8cbf1bc4d9c56326e293eac95f913c71 Mon Sep 17 00:00:00 2001 From: Nikita Karetnikov Date: Tue, 30 Apr 2024 06:53:13 +0200 Subject: [PATCH 07/10] Send updated spec to server --- .../components/EnvironmentDetails.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/features/environmentDetails/components/EnvironmentDetails.tsx b/src/features/environmentDetails/components/EnvironmentDetails.tsx index 7b8e690e..999e5cb4 100644 --- a/src/features/environmentDetails/components/EnvironmentDetails.tsx +++ b/src/features/environmentDetails/components/EnvironmentDetails.tsx @@ -152,19 +152,33 @@ export const EnvironmentDetails = ({ } }, [currentBuild]); - const updateEnvironment = async (code: IUpdateEnvironmentArgs) => { + const updateEnvironment = async ( + code: IUpdateEnvironmentArgs, + is_lockfile: boolean + ) => { if (!selectedEnvironment) { return; } const namespace = selectedEnvironment.namespace.name; const environment = selectedEnvironment.name; - const environmentInfo = { - specification: `${stringify( - code - )}\ndescription: ${description}\nname: ${environment}\nprefix: null`, - namespace - }; + let environmentInfo; + if (is_lockfile) { + environmentInfo = { + namespace, + specification: stringify(code), + environment_name: environment, + environment_description: description, + is_lockfile: true + }; + } else { + environmentInfo = { + specification: `${stringify( + code + )}\ndescription: ${description}\nname: ${environment}\nprefix: null`, + namespace + }; + } try { const { data } = await createOrUpdate(environmentInfo).unwrap(); From c71fce36ec7c9d5bc700d0af4f00fe568f17c463 Mon Sep 17 00:00:00 2001 From: Nikita Karetnikov Date: Tue, 30 Apr 2024 09:40:25 +0200 Subject: [PATCH 08/10] Get channels from lockfile --- src/features/channels/channelsSlice.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/features/channels/channelsSlice.ts b/src/features/channels/channelsSlice.ts index 24c63bd8..ce313649 100644 --- a/src/features/channels/channelsSlice.ts +++ b/src/features/channels/channelsSlice.ts @@ -29,9 +29,14 @@ export const channelsSlice = createSlice({ } } ) => { - // channels can be undefined if a lockfile specification is provided - // TODO: parse the lockfile and populate the channels - const channels = spec?.channels ?? []; + // channels can be undefined if a lockfile specification is provided, + // try getting channels from metadata in that case + const channels = + spec?.channels ?? + spec?.lockfile?.metadata?.channels?.map( + (channel: any) => channel?.url + ) ?? + []; state.channels = channels; } From b25f773b1ec29b99bdf20e8d8858cc5a0b4f841f Mon Sep 17 00:00:00 2001 From: Nikita Karetnikov Date: Tue, 30 Apr 2024 10:40:05 +0200 Subject: [PATCH 09/10] Get dependencies from lockfile --- .../requestedPackages/requestedPackagesSlice.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/features/requestedPackages/requestedPackagesSlice.ts b/src/features/requestedPackages/requestedPackagesSlice.ts index 9a39e3e9..3bde3b12 100644 --- a/src/features/requestedPackages/requestedPackagesSlice.ts +++ b/src/features/requestedPackages/requestedPackagesSlice.ts @@ -83,9 +83,14 @@ export const requestedPackagesSlice = createSlice({ } } ) => { - // dependencies can be undefined if a lockfile specification is provided - // TODO: parse the lockfile and populate the dependencies - const dependencies = spec?.dependencies ?? []; + // dependencies can be undefined if a lockfile specification is provided, + // try getting dependencies from lockfile conda packages + const dependencies = + spec?.dependencies ?? + spec?.lockfile?.package + ?.filter((p: any) => p?.manager === "conda") + ?.map((p: any) => `${p?.name}==${p?.version}`) ?? + []; state.requestedPackages = dependencies; state.packagesWithLatestVersions = {}; From d2a7e9a709904d3d64d5e6c2e6f8929679abaa5d Mon Sep 17 00:00:00 2001 From: gabalafou Date: Sun, 5 May 2024 12:26:17 +0200 Subject: [PATCH 10/10] gabs edits --- src/common/models/CondaSpecification.ts | 5 ++- src/common/models/Lockfile.ts | 2 + src/common/models/index.ts | 1 + src/features/channels/channelsSlice.ts | 18 ++++---- .../components/EnvironmentCreate.tsx | 4 +- .../Specification/SpecificationCreate.tsx | 28 ++++++++----- .../components/EnvironmentDetails.tsx | 4 +- .../Specification/SpecificationEdit.tsx | 41 ++++++++++++------- src/features/lockfile/lockfileSlice.ts | 3 +- .../requestedPackagesSlice.ts | 19 +++++---- src/features/yamlEditor/components/editor.tsx | 4 +- .../SpecificationCreate.test.tsx | 4 +- 12 files changed, 79 insertions(+), 54 deletions(-) create mode 100644 src/common/models/Lockfile.ts diff --git a/src/common/models/CondaSpecification.ts b/src/common/models/CondaSpecification.ts index fe52cb8e..f6208261 100644 --- a/src/common/models/CondaSpecification.ts +++ b/src/common/models/CondaSpecification.ts @@ -1,4 +1,5 @@ -import { CondaSpecificationPip } from "./CondaSpecificationPip"; +import type { CondaSpecificationPip } from "./CondaSpecificationPip"; +import type { Lockfile } from "./Lockfile"; export type CondaSpecification = { name: string; @@ -6,5 +7,5 @@ export type CondaSpecification = { dependencies: (string | CondaSpecificationPip)[]; variables: Record; prefix?: string | null; - lockfile?: any | null; + lockfile?: Lockfile; }; diff --git a/src/common/models/Lockfile.ts b/src/common/models/Lockfile.ts new file mode 100644 index 00000000..292d6902 --- /dev/null +++ b/src/common/models/Lockfile.ts @@ -0,0 +1,2 @@ +// TODO: define lockfile type better +export type Lockfile = Record; diff --git a/src/common/models/index.ts b/src/common/models/index.ts index 34bd1cb4..cb7c4116 100644 --- a/src/common/models/index.ts +++ b/src/common/models/index.ts @@ -7,3 +7,4 @@ export * from "./Artifact"; export * from "./BuildPackage"; export * from "./BuildArtifact"; export * from "./Namespace"; +export * from "./Lockfile"; diff --git a/src/features/channels/channelsSlice.ts b/src/features/channels/channelsSlice.ts index ce313649..6ab5c7d9 100644 --- a/src/features/channels/channelsSlice.ts +++ b/src/features/channels/channelsSlice.ts @@ -29,14 +29,16 @@ export const channelsSlice = createSlice({ } } ) => { - // channels can be undefined if a lockfile specification is provided, - // try getting channels from metadata in that case - const channels = - spec?.channels ?? - spec?.lockfile?.metadata?.channels?.map( - (channel: any) => channel?.url - ) ?? - []; + let channels = []; + + if (spec.channels) { + channels = spec.channels; + } else if (spec.lockfile?.metadata?.channels) { + channels = spec.lockfile.metadata.channels.map( + // Note: in the lockfile spec, a channel URL can be a string identifier like "conda-forge" + (channel: { url: string }) => channel.url + ); + } state.channels = channels; } diff --git a/src/features/environmentCreate/components/EnvironmentCreate.tsx b/src/features/environmentCreate/components/EnvironmentCreate.tsx index f84fd149..45003a9b 100644 --- a/src/features/environmentCreate/components/EnvironmentCreate.tsx +++ b/src/features/environmentCreate/components/EnvironmentCreate.tsx @@ -55,11 +55,11 @@ export const EnvironmentCreate = ({ environmentNotification }: IEnvCreate) => { const createEnvironment = async ( code: ICreateEnvironmentArgs, - is_lockfile: boolean + isLockfile: boolean ) => { const namespace = newEnvironment?.namespace; let environmentInfo; - if (is_lockfile) { + if (isLockfile) { environmentInfo = { namespace, specification: stringify(code), diff --git a/src/features/environmentCreate/components/Specification/SpecificationCreate.tsx b/src/features/environmentCreate/components/Specification/SpecificationCreate.tsx index 209c84a2..0c8f6eaf 100644 --- a/src/features/environmentCreate/components/Specification/SpecificationCreate.tsx +++ b/src/features/environmentCreate/components/Specification/SpecificationCreate.tsx @@ -1,5 +1,9 @@ import React, { useCallback, useEffect, useState } from "react"; import Box from "@mui/material/Box"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; import { ChannelsEdit } from "../../../../features/channels"; import { BlockContainerEditMode } from "../../../../components"; import { StyledButtonPrimary } from "../../../../styles"; @@ -13,9 +17,6 @@ import { environmentCreateStateCleared } from "../../environmentCreateSlice"; import { getStylesForStyleType } from "../../../../utils/helpers"; -import Select, { SelectChangeEvent } from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import FormControl from "@mui/material/FormControl"; export const SpecificationCreate = ({ onCreateEnvironment }: any) => { const dispatch = useAppDispatch(); @@ -95,14 +96,16 @@ export const SpecificationCreate = ({ onCreateEnvironment }: any) => { variables: editorContent.variables }) ); + } else { + // TODO: sync GUI with lockfile code } - // Do nothing when specificationType === lockfile } else { setEditorContent({ dependencies: requestedPackages, variables: environmentVariables, channels }); + // TODO: sync lockfile code with GUI } setShow(value); @@ -130,15 +133,15 @@ export const SpecificationCreate = ({ onCreateEnvironment }: any) => { const handleSubmit = () => { let code; - let is_lockfile; + let isLockfile; if (show) { if (specificationType === "specification") { code = editorContent; - is_lockfile = false; + isLockfile = false; } else { code = editorContentLockfile; - is_lockfile = true; + isLockfile = true; } } else { code = { @@ -146,10 +149,10 @@ export const SpecificationCreate = ({ onCreateEnvironment }: any) => { variables: environmentVariables, channels }; - is_lockfile = false; + isLockfile = false; } - onCreateEnvironment(code, is_lockfile); + onCreateEnvironment(code, isLockfile); }; useEffect(() => { @@ -168,13 +171,16 @@ export const SpecificationCreate = ({ onCreateEnvironment }: any) => { {show ? ( <> + Format {specificationType === "specification" ? ( diff --git a/src/features/environmentDetails/components/EnvironmentDetails.tsx b/src/features/environmentDetails/components/EnvironmentDetails.tsx index 999e5cb4..1138c112 100644 --- a/src/features/environmentDetails/components/EnvironmentDetails.tsx +++ b/src/features/environmentDetails/components/EnvironmentDetails.tsx @@ -154,7 +154,7 @@ export const EnvironmentDetails = ({ const updateEnvironment = async ( code: IUpdateEnvironmentArgs, - is_lockfile: boolean + isLockfile: boolean ) => { if (!selectedEnvironment) { return; @@ -163,7 +163,7 @@ export const EnvironmentDetails = ({ const namespace = selectedEnvironment.namespace.name; const environment = selectedEnvironment.name; let environmentInfo; - if (is_lockfile) { + if (isLockfile) { environmentInfo = { namespace, specification: stringify(code), diff --git a/src/features/environmentDetails/components/Specification/SpecificationEdit.tsx b/src/features/environmentDetails/components/Specification/SpecificationEdit.tsx index c1429594..bafbf9b4 100644 --- a/src/features/environmentDetails/components/Specification/SpecificationEdit.tsx +++ b/src/features/environmentDetails/components/Specification/SpecificationEdit.tsx @@ -7,6 +7,10 @@ import React, { } from "react"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; import { cloneDeep, debounce } from "lodash"; import { stringify } from "yaml"; import { BlockContainerEditMode } from "../../../../components"; @@ -25,17 +29,17 @@ import { import { CodeEditor } from "../../../../features/yamlEditor"; import { useAppDispatch, useAppSelector } from "../../../../hooks"; import { StyledButtonPrimary } from "../../../../styles"; -import { CondaSpecificationPip } from "../../../../common/models"; -import Select, { SelectChangeEvent } from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import FormControl from "@mui/material/FormControl"; +import type { + CondaSpecificationPip, + Lockfile +} from "../../../../common/models"; interface ISpecificationEdit { descriptionUpdated: boolean; defaultEnvVersIsChanged: boolean; onSpecificationIsChanged: (specificationIsChanged: boolean) => void; onDefaultEnvIsChanged: (defaultEnvVersIsChanged: boolean) => void; - onUpdateEnvironment: (specification: any, is_lockfile: boolean) => void; + onUpdateEnvironment: (specification: any, isLockfile: boolean) => void; onShowDialogAlert: (showDialog: boolean) => void; } export const SpecificationEdit = ({ @@ -73,7 +77,7 @@ export const SpecificationEdit = ({ variables: environmentVariables, channels }); - const [codeLockfile, setCodeLockfile] = useState(lockfile); + const [codeLockfile, setCodeLockfile] = useState(lockfile); const [envIsUpdated, setEnvIsUpdated] = useState(false); const initialChannels = useRef(cloneDeep(channels)); @@ -157,7 +161,7 @@ export const SpecificationEdit = ({ 200 ); - const onUpdateEditorLockfile = debounce((lockfile: any) => { + const onUpdateEditorLockfile = debounce((lockfile: Lockfile) => { const isDifferentLockfile = JSON.stringify(lockfile) !== stringifiedInitialLockfile; @@ -172,18 +176,22 @@ export const SpecificationEdit = ({ const onToggleEditorView = (value: boolean) => { if (show) { + // Code Editor -> GUI if (specificationType === "specification") { dispatch(updatePackages(code.dependencies)); dispatch(updateChannels(code.channels)); dispatch(updateEnvironmentVariables(code.variables)); + } else { + // TODO: sync GUI with lockfile code } - // Do nothing when specificationType === lockfile } else { + // GUI -> Code Editor setCode({ dependencies: requestedPackages, variables: environmentVariables, channels }); + // TODO: sync lockfile code with GUI } setShow(value); @@ -191,15 +199,15 @@ export const SpecificationEdit = ({ const onEditEnvironment = () => { let envContent; - let is_lockfile; + let isLockfile; if (show) { if (specificationType === "specification") { envContent = code; - is_lockfile = false; + isLockfile = false; } else { envContent = codeLockfile; - is_lockfile = true; + isLockfile = true; } } else { envContent = { @@ -207,10 +215,10 @@ export const SpecificationEdit = ({ variables: environmentVariables, channels }; - is_lockfile = false; + isLockfile = false; } - onUpdateEnvironment(envContent, is_lockfile); + onUpdateEnvironment(envContent, isLockfile); }; const onCancelEdition = () => { @@ -266,13 +274,16 @@ export const SpecificationEdit = ({ {show ? ( <> + Format {specificationType === "specification" ? ( diff --git a/src/features/lockfile/lockfileSlice.ts b/src/features/lockfile/lockfileSlice.ts index 49c309f7..595b43bb 100644 --- a/src/features/lockfile/lockfileSlice.ts +++ b/src/features/lockfile/lockfileSlice.ts @@ -1,8 +1,9 @@ import { createSlice } from "@reduxjs/toolkit"; import { environmentDetailsApiSlice } from "../environmentDetails"; +import type { Lockfile } from "../../common/models/Lockfile"; export interface ILockfileState { - lockfile: any; + lockfile: Lockfile; } const initialState: ILockfileState = { lockfile: {} }; diff --git a/src/features/requestedPackages/requestedPackagesSlice.ts b/src/features/requestedPackages/requestedPackagesSlice.ts index 3bde3b12..83c6efdf 100644 --- a/src/features/requestedPackages/requestedPackagesSlice.ts +++ b/src/features/requestedPackages/requestedPackagesSlice.ts @@ -83,20 +83,21 @@ export const requestedPackagesSlice = createSlice({ } } ) => { - // dependencies can be undefined if a lockfile specification is provided, - // try getting dependencies from lockfile conda packages - const dependencies = - spec?.dependencies ?? - spec?.lockfile?.package - ?.filter((p: any) => p?.manager === "conda") - ?.map((p: any) => `${p?.name}==${p?.version}`) ?? - []; + let dependencies = []; + + if (spec.dependencies) { + dependencies = spec.dependencies; + } else if (spec.lockfile?.package) { + dependencies = spec.lockfile.package + .filter((p: Record) => p.manager === "conda") + .map((p: Record) => `${p?.name}==${p?.version}`); + } state.requestedPackages = dependencies; state.packagesWithLatestVersions = {}; state.versionsWithConstraints = {}; - dependencies.forEach(dep => { + dependencies.forEach((dep: string | CondaSpecificationPip) => { if (typeof dep === "string") { const { constraint, name, version } = requestedPackageParser(dep); diff --git a/src/features/yamlEditor/components/editor.tsx b/src/features/yamlEditor/components/editor.tsx index 64ea571f..a1665fa4 100644 --- a/src/features/yamlEditor/components/editor.tsx +++ b/src/features/yamlEditor/components/editor.tsx @@ -8,8 +8,8 @@ import { greenAccentTheme } from "../themes"; import { PrefContext } from "../../../preferences"; export interface ICodeEditor { - code: any; - onChangeEditor: (code: any) => void; + code: string; + onChangeEditor: (parsedCode: unknown) => void; } export const CodeEditor = ({ code, onChangeEditor }: ICodeEditor) => { diff --git a/test/environmentCreate/SpecificationCreate.test.tsx b/test/environmentCreate/SpecificationCreate.test.tsx index 21a303e2..3889e79b 100644 --- a/test/environmentCreate/SpecificationCreate.test.tsx +++ b/test/environmentCreate/SpecificationCreate.test.tsx @@ -49,14 +49,14 @@ describe("", () => { fireEvent.click(switchButton); fireEvent.click(createButton); - const is_lockfile = false; + const isLockfile = false; expect(mockOnCreateEnvironment).toHaveBeenCalledWith( { channels: [], dependencies: [], variables: {} }, - is_lockfile + isLockfile ); });