-
Notifications
You must be signed in to change notification settings - Fork 899
feat: allow users to duplicate workspaces by parameters #10362
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
Changes from 1 commit
9e4f999
112bc95
15fdfbf
d007b86
294156e
4554895
25bacf2
0947031
1d4d4d7
d71acf6
c0a8c56
0b3e954
7a763a9
da488fa
5c7242f
98d1b1b
aeacda5
230a4f1
75b1839
7cf446f
bf21656
38ba3b2
923d080
8b3d4dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ import TextField from "@mui/material/TextField"; | |
import * as TypesGen from "api/typesGenerated"; | ||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; | ||
import { FormikContextType, useFormik } from "formik"; | ||
import { FC, useEffect, useState } from "react"; | ||
import { FC, useEffect, useReducer, useState } from "react"; | ||
import { | ||
getFormHelpers, | ||
nameValidator, | ||
|
@@ -29,11 +29,18 @@ import { | |
import { ExternalAuth } from "./ExternalAuth"; | ||
import { ErrorAlert } from "components/Alert/ErrorAlert"; | ||
import { Stack } from "components/Stack/Stack"; | ||
import { type ExternalAuthPollingState } from "./CreateWorkspacePage"; | ||
import { | ||
CreateWorkspaceMode, | ||
type ExternalAuthPollingState, | ||
} from "./CreateWorkspacePage"; | ||
import { useSearchParams } from "react-router-dom"; | ||
import { CreateWSPermissions } from "./permissions"; | ||
import { useTheme } from "@emotion/react"; | ||
import { Margins } from "components/Margins/Margins"; | ||
import { Alert } from "components/Alert/Alert"; | ||
|
||
export interface CreateWorkspacePageViewProps { | ||
mode: CreateWorkspaceMode; | ||
error: unknown; | ||
defaultName: string; | ||
defaultOwner: TypesGen.User; | ||
|
@@ -54,6 +61,7 @@ export interface CreateWorkspacePageViewProps { | |
} | ||
|
||
export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({ | ||
mode, | ||
error, | ||
defaultName, | ||
defaultOwner, | ||
|
@@ -112,133 +120,143 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({ | |
); | ||
|
||
return ( | ||
<FullPageHorizontalForm title="New workspace" onCancel={onCancel}> | ||
<HorizontalForm onSubmit={form.handleSubmit}> | ||
{Boolean(error) && <ErrorAlert error={error} />} | ||
{/* General info */} | ||
<FormSection | ||
title="General" | ||
description="The template and name of your new workspace." | ||
> | ||
<FormFields> | ||
<SelectedTemplate template={template} /> | ||
{versionId && versionId !== template.active_version_id && ( | ||
<Stack spacing={1} className={styles.hasDescription}> | ||
<TextField | ||
disabled | ||
fullWidth | ||
value={versionId} | ||
label="Version ID" | ||
/> | ||
<span className={styles.description}> | ||
This parameter has been preset, and cannot be modified. | ||
</span> | ||
</Stack> | ||
)} | ||
<TextField | ||
{...getFieldHelpers("name")} | ||
disabled={creatingWorkspace} | ||
onChange={onChangeTrimmed(form)} | ||
autoFocus | ||
fullWidth | ||
label="Workspace Name" | ||
/> | ||
</FormFields> | ||
</FormSection> | ||
<> | ||
{mode === "duplicate" && <DuplicateWarningMessage />} | ||
|
||
{permissions.createWorkspaceForUser && ( | ||
<FullPageHorizontalForm title="New workspace" onCancel={onCancel}> | ||
<HorizontalForm onSubmit={form.handleSubmit}> | ||
{Boolean(error) && <ErrorAlert error={error} />} | ||
{/* General info */} | ||
<FormSection | ||
title="Workspace Owner" | ||
description="Only admins can create workspace for other users." | ||
title="General" | ||
description="The template and name of your new workspace." | ||
> | ||
<FormFields> | ||
<UserAutocomplete | ||
value={owner} | ||
onChange={(user) => { | ||
setOwner(user ?? defaultOwner); | ||
}} | ||
label="Owner" | ||
size="medium" | ||
<SelectedTemplate template={template} /> | ||
{versionId && versionId !== template.active_version_id && ( | ||
<Stack spacing={1} className={styles.hasDescription}> | ||
<TextField | ||
disabled | ||
fullWidth | ||
value={versionId} | ||
label="Version ID" | ||
/> | ||
<span className={styles.description}> | ||
This parameter has been preset, and cannot be modified. | ||
</span> | ||
</Stack> | ||
)} | ||
<TextField | ||
{...getFieldHelpers("name")} | ||
disabled={creatingWorkspace} | ||
onChange={onChangeTrimmed(form)} | ||
autoFocus | ||
fullWidth | ||
label="Workspace Name" | ||
/> | ||
</FormFields> | ||
</FormSection> | ||
)} | ||
|
||
{externalAuth && externalAuth.length > 0 && ( | ||
<FormSection | ||
title="External Authentication" | ||
description="This template requires authentication to external services." | ||
> | ||
<FormFields> | ||
{externalAuth.map((auth) => ( | ||
<ExternalAuth | ||
key={auth.id} | ||
authenticateURL={auth.authenticate_url} | ||
authenticated={auth.authenticated} | ||
externalAuthPollingState={externalAuthPollingState} | ||
startPollingExternalAuth={startPollingExternalAuth} | ||
displayName={auth.display_name} | ||
displayIcon={auth.display_icon} | ||
error={externalAuthErrors[auth.id]} | ||
{permissions.createWorkspaceForUser && ( | ||
<FormSection | ||
title="Workspace Owner" | ||
description="Only admins can create workspace for other users." | ||
> | ||
<FormFields> | ||
<UserAutocomplete | ||
value={owner} | ||
onChange={(user) => { | ||
setOwner(user ?? defaultOwner); | ||
}} | ||
label="Owner" | ||
size="medium" | ||
/> | ||
))} | ||
</FormFields> | ||
</FormSection> | ||
)} | ||
</FormFields> | ||
</FormSection> | ||
)} | ||
|
||
{parameters && ( | ||
<> | ||
<MutableTemplateParametersSection | ||
templateParameters={parameters} | ||
getInputProps={(parameter, index) => { | ||
return { | ||
...getFieldHelpers( | ||
"rich_parameter_values[" + index + "].value", | ||
), | ||
onChange: async (value) => { | ||
await form.setFieldValue("rich_parameter_values." + index, { | ||
name: parameter.name, | ||
value: value, | ||
}); | ||
}, | ||
disabled: | ||
disabledParamsList?.includes( | ||
parameter.name.toLowerCase().replace(/ /g, "_"), | ||
) || creatingWorkspace, | ||
}; | ||
}} | ||
/> | ||
<ImmutableTemplateParametersSection | ||
templateParameters={parameters} | ||
classes={{ root: styles.warningSection }} | ||
getInputProps={(parameter, index) => { | ||
return { | ||
...getFieldHelpers( | ||
"rich_parameter_values[" + index + "].value", | ||
), | ||
onChange: async (value) => { | ||
await form.setFieldValue("rich_parameter_values." + index, { | ||
name: parameter.name, | ||
value: value, | ||
}); | ||
}, | ||
disabled: | ||
disabledParamsList?.includes( | ||
parameter.name.toLowerCase().replace(/ /g, "_"), | ||
) || creatingWorkspace, | ||
}; | ||
}} | ||
/> | ||
</> | ||
)} | ||
{externalAuth && externalAuth.length > 0 && ( | ||
<FormSection | ||
title="External Authentication" | ||
description="This template requires authentication to external services." | ||
> | ||
<FormFields> | ||
{externalAuth.map((auth) => ( | ||
<ExternalAuth | ||
key={auth.id} | ||
authenticateURL={auth.authenticate_url} | ||
authenticated={auth.authenticated} | ||
externalAuthPollingState={externalAuthPollingState} | ||
startPollingExternalAuth={startPollingExternalAuth} | ||
displayName={auth.display_name} | ||
displayIcon={auth.display_icon} | ||
error={externalAuthErrors[auth.id]} | ||
/> | ||
))} | ||
</FormFields> | ||
</FormSection> | ||
)} | ||
|
||
<FormFooter | ||
onCancel={onCancel} | ||
isLoading={creatingWorkspace} | ||
submitLabel="Create Workspace" | ||
/> | ||
</HorizontalForm> | ||
</FullPageHorizontalForm> | ||
{parameters && ( | ||
<> | ||
<MutableTemplateParametersSection | ||
templateParameters={parameters} | ||
getInputProps={(parameter, index) => { | ||
return { | ||
...getFieldHelpers( | ||
"rich_parameter_values[" + index + "].value", | ||
), | ||
onChange: async (value) => { | ||
await form.setFieldValue( | ||
"rich_parameter_values." + index, | ||
{ | ||
name: parameter.name, | ||
value: value, | ||
}, | ||
); | ||
}, | ||
disabled: | ||
disabledParamsList?.includes( | ||
parameter.name.toLowerCase().replace(/ /g, "_"), | ||
) || creatingWorkspace, | ||
}; | ||
}} | ||
/> | ||
<ImmutableTemplateParametersSection | ||
templateParameters={parameters} | ||
classes={{ root: styles.warningSection }} | ||
getInputProps={(parameter, index) => { | ||
return { | ||
...getFieldHelpers( | ||
"rich_parameter_values[" + index + "].value", | ||
), | ||
onChange: async (value) => { | ||
await form.setFieldValue( | ||
"rich_parameter_values." + index, | ||
{ | ||
name: parameter.name, | ||
value: value, | ||
}, | ||
); | ||
}, | ||
disabled: | ||
disabledParamsList?.includes( | ||
parameter.name.toLowerCase().replace(/ /g, "_"), | ||
) || creatingWorkspace, | ||
}; | ||
}} | ||
/> | ||
</> | ||
)} | ||
|
||
<FormFooter | ||
onCancel={onCancel} | ||
isLoading={creatingWorkspace} | ||
submitLabel="Create Workspace" | ||
/> | ||
</HorizontalForm> | ||
</FullPageHorizontalForm> | ||
</> | ||
); | ||
}; | ||
|
||
|
@@ -279,6 +297,31 @@ const useExternalAuthVerification = ( | |
}; | ||
}; | ||
|
||
function DuplicateWarningMessage() { | ||
const [isDismissed, dismiss] = useReducer(() => true, false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is clever, but maybe too clever. 😅 it took me a second to realize what this does, and others who are less familiar with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's fair, and I figured I'd get some pushback on that. I use the action-less reducer pattern a lot in my personal code, where I want to make my expected state transitions super, super explicit (in this case, making it clear that state can only ever transition from false to true, and shouldn't be undone). It helps that useReducer has function overloads to augment the type of the dispatch if it is action-less But as far as readability/communication, it's probably a bit much/too convoluted for a simple use case like this. I'll change this |
||
const theme = useTheme(); | ||
|
||
if (isDismissed) { | ||
return null; | ||
} | ||
|
||
// Setup looks a little hokey (having an Alert already fully configured to | ||
// listen to dismissals, on top of more dismissal state), but relying solely | ||
// on the Alert API wouldn't get rid of the div and horizontal margin helper | ||
// after the dismiss happens. Not using CSS margins because those can be a | ||
// style maintenance nightmare over time | ||
return ( | ||
<div css={{ paddingTop: theme.spacing(6) }}> | ||
<Margins size="medium"> | ||
<Alert severity="warning" dismissible onDismiss={dismiss}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like this should be an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, agreed. Will get that fixed up |
||
Duplicating a workspace only copies its parameters. No state from the | ||
old workspace is copied over. | ||
</Alert> | ||
</Margins> | ||
</div> | ||
); | ||
} | ||
|
||
const useStyles = makeStyles((theme) => ({ | ||
hasDescription: { | ||
paddingBottom: theme.spacing(2), | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only thing that changes with this chunk of code is that a React fragment got added, so I could squeeze in the
DuplicateWarningMessage
component. Everything else is the same – it just changed by an indentation level.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could also put it next to the
{Boolean(error) ...
and that might look nice