-
Notifications
You must be signed in to change notification settings - Fork 357
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: workspace task config policies UI [CM-478] (#9950)
- Loading branch information
1 parent
924f663
commit cf9bdc8
Showing
9 changed files
with
310 additions
and
6 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import UIProvider, { DefaultTheme } from 'hew/Theme'; | ||
import { ConfirmationProvider } from 'hew/useConfirm'; | ||
|
||
import ConfigPolicies from './ConfigPolicies'; | ||
import { ThemeProvider } from './ThemeProvider'; | ||
|
||
const mocks = vi.hoisted(() => { | ||
return { | ||
canModifyWorkspaceConfigPolicies: false, | ||
}; | ||
}); | ||
|
||
vi.mock('hooks/usePermissions', () => { | ||
const usePermissions = vi.fn(() => { | ||
return { | ||
canModifyWorkspaceConfigPolicies: mocks.canModifyWorkspaceConfigPolicies, | ||
}; | ||
}); | ||
return { | ||
default: usePermissions, | ||
}; | ||
}); | ||
|
||
vi.mock('@uiw/react-codemirror', () => ({ | ||
__esModule: true, | ||
default: () => <></>, | ||
})); | ||
|
||
vi.mock('services/api', () => ({ | ||
getWorkspaceConfigPolicies: () => Promise.resolve({ configPolicies: {} }), | ||
})); | ||
|
||
const setup = () => { | ||
render( | ||
<UIProvider theme={DefaultTheme.Light}> | ||
<ThemeProvider> | ||
<ConfirmationProvider> | ||
<ConfigPolicies /> | ||
</ConfirmationProvider> | ||
</ThemeProvider> | ||
</UIProvider>, | ||
); | ||
}; | ||
|
||
describe('Config Policies', () => { | ||
it('allows changes with permissions', async () => { | ||
mocks.canModifyWorkspaceConfigPolicies = true; | ||
setup(); | ||
expect(await screen.findByRole('button', { name: 'Apply' })).toBeInTheDocument(); | ||
}); | ||
|
||
it('prevents changes without permissions', () => { | ||
mocks.canModifyWorkspaceConfigPolicies = false; | ||
setup(); | ||
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import Alert from 'hew/Alert'; | ||
import Button from 'hew/Button'; | ||
import CodeEditor from 'hew/CodeEditor'; | ||
import Column from 'hew/Column'; | ||
import Form, { hasErrors } from 'hew/Form'; | ||
import Row from 'hew/Row'; | ||
import Spinner from 'hew/Spinner'; | ||
import { useToast } from 'hew/Toast'; | ||
import useConfirm from 'hew/useConfirm'; | ||
import { Loadable, NotLoaded } from 'hew/utils/loadable'; | ||
import yaml from 'js-yaml'; | ||
import { useState } from 'react'; | ||
|
||
import { useAsync } from 'hooks/useAsync'; | ||
import usePermissions from 'hooks/usePermissions'; | ||
import { getWorkspaceConfigPolicies, updateWorkspaceConfigPolicies } from 'services/api'; | ||
import handleError from 'utils/error'; | ||
|
||
interface Props { | ||
workspaceId?: number; | ||
} | ||
|
||
type FormInputs = { | ||
task: string; | ||
}; | ||
|
||
const ConfigPolicies: React.FC<Props> = ({ workspaceId }: Props) => { | ||
const confirm = useConfirm(); | ||
const { openToast } = useToast(); | ||
const { canModifyWorkspaceConfigPolicies, loading: rbacLoading } = usePermissions(); | ||
const [form] = Form.useForm<FormInputs>(); | ||
|
||
const [disabled, setDisabled] = useState(true); | ||
|
||
const APPLY_MESSAGE = "You're about to apply these config policies to this workspace."; | ||
const VIEW_MESSAGE = 'An admin applied these config policies to this workspace.'; | ||
|
||
const updatePolicies = async () => { | ||
if (workspaceId) { | ||
try { | ||
await updateWorkspaceConfigPolicies({ | ||
configPolicies: form.getFieldValue('task'), | ||
workloadType: 'NTSC', | ||
workspaceId, | ||
}); | ||
openToast({ title: 'Config policies updated' }); | ||
} catch (error) { | ||
handleError(error); | ||
} | ||
} | ||
}; | ||
|
||
const confirmApply = () => { | ||
confirm({ | ||
content: ( | ||
<span> | ||
This will impact{' '} | ||
<strong> | ||
<u>all</u> | ||
</strong>{' '} | ||
underlying projects and their experiments in this workspace. | ||
</span> | ||
), | ||
okText: 'Apply', | ||
onConfirm: updatePolicies, | ||
onError: handleError, | ||
size: 'medium', | ||
title: APPLY_MESSAGE, | ||
}); | ||
}; | ||
|
||
const loadableTaskConfigPolicies: Loadable<string | undefined> = useAsync(async () => { | ||
if (workspaceId) { | ||
const response = await getWorkspaceConfigPolicies({ | ||
workloadType: 'NTSC', | ||
workspaceId, | ||
}); | ||
return response.configPolicies; | ||
} | ||
return NotLoaded; | ||
}, [workspaceId]); | ||
|
||
const initialTaskYAML = yaml.dump(loadableTaskConfigPolicies.getOrElse(undefined)); | ||
|
||
const handleChange = () => { | ||
setDisabled(hasErrors(form) || form.getFieldValue('task') === initialTaskYAML); | ||
}; | ||
|
||
if (rbacLoading) return <Spinner spinning />; | ||
|
||
return ( | ||
<Column> | ||
<Row width="fill"> | ||
<div style={{ width: '100%' }}> | ||
{canModifyWorkspaceConfigPolicies ? ( | ||
<Alert | ||
action={ | ||
<Button disabled={disabled} onClick={confirmApply}> | ||
Apply | ||
</Button> | ||
} | ||
message={APPLY_MESSAGE} | ||
showIcon | ||
/> | ||
) : ( | ||
<Alert message={VIEW_MESSAGE} showIcon /> | ||
)} | ||
</div> | ||
</Row> | ||
<Row width="fill"> | ||
<div style={{ width: '100%' }}> | ||
<Form form={form} onFieldsChange={handleChange}> | ||
<Form.Item | ||
name="task" | ||
rules={[ | ||
{ | ||
validator: (_, value) => { | ||
try { | ||
yaml.load(value); | ||
return Promise.resolve(); | ||
} catch (err: unknown) { | ||
return Promise.reject( | ||
new Error( | ||
`Invalid YAML on line ${(err as { mark: { line: string } }).mark.line}.`, | ||
), | ||
); | ||
} | ||
}, | ||
}, | ||
]}> | ||
<CodeEditor | ||
file={initialTaskYAML} | ||
files={[{ key: 'task', title: 'Task Config Policies' }]} | ||
onError={(error) => { | ||
handleError(error); | ||
}} | ||
/> | ||
</Form.Item> | ||
</Form> | ||
</div> | ||
</Row> | ||
</Column> | ||
); | ||
}; | ||
|
||
export default ConfigPolicies; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.