@plone-collective/volto-identity 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changelog.draft +261 -0
- package/.release-it.json +34 -0
- package/CHANGELOG.md +269 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/babel.config.js +17 -0
- package/locales/de/LC_MESSAGES/volto.po +1162 -0
- package/locales/en/LC_MESSAGES/volto.po +1162 -0
- package/locales/es/LC_MESSAGES/volto.po +1169 -0
- package/locales/pt_BR/LC_MESSAGES/volto.po +1167 -0
- package/locales/volto.pot +1164 -0
- package/news/.changelog_template.jinja +15 -0
- package/news/.gitkeep +0 -0
- package/package.json +66 -0
- package/public/.gitkeep +0 -0
- package/src/actions/actions.test.ts +226 -0
- package/src/actions/index.ts +530 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/Applications/Applications.tsx +169 -0
- package/src/components/Applications/ApplicationsPanel.scss +59 -0
- package/src/components/Applications/ApplicationsPanel.stories.tsx +82 -0
- package/src/components/Applications/ApplicationsPanel.test.tsx +235 -0
- package/src/components/Applications/ApplicationsPanel.tsx +342 -0
- package/src/components/Callback/Callback.scss +17 -0
- package/src/components/Callback/Callback.stories.tsx +43 -0
- package/src/components/Callback/Callback.test.tsx +262 -0
- package/src/components/Callback/Callback.tsx +170 -0
- package/src/components/Consent/Consent.test.tsx +75 -0
- package/src/components/Consent/Consent.tsx +98 -0
- package/src/components/Consent/ConsentPanel.scss +37 -0
- package/src/components/Consent/ConsentPanel.stories.tsx +80 -0
- package/src/components/Consent/ConsentPanel.test.tsx +138 -0
- package/src/components/Consent/ConsentPanel.tsx +162 -0
- package/src/components/ControlPanel/ClientsControlPanel.stories.tsx +38 -0
- package/src/components/ControlPanel/ClientsControlPanel.tsx +321 -0
- package/src/components/ControlPanel/ClientsPanel.scss +21 -0
- package/src/components/ControlPanel/ClientsPanel.stories.tsx +71 -0
- package/src/components/ControlPanel/ClientsPanel.test.tsx +267 -0
- package/src/components/ControlPanel/ClientsPanel.tsx +386 -0
- package/src/components/ControlPanel/ConfirmModal.stories.tsx +35 -0
- package/src/components/ControlPanel/ConfirmModal.test.tsx +66 -0
- package/src/components/ControlPanel/ConfirmModal.tsx +68 -0
- package/src/components/ControlPanel/ProviderForm.stories.tsx +107 -0
- package/src/components/ControlPanel/ProvidersControlPanel.scss +26 -0
- package/src/components/ControlPanel/ProvidersControlPanel.stories.tsx +88 -0
- package/src/components/ControlPanel/ProvidersControlPanel.tsx +600 -0
- package/src/components/ControlPanel/RenderUsers.stories.tsx +112 -0
- package/src/components/ControlPanel/RenderUsers.test.tsx +145 -0
- package/src/components/ControlPanel/RenderUsers.tsx +263 -0
- package/src/components/ControlPanel/SecretReveal.scss +32 -0
- package/src/components/ControlPanel/SecretReveal.stories.tsx +33 -0
- package/src/components/ControlPanel/SecretReveal.test.tsx +101 -0
- package/src/components/ControlPanel/SecretReveal.tsx +105 -0
- package/src/components/ControlPanel/UserAccount.stories.tsx +69 -0
- package/src/components/ControlPanel/UserAccount.test.tsx +117 -0
- package/src/components/ControlPanel/UserAccount.tsx +125 -0
- package/src/components/ControlPanel/UserAccountPanel.scss +38 -0
- package/src/components/ControlPanel/UserAccountPanel.stories.tsx +77 -0
- package/src/components/ControlPanel/UserAccountPanel.test.tsx +173 -0
- package/src/components/ControlPanel/UserAccountPanel.tsx +273 -0
- package/src/components/FirstLogin/FirstLogin.scss +12 -0
- package/src/components/FirstLogin/FirstLogin.stories.tsx +50 -0
- package/src/components/FirstLogin/FirstLogin.tsx +94 -0
- package/src/components/Identities/Identities.stories.tsx +51 -0
- package/src/components/Identities/Identities.test.tsx +134 -0
- package/src/components/Identities/Identities.tsx +217 -0
- package/src/components/Identities/IdentitiesList.scss +49 -0
- package/src/components/Identities/IdentitiesList.stories.tsx +71 -0
- package/src/components/Identities/IdentitiesList.test.tsx +218 -0
- package/src/components/Identities/IdentitiesList.tsx +207 -0
- package/src/components/Identities/ProfileEmails.scss +62 -0
- package/src/components/Identities/ProfileEmails.stories.tsx +75 -0
- package/src/components/Identities/ProfileEmails.test.tsx +132 -0
- package/src/components/Identities/ProfileEmails.tsx +217 -0
- package/src/components/Login/Login.stories.tsx +63 -0
- package/src/components/Login/Login.test.tsx +189 -0
- package/src/components/Login/Login.tsx +163 -0
- package/src/components/Login/LoginForm.scss +39 -0
- package/src/components/Login/LoginForm.stories.tsx +112 -0
- package/src/components/Login/LoginForm.test.tsx +483 -0
- package/src/components/Login/LoginForm.tsx +321 -0
- package/src/components/Login/LoginOverlay.scss +44 -0
- package/src/components/Login/LoginOverlay.stories.tsx +73 -0
- package/src/components/Login/LoginOverlay.test.tsx +98 -0
- package/src/components/Login/LoginOverlay.tsx +107 -0
- package/src/components/Login/LoginPanel.scss +50 -0
- package/src/components/Login/LoginPanel.stories.tsx +40 -0
- package/src/components/Login/LoginPanel.tsx +60 -0
- package/src/components/Login/MagicLinkForm.scss +25 -0
- package/src/components/Login/MagicLinkForm.stories.tsx +27 -0
- package/src/components/Login/MagicLinkForm.tsx +133 -0
- package/src/components/Login/PasswordForm.scss +82 -0
- package/src/components/Login/PasswordForm.stories.tsx +26 -0
- package/src/components/Login/PasswordForm.tsx +159 -0
- package/src/components/Login/ProviderButton.scss +103 -0
- package/src/components/Login/ProviderButton.stories.tsx +74 -0
- package/src/components/Login/ProviderButton.test.tsx +99 -0
- package/src/components/Login/ProviderButton.tsx +97 -0
- package/src/components/ProfileGate/ProfileGate.stories.tsx +118 -0
- package/src/components/ProfileGate/ProfileGate.test.tsx +370 -0
- package/src/components/ProfileGate/ProfileGate.tsx +172 -0
- package/src/components/Toolbar/PersonalTools.stories.tsx +111 -0
- package/src/components/Toolbar/PersonalTools.test.tsx +165 -0
- package/src/components/Toolbar/PersonalTools.tsx +177 -0
- package/src/components/Toolbar/Toolbar.jsx +766 -0
- package/src/components/UserMenu/ApplicationsMenuItem.stories.tsx +62 -0
- package/src/components/UserMenu/ApplicationsMenuItem.test.tsx +109 -0
- package/src/components/UserMenu/ApplicationsMenuItem.tsx +82 -0
- package/src/components/UserMenu/IdentitiesMenuItem.stories.tsx +35 -0
- package/src/components/UserMenu/IdentitiesMenuItem.test.tsx +103 -0
- package/src/components/UserMenu/IdentitiesMenuItem.tsx +34 -0
- package/src/components/UserMenu/MenuItem.stories.tsx +32 -0
- package/src/components/UserMenu/MenuItem.tsx +48 -0
- package/src/components/UserMenu/ProfileMenuItem.stories.tsx +76 -0
- package/src/components/UserMenu/ProfileMenuItem.test.tsx +90 -0
- package/src/components/UserMenu/ProfileMenuItem.tsx +60 -0
- package/src/components/UserMenu/UserAvatar.scss +23 -0
- package/src/components/UserMenu/UserAvatar.stories.tsx +121 -0
- package/src/components/UserMenu/UserAvatar.test.tsx +120 -0
- package/src/components/UserMenu/UserAvatar.tsx +119 -0
- package/src/components/UserMenu/UserMenuPlugs.stories.tsx +101 -0
- package/src/components/UserMenu/UserMenuPlugs.test.tsx +202 -0
- package/src/components/UserMenu/UserMenuPlugs.tsx +142 -0
- package/src/components/UserMenu/UserProfileLoader.test.tsx +211 -0
- package/src/components/UserMenu/UserProfileLoader.tsx +54 -0
- package/src/components/Views/GroupView.scss +36 -0
- package/src/components/Views/GroupView.stories.tsx +98 -0
- package/src/components/Views/GroupView.test.tsx +137 -0
- package/src/components/Views/GroupView.tsx +178 -0
- package/src/components/Views/ProfileView.scss +27 -0
- package/src/components/Views/ProfileView.stories.tsx +34 -0
- package/src/components/Views/ProfileView.test.tsx +82 -0
- package/src/components/Views/ProfileView.tsx +100 -0
- package/src/components/Widgets/ProviderIconWidget.scss +21 -0
- package/src/components/Widgets/ProviderIconWidget.stories.tsx +30 -0
- package/src/components/Widgets/ProviderIconWidget.test.tsx +55 -0
- package/src/components/Widgets/ProviderIconWidget.tsx +143 -0
- package/src/config/menu.ts +35 -0
- package/src/config/reducers.ts +7 -0
- package/src/config/routes.test.ts +177 -0
- package/src/config/routes.ts +143 -0
- package/src/config/settings.test.ts +34 -0
- package/src/config/settings.ts +72 -0
- package/src/config/views.ts +30 -0
- package/src/config/widgets.ts +25 -0
- package/src/constants/ActionTypes.ts +42 -0
- package/src/constants/vocabularies.ts +20 -0
- package/src/customizations/volto/components/manage/Controlpanels/Users/RenderUsers.tsx +23 -0
- package/src/customizations/volto/components/manage/Toolbar/PersonalTools.tsx +21 -0
- package/src/customizations/volto/components/manage/Toolbar/Toolbar.jsx +21 -0
- package/src/helpers/avatar.test.ts +79 -0
- package/src/helpers/avatar.ts +85 -0
- package/src/helpers/callback.test.ts +60 -0
- package/src/helpers/callback.ts +71 -0
- package/src/helpers/clientSchema.test.ts +182 -0
- package/src/helpers/clientSchema.ts +181 -0
- package/src/helpers/firstLogin.test.ts +90 -0
- package/src/helpers/firstLogin.ts +76 -0
- package/src/helpers/groupmap.test.ts +81 -0
- package/src/helpers/groupmap.ts +50 -0
- package/src/helpers/identities.ts +22 -0
- package/src/helpers/navigate.test.ts +132 -0
- package/src/helpers/navigate.ts +126 -0
- package/src/helpers/profileGate.test.ts +150 -0
- package/src/helpers/profileGate.ts +191 -0
- package/src/helpers/profileSource.ts +41 -0
- package/src/helpers/propertymap.test.ts +71 -0
- package/src/helpers/propertymap.ts +43 -0
- package/src/helpers/providerSchema.test.ts +410 -0
- package/src/helpers/providerSchema.ts +362 -0
- package/src/helpers/returnUrl.test.ts +42 -0
- package/src/helpers/returnUrl.ts +41 -0
- package/src/helpers/rowmap.ts +110 -0
- package/src/helpers/showPloneLogin.test.ts +78 -0
- package/src/helpers/showPloneLogin.ts +55 -0
- package/src/helpers/token.test.ts +62 -0
- package/src/helpers/token.ts +71 -0
- package/src/index.ts +22 -0
- package/src/reducers/index.ts +369 -0
- package/src/reducers/reducers.test.ts +369 -0
- package/src/stories/fixtures.tsx +640 -0
- package/src/storybook/withPage.test.tsx +90 -0
- package/src/storybook/withPage.tsx +110 -0
- package/src/storybook/withUserMenu.test.tsx +78 -0
- package/src/storybook/withUserMenu.tsx +136 -0
- package/src/styles.css +262 -0
- package/src/testing.tsx +86 -0
- package/src/types.ts +441 -0
- package/towncrier.toml +38 -0
- package/tsconfig.json +22 -0
- package/vitest.config.mjs +47 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The providers control panel.
|
|
3
|
+
*
|
|
4
|
+
* Shaped after `volto-light-theme`'s Themes panel: a table of what exists,
|
|
5
|
+
* with the add and save actions living in the toolbar rather than inline, and
|
|
6
|
+
* the form itself rendered by Volto's own `Form` from a schema. Nothing here
|
|
7
|
+
* lays out an input; the driver describes its fields and Volto renders them,
|
|
8
|
+
* which is what keeps this panel looking like every other one.
|
|
9
|
+
* @module components/ControlPanel/ProvidersControlPanel
|
|
10
|
+
*/
|
|
11
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
13
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
14
|
+
import { createPortal } from 'react-dom';
|
|
15
|
+
import { Button, Container, Segment, Table } from 'semantic-ui-react';
|
|
16
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
17
|
+
import { toast } from 'react-toastify';
|
|
18
|
+
|
|
19
|
+
import { Helmet } from '@plone/volto/helpers/Helmet/Helmet';
|
|
20
|
+
import { useClient } from '@plone/volto/hooks/client/useClient';
|
|
21
|
+
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
22
|
+
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
|
|
23
|
+
import Toast from '@plone/volto/components/manage/Toast/Toast';
|
|
24
|
+
import { Form } from '@plone/volto/components/manage/Form';
|
|
25
|
+
import {
|
|
26
|
+
getControlpanel,
|
|
27
|
+
updateControlpanel,
|
|
28
|
+
} from '@plone/volto/actions/controlpanels/controlpanels';
|
|
29
|
+
|
|
30
|
+
import addSVG from '@plone/volto/icons/add.svg';
|
|
31
|
+
import backSVG from '@plone/volto/icons/back.svg';
|
|
32
|
+
import clearSVG from '@plone/volto/icons/clear.svg';
|
|
33
|
+
import deleteSVG from '@plone/volto/icons/delete.svg';
|
|
34
|
+
import pencilSVG from '@plone/volto/icons/pencil.svg';
|
|
35
|
+
import worldSVG from '@plone/volto/icons/world.svg';
|
|
36
|
+
import saveSVG from '@plone/volto/icons/save.svg';
|
|
37
|
+
import configurationSVG from '@plone/volto/icons/configuration.svg';
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
createProvider,
|
|
41
|
+
deleteProvider,
|
|
42
|
+
listDrivers,
|
|
43
|
+
listProviders,
|
|
44
|
+
testProvider,
|
|
45
|
+
updateProvider,
|
|
46
|
+
} from '../../actions';
|
|
47
|
+
|
|
48
|
+
import {
|
|
49
|
+
CONFIG_PREFIX,
|
|
50
|
+
fromFormData,
|
|
51
|
+
providerSchema,
|
|
52
|
+
suggestedProviderId,
|
|
53
|
+
toFormData,
|
|
54
|
+
} from '../../helpers/providerSchema';
|
|
55
|
+
import type { ConfiguredProvider, Driver } from '../../types';
|
|
56
|
+
|
|
57
|
+
import './ProvidersControlPanel.scss';
|
|
58
|
+
import ConfirmModal from './ConfirmModal';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The configlet id, which is also the name the site-wide settings are served
|
|
62
|
+
* under at `@controlpanels/<id>`.
|
|
63
|
+
*/
|
|
64
|
+
const CONFIGLET_ID = 'identity-providers';
|
|
65
|
+
|
|
66
|
+
const messages = defineMessages({
|
|
67
|
+
title: { id: 'Identity providers', defaultMessage: 'Identity providers' },
|
|
68
|
+
add: { id: 'Add provider', defaultMessage: 'Add provider' },
|
|
69
|
+
back: { id: 'Back', defaultMessage: 'Back' },
|
|
70
|
+
save: { id: 'Save', defaultMessage: 'Save' },
|
|
71
|
+
cancel: { id: 'Cancel', defaultMessage: 'Cancel' },
|
|
72
|
+
edit: { id: 'Edit', defaultMessage: 'Edit' },
|
|
73
|
+
test: { id: 'Test connection', defaultMessage: 'Test connection' },
|
|
74
|
+
delete: { id: 'Delete', defaultMessage: 'Delete' },
|
|
75
|
+
saved: { id: 'Changes saved', defaultMessage: 'Changes saved' },
|
|
76
|
+
deleted: { id: 'Provider deleted', defaultMessage: 'Provider deleted' },
|
|
77
|
+
settings: { id: 'Settings', defaultMessage: 'Settings' },
|
|
78
|
+
settingsUnavailable: {
|
|
79
|
+
id: 'The site settings could not be read',
|
|
80
|
+
defaultMessage:
|
|
81
|
+
'The site settings could not be read, so they cannot be edited here. ' +
|
|
82
|
+
'The most likely cause is a settings field with no registry record, ' +
|
|
83
|
+
'which happens when the add-on gained one and its profile has not ' +
|
|
84
|
+
'been reapplied since.',
|
|
85
|
+
},
|
|
86
|
+
noCallback: {
|
|
87
|
+
id: 'No login callback URL is configured',
|
|
88
|
+
defaultMessage:
|
|
89
|
+
'No login callback URL is configured, so no provider can complete a ' +
|
|
90
|
+
'sign-in. Set it under Settings.',
|
|
91
|
+
},
|
|
92
|
+
error: { id: 'Error', defaultMessage: 'Error' },
|
|
93
|
+
empty: {
|
|
94
|
+
id: 'No providers are configured yet.',
|
|
95
|
+
defaultMessage: 'No providers are configured yet.',
|
|
96
|
+
},
|
|
97
|
+
noDrivers: {
|
|
98
|
+
id: 'No drivers are installed.',
|
|
99
|
+
defaultMessage:
|
|
100
|
+
'No drivers are installed, so there is nothing to configure. Install ' +
|
|
101
|
+
'an add-on that registers one.',
|
|
102
|
+
},
|
|
103
|
+
columnTitle: { id: 'Title', defaultMessage: 'Title' },
|
|
104
|
+
columnId: { id: 'Id', defaultMessage: 'Id' },
|
|
105
|
+
columnDriver: { id: 'Driver', defaultMessage: 'Driver' },
|
|
106
|
+
columnEnabled: { id: 'Enabled', defaultMessage: 'Enabled' },
|
|
107
|
+
columnActions: { id: 'Actions', defaultMessage: 'Actions' },
|
|
108
|
+
yes: { id: 'Yes', defaultMessage: 'Yes' },
|
|
109
|
+
no: { id: 'No', defaultMessage: 'No' },
|
|
110
|
+
reached: {
|
|
111
|
+
id: 'Reached {endpoint}',
|
|
112
|
+
defaultMessage: 'Reached {endpoint}',
|
|
113
|
+
},
|
|
114
|
+
reachedNoJwks: {
|
|
115
|
+
id: 'Reached {endpoint} (no key set published)',
|
|
116
|
+
defaultMessage: 'Reached {endpoint} (no key set published)',
|
|
117
|
+
},
|
|
118
|
+
confirmDelete: {
|
|
119
|
+
id: 'Delete this provider?',
|
|
120
|
+
defaultMessage:
|
|
121
|
+
'Delete this provider? Identities already linked through it keep the ' +
|
|
122
|
+
'stored id and stop resolving.',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const ProvidersControlPanel: React.FC = () => {
|
|
127
|
+
const intl = useIntl();
|
|
128
|
+
const dispatch = useDispatch();
|
|
129
|
+
const isClient = useClient();
|
|
130
|
+
const { pathname } = useLocation();
|
|
131
|
+
const formRef = useRef<any>(null);
|
|
132
|
+
|
|
133
|
+
const [editing, setEditing] = useState<string | null>(null);
|
|
134
|
+
const [adding, setAdding] = useState(false);
|
|
135
|
+
const [editingSettings, setEditingSettings] = useState(false);
|
|
136
|
+
// Which driver the add form is currently on. The schema depends on it, so
|
|
137
|
+
// it is tracked as the form changes rather than read at submit time.
|
|
138
|
+
const [draftDriver, setDraftDriver] = useState<string | undefined>(undefined);
|
|
139
|
+
// Whether the operator has written a provider id of their own. Until they
|
|
140
|
+
// do, choosing a driver names the provider after it; once they have, a
|
|
141
|
+
// later driver change must not overwrite what they typed.
|
|
142
|
+
const [idTouched, setIdTouched] = useState(false);
|
|
143
|
+
// The live form state. A ref rather than state on purpose: it changes on
|
|
144
|
+
// every keystroke and nothing here needs to re-render for that. It is read
|
|
145
|
+
// once, when the driver changes, to carry what has already been typed
|
|
146
|
+
// across the remount the new schema needs.
|
|
147
|
+
const draft = useRef<Record<string, unknown>>({});
|
|
148
|
+
const [error, setError] = useState<unknown>(null);
|
|
149
|
+
|
|
150
|
+
const providers = useSelector((state: any) => state.configuredProviders) as {
|
|
151
|
+
data?: ConfiguredProvider[];
|
|
152
|
+
loading?: boolean;
|
|
153
|
+
};
|
|
154
|
+
const drivers = useSelector((state: any) => state.identityDrivers) as {
|
|
155
|
+
data?: Driver[];
|
|
156
|
+
};
|
|
157
|
+
const check = useSelector((state: any) => state.providerTest);
|
|
158
|
+
// The provider's own fields, serialized by the backend from the interface
|
|
159
|
+
// its registry records are bound to. The driver's half rides on each entry
|
|
160
|
+
// of `identityDrivers`.
|
|
161
|
+
const servedSchema = useSelector(
|
|
162
|
+
(state: any) => state.providerFormSchema?.data,
|
|
163
|
+
);
|
|
164
|
+
// The site-wide settings, served by the same configlet id. The callback
|
|
165
|
+
// URL lives here rather than on a provider: it is one route in the
|
|
166
|
+
// frontend, registered identically with every provider.
|
|
167
|
+
const settings = useSelector(
|
|
168
|
+
(state: any) => state.controlpanels?.controlpanel,
|
|
169
|
+
);
|
|
170
|
+
// Its request state separately: the reducer clears `controlpanel` to null
|
|
171
|
+
// while pending *and* on failure, so the value alone cannot tell "still
|
|
172
|
+
// loading" from "the backend refused".
|
|
173
|
+
const settingsRequest = useSelector((state: any) => state.controlpanels?.get);
|
|
174
|
+
|
|
175
|
+
const items = providers?.data ?? [];
|
|
176
|
+
// Memoized because the schema is built from it: a fresh [] on every render
|
|
177
|
+
// would rebuild the schema on every render, and Form would lose its state.
|
|
178
|
+
const driverList = useMemo(() => drivers?.data ?? [], [drivers?.data]);
|
|
179
|
+
|
|
180
|
+
const refresh = () => {
|
|
181
|
+
dispatch(listProviders());
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
dispatch(listProviders());
|
|
186
|
+
dispatch(listDrivers());
|
|
187
|
+
dispatch(getControlpanel(CONFIGLET_ID));
|
|
188
|
+
}, [dispatch]);
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (check?.loaded && check?.data) {
|
|
192
|
+
const result = check.data;
|
|
193
|
+
if (result.ok) {
|
|
194
|
+
toast.success(
|
|
195
|
+
<Toast
|
|
196
|
+
success
|
|
197
|
+
title={intl.formatMessage(messages.test)}
|
|
198
|
+
content={intl.formatMessage(
|
|
199
|
+
result.has_jwks ? messages.reached : messages.reachedNoJwks,
|
|
200
|
+
{ endpoint: result.token_endpoint },
|
|
201
|
+
)}
|
|
202
|
+
/>,
|
|
203
|
+
);
|
|
204
|
+
} else {
|
|
205
|
+
toast.error(
|
|
206
|
+
<Toast
|
|
207
|
+
error
|
|
208
|
+
title={intl.formatMessage(messages.test)}
|
|
209
|
+
content={result.error}
|
|
210
|
+
/>,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}, [check?.loaded, check?.data, intl]);
|
|
215
|
+
|
|
216
|
+
const fail = (err: any) => {
|
|
217
|
+
setError(err);
|
|
218
|
+
toast.error(
|
|
219
|
+
<Toast
|
|
220
|
+
error
|
|
221
|
+
title={intl.formatMessage(messages.error)}
|
|
222
|
+
content={err?.response?.body?.error?.message ?? String(err)}
|
|
223
|
+
/>,
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const succeed = (message: string) => {
|
|
228
|
+
toast.success(<Toast success title={message} />);
|
|
229
|
+
closeForm();
|
|
230
|
+
refresh();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
function closeForm() {
|
|
234
|
+
setAdding(false);
|
|
235
|
+
setEditing(null);
|
|
236
|
+
setEditingSettings(false);
|
|
237
|
+
setDraftDriver(undefined);
|
|
238
|
+
setIdTouched(false);
|
|
239
|
+
setError(null);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const current = editing
|
|
243
|
+
? items.find((provider) => provider.id === editing)
|
|
244
|
+
: undefined;
|
|
245
|
+
const isForm = adding || editing !== null || editingSettings;
|
|
246
|
+
const callbackUrl = settings?.data?.callback_url;
|
|
247
|
+
// Volto's Form reads schema.fieldsets on the first render, so opening the
|
|
248
|
+
// settings without one is a crash rather than an empty form.
|
|
249
|
+
const settingsReady = Boolean(settings?.schema);
|
|
250
|
+
|
|
251
|
+
const schema = useMemo(
|
|
252
|
+
() =>
|
|
253
|
+
providerSchema(
|
|
254
|
+
servedSchema,
|
|
255
|
+
driverList,
|
|
256
|
+
adding ? draftDriver : current?.driver,
|
|
257
|
+
adding,
|
|
258
|
+
intl,
|
|
259
|
+
),
|
|
260
|
+
[servedSchema, driverList, adding, draftDriver, current?.driver, intl],
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Not memoized: `Form` reads this once, when it mounts, so recomputing it
|
|
264
|
+
// on a render it will ignore costs nothing.
|
|
265
|
+
//
|
|
266
|
+
// For the add form it is whatever has been typed so far, minus the previous
|
|
267
|
+
// driver's settings -- those mean nothing to the driver just chosen. What
|
|
268
|
+
// replaces them is `Form`'s own job: it seeds an add form from the schema's
|
|
269
|
+
// defaults, which is how the driver's sane values reach the fields.
|
|
270
|
+
const formData = adding
|
|
271
|
+
? {
|
|
272
|
+
...toFormData(undefined, {
|
|
273
|
+
propertymap: driverList.find((d) => d.id === draftDriver)
|
|
274
|
+
?.default_propertymap,
|
|
275
|
+
groupmap: driverList.find((d) => d.id === draftDriver)
|
|
276
|
+
?.default_groupmap,
|
|
277
|
+
}),
|
|
278
|
+
...Object.fromEntries(
|
|
279
|
+
Object.entries(draft.current).filter(
|
|
280
|
+
// The mapping goes the way the settings do when the driver
|
|
281
|
+
// changes: it is written in the claim names of the driver that
|
|
282
|
+
// was chosen, so carrying it over would keep a map for a
|
|
283
|
+
// provider that no longer exists.
|
|
284
|
+
([key]) =>
|
|
285
|
+
!key.startsWith(CONFIG_PREFIX) &&
|
|
286
|
+
key !== 'propertymap' &&
|
|
287
|
+
key !== 'groupmap',
|
|
288
|
+
),
|
|
289
|
+
),
|
|
290
|
+
// Read on the remount a driver change causes, so the suggestion
|
|
291
|
+
// follows the driver for as long as the operator lets it.
|
|
292
|
+
...(idTouched
|
|
293
|
+
? {}
|
|
294
|
+
: { id: suggestedProviderId(driverList, draftDriver) }),
|
|
295
|
+
}
|
|
296
|
+
: toFormData(current);
|
|
297
|
+
|
|
298
|
+
const onSubmit = (data: Record<string, unknown>) => {
|
|
299
|
+
const payload = fromFormData(data);
|
|
300
|
+
if (adding) {
|
|
301
|
+
(dispatch(createProvider(payload)) as any)
|
|
302
|
+
.then(() => succeed(intl.formatMessage(messages.saved)))
|
|
303
|
+
.catch(fail);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const { id: _id, ...rest } = payload;
|
|
307
|
+
(dispatch(updateProvider(editing as string, rest)) as any)
|
|
308
|
+
.then(() => succeed(intl.formatMessage(messages.saved)))
|
|
309
|
+
.catch(fail);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const onSaveSettings = (data: Record<string, unknown>) => {
|
|
313
|
+
const { '@id': _atId, ...values } = data ?? {};
|
|
314
|
+
(dispatch(updateControlpanel(settings['@id'], values)) as any)
|
|
315
|
+
.then(() => {
|
|
316
|
+
succeed(intl.formatMessage(messages.saved));
|
|
317
|
+
dispatch(getControlpanel(CONFIGLET_ID));
|
|
318
|
+
})
|
|
319
|
+
.catch(fail);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Which provider the question is being asked about, or null when it is
|
|
323
|
+
// not being asked. State rather than a blocking call: the panel stays
|
|
324
|
+
// interactive and the dialog is part of the page.
|
|
325
|
+
const [confirming, setConfirming] = React.useState<ConfiguredProvider | null>(
|
|
326
|
+
null,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const onDelete = (provider: ConfiguredProvider) => {
|
|
330
|
+
setConfirming(provider);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const onConfirmDelete = () => {
|
|
334
|
+
const provider = confirming;
|
|
335
|
+
setConfirming(null);
|
|
336
|
+
if (!provider) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
(dispatch(deleteProvider(provider.id)) as any)
|
|
340
|
+
.then(() => succeed(intl.formatMessage(messages.deleted)))
|
|
341
|
+
.catch(fail);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div id="page-controlpanel" className="identity-controlpanel">
|
|
346
|
+
<ConfirmModal
|
|
347
|
+
open={confirming !== null}
|
|
348
|
+
header={confirming?.title || confirming?.id || ''}
|
|
349
|
+
content={intl.formatMessage(messages.confirmDelete)}
|
|
350
|
+
onCancel={() => setConfirming(null)}
|
|
351
|
+
onConfirm={onConfirmDelete}
|
|
352
|
+
/>
|
|
353
|
+
<Helmet title={intl.formatMessage(messages.title)} />
|
|
354
|
+
<Container>
|
|
355
|
+
{editingSettings && !settingsReady ? (
|
|
356
|
+
<Segment.Group raised>
|
|
357
|
+
<Segment className="primary">
|
|
358
|
+
{intl.formatMessage(messages.settings)}
|
|
359
|
+
</Segment>
|
|
360
|
+
<Segment>
|
|
361
|
+
<p role="alert" className="identity-error">
|
|
362
|
+
{intl.formatMessage(messages.settingsUnavailable)}
|
|
363
|
+
</p>
|
|
364
|
+
{settingsRequest?.error ? (
|
|
365
|
+
<pre className="identity-controlpanel__detail">
|
|
366
|
+
{settingsRequest.error?.response?.body?.message ??
|
|
367
|
+
String(settingsRequest.error)}
|
|
368
|
+
</pre>
|
|
369
|
+
) : null}
|
|
370
|
+
</Segment>
|
|
371
|
+
</Segment.Group>
|
|
372
|
+
) : isForm ? (
|
|
373
|
+
<Form
|
|
374
|
+
ref={formRef}
|
|
375
|
+
// A new driver is a new set of fields, and `Form` seeds an add
|
|
376
|
+
// form's defaults in its constructor -- once, from the schema it
|
|
377
|
+
// mounted with. Without a remount the fields a driver declares
|
|
378
|
+
// appear empty however good its defaults are, because the only
|
|
379
|
+
// moment they would have been applied is already past.
|
|
380
|
+
key={
|
|
381
|
+
adding ? `add-${draftDriver ?? 'none'}` : editing ?? 'settings'
|
|
382
|
+
}
|
|
383
|
+
title={
|
|
384
|
+
editingSettings
|
|
385
|
+
? intl.formatMessage(messages.settings)
|
|
386
|
+
: adding
|
|
387
|
+
? intl.formatMessage(messages.add)
|
|
388
|
+
: current?.title || current?.id
|
|
389
|
+
}
|
|
390
|
+
// The settings schema comes from the backend, which already
|
|
391
|
+
// serves it for the Classic form; nothing is described twice.
|
|
392
|
+
schema={editingSettings ? settings?.schema : schema}
|
|
393
|
+
formData={editingSettings ? settings?.data : formData}
|
|
394
|
+
requestError={error}
|
|
395
|
+
onSubmit={editingSettings ? onSaveSettings : onSubmit}
|
|
396
|
+
onCancel={closeForm}
|
|
397
|
+
onChangeFormData={(data: Record<string, unknown>) => {
|
|
398
|
+
draft.current = data;
|
|
399
|
+
// The driver decides which settings exist, so choosing one has
|
|
400
|
+
// to rebuild the schema rather than wait for submit.
|
|
401
|
+
if (adding && data.driver !== draftDriver) {
|
|
402
|
+
setDraftDriver(data.driver as string);
|
|
403
|
+
}
|
|
404
|
+
// Compared against the suggestion for the driver the form is
|
|
405
|
+
// still on: a driver change arrives here with the previous
|
|
406
|
+
// driver's id, which is the suggestion and not a typed one.
|
|
407
|
+
if (
|
|
408
|
+
adding &&
|
|
409
|
+
!idTouched &&
|
|
410
|
+
data.id !== suggestedProviderId(driverList, draftDriver)
|
|
411
|
+
) {
|
|
412
|
+
setIdTouched(true);
|
|
413
|
+
}
|
|
414
|
+
}}
|
|
415
|
+
hideActions
|
|
416
|
+
/>
|
|
417
|
+
) : (
|
|
418
|
+
<Segment.Group raised>
|
|
419
|
+
<Segment className="primary">
|
|
420
|
+
{intl.formatMessage(messages.title)}
|
|
421
|
+
</Segment>
|
|
422
|
+
{settings?.data && !callbackUrl ? (
|
|
423
|
+
// Without it every sign-in fails at the last step with an
|
|
424
|
+
// error only the log shows. Better said here, before anyone
|
|
425
|
+
// configures a provider and wonders why it does not work.
|
|
426
|
+
<Segment className="identity-controlpanel__warning" secondary>
|
|
427
|
+
<strong>{intl.formatMessage(messages.noCallback)}</strong>
|
|
428
|
+
</Segment>
|
|
429
|
+
) : null}
|
|
430
|
+
<Segment>
|
|
431
|
+
{items.length ? (
|
|
432
|
+
<Table selectable compact>
|
|
433
|
+
<Table.Header>
|
|
434
|
+
<Table.Row>
|
|
435
|
+
<Table.HeaderCell>
|
|
436
|
+
{intl.formatMessage(messages.columnTitle)}
|
|
437
|
+
</Table.HeaderCell>
|
|
438
|
+
<Table.HeaderCell>
|
|
439
|
+
{intl.formatMessage(messages.columnId)}
|
|
440
|
+
</Table.HeaderCell>
|
|
441
|
+
<Table.HeaderCell>
|
|
442
|
+
{intl.formatMessage(messages.columnDriver)}
|
|
443
|
+
</Table.HeaderCell>
|
|
444
|
+
<Table.HeaderCell>
|
|
445
|
+
{intl.formatMessage(messages.columnEnabled)}
|
|
446
|
+
</Table.HeaderCell>
|
|
447
|
+
<Table.HeaderCell textAlign="right">
|
|
448
|
+
{intl.formatMessage(messages.columnActions)}
|
|
449
|
+
</Table.HeaderCell>
|
|
450
|
+
</Table.Row>
|
|
451
|
+
</Table.Header>
|
|
452
|
+
<Table.Body>
|
|
453
|
+
{items.map((provider) => (
|
|
454
|
+
<Table.Row
|
|
455
|
+
key={provider['@id']}
|
|
456
|
+
data-provider={provider.id}
|
|
457
|
+
>
|
|
458
|
+
<Table.Cell>{provider.title || provider.id}</Table.Cell>
|
|
459
|
+
<Table.Cell>
|
|
460
|
+
<code>{provider.id}</code>
|
|
461
|
+
</Table.Cell>
|
|
462
|
+
<Table.Cell>{provider.driver}</Table.Cell>
|
|
463
|
+
<Table.Cell>
|
|
464
|
+
{intl.formatMessage(
|
|
465
|
+
provider.enabled ? messages.yes : messages.no,
|
|
466
|
+
)}
|
|
467
|
+
</Table.Cell>
|
|
468
|
+
<Table.Cell textAlign="right">
|
|
469
|
+
<Button
|
|
470
|
+
basic
|
|
471
|
+
icon
|
|
472
|
+
aria-label={intl.formatMessage(messages.edit)}
|
|
473
|
+
title={intl.formatMessage(messages.edit)}
|
|
474
|
+
onClick={() => setEditing(provider.id)}
|
|
475
|
+
>
|
|
476
|
+
<Icon name={pencilSVG} size="20px" />
|
|
477
|
+
</Button>
|
|
478
|
+
<Button
|
|
479
|
+
basic
|
|
480
|
+
icon
|
|
481
|
+
aria-label={intl.formatMessage(messages.test)}
|
|
482
|
+
title={intl.formatMessage(messages.test)}
|
|
483
|
+
onClick={() => dispatch(testProvider(provider.id))}
|
|
484
|
+
>
|
|
485
|
+
<Icon name={worldSVG} size="20px" />
|
|
486
|
+
</Button>
|
|
487
|
+
<Button
|
|
488
|
+
basic
|
|
489
|
+
icon
|
|
490
|
+
data-action="delete"
|
|
491
|
+
aria-label={intl.formatMessage(messages.delete)}
|
|
492
|
+
title={intl.formatMessage(messages.delete)}
|
|
493
|
+
onClick={() => onDelete(provider)}
|
|
494
|
+
>
|
|
495
|
+
<Icon name={deleteSVG} size="20px" />
|
|
496
|
+
</Button>
|
|
497
|
+
</Table.Cell>
|
|
498
|
+
</Table.Row>
|
|
499
|
+
))}
|
|
500
|
+
</Table.Body>
|
|
501
|
+
</Table>
|
|
502
|
+
) : (
|
|
503
|
+
<p className="identity-controlpanel__empty identity-note">
|
|
504
|
+
{intl.formatMessage(
|
|
505
|
+
driverList.length ? messages.empty : messages.noDrivers,
|
|
506
|
+
)}
|
|
507
|
+
</p>
|
|
508
|
+
)}
|
|
509
|
+
</Segment>
|
|
510
|
+
</Segment.Group>
|
|
511
|
+
)}
|
|
512
|
+
</Container>
|
|
513
|
+
{isClient &&
|
|
514
|
+
createPortal(
|
|
515
|
+
<Toolbar
|
|
516
|
+
pathname={pathname}
|
|
517
|
+
hideDefaultViewButtons
|
|
518
|
+
inner={
|
|
519
|
+
isForm ? (
|
|
520
|
+
<>
|
|
521
|
+
{/* No Save for a form that is not there; Cancel still is,
|
|
522
|
+
so the error view is not a dead end. */}
|
|
523
|
+
{editingSettings && !settingsReady ? null : (
|
|
524
|
+
<Button
|
|
525
|
+
id="toolbar-save"
|
|
526
|
+
className="save"
|
|
527
|
+
aria-label={intl.formatMessage(messages.save)}
|
|
528
|
+
onClick={() => formRef.current?.onSubmit()}
|
|
529
|
+
>
|
|
530
|
+
<Icon
|
|
531
|
+
name={saveSVG}
|
|
532
|
+
className="circled"
|
|
533
|
+
size="30px"
|
|
534
|
+
title={intl.formatMessage(messages.save)}
|
|
535
|
+
/>
|
|
536
|
+
</Button>
|
|
537
|
+
)}
|
|
538
|
+
<Button
|
|
539
|
+
className="cancel"
|
|
540
|
+
aria-label={intl.formatMessage(messages.cancel)}
|
|
541
|
+
onClick={closeForm}
|
|
542
|
+
>
|
|
543
|
+
<Icon
|
|
544
|
+
name={clearSVG}
|
|
545
|
+
className="circled"
|
|
546
|
+
size="30px"
|
|
547
|
+
title={intl.formatMessage(messages.cancel)}
|
|
548
|
+
/>
|
|
549
|
+
</Button>
|
|
550
|
+
</>
|
|
551
|
+
) : (
|
|
552
|
+
<>
|
|
553
|
+
<Button
|
|
554
|
+
id="toolbar-settings"
|
|
555
|
+
aria-label={intl.formatMessage(messages.settings)}
|
|
556
|
+
onClick={() => setEditingSettings(true)}
|
|
557
|
+
>
|
|
558
|
+
<Icon
|
|
559
|
+
name={configurationSVG}
|
|
560
|
+
className="circled"
|
|
561
|
+
size="30px"
|
|
562
|
+
title={intl.formatMessage(messages.settings)}
|
|
563
|
+
/>
|
|
564
|
+
</Button>
|
|
565
|
+
{driverList.length ? (
|
|
566
|
+
<Button
|
|
567
|
+
id="toolbar-add"
|
|
568
|
+
aria-label={intl.formatMessage(messages.add)}
|
|
569
|
+
onClick={() => setAdding(true)}
|
|
570
|
+
>
|
|
571
|
+
<Icon
|
|
572
|
+
name={addSVG}
|
|
573
|
+
className="circled"
|
|
574
|
+
size="30px"
|
|
575
|
+
title={intl.formatMessage(messages.add)}
|
|
576
|
+
/>
|
|
577
|
+
</Button>
|
|
578
|
+
) : null}
|
|
579
|
+
{/* A router link, not an anchor: an `href` here left the
|
|
580
|
+
toolbar's back button reloading the whole application
|
|
581
|
+
to reach a route Volto already has. */}
|
|
582
|
+
<Link className="item" to="/controlpanel">
|
|
583
|
+
<Icon
|
|
584
|
+
name={backSVG}
|
|
585
|
+
className="circled"
|
|
586
|
+
size="30px"
|
|
587
|
+
title={intl.formatMessage(messages.back)}
|
|
588
|
+
/>
|
|
589
|
+
</Link>
|
|
590
|
+
</>
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
/>,
|
|
594
|
+
document.getElementById('toolbar') as HTMLElement,
|
|
595
|
+
)}
|
|
596
|
+
</div>
|
|
597
|
+
);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
export default ProvidersControlPanel;
|