update
This commit is contained in:
commit
a4b858cdc0
10 changed files with 96 additions and 72 deletions
|
@ -31,6 +31,7 @@ import { useAlerts } from "../components/alert/Alerts";
|
||||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
import { exportClient } from "../util";
|
import { exportClient } from "../util";
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
|
import { useDownloadDialog } from "../components/download-dialog/DownloadDialog";
|
||||||
|
|
||||||
export const ClientSettings = () => {
|
export const ClientSettings = () => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
|
@ -74,6 +75,11 @@ export const ClientSettings = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [toggleDownloadDialog, DownloadDialog] = useDownloadDialog({
|
||||||
|
id,
|
||||||
|
protocol: form.getValues("protocol"),
|
||||||
|
});
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (await form.trigger()) {
|
if (await form.trigger()) {
|
||||||
const redirectUris = toValue(form.getValues()["redirectUris"]);
|
const redirectUris = toValue(form.getValues()["redirectUris"]);
|
||||||
|
@ -89,6 +95,7 @@ export const ClientSettings = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
|
<DownloadDialog />
|
||||||
<Controller
|
<Controller
|
||||||
name="enabled"
|
name="enabled"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
@ -110,6 +117,9 @@ export const ClientSettings = () => {
|
||||||
titleKey={name}
|
titleKey={name}
|
||||||
subKey="clients:clientsExplain"
|
subKey="clients:clientsExplain"
|
||||||
selectItems={[
|
selectItems={[
|
||||||
|
<SelectOption key="download" value="download">
|
||||||
|
{t("downloadAdapterConfig")}
|
||||||
|
</SelectOption>,
|
||||||
<SelectOption key="export" value="export">
|
<SelectOption key="export" value="export">
|
||||||
{t("common:export")}
|
{t("common:export")}
|
||||||
</SelectOption>,
|
</SelectOption>,
|
||||||
|
@ -131,6 +141,8 @@ export const ClientSettings = () => {
|
||||||
exportClient(form.getValues());
|
exportClient(form.getValues());
|
||||||
} else if (value === "delete") {
|
} else if (value === "delete") {
|
||||||
toggleDeleteDialog();
|
toggleDeleteDialog();
|
||||||
|
} else if (value === "download") {
|
||||||
|
toggleDownloadDialog();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
"clientDeleteError": "Could not delete client:",
|
"clientDeleteError": "Could not delete client:",
|
||||||
"clientDeleteConfirmTitle": "Delete client?",
|
"clientDeleteConfirmTitle": "Delete client?",
|
||||||
"disableConfirmTitle": "Disable client?",
|
"disableConfirmTitle": "Disable client?",
|
||||||
|
"downloadAdapterConfig": "Download adapter config",
|
||||||
"disableConfirm": "If you disable this client, you cannot initiate a login or obtain access tokens.",
|
"disableConfirm": "If you disable this client, you cannot initiate a login or obtain access tokens.",
|
||||||
"clientDeleteConfirm": "If you delete this client, all associated data will be removed.",
|
"clientDeleteConfirm": "If you delete this client, all associated data will be removed.",
|
||||||
"clientAuthentication": "Client authentication",
|
"clientAuthentication": "Client authentication",
|
||||||
|
|
|
@ -11,11 +11,15 @@ import {
|
||||||
StackItem,
|
StackItem,
|
||||||
TextArea,
|
TextArea,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
import FileSaver from "file-saver";
|
||||||
|
|
||||||
import { ConfirmDialogModal } from "../confirm-dialog/ConfirmDialog";
|
import { ConfirmDialogModal } from "../confirm-dialog/ConfirmDialog";
|
||||||
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
|
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
|
||||||
import { RealmContext } from "../../context/realm-context/RealmContext";
|
import { RealmContext } from "../../context/realm-context/RealmContext";
|
||||||
import { HelpItem } from "../help-enabler/HelpItem";
|
import { HelpItem } from "../help-enabler/HelpItem";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||||
|
import { HelpContext } from "../help-enabler/HelpHeader";
|
||||||
|
|
||||||
export type DownloadDialogProps = {
|
export type DownloadDialogProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -27,39 +31,6 @@ type DownloadDialogModalProps = DownloadDialogProps & {
|
||||||
toggleDialog: () => void;
|
toggleDialog: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const serverInfo = [
|
|
||||||
{
|
|
||||||
id: "keycloak-oidc-jboss-subsystem-cli",
|
|
||||||
protocol: "openid-connect",
|
|
||||||
downloadOnly: false,
|
|
||||||
displayType: "Keycloak OIDC JBoss Subsystem CLI",
|
|
||||||
helpText:
|
|
||||||
"CLI script you must edit and apply to your client app server. This type of configuration is useful when you can't or don't want to crack open your WAR file.",
|
|
||||||
filename: "keycloak-oidc-subsystem.cli",
|
|
||||||
mediaType: "text/plain",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "keycloak-oidc-jboss-subsystem",
|
|
||||||
protocol: "openid-connect",
|
|
||||||
downloadOnly: false,
|
|
||||||
displayType: "Keycloak OIDC JBoss Subsystem XML",
|
|
||||||
helpText:
|
|
||||||
"XML snippet you must edit and add to the Keycloak OIDC subsystem on your client app server. This type of configuration is useful when you can't or don't want to crack open your WAR file.",
|
|
||||||
filename: "keycloak-oidc-subsystem.xml",
|
|
||||||
mediaType: "application/xml",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "keycloak-oidc-keycloak-json",
|
|
||||||
protocol: "openid-connect",
|
|
||||||
downloadOnly: false,
|
|
||||||
displayType: "Keycloak OIDC JSON",
|
|
||||||
helpText:
|
|
||||||
"keycloak.json file used by the Keycloak OIDC client adapter to configure clients. This must be saved to a keycloak.json file and put in your WEB-INF directory of your WAR file. You may also want to tweak this file after you download it.",
|
|
||||||
filename: "keycloak.json",
|
|
||||||
mediaType: "application/json",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const useDownloadDialog = (
|
export const useDownloadDialog = (
|
||||||
props: DownloadDialogProps
|
props: DownloadDialogProps
|
||||||
): [() => void, () => ReactElement] => {
|
): [() => void, () => ReactElement] => {
|
||||||
|
@ -84,8 +55,10 @@ export const DownloadDialog = ({
|
||||||
const httpClient = useContext(HttpClientContext)!;
|
const httpClient = useContext(HttpClientContext)!;
|
||||||
const { realm } = useContext(RealmContext);
|
const { realm } = useContext(RealmContext);
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
|
const { enabled } = useContext(HelpContext);
|
||||||
|
const serverInfo = useServerInfo();
|
||||||
|
|
||||||
const configFormats = serverInfo; //serverInfo.clientInstallations[protocol];
|
const configFormats = serverInfo.clientInstallations[protocol];
|
||||||
const [selected, setSelected] = useState(
|
const [selected, setSelected] = useState(
|
||||||
configFormats[configFormats.length - 1].id
|
configFormats[configFormats.length - 1].id
|
||||||
);
|
);
|
||||||
|
@ -95,21 +68,28 @@ export const DownloadDialog = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const response = await httpClient.doGet<string>(
|
const response = await httpClient.doGet<string>(
|
||||||
`admin/${realm}/master/clients/${id}/installation/providers/${selected}`
|
`/admin/realms/${realm}/clients/${id}/installation/providers/${selected}`
|
||||||
);
|
);
|
||||||
setSnippet(response.data!);
|
setSnippet(await response.text());
|
||||||
})();
|
})();
|
||||||
}, [selected]);
|
}, [selected, snippet]);
|
||||||
return (
|
return (
|
||||||
<ConfirmDialogModal
|
<ConfirmDialogModal
|
||||||
titleKey={t("clients:downloadAdaptorTitle")}
|
titleKey={t("clients:downloadAdaptorTitle")}
|
||||||
continueButtonLabel={t("download")}
|
continueButtonLabel={t("download")}
|
||||||
onConfirm={() => {}}
|
onConfirm={() => {
|
||||||
|
const config = configFormats.find((config) => config.id === selected)!;
|
||||||
|
FileSaver.saveAs(
|
||||||
|
new Blob([snippet], { type: config.mediaType }),
|
||||||
|
config.filename
|
||||||
|
);
|
||||||
|
}}
|
||||||
open={open}
|
open={open}
|
||||||
toggleDialog={toggleDialog}
|
toggleDialog={toggleDialog}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<Stack hasGutter>
|
<Stack hasGutter>
|
||||||
|
{enabled && (
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Alert
|
<Alert
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -124,6 +104,7 @@ export const DownloadDialog = ({
|
||||||
}
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
)}
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId="type"
|
fieldId="type"
|
||||||
|
@ -171,7 +152,7 @@ export const DownloadDialog = ({
|
||||||
<HelpItem
|
<HelpItem
|
||||||
helpText={t("clients-help:details")}
|
helpText={t("clients-help:details")}
|
||||||
forLabel={t("clients:details")}
|
forLabel={t("clients:details")}
|
||||||
forID=""
|
forID="details"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
EmptyStateSecondaryActions,
|
EmptyStateSecondaryActions,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { PlusCircleIcon } from "@patternfly/react-icons";
|
import { PlusCircleIcon } from "@patternfly/react-icons";
|
||||||
|
import { SearchIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
export type Action = {
|
export type Action = {
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -19,8 +20,10 @@ export type Action = {
|
||||||
export type ListEmptyStateProps = {
|
export type ListEmptyStateProps = {
|
||||||
message: string;
|
message: string;
|
||||||
instructions: string;
|
instructions: string;
|
||||||
primaryActionText: string;
|
primaryActionText?: string;
|
||||||
onPrimaryAction: MouseEventHandler<HTMLButtonElement>;
|
onPrimaryAction?: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
hasIcon?: boolean;
|
||||||
|
isSearchVariant?: boolean;
|
||||||
secondaryActions?: Action[];
|
secondaryActions?: Action[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,26 +31,34 @@ export const ListEmptyState = ({
|
||||||
message,
|
message,
|
||||||
instructions,
|
instructions,
|
||||||
onPrimaryAction,
|
onPrimaryAction,
|
||||||
|
hasIcon,
|
||||||
|
isSearchVariant,
|
||||||
primaryActionText,
|
primaryActionText,
|
||||||
secondaryActions,
|
secondaryActions,
|
||||||
}: ListEmptyStateProps) => {
|
}: ListEmptyStateProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EmptyState variant="large">
|
<EmptyState variant="large">
|
||||||
|
{hasIcon && isSearchVariant ? (
|
||||||
|
<EmptyStateIcon icon={SearchIcon} />
|
||||||
|
) : (
|
||||||
<EmptyStateIcon icon={PlusCircleIcon} />
|
<EmptyStateIcon icon={PlusCircleIcon} />
|
||||||
|
)}
|
||||||
<Title headingLevel="h4" size="lg">
|
<Title headingLevel="h4" size="lg">
|
||||||
{message}
|
{message}
|
||||||
</Title>
|
</Title>
|
||||||
<EmptyStateBody>{instructions}</EmptyStateBody>
|
<EmptyStateBody>{instructions}</EmptyStateBody>
|
||||||
|
{primaryActionText && (
|
||||||
<Button variant="primary" onClick={onPrimaryAction}>
|
<Button variant="primary" onClick={onPrimaryAction}>
|
||||||
{primaryActionText}
|
{primaryActionText}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
{secondaryActions && (
|
{secondaryActions && (
|
||||||
<EmptyStateSecondaryActions>
|
<EmptyStateSecondaryActions>
|
||||||
{secondaryActions.map((action) => (
|
{secondaryActions.map((action) => (
|
||||||
<Button
|
<Button
|
||||||
key={action.text}
|
key={action.text}
|
||||||
variant={action.type || ButtonVariant.primary}
|
variant={action.type || ButtonVariant.secondary}
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
>
|
>
|
||||||
{action.text}
|
{action.text}
|
||||||
|
|
|
@ -47,8 +47,8 @@ exports[`<ListEmptyState /> render 1`] = `
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="pf-c-button pf-m-primary"
|
class="pf-c-button pf-m-secondary"
|
||||||
data-ouia-component-id="OUIA-Generated-Button-primary-2"
|
data-ouia-component-id="OUIA-Generated-Button-secondary-1"
|
||||||
data-ouia-component-type="PF4/Button"
|
data-ouia-component-type="PF4/Button"
|
||||||
data-ouia-safe="true"
|
data-ouia-safe="true"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Keycloak, { KeycloakInstance } from "keycloak-js";
|
import Keycloak, { KeycloakInstance } from "keycloak-js";
|
||||||
const keycloak: KeycloakInstance = Keycloak();
|
const keycloak: KeycloakInstance = Keycloak("/keycloak.json");
|
||||||
|
|
||||||
export default async function (): Promise<KeycloakInstance> {
|
export default async function (): Promise<KeycloakInstance> {
|
||||||
await keycloak.init({ onLoad: "check-sso", pkceMethod: "S256" }).catch(() => {
|
await keycloak.init({ onLoad: "check-sso", pkceMethod: "S256" }).catch(() => {
|
||||||
|
|
|
@ -76,6 +76,7 @@ export const GroupsCreateModal = ({
|
||||||
>
|
>
|
||||||
<Form isHorizontal>
|
<Form isHorizontal>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
name="create-modal-group"
|
||||||
label={t("name")}
|
label={t("name")}
|
||||||
fieldId="group-id"
|
fieldId="group-id"
|
||||||
helperTextInvalid={t("common:required")}
|
helperTextInvalid={t("common:required")}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from "./models/server-info";
|
} from "./models/server-info";
|
||||||
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
|
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
|
||||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
@ -90,10 +91,10 @@ export const GroupsSection = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<ViewHeader titleKey="groups:groups" subKey="groups:groupsDescription" />
|
<ViewHeader titleKey="groups:groups" subKey="groups:groupsDescription" />
|
||||||
<PageSection variant={PageSectionVariants.light}>
|
<PageSection variant={PageSectionVariants.light}>
|
||||||
{rawData ? (
|
{rawData && rawData.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<TableToolbar
|
<TableToolbar
|
||||||
inputGroupName="groupsToolbarTextInput"
|
inputGroupName="groupsToolbarTextInput"
|
||||||
|
@ -125,7 +126,17 @@ export const GroupsSection = () => {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GroupsList list={filteredData || rawData} refresh={loader} />
|
{rawData && (
|
||||||
|
<GroupsList list={filteredData ? filteredData : rawData} refresh={loader}/>
|
||||||
|
)}
|
||||||
|
{filteredData && filteredData.length === 0 && (
|
||||||
|
<ListEmptyState
|
||||||
|
hasIcon={true}
|
||||||
|
isSearchVariant={true}
|
||||||
|
message={t("noSearchResults")}
|
||||||
|
instructions={t("noSearchResultsInstructions")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableToolbar>
|
</TableToolbar>
|
||||||
<GroupsCreateModal
|
<GroupsCreateModal
|
||||||
isCreateModalOpen={isCreateModalOpen}
|
isCreateModalOpen={isCreateModalOpen}
|
||||||
|
@ -137,11 +148,14 @@ export const GroupsSection = () => {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="pf-u-text-align-center">
|
<ListEmptyState
|
||||||
<Spinner />
|
hasIcon={true}
|
||||||
</div>
|
message={t("noGroupsInThisRealm")}
|
||||||
|
instructions={t("noGroupsInThisRealmInstructions")}
|
||||||
|
primaryActionText={t("createGroup")}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,10 @@
|
||||||
"groupCreated": "Group created",
|
"groupCreated": "Group created",
|
||||||
"couldNotCreateGroup": "Could not create group",
|
"couldNotCreateGroup": "Could not create group",
|
||||||
"createAGroup": "Create a group",
|
"createAGroup": "Create a group",
|
||||||
"create": "Create"
|
"create": "Create",
|
||||||
|
"noSearchResults": "No search results",
|
||||||
|
"noSearchResultsInstructions" : "Click on the search bar above to search for groups",
|
||||||
|
"noGroupsInThisRealm" : "No groups in this Realm",
|
||||||
|
"noGroupsInThisRealmInstructions" : "You haven't created any groups in this realm. Create a group to get started."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15237,7 +15237,7 @@ prepend-http@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||||
|
|
||||||
prettier@^2.0.5:
|
prettier@2.1.2:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5"
|
||||||
integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==
|
integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==
|
||||||
|
|
Loading…
Reference in a new issue