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 = {
|
const emptyConfig = {
|
||||||
...testConfig,
|
...testConfig,
|
||||||
settings: { ...testConfig.settings, clientId: '', clientSecret: '' },
|
settings: { ...testConfig.settings, enabled: false, clientId: '', clientSecret: '' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function setup(jsx: JSX.Element) {
|
function setup(jsx: JSX.Element) {
|
||||||
@ -79,7 +79,6 @@ describe('ProviderConfigForm', () => {
|
|||||||
|
|
||||||
it('renders all fields correctly', async () => {
|
it('renders all fields correctly', async () => {
|
||||||
setup(<ProviderConfigForm config={testConfig} provider={testConfig.provider} />);
|
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('textbox', { name: /Client ID/i })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('combobox', { name: /Team IDs/i })).toBeInTheDocument();
|
expect(screen.getByRole('combobox', { name: /Team IDs/i })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('combobox', { name: /Allowed organizations/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();
|
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} />);
|
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.getByRole('textbox', { name: /Client ID/i }), 'test-client-id');
|
||||||
await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret');
|
await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret');
|
||||||
@ -96,7 +95,7 @@ describe('ProviderConfigForm', () => {
|
|||||||
// Add two orgs
|
// 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-org1{enter}');
|
||||||
await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org2{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(() => {
|
await waitFor(() => {
|
||||||
expect(putMock).toHaveBeenCalledWith(
|
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} />);
|
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
|
// Should show an alert for empty client ID
|
||||||
expect(await screen.findAllByRole('alert')).toHaveLength(1);
|
expect(await screen.findAllByRole('alert')).toHaveLength(1);
|
||||||
@ -133,7 +176,9 @@ describe('ProviderConfigForm', () => {
|
|||||||
|
|
||||||
it('should delete the current config', async () => {
|
it('should delete the current config', async () => {
|
||||||
const { user } = setup(<ProviderConfigForm config={emptyConfig} provider={emptyConfig.provider} />);
|
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();
|
expect(screen.getByRole('dialog', { name: /Reset/i })).toBeInTheDocument();
|
||||||
|
|
||||||
|
@ -3,7 +3,19 @@ import { useForm } from 'react-hook-form';
|
|||||||
|
|
||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
import { getAppEvents, getBackendSrv, isFetchError, locationService, reportInteraction } from '@grafana/runtime';
|
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 { FormPrompt } from '../../core/components/FormPrompt/FormPrompt';
|
||||||
import { Page } from '../../core/components/Page/Page';
|
import { Page } from '../../core/components/Page/Page';
|
||||||
@ -39,6 +51,18 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf
|
|||||||
const sections = sectionFields[provider];
|
const sections = sectionFields[provider];
|
||||||
const [resetConfig, setResetConfig] = useState(false);
|
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) => {
|
const onSubmit = async (data: SSOProviderDTO) => {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setSubmitError(false);
|
setSubmitError(false);
|
||||||
@ -114,95 +138,103 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isEnabled = config?.settings.enabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page.Contents isLoading={isLoading}>
|
<Page.Contents isLoading={isLoading}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} style={{ maxWidth: '600px' }}>
|
<form onSubmit={handleSubmit(onSubmit)} style={{ maxWidth: '600px' }}>
|
||||||
<>
|
<FormPrompt
|
||||||
<FormPrompt
|
confirmRedirect={!!Object.keys(dirtyFields).length && !dataSubmitted}
|
||||||
confirmRedirect={!!Object.keys(dirtyFields).length && !dataSubmitted}
|
onDiscard={() => {
|
||||||
onDiscard={() => {
|
reportInteraction('grafana_authentication_ssosettings_abandoned', {
|
||||||
reportInteraction('grafana_authentication_ssosettings_abandoned', {
|
provider,
|
||||||
provider,
|
});
|
||||||
});
|
reset();
|
||||||
reset();
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<Field label="Enabled" hidden={true}>
|
||||||
<Field label="Enabled">
|
<Switch {...register('enabled')} id="enabled" label={'Enabled'} />
|
||||||
<Switch {...register('enabled')} id="enabled" label={'Enabled'} />
|
</Field>
|
||||||
</Field>
|
{sections ? (
|
||||||
{sections ? (
|
<Stack gap={2} direction={'column'}>
|
||||||
<Stack gap={2} direction={'column'}>
|
{sections
|
||||||
{sections
|
.filter((section) => !section.hidden)
|
||||||
.filter((section) => !section.hidden)
|
.map((section, index) => {
|
||||||
.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) => {
|
|
||||||
return (
|
return (
|
||||||
<FieldRenderer
|
<CollapsableSection label={section.name} isOpen={index === 0} key={section.name}>
|
||||||
key={field}
|
{section.fields
|
||||||
field={field}
|
.filter((field) => (typeof field !== 'string' ? !field.hidden : true))
|
||||||
control={control}
|
.map((field) => {
|
||||||
errors={errors}
|
return (
|
||||||
setValue={setValue}
|
<FieldRenderer
|
||||||
register={register}
|
key={typeof field === 'string' ? field : field.name}
|
||||||
watch={watch}
|
field={field}
|
||||||
unregister={unregister}
|
control={control}
|
||||||
provider={provider}
|
errors={errors}
|
||||||
secretConfigured={!!config?.settings.clientSecret}
|
setValue={setValue}
|
||||||
/>
|
register={register}
|
||||||
|
watch={watch}
|
||||||
|
unregister={unregister}
|
||||||
|
provider={provider}
|
||||||
|
secretConfigured={!!config?.settings.clientSecret}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CollapsableSection>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</Stack>
|
||||||
)}
|
) : (
|
||||||
<Box display={'flex'} gap={2} marginTop={6}>
|
<>
|
||||||
<Field>
|
{providerFields.map((field) => {
|
||||||
<Button type={'submit'} disabled={isSaving}>
|
return (
|
||||||
{isSaving ? 'Saving...' : 'Save'}
|
<FieldRenderer
|
||||||
</Button>
|
key={field}
|
||||||
</Field>
|
field={field}
|
||||||
<Field>
|
control={control}
|
||||||
<LinkButton href={'/admin/authentication'} variant={'secondary'}>
|
errors={errors}
|
||||||
Discard
|
setValue={setValue}
|
||||||
</LinkButton>
|
register={register}
|
||||||
</Field>
|
watch={watch}
|
||||||
<Field>
|
unregister={unregister}
|
||||||
<Button
|
provider={provider}
|
||||||
variant={'secondary'}
|
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'}
|
hidden={config?.source === 'system'}
|
||||||
onClick={(event) => {
|
/>
|
||||||
setResetConfig(true);
|
</Dropdown>
|
||||||
}}
|
</Stack>
|
||||||
>
|
</Box>
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</Field>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
</form>
|
</form>
|
||||||
{resetConfig && (
|
{resetConfig && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
|
|||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
|
import { Badge, Stack, Text } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
@ -66,7 +67,20 @@ export const ProviderConfigPage = ({ config, loadProviders, isLoading, provider
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
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} />
|
<ProviderConfigForm config={config} isLoading={isLoading} provider={provider} />
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user