Added the credentials tab (#196)
* initial version credentials tab * added signed jwt * submit token endpoint signing alg * added x509 * show tab when public client * fixed test default confimnation dailog size is small * pr review comments
This commit is contained in:
parent
6b3990e1b9
commit
2393b40c9b
15 changed files with 711 additions and 247 deletions
199
src/clients/ClientDetails.tsx
Normal file
199
src/clients/ClientDetails.tsx
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
AlertVariant,
|
||||||
|
ButtonVariant,
|
||||||
|
DropdownItem,
|
||||||
|
PageSection,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
TabTitleText,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ClientSettings } from "./ClientSettings";
|
||||||
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
|
import { useDownloadDialog } from "../components/download-dialog/DownloadDialog";
|
||||||
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
|
import { HttpClientContext } from "../context/http-service/HttpClientContext";
|
||||||
|
import { RealmContext } from "../context/realm-context/RealmContext";
|
||||||
|
import { Credentials } from "./credentials/Credentials";
|
||||||
|
import { ClientRepresentation } from "../realm/models/Realm";
|
||||||
|
import {
|
||||||
|
convertFormValuesToObject,
|
||||||
|
convertToFormValues,
|
||||||
|
exportClient,
|
||||||
|
} from "../util";
|
||||||
|
import {
|
||||||
|
convertToMultiline,
|
||||||
|
toValue,
|
||||||
|
} from "../components/multi-line-input/MultiLineInput";
|
||||||
|
|
||||||
|
export const ClientDetails = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const httpClient = useContext(HttpClientContext)!;
|
||||||
|
const { realm } = useContext(RealmContext);
|
||||||
|
const { addAlert } = useAlerts();
|
||||||
|
|
||||||
|
const form = useForm();
|
||||||
|
const publicClient = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "publicClient",
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const url = `/admin/realms/${realm}/clients/${id}`;
|
||||||
|
|
||||||
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
titleKey: "clients:clientDeleteConfirmTitle",
|
||||||
|
messageKey: "clients:clientDeleteConfirm",
|
||||||
|
continueButtonLabel: "common:delete",
|
||||||
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
|
onConfirm: () => {
|
||||||
|
try {
|
||||||
|
httpClient.doDelete(`/admin/realms/${realm}/clients/${id}`);
|
||||||
|
addAlert(t("clientDeletedSuccess"), AlertVariant.success);
|
||||||
|
} catch (error) {
|
||||||
|
addAlert(`${t("clientDeleteError")} ${error}`, AlertVariant.danger);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [toggleDownloadDialog, DownloadDialog] = useDownloadDialog({
|
||||||
|
id,
|
||||||
|
protocol: form.getValues("protocol"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupForm = (client: ClientRepresentation) => {
|
||||||
|
form.reset(client);
|
||||||
|
Object.entries(client).map((entry) => {
|
||||||
|
if (entry[0] === "redirectUris") {
|
||||||
|
form.setValue(entry[0], convertToMultiline(entry[1]));
|
||||||
|
} else if (entry[0] === "attributes") {
|
||||||
|
convertToFormValues(entry[1], "attributes", form.setValue);
|
||||||
|
} else {
|
||||||
|
form.setValue(entry[0], entry[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const fetchedClient = await httpClient.doGet<ClientRepresentation>(url);
|
||||||
|
if (fetchedClient.data) {
|
||||||
|
setName(fetchedClient.data.clientId);
|
||||||
|
setupForm(fetchedClient.data);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
if (await form.trigger()) {
|
||||||
|
const redirectUris = toValue(form.getValues()["redirectUris"]);
|
||||||
|
const attributes = form.getValues()["attributes"]
|
||||||
|
? convertFormValuesToObject(form.getValues()["attributes"])
|
||||||
|
: {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = {
|
||||||
|
...form.getValues(),
|
||||||
|
redirectUris,
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
await httpClient.doPut(url, client);
|
||||||
|
setupForm(client as ClientRepresentation);
|
||||||
|
addAlert(t("clientSaveSuccess"), AlertVariant.success);
|
||||||
|
} catch (error) {
|
||||||
|
addAlert(`${t("clientSaveError")} '${error}'`, AlertVariant.danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteConfirm />
|
||||||
|
<DownloadDialog />
|
||||||
|
<Controller
|
||||||
|
name="enabled"
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={true}
|
||||||
|
render={({ onChange, value }) => {
|
||||||
|
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
||||||
|
titleKey: "clients:disableConfirmTitle",
|
||||||
|
messageKey: "clients:disableConfirm",
|
||||||
|
continueButtonLabel: "common:disable",
|
||||||
|
onConfirm: () => {
|
||||||
|
onChange(!value);
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DisableConfirm />
|
||||||
|
<ViewHeader
|
||||||
|
titleKey={name}
|
||||||
|
subKey="clients:clientsExplain"
|
||||||
|
dropdownItems={[
|
||||||
|
<DropdownItem
|
||||||
|
key="download"
|
||||||
|
onClick={() => toggleDownloadDialog()}
|
||||||
|
>
|
||||||
|
{t("downloadAdapterConfig")}
|
||||||
|
</DropdownItem>,
|
||||||
|
<DropdownItem
|
||||||
|
key="export"
|
||||||
|
onClick={() => exportClient(form.getValues())}
|
||||||
|
>
|
||||||
|
{t("common:export")}
|
||||||
|
</DropdownItem>,
|
||||||
|
<DropdownItem
|
||||||
|
key="delete"
|
||||||
|
onClick={() => toggleDeleteDialog()}
|
||||||
|
>
|
||||||
|
{t("common:delete")}
|
||||||
|
</DropdownItem>,
|
||||||
|
]}
|
||||||
|
isEnabled={value}
|
||||||
|
onToggle={(value) => {
|
||||||
|
if (!value) {
|
||||||
|
toggleDisableDialog();
|
||||||
|
} else {
|
||||||
|
onChange(value);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PageSection variant="light">
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onSelect={(_, key) => setActiveTab(key as number)}
|
||||||
|
isBox
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
eventKey={0}
|
||||||
|
title={<TabTitleText>{t("settings")}</TabTitleText>}
|
||||||
|
>
|
||||||
|
<ClientSettings form={form} save={save} />
|
||||||
|
</Tab>
|
||||||
|
{publicClient && (
|
||||||
|
<Tab
|
||||||
|
eventKey={1}
|
||||||
|
title={<TabTitleText>{t("credentials")}</TabTitleText>}
|
||||||
|
>
|
||||||
|
<Credentials clientId={id} form={form} save={save} />
|
||||||
|
</Tab>
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
|
@ -6,245 +6,119 @@ import {
|
||||||
Form,
|
Form,
|
||||||
Switch,
|
Switch,
|
||||||
TextArea,
|
TextArea,
|
||||||
PageSection,
|
|
||||||
ActionGroup,
|
ActionGroup,
|
||||||
Button,
|
Button,
|
||||||
AlertVariant,
|
|
||||||
ButtonVariant,
|
|
||||||
DropdownItem,
|
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useParams } from "react-router-dom";
|
import { Controller, UseFormMethods } from "react-hook-form";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import { ScrollForm } from "../components/scroll-form/ScrollForm";
|
import { ScrollForm } from "../components/scroll-form/ScrollForm";
|
||||||
import { ClientDescription } from "./ClientDescription";
|
import { ClientDescription } from "./ClientDescription";
|
||||||
import { CapabilityConfig } from "./add/CapabilityConfig";
|
import { CapabilityConfig } from "./add/CapabilityConfig";
|
||||||
import { RealmContext } from "../context/realm-context/RealmContext";
|
import { MultiLineInput } from "../components/multi-line-input/MultiLineInput";
|
||||||
import { HttpClientContext } from "../context/http-service/HttpClientContext";
|
|
||||||
import { ClientRepresentation } from "../realm/models/Realm";
|
|
||||||
import {
|
|
||||||
convertToMultiline,
|
|
||||||
MultiLineInput,
|
|
||||||
toValue,
|
|
||||||
} from "../components/multi-line-input/MultiLineInput";
|
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
|
||||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
|
||||||
import { exportClient } from "../util";
|
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
|
||||||
import { useDownloadDialog } from "../components/download-dialog/DownloadDialog";
|
|
||||||
import { FormAccess } from "../components/form-access/FormAccess";
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
|
|
||||||
export const ClientSettings = () => {
|
type ClientSettingsProps = {
|
||||||
|
form: UseFormMethods;
|
||||||
|
save: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const httpClient = useContext(HttpClientContext)!;
|
|
||||||
const { realm } = useContext(RealmContext);
|
|
||||||
const { addAlert } = useAlerts();
|
|
||||||
|
|
||||||
const { id } = useParams<{ id: string }>();
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const form = useForm();
|
|
||||||
const url = `/admin/realms/${realm}/clients/${id}`;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const fetchedClient = await httpClient.doGet<ClientRepresentation>(url);
|
|
||||||
if (fetchedClient.data) {
|
|
||||||
setName(fetchedClient.data.clientId);
|
|
||||||
Object.entries(fetchedClient.data).map((entry) => {
|
|
||||||
if (entry[0] !== "redirectUris") {
|
|
||||||
form.setValue(entry[0], entry[1]);
|
|
||||||
} else if (entry[1] && entry[1].length > 0) {
|
|
||||||
form.setValue(entry[0], convertToMultiline(entry[1]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
|
||||||
titleKey: "clients:clientDeleteConfirmTitle",
|
|
||||||
messageKey: "clients:clientDeleteConfirm",
|
|
||||||
continueButtonLabel: "common:delete",
|
|
||||||
continueButtonVariant: ButtonVariant.danger,
|
|
||||||
onConfirm: () => {
|
|
||||||
try {
|
|
||||||
httpClient.doDelete(`/admin/realms/${realm}/clients/${id}`);
|
|
||||||
addAlert(t("clientDeletedSuccess"), AlertVariant.success);
|
|
||||||
} catch (error) {
|
|
||||||
addAlert(`${t("clientDeleteError")} ${error}`, AlertVariant.danger);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [toggleDownloadDialog, DownloadDialog] = useDownloadDialog({
|
|
||||||
id,
|
|
||||||
protocol: form.getValues("protocol"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
if (await form.trigger()) {
|
|
||||||
const redirectUris = toValue(form.getValues()["redirectUris"]);
|
|
||||||
try {
|
|
||||||
httpClient.doPut(url, { ...form.getValues(), redirectUris });
|
|
||||||
addAlert(t("clientSaveSuccess"), AlertVariant.success);
|
|
||||||
} catch (error) {
|
|
||||||
addAlert(`${t("clientSaveError")} '${error}'`, AlertVariant.danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteConfirm />
|
<ScrollForm
|
||||||
<DownloadDialog />
|
sections={[
|
||||||
<Controller
|
t("capabilityConfig"),
|
||||||
name="enabled"
|
t("generalSettings"),
|
||||||
control={form.control}
|
t("accessSettings"),
|
||||||
defaultValue={true}
|
t("loginSettings"),
|
||||||
render={({ onChange, value }) => {
|
]}
|
||||||
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
>
|
||||||
titleKey: "clients:disableConfirmTitle",
|
<CapabilityConfig form={form} />
|
||||||
messageKey: "clients:disableConfirm",
|
<Form isHorizontal>
|
||||||
continueButtonLabel: "common:disable",
|
<ClientDescription form={form} />
|
||||||
onConfirm: () => {
|
</Form>
|
||||||
onChange(!value);
|
<FormAccess isHorizontal role="manage-clients">
|
||||||
save();
|
<FormGroup label={t("rootUrl")} fieldId="kc-root-url">
|
||||||
},
|
<TextInput
|
||||||
});
|
type="text"
|
||||||
return (
|
id="kc-root-url"
|
||||||
<>
|
name="rootUrl"
|
||||||
<DisableConfirm />
|
ref={form.register}
|
||||||
<ViewHeader
|
/>
|
||||||
titleKey={name}
|
</FormGroup>
|
||||||
subKey="clients:clientsExplain"
|
<FormGroup label={t("validRedirectUri")} fieldId="kc-redirect">
|
||||||
dropdownItems={[
|
<MultiLineInput form={form} name="redirectUris" />
|
||||||
<DropdownItem
|
</FormGroup>
|
||||||
key="download"
|
<FormGroup label={t("homeURL")} fieldId="kc-home-url">
|
||||||
onClick={() => toggleDownloadDialog()}
|
<TextInput
|
||||||
>
|
type="text"
|
||||||
{t("downloadAdapterConfig")}
|
id="kc-home-url"
|
||||||
</DropdownItem>,
|
name="baseUrl"
|
||||||
<DropdownItem
|
ref={form.register}
|
||||||
key="export"
|
/>
|
||||||
onClick={() => exportClient(form.getValues())}
|
</FormGroup>
|
||||||
>
|
</FormAccess>
|
||||||
{t("common:export")}
|
<FormAccess isHorizontal role="manage-clients">
|
||||||
</DropdownItem>,
|
<FormGroup
|
||||||
<DropdownItem
|
label={t("consentRequired")}
|
||||||
key="delete"
|
fieldId="kc-consent"
|
||||||
onClick={() => toggleDeleteDialog()}
|
hasNoPaddingTop
|
||||||
>
|
>
|
||||||
{t("common:delete")}
|
<Controller
|
||||||
</DropdownItem>,
|
name="consentRequired"
|
||||||
]}
|
defaultValue={false}
|
||||||
isEnabled={value}
|
control={form.control}
|
||||||
onToggle={(value) => {
|
render={({ onChange, value }) => (
|
||||||
if (!value) {
|
<Switch
|
||||||
toggleDisableDialog();
|
id="kc-consent"
|
||||||
} else {
|
label={t("common:on")}
|
||||||
onChange(value);
|
labelOff={t("common:off")}
|
||||||
save();
|
isChecked={value}
|
||||||
}
|
onChange={onChange}
|
||||||
}}
|
/>
|
||||||
/>
|
)}
|
||||||
</>
|
/>
|
||||||
);
|
</FormGroup>
|
||||||
}}
|
<FormGroup
|
||||||
/>
|
label={t("displayOnClient")}
|
||||||
<PageSection variant="light">
|
fieldId="kc-display-on-client"
|
||||||
<ScrollForm
|
hasNoPaddingTop
|
||||||
sections={[
|
>
|
||||||
t("capabilityConfig"),
|
<Controller
|
||||||
t("generalSettings"),
|
name="alwaysDisplayInConsole"
|
||||||
t("accessSettings"),
|
defaultValue={false}
|
||||||
t("loginSettings"),
|
control={form.control}
|
||||||
]}
|
render={({ onChange, value }) => (
|
||||||
>
|
<Switch
|
||||||
<CapabilityConfig form={form} />
|
id="kc-display-on-client"
|
||||||
<Form isHorizontal>
|
label={t("common:on")}
|
||||||
<ClientDescription form={form} />
|
labelOff={t("common:off")}
|
||||||
</Form>
|
isChecked={value}
|
||||||
<FormAccess isHorizontal role="manage-clients">
|
onChange={onChange}
|
||||||
<FormGroup label={t("rootUrl")} fieldId="kc-root-url">
|
/>
|
||||||
<TextInput
|
)}
|
||||||
type="text"
|
/>
|
||||||
id="kc-root-url"
|
</FormGroup>
|
||||||
name="rootUrl"
|
<FormGroup
|
||||||
ref={form.register}
|
label={t("consentScreenText")}
|
||||||
/>
|
fieldId="kc-consent-screen-text"
|
||||||
</FormGroup>
|
>
|
||||||
<FormGroup label={t("validRedirectUri")} fieldId="kc-redirect">
|
<TextArea
|
||||||
<MultiLineInput form={form} name="redirectUris" />
|
id="kc-consent-screen-text"
|
||||||
</FormGroup>
|
name="consentText"
|
||||||
<FormGroup label={t("homeURL")} fieldId="kc-home-url">
|
ref={form.register}
|
||||||
<TextInput
|
/>
|
||||||
type="text"
|
</FormGroup>
|
||||||
id="kc-home-url"
|
<ActionGroup>
|
||||||
name="baseUrl"
|
<Button variant="primary" onClick={() => save()}>
|
||||||
ref={form.register}
|
{t("common:save")}
|
||||||
/>
|
</Button>
|
||||||
</FormGroup>
|
<Button variant="link">{t("common:cancel")}</Button>
|
||||||
</FormAccess>
|
</ActionGroup>
|
||||||
<FormAccess isHorizontal role="manage-clients">
|
</FormAccess>
|
||||||
<FormGroup
|
</ScrollForm>
|
||||||
label={t("consentRequired")}
|
|
||||||
fieldId="kc-consent"
|
|
||||||
hasNoPaddingTop
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="consentRequired"
|
|
||||||
defaultValue={false}
|
|
||||||
control={form.control}
|
|
||||||
render={({ onChange, value }) => (
|
|
||||||
<Switch
|
|
||||||
id="kc-consent"
|
|
||||||
label={t("common:on")}
|
|
||||||
labelOff={t("common:off")}
|
|
||||||
isChecked={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup
|
|
||||||
label={t("displayOnClient")}
|
|
||||||
fieldId="kc-display-on-client"
|
|
||||||
hasNoPaddingTop
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="alwaysDisplayInConsole"
|
|
||||||
defaultValue={false}
|
|
||||||
control={form.control}
|
|
||||||
render={({ onChange, value }) => (
|
|
||||||
<Switch
|
|
||||||
id="kc-display-on-client"
|
|
||||||
label={t("common:on")}
|
|
||||||
labelOff={t("common:off")}
|
|
||||||
isChecked={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup
|
|
||||||
label={t("consentScreenText")}
|
|
||||||
fieldId="kc-consent-screen-text"
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
id="kc-consent-screen-text"
|
|
||||||
name="consentText"
|
|
||||||
ref={form.register}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<ActionGroup>
|
|
||||||
<Button variant="primary" onClick={() => save()}>
|
|
||||||
{t("common:save")}
|
|
||||||
</Button>
|
|
||||||
<Button variant="link">{t("common:cancel")}</Button>
|
|
||||||
</ActionGroup>
|
|
||||||
</FormAccess>
|
|
||||||
</ScrollForm>
|
|
||||||
</PageSection>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
34
src/clients/credentials/ClientSecret.tsx
Normal file
34
src/clients/credentials/ClientSecret.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ClipboardCopy,
|
||||||
|
FormGroup,
|
||||||
|
Split,
|
||||||
|
SplitItem,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
export type ClientSecretProps = {
|
||||||
|
secret: string;
|
||||||
|
toggle: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClientSecret = ({ secret, toggle }: ClientSecretProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
return (
|
||||||
|
<FormGroup label={t("clientSecret")} fieldId="kc-client-secret">
|
||||||
|
<Split hasGutter>
|
||||||
|
<SplitItem isFilled>
|
||||||
|
<ClipboardCopy id="kc-client-secret" isReadOnly>
|
||||||
|
{secret}
|
||||||
|
</ClipboardCopy>
|
||||||
|
</SplitItem>
|
||||||
|
<SplitItem>
|
||||||
|
<Button variant="secondary" onClick={toggle}>
|
||||||
|
{t("regenerate")}
|
||||||
|
</Button>
|
||||||
|
</SplitItem>
|
||||||
|
</Split>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
230
src/clients/credentials/Credentials.tsx
Normal file
230
src/clients/credentials/Credentials.tsx
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
import {
|
||||||
|
ActionGroup,
|
||||||
|
AlertVariant,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
ClipboardCopy,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
SelectVariant,
|
||||||
|
Split,
|
||||||
|
SplitItem,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import { Controller, UseFormMethods, useWatch } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||||
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
|
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
|
||||||
|
import { RealmContext } from "../../context/realm-context/RealmContext";
|
||||||
|
import { ClientSecret } from "./ClientSecret";
|
||||||
|
import { SignedJWT } from "./SignedJWT";
|
||||||
|
import { X509 } from "./X509";
|
||||||
|
|
||||||
|
type ClientAuthenticatorProviders = {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Secret = {
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AccessToken = {
|
||||||
|
registrationAccessToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CredentialsProps = {
|
||||||
|
clientId: string;
|
||||||
|
form: UseFormMethods;
|
||||||
|
save: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const httpClient = useContext(HttpClientContext)!;
|
||||||
|
const { realm } = useContext(RealmContext);
|
||||||
|
const { addAlert } = useAlerts();
|
||||||
|
const clientAuthenticatorType = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "clientAuthenticatorType",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [providers, setProviders] = useState<ClientAuthenticatorProviders[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [secret, setSecret] = useState("");
|
||||||
|
const [accessToken, setAccessToken] = useState("");
|
||||||
|
const [open, isOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const response = await httpClient.doGet<ClientAuthenticatorProviders[]>(
|
||||||
|
`/admin/realms/${realm}/authentication/client-authenticator-providers`
|
||||||
|
);
|
||||||
|
setProviders(response.data!);
|
||||||
|
|
||||||
|
const secretResponse = await httpClient.doGet<Secret>(
|
||||||
|
`/admin/realms/${realm}/clients/${clientId}/client-secret`
|
||||||
|
);
|
||||||
|
setSecret(secretResponse.data!.value);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function regenerate<T>(
|
||||||
|
endpoint: string,
|
||||||
|
message: string
|
||||||
|
): Promise<T | undefined> {
|
||||||
|
try {
|
||||||
|
const response = await httpClient.doPost<T>(
|
||||||
|
`/admin/realms/${realm}/clients/${clientId}/${endpoint}`,
|
||||||
|
{ client: clientId, realm }
|
||||||
|
);
|
||||||
|
addAlert(t(`${message}Success`), AlertVariant.success);
|
||||||
|
return response.data!;
|
||||||
|
} catch (error) {
|
||||||
|
addAlert(t(`${message}Error`, { error }), AlertVariant.danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const regenerateClientSecret = async () => {
|
||||||
|
const secret = await regenerate<Secret>("client-secret", "clientSecret");
|
||||||
|
setSecret(secret?.value || "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const [toggleClientSecretConfirm, ClientSecretConfirm] = useConfirmDialog({
|
||||||
|
titleKey: "clients:confirmClientSecretTitle",
|
||||||
|
messageKey: "clients:confirmClientSecretBody",
|
||||||
|
continueButtonLabel: "common:yes",
|
||||||
|
cancelButtonLabel: "common:no",
|
||||||
|
onConfirm: regenerateClientSecret,
|
||||||
|
});
|
||||||
|
|
||||||
|
const regenerateAccessToken = async () => {
|
||||||
|
const accessToken = await regenerate<AccessToken>(
|
||||||
|
"registration-access-token",
|
||||||
|
"accessToken"
|
||||||
|
);
|
||||||
|
setAccessToken(accessToken?.registrationAccessToken || "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const [toggleAccessTokenConfirm, AccessTokenConfirm] = useConfirmDialog({
|
||||||
|
titleKey: "clients:confirmAccessTokenTitle",
|
||||||
|
messageKey: "clients:confirmAccessTokenBody",
|
||||||
|
continueButtonLabel: "common:yes",
|
||||||
|
cancelButtonLabel: "common:no",
|
||||||
|
onConfirm: regenerateAccessToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form isHorizontal className="pf-u-mt-md">
|
||||||
|
<ClientSecretConfirm />
|
||||||
|
<AccessTokenConfirm />
|
||||||
|
<Card isFlat>
|
||||||
|
<CardBody>
|
||||||
|
<FormGroup
|
||||||
|
label={t("clientAuthenticator")}
|
||||||
|
fieldId="kc-client-authenticator-type"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:client-authenticator-type"
|
||||||
|
forLabel={t("clientAuthenticator")}
|
||||||
|
forID="kc-client-authenticator-type"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="clientAuthenticatorType"
|
||||||
|
control={form.control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
toggleId="kc-client-authenticator-type"
|
||||||
|
required
|
||||||
|
onToggle={() => isOpen(!open)}
|
||||||
|
onSelect={(_, value) => {
|
||||||
|
onChange(value as string);
|
||||||
|
isOpen(false);
|
||||||
|
}}
|
||||||
|
selections={value}
|
||||||
|
variant={SelectVariant.single}
|
||||||
|
aria-label={t("clientAuthenticator")}
|
||||||
|
isOpen={open}
|
||||||
|
>
|
||||||
|
{providers.map((option) => (
|
||||||
|
<SelectOption
|
||||||
|
selected={option.id === value}
|
||||||
|
key={option.id}
|
||||||
|
value={option.id}
|
||||||
|
>
|
||||||
|
{option.displayName}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{clientAuthenticatorType === "client-jwt" && (
|
||||||
|
<SignedJWT form={form} />
|
||||||
|
)}
|
||||||
|
{clientAuthenticatorType === "client-x509" && <X509 form={form} />}
|
||||||
|
<ActionGroup>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => save()}
|
||||||
|
isDisabled={!form.formState.isDirty}
|
||||||
|
>
|
||||||
|
{t("common:save")}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</CardBody>
|
||||||
|
<CardBody>
|
||||||
|
{(clientAuthenticatorType === "client-secret" ||
|
||||||
|
clientAuthenticatorType === "client-secret-jwt") && (
|
||||||
|
<>
|
||||||
|
<Divider className="pf-u-mb-md" />
|
||||||
|
<ClientSecret
|
||||||
|
secret={secret}
|
||||||
|
toggle={toggleClientSecretConfirm}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
<Card isFlat>
|
||||||
|
<CardBody>
|
||||||
|
<FormGroup
|
||||||
|
label={t("registrationAccessToken")}
|
||||||
|
fieldId="kc-access-token"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:registration-access-token"
|
||||||
|
forLabel={t("registrationAccessToken")}
|
||||||
|
forID="kc-access-token"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Split hasGutter>
|
||||||
|
<SplitItem isFilled>
|
||||||
|
<ClipboardCopy id="kc-access-token" isReadOnly>
|
||||||
|
{accessToken}
|
||||||
|
</ClipboardCopy>
|
||||||
|
</SplitItem>
|
||||||
|
<SplitItem>
|
||||||
|
<Button variant="secondary" onClick={toggleAccessTokenConfirm}>
|
||||||
|
{t("regenerate")}
|
||||||
|
</Button>
|
||||||
|
</SplitItem>
|
||||||
|
</Split>
|
||||||
|
</FormGroup>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
69
src/clients/credentials/SignedJWT.tsx
Normal file
69
src/clients/credentials/SignedJWT.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, UseFormMethods } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
SelectVariant,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||||
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
|
import { sortProviders } from "../../util";
|
||||||
|
|
||||||
|
export type SignedJWTProps = {
|
||||||
|
form: UseFormMethods;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SignedJWT = ({ form }: SignedJWTProps) => {
|
||||||
|
const providers = sortProviders(
|
||||||
|
useServerInfo().providers.clientSignature.providers
|
||||||
|
);
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
|
||||||
|
const [open, isOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup
|
||||||
|
label={t("signatureAlgorithm")}
|
||||||
|
fieldId="kc-signature-algorithm"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:signature-algorithm"
|
||||||
|
forLabel={t("signatureAlgorithm")}
|
||||||
|
forID="kc-signature-algorithm"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="attributes.token_endpoint_auth_signing_alg"
|
||||||
|
defaultValue={providers[0]}
|
||||||
|
control={form.control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
toggleId="kc-signature-algorithm"
|
||||||
|
onToggle={() => isOpen(!open)}
|
||||||
|
onSelect={(_, value) => {
|
||||||
|
onChange(value as string);
|
||||||
|
isOpen(false);
|
||||||
|
}}
|
||||||
|
selections={value}
|
||||||
|
variant={SelectVariant.single}
|
||||||
|
aria-label={t("signatureAlgorithm")}
|
||||||
|
isOpen={open}
|
||||||
|
>
|
||||||
|
{providers.map((option) => (
|
||||||
|
<SelectOption
|
||||||
|
selected={option === value}
|
||||||
|
key={option}
|
||||||
|
value={option}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
33
src/clients/credentials/X509.tsx
Normal file
33
src/clients/credentials/X509.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { UseFormMethods } from "react-hook-form";
|
||||||
|
import { FormGroup, TextInput } from "@patternfly/react-core";
|
||||||
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
|
export type X509Props = {
|
||||||
|
form: UseFormMethods;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const X509 = ({ form }: X509Props) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t("subject")}
|
||||||
|
fieldId="kc-subject"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:subject"
|
||||||
|
forLabel={t("subject")}
|
||||||
|
forID="kc-subject"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
ref={form.register()}
|
||||||
|
type="text"
|
||||||
|
id="kc-subject"
|
||||||
|
name="attributes.x509_subjectdn"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,6 +1,10 @@
|
||||||
{
|
{
|
||||||
"clients-help": {
|
"clients-help": {
|
||||||
"downloadType": "this is information about the download type",
|
"downloadType": "this is information about the download type",
|
||||||
"details": "this is information about the details"
|
"details": "this is information about the details",
|
||||||
|
"client-authenticator-type": "Client Authenticator used for authentication of this client against Keycloak server",
|
||||||
|
"registration-access-token": "The registration access token provides access for clients to the client registration service.",
|
||||||
|
"signature-algorithm": "JWA algorithm, which the client needs to use when signing a JWT for authentication. If left blank, the client is allowed to use any algorithm.",
|
||||||
|
"subject": "A regular expression for validating Subject DN in the Client Certificate. Use \"(.*?)(?:$)\" to match all kind of expressions."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"formatOption": "Format option",
|
"formatOption": "Format option",
|
||||||
"downloadAdaptorTitle": "Download adaptor configs",
|
"downloadAdaptorTitle": "Download adaptor configs",
|
||||||
|
"settings": "Settings",
|
||||||
|
"credentials": "Credentials",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"clientList": "Client list",
|
"clientList": "Client list",
|
||||||
"clientSettings": "Client details",
|
"clientSettings": "Client details",
|
||||||
|
@ -46,6 +48,20 @@
|
||||||
"validRedirectUri": "Valid redirect URIs",
|
"validRedirectUri": "Valid redirect URIs",
|
||||||
"loginTheme": "Login theme",
|
"loginTheme": "Login theme",
|
||||||
"consentRequired": "Consent required",
|
"consentRequired": "Consent required",
|
||||||
|
"clientAuthenticator": "Client Authenticator",
|
||||||
|
"clientSecret": "Client secret",
|
||||||
|
"regenerate": "Regenerate",
|
||||||
|
"confirmClientSecretTitle": "Regenerate secret for this client?",
|
||||||
|
"confirmClientSecretBody": "If you regenerate secret, the Keycloak database will be updated and you will need to download a new adapter for this client.",
|
||||||
|
"confirmAccessTokenTitle": "Regenerate registration access token?",
|
||||||
|
"confirmAccessTokenBody": "If you regenerate registration access token, the access data regarding the client registration service will be updated.",
|
||||||
|
"clientSecretSuccess": "Client secret regenerated",
|
||||||
|
"clientSecretError": "Could not regenerate client secret due to: {{error}}",
|
||||||
|
"registrationAccessToken": "Registration access token",
|
||||||
|
"accessTokenSuccess": "Access token regenerated",
|
||||||
|
"accessTokenError": "Could not regenerate access token due to: {{error}}",
|
||||||
|
"signatureAlgorithm": "Signature algorithm",
|
||||||
|
"subject": "Subject DN",
|
||||||
"searchForClient": "Search for client"
|
"searchForClient": "Search for client"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
"unknownUser": "Anonymous",
|
"unknownUser": "Anonymous",
|
||||||
|
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
|
|
@ -54,7 +54,7 @@ export const ConfirmDialogModal = ({
|
||||||
onCancel,
|
onCancel,
|
||||||
children,
|
children,
|
||||||
open = true,
|
open = true,
|
||||||
variant = ModalVariant.default,
|
variant = ModalVariant.small,
|
||||||
toggleDialog,
|
toggleDialog,
|
||||||
}: ConfirmDialogModalProps) => {
|
}: ConfirmDialogModalProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
|
@ -48,7 +48,7 @@ exports[`Confirmation dialog renders simple confirm dialog 1`] = `
|
||||||
ouiaSafe={true}
|
ouiaSafe={true}
|
||||||
showClose={true}
|
showClose={true}
|
||||||
title="Delete app02?"
|
title="Delete app02?"
|
||||||
variant="default"
|
variant="small"
|
||||||
>
|
>
|
||||||
<Portal
|
<Portal
|
||||||
containerInfo={
|
containerInfo={
|
||||||
|
@ -63,8 +63,8 @@ exports[`Confirmation dialog renders simple confirm dialog 1`] = `
|
||||||
aria-describedby="pf-modal-part-3"
|
aria-describedby="pf-modal-part-3"
|
||||||
aria-labelledby="pf-modal-part-2"
|
aria-labelledby="pf-modal-part-2"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
class="pf-c-modal-box"
|
class="pf-c-modal-box pf-m-sm"
|
||||||
data-ouia-component-id="OUIA-Generated-Modal-default-2"
|
data-ouia-component-id="OUIA-Generated-Modal-small-2"
|
||||||
data-ouia-component-type="PF4/ModalContent"
|
data-ouia-component-type="PF4/ModalContent"
|
||||||
data-ouia-safe="true"
|
data-ouia-safe="true"
|
||||||
id="pf-modal-part-1"
|
id="pf-modal-part-1"
|
||||||
|
@ -170,11 +170,11 @@ exports[`Confirmation dialog renders simple confirm dialog 1`] = `
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
labelId="pf-modal-part-2"
|
labelId="pf-modal-part-2"
|
||||||
onClose={[Function]}
|
onClose={[Function]}
|
||||||
ouiaId="OUIA-Generated-Modal-default-2"
|
ouiaId="OUIA-Generated-Modal-small-2"
|
||||||
ouiaSafe={true}
|
ouiaSafe={true}
|
||||||
showClose={true}
|
showClose={true}
|
||||||
title="Delete app02?"
|
title="Delete app02?"
|
||||||
variant="default"
|
variant="small"
|
||||||
>
|
>
|
||||||
<Backdrop>
|
<Backdrop>
|
||||||
<div
|
<div
|
||||||
|
@ -198,20 +198,20 @@ exports[`Confirmation dialog renders simple confirm dialog 1`] = `
|
||||||
aria-label=""
|
aria-label=""
|
||||||
aria-labelledby="pf-modal-part-2"
|
aria-labelledby="pf-modal-part-2"
|
||||||
className=""
|
className=""
|
||||||
data-ouia-component-id="OUIA-Generated-Modal-default-2"
|
data-ouia-component-id="OUIA-Generated-Modal-small-2"
|
||||||
data-ouia-component-type="PF4/ModalContent"
|
data-ouia-component-type="PF4/ModalContent"
|
||||||
data-ouia-safe={true}
|
data-ouia-safe={true}
|
||||||
id="pf-modal-part-1"
|
id="pf-modal-part-1"
|
||||||
style={Object {}}
|
style={Object {}}
|
||||||
variant="default"
|
variant="small"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-describedby="pf-modal-part-3"
|
aria-describedby="pf-modal-part-3"
|
||||||
aria-label={null}
|
aria-label={null}
|
||||||
aria-labelledby="pf-modal-part-2"
|
aria-labelledby="pf-modal-part-2"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
className="pf-c-modal-box"
|
className="pf-c-modal-box pf-m-sm"
|
||||||
data-ouia-component-id="OUIA-Generated-Modal-default-2"
|
data-ouia-component-id="OUIA-Generated-Modal-small-2"
|
||||||
data-ouia-component-type="PF4/ModalContent"
|
data-ouia-component-type="PF4/ModalContent"
|
||||||
data-ouia-safe={true}
|
data-ouia-safe={true}
|
||||||
id="pf-modal-part-1"
|
id="pf-modal-part-1"
|
||||||
|
|
|
@ -15,7 +15,7 @@ type MultiLine = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function convertToMultiline(fields: string[]): MultiLine[] {
|
export function convertToMultiline(fields: string[]): MultiLine[] {
|
||||||
return fields.map((field) => {
|
return (fields && fields.length > 0 ? fields : [""]).map((field) => {
|
||||||
return { value: field };
|
return { value: field };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { createContext, ReactNode, useContext } from "react";
|
import React, { createContext, ReactNode, useContext } from "react";
|
||||||
import { ServerInfoRepresentation } from "./server-info";
|
import { ServerInfoRepresentation } from "./server-info";
|
||||||
import { HttpClientContext } from "../http-service/HttpClientContext";
|
import { HttpClientContext } from "../http-service/HttpClientContext";
|
||||||
import { sortProvider } from "../../util";
|
import { sortProviders } from "../../util";
|
||||||
import { DataLoader } from "../../components/data-loader/DataLoader";
|
import { DataLoader } from "../../components/data-loader/DataLoader";
|
||||||
|
|
||||||
export const ServerInfoContext = createContext<ServerInfoRepresentation>(
|
export const ServerInfoContext = createContext<ServerInfoRepresentation>(
|
||||||
|
@ -11,10 +11,7 @@ export const ServerInfoContext = createContext<ServerInfoRepresentation>(
|
||||||
export const useServerInfo = () => useContext(ServerInfoContext);
|
export const useServerInfo = () => useContext(ServerInfoContext);
|
||||||
|
|
||||||
export const useLoginProviders = () => {
|
export const useLoginProviders = () => {
|
||||||
const serverInfo = Object.entries(
|
return sortProviders(useServerInfo().providers["login-protocol"].providers);
|
||||||
useServerInfo().providers["login-protocol"].providers
|
|
||||||
);
|
|
||||||
return [...new Map(serverInfo.sort(sortProvider)).keys()];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServerInfoProvider = ({ children }: { children: ReactNode }) => {
|
export const ServerInfoProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { AuthenticationSection } from "./authentication/AuthenticationSection";
|
||||||
import { ClientScopeForm } from "./client-scopes/form/ClientScopeForm";
|
import { ClientScopeForm } from "./client-scopes/form/ClientScopeForm";
|
||||||
import { ClientScopesSection } from "./client-scopes/ClientScopesSection";
|
import { ClientScopesSection } from "./client-scopes/ClientScopesSection";
|
||||||
import { NewClientForm } from "./clients/add/NewClientForm";
|
import { NewClientForm } from "./clients/add/NewClientForm";
|
||||||
import { ClientSettings } from "./clients/ClientSettings";
|
|
||||||
import { ClientsSection } from "./clients/ClientsSection";
|
import { ClientsSection } from "./clients/ClientsSection";
|
||||||
import { ImportForm } from "./clients/import/ImportForm";
|
import { ImportForm } from "./clients/import/ImportForm";
|
||||||
import { EventsSection } from "./events/EventsSection";
|
import { EventsSection } from "./events/EventsSection";
|
||||||
|
@ -20,6 +19,7 @@ import { UsersSection } from "./user/UsersSection";
|
||||||
import { MappingDetails } from "./client-scopes/details/MappingDetails";
|
import { MappingDetails } from "./client-scopes/details/MappingDetails";
|
||||||
|
|
||||||
import { AccessType } from "./context/whoami/who-am-i-model";
|
import { AccessType } from "./context/whoami/who-am-i-model";
|
||||||
|
import { ClientDetails } from "./clients/ClientDetails";
|
||||||
|
|
||||||
export type RouteDef = {
|
export type RouteDef = {
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -39,7 +39,7 @@ export const routes: RoutesFn = (t: TFunction) => [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/clients/:id",
|
path: "/clients/:id",
|
||||||
component: ClientSettings,
|
component: ClientDetails,
|
||||||
breadcrumb: t("clients:clientSettings"),
|
breadcrumb: t("clients:clientSettings"),
|
||||||
access: "view-clients",
|
access: "view-clients",
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,13 @@ import FileSaver from "file-saver";
|
||||||
import { ClientRepresentation } from "./clients/models/client-model";
|
import { ClientRepresentation } from "./clients/models/client-model";
|
||||||
import { ProviderRepresentation } from "./context/server-info/server-info";
|
import { ProviderRepresentation } from "./context/server-info/server-info";
|
||||||
|
|
||||||
export const sortProvider = (
|
export const sortProviders = (providers: {
|
||||||
|
[index: string]: ProviderRepresentation;
|
||||||
|
}) => {
|
||||||
|
return [...new Map(Object.entries(providers).sort(sortProvider)).keys()];
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortProvider = (
|
||||||
a: [string, ProviderRepresentation],
|
a: [string, ProviderRepresentation],
|
||||||
b: [string, ProviderRepresentation]
|
b: [string, ProviderRepresentation]
|
||||||
) => {
|
) => {
|
||||||
|
|
Loading…
Reference in a new issue