mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: Add Save and enable, Disable buttons to SSO UI (#83672)
* Add Save and enable and Disable button * Change to use Dropdown, reorder buttons * Improve UI * Update public/app/features/auth-config/ProviderConfigForm.tsx * Apply suggestions from code review * Use Stack instead of separate Fields --------- Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
parent
f0dce33034
commit
0218e94d93
@ -62,7 +62,7 @@ const testConfig: SSOProvider = {
|
||||
|
||||
const emptyConfig = {
|
||||
...testConfig,
|
||||
settings: { ...testConfig.settings, clientId: '', clientSecret: '' },
|
||||
settings: { ...testConfig.settings, enabled: false, clientId: '', clientSecret: '' },
|
||||
};
|
||||
|
||||
function setup(jsx: JSX.Element) {
|
||||
@ -79,7 +79,6 @@ describe('ProviderConfigForm', () => {
|
||||
|
||||
it('renders all fields correctly', async () => {
|
||||
setup(<ProviderConfigForm config={testConfig} provider={testConfig.provider} />);
|
||||
expect(screen.getByRole('checkbox', { name: /Enabled/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Client ID/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox', { name: /Team IDs/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox', { name: /Allowed organizations/i })).toBeInTheDocument();
|
||||
@ -87,7 +86,7 @@ describe('ProviderConfigForm', () => {
|
||||
expect(screen.getByRole('link', { name: /Discard/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should save correct data on form submit', async () => {
|
||||
it('should save and enable on form submit', async () => {
|
||||
const { user } = setup(<ProviderConfigForm config={emptyConfig} provider={emptyConfig.provider} />);
|
||||
await user.type(screen.getByRole('textbox', { name: /Client ID/i }), 'test-client-id');
|
||||
await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret');
|
||||
@ -96,7 +95,7 @@ describe('ProviderConfigForm', () => {
|
||||
// Add two orgs
|
||||
await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org1{enter}');
|
||||
await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org2{enter}');
|
||||
await user.click(screen.getByRole('button', { name: /Save/i }));
|
||||
await user.click(screen.getByRole('button', { name: /Save and enable/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(putMock).toHaveBeenCalledWith(
|
||||
@ -123,9 +122,53 @@ describe('ProviderConfigForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate required fields', async () => {
|
||||
it('should save on form submit', async () => {
|
||||
const { user } = setup(<ProviderConfigForm config={emptyConfig} provider={emptyConfig.provider} />);
|
||||
await user.click(screen.getByRole('button', { name: /Save/i }));
|
||||
await user.type(screen.getByRole('textbox', { name: /Client ID/i }), 'test-client-id');
|
||||
await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret');
|
||||
// Type a team name and press enter to select it
|
||||
await user.type(screen.getByRole('combobox', { name: /Team IDs/i }), '12324{enter}');
|
||||
// Add two orgs
|
||||
await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org1{enter}');
|
||||
await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org2{enter}');
|
||||
await user.click(screen.getByText('Save'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(putMock).toHaveBeenCalledWith(
|
||||
'/api/v1/sso-settings/github',
|
||||
{
|
||||
id: '300f9b7c-0488-40db-9763-a22ce8bf6b3e',
|
||||
provider: 'github',
|
||||
settings: {
|
||||
name: 'GitHub',
|
||||
allowedOrganizations: 'test-org1,test-org2',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
teamIds: '12324',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
{ showErrorAlert: false }
|
||||
);
|
||||
|
||||
expect(reportInteractionMock).toHaveBeenCalledWith('grafana_authentication_ssosettings_saved', {
|
||||
provider: 'github',
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate required fields on Save', async () => {
|
||||
const { user } = setup(<ProviderConfigForm config={emptyConfig} provider={emptyConfig.provider} />);
|
||||
await user.click(screen.getByText('Save'));
|
||||
|
||||
// Should show an alert for empty client ID
|
||||
expect(await screen.findAllByRole('alert')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should validate required fields on Save and enable', async () => {
|
||||
const { user } = setup(<ProviderConfigForm config={emptyConfig} provider={emptyConfig.provider} />);
|
||||
await user.click(screen.getByRole('button', { name: /Save and enable/i }));
|
||||
|
||||
// Should show an alert for empty client ID
|
||||
expect(await screen.findAllByRole('alert')).toHaveLength(1);
|
||||
@ -133,7 +176,9 @@ describe('ProviderConfigForm', () => {
|
||||
|
||||
it('should delete the current config', async () => {
|
||||
const { user } = setup(<ProviderConfigForm config={emptyConfig} provider={emptyConfig.provider} />);
|
||||
await user.click(screen.getByRole('button', { name: /Reset/i }));
|
||||
await user.click(screen.getByTitle(/More actions/i));
|
||||
|
||||
await user.click(screen.getByRole('menuitem', { name: /Reset to default values/i }));
|
||||
|
||||
expect(screen.getByRole('dialog', { name: /Reset/i })).toBeInTheDocument();
|
||||
|
||||
|
@ -3,7 +3,19 @@ import { useForm } from 'react-hook-form';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents, getBackendSrv, isFetchError, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { Box, Button, CollapsableSection, ConfirmModal, Field, LinkButton, Stack, Switch } from '@grafana/ui';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CollapsableSection,
|
||||
ConfirmModal,
|
||||
Dropdown,
|
||||
Field,
|
||||
IconButton,
|
||||
LinkButton,
|
||||
Menu,
|
||||
Stack,
|
||||
Switch,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { FormPrompt } from '../../core/components/FormPrompt/FormPrompt';
|
||||
import { Page } from '../../core/components/Page/Page';
|
||||
@ -39,6 +51,18 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf
|
||||
const sections = sectionFields[provider];
|
||||
const [resetConfig, setResetConfig] = useState(false);
|
||||
|
||||
const additionalActionsMenu = (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
label="Reset to default values"
|
||||
icon="history-alt"
|
||||
onClick={() => {
|
||||
setResetConfig(true);
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const onSubmit = async (data: SSOProviderDTO) => {
|
||||
setIsSaving(true);
|
||||
setSubmitError(false);
|
||||
@ -114,95 +138,103 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf
|
||||
}
|
||||
};
|
||||
|
||||
const isEnabled = config?.settings.enabled;
|
||||
|
||||
return (
|
||||
<Page.Contents isLoading={isLoading}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} style={{ maxWidth: '600px' }}>
|
||||
<>
|
||||
<FormPrompt
|
||||
confirmRedirect={!!Object.keys(dirtyFields).length && !dataSubmitted}
|
||||
onDiscard={() => {
|
||||
reportInteraction('grafana_authentication_ssosettings_abandoned', {
|
||||
provider,
|
||||
});
|
||||
reset();
|
||||
}}
|
||||
/>
|
||||
<Field label="Enabled">
|
||||
<Switch {...register('enabled')} id="enabled" label={'Enabled'} />
|
||||
</Field>
|
||||
{sections ? (
|
||||
<Stack gap={2} direction={'column'}>
|
||||
{sections
|
||||
.filter((section) => !section.hidden)
|
||||
.map((section, index) => {
|
||||
return (
|
||||
<CollapsableSection label={section.name} isOpen={index === 0} key={section.name}>
|
||||
{section.fields
|
||||
.filter((field) => (typeof field !== 'string' ? !field.hidden : true))
|
||||
.map((field) => {
|
||||
return (
|
||||
<FieldRenderer
|
||||
key={typeof field === 'string' ? field : field.name}
|
||||
field={field}
|
||||
control={control}
|
||||
errors={errors}
|
||||
setValue={setValue}
|
||||
register={register}
|
||||
watch={watch}
|
||||
unregister={unregister}
|
||||
provider={provider}
|
||||
secretConfigured={!!config?.settings.clientSecret}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CollapsableSection>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
{providerFields.map((field) => {
|
||||
<FormPrompt
|
||||
confirmRedirect={!!Object.keys(dirtyFields).length && !dataSubmitted}
|
||||
onDiscard={() => {
|
||||
reportInteraction('grafana_authentication_ssosettings_abandoned', {
|
||||
provider,
|
||||
});
|
||||
reset();
|
||||
}}
|
||||
/>
|
||||
<Field label="Enabled" hidden={true}>
|
||||
<Switch {...register('enabled')} id="enabled" label={'Enabled'} />
|
||||
</Field>
|
||||
{sections ? (
|
||||
<Stack gap={2} direction={'column'}>
|
||||
{sections
|
||||
.filter((section) => !section.hidden)
|
||||
.map((section, index) => {
|
||||
return (
|
||||
<FieldRenderer
|
||||
key={field}
|
||||
field={field}
|
||||
control={control}
|
||||
errors={errors}
|
||||
setValue={setValue}
|
||||
register={register}
|
||||
watch={watch}
|
||||
unregister={unregister}
|
||||
provider={provider}
|
||||
secretConfigured={!!config?.settings.clientSecret}
|
||||
/>
|
||||
<CollapsableSection label={section.name} isOpen={index === 0} key={section.name}>
|
||||
{section.fields
|
||||
.filter((field) => (typeof field !== 'string' ? !field.hidden : true))
|
||||
.map((field) => {
|
||||
return (
|
||||
<FieldRenderer
|
||||
key={typeof field === 'string' ? field : field.name}
|
||||
field={field}
|
||||
control={control}
|
||||
errors={errors}
|
||||
setValue={setValue}
|
||||
register={register}
|
||||
watch={watch}
|
||||
unregister={unregister}
|
||||
provider={provider}
|
||||
secretConfigured={!!config?.settings.clientSecret}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CollapsableSection>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<Box display={'flex'} gap={2} marginTop={6}>
|
||||
<Field>
|
||||
<Button type={'submit'} disabled={isSaving}>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</Field>
|
||||
<Field>
|
||||
<LinkButton href={'/admin/authentication'} variant={'secondary'}>
|
||||
Discard
|
||||
</LinkButton>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
{providerFields.map((field) => {
|
||||
return (
|
||||
<FieldRenderer
|
||||
key={field}
|
||||
field={field}
|
||||
control={control}
|
||||
errors={errors}
|
||||
setValue={setValue}
|
||||
register={register}
|
||||
watch={watch}
|
||||
unregister={unregister}
|
||||
provider={provider}
|
||||
secretConfigured={!!config?.settings.clientSecret}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<Box display={'flex'} gap={2} marginTop={5}>
|
||||
<Stack alignItems={'center'} gap={2}>
|
||||
<Button
|
||||
type={'submit'}
|
||||
disabled={isSaving}
|
||||
onClick={() => setValue('enabled', !isEnabled)}
|
||||
variant={isEnabled ? 'secondary' : undefined}
|
||||
>
|
||||
{isSaving ? (isEnabled ? 'Disabling...' : 'Saving...') : isEnabled ? 'Disable' : 'Save and enable'}
|
||||
</Button>
|
||||
|
||||
<Button type={'submit'} disabled={isSaving} variant={'secondary'}>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
<LinkButton href={'/admin/authentication'} variant={'secondary'}>
|
||||
Discard
|
||||
</LinkButton>
|
||||
|
||||
<Dropdown overlay={additionalActionsMenu} placement="bottom-start">
|
||||
<IconButton
|
||||
tooltip="More actions"
|
||||
title="More actions"
|
||||
tooltipPlacement="top"
|
||||
size="md"
|
||||
variant="secondary"
|
||||
name="ellipsis-v"
|
||||
hidden={config?.source === 'system'}
|
||||
onClick={(event) => {
|
||||
setResetConfig(true);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</Field>
|
||||
</Box>
|
||||
</>
|
||||
/>
|
||||
</Dropdown>
|
||||
</Stack>
|
||||
</Box>
|
||||
</form>
|
||||
{resetConfig && (
|
||||
<ConfirmModal
|
||||
|
@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { Badge, Stack, Text } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
@ -66,7 +67,20 @@ export const ProviderConfigPage = ({ config, loadProviders, isLoading, provider
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Page navId="authentication" pageNav={pageNav}>
|
||||
<Page
|
||||
navId="authentication"
|
||||
pageNav={pageNav}
|
||||
renderTitle={(title) => (
|
||||
<Stack gap={2} alignItems="center">
|
||||
<Text variant={'h1'}>{title}</Text>
|
||||
<Badge
|
||||
text={config.settings.enabled ? 'Enabled' : 'Not enabled'}
|
||||
color={config.settings.enabled ? 'green' : 'blue'}
|
||||
icon={config.settings.enabled ? 'check' : undefined}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
>
|
||||
<ProviderConfigForm config={config} isLoading={isLoading} provider={provider} />
|
||||
</Page>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user