created bookmarkable tabs component (#296)
This commit is contained in:
parent
45cda0a969
commit
0d9b4ff054
6 changed files with 103 additions and 51 deletions
|
@ -12,7 +12,6 @@ import {
|
||||||
SelectVariant,
|
SelectVariant,
|
||||||
Switch,
|
Switch,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
|
||||||
TabTitleText,
|
TabTitleText,
|
||||||
TextInput,
|
TextInput,
|
||||||
ValidatedOptions,
|
ValidatedOptions,
|
||||||
|
@ -26,6 +25,7 @@ import {
|
||||||
useAdminClient,
|
useAdminClient,
|
||||||
asyncStateFetch,
|
asyncStateFetch,
|
||||||
} from "../../context/auth/AdminClient";
|
} from "../../context/auth/AdminClient";
|
||||||
|
import { KeycloakTabs } from "../../components/keycloak-tabs/KeycloakTabs";
|
||||||
import { useAlerts } from "../../components/alert/Alerts";
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
|
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
|
||||||
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
||||||
|
@ -39,7 +39,6 @@ export const ClientScopeForm = () => {
|
||||||
>();
|
>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [clientScope, setClientScope] = useState<ClientScopeRepresentation>();
|
const [clientScope, setClientScope] = useState<ClientScopeRepresentation>();
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
|
||||||
|
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const providers = useLoginProviders();
|
const providers = useLoginProviders();
|
||||||
|
@ -103,13 +102,9 @@ export const ClientScopeForm = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageSection variant="light">
|
<PageSection variant="light">
|
||||||
<Tabs
|
<KeycloakTabs isBox>
|
||||||
activeKey={activeTab}
|
|
||||||
onSelect={(_, key) => setActiveTab(key as number)}
|
|
||||||
isBox
|
|
||||||
>
|
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={0}
|
eventKey="settings"
|
||||||
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
|
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
|
@ -312,14 +307,14 @@ export const ClientScopeForm = () => {
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
isHidden={!id}
|
isHidden={!id}
|
||||||
eventKey={1}
|
eventKey="mappers"
|
||||||
title={<TabTitleText>{t("mappers")}</TabTitleText>}
|
title={<TabTitleText>{t("mappers")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
{clientScope && (
|
{clientScope && (
|
||||||
<MapperList clientScope={clientScope} refresh={refresh} />
|
<MapperList clientScope={clientScope} refresh={refresh} />
|
||||||
)}
|
)}
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</KeycloakTabs>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
PageSection,
|
PageSection,
|
||||||
Spinner,
|
Spinner,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
|
||||||
TabTitleText,
|
TabTitleText,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -33,6 +32,7 @@ import {
|
||||||
import { ClientScopes } from "./scopes/ClientScopes";
|
import { ClientScopes } from "./scopes/ClientScopes";
|
||||||
import { EvaluateScopes } from "./scopes/EvaluateScopes";
|
import { EvaluateScopes } from "./scopes/EvaluateScopes";
|
||||||
import { ServiceAccount } from "./service-account/ServiceAccount";
|
import { ServiceAccount } from "./service-account/ServiceAccount";
|
||||||
|
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
||||||
|
|
||||||
type ClientDetailHeaderProps = {
|
type ClientDetailHeaderProps = {
|
||||||
onChange: (...event: any[]) => void;
|
onChange: (...event: any[]) => void;
|
||||||
|
@ -106,8 +106,6 @@ export const ClientDetails = () => {
|
||||||
|
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
|
||||||
const [activeTab2, setActiveTab2] = useState(30);
|
|
||||||
const [client, setClient] = useState<ClientRepresentation>();
|
const [client, setClient] = useState<ClientRepresentation>();
|
||||||
|
|
||||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
@ -203,57 +201,49 @@ export const ClientDetails = () => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<PageSection variant="light">
|
<PageSection variant="light">
|
||||||
<Tabs
|
<KeycloakTabs isBox>
|
||||||
activeKey={activeTab}
|
|
||||||
onSelect={(_, key) => setActiveTab(key as number)}
|
|
||||||
isBox
|
|
||||||
>
|
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={0}
|
eventKey="settings"
|
||||||
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
|
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<ClientSettings form={form} save={save} />
|
<ClientSettings form={form} save={save} />
|
||||||
</Tab>
|
</Tab>
|
||||||
{publicClient && (
|
{publicClient && (
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={1}
|
eventKey="credentials"
|
||||||
title={<TabTitleText>{t("credentials")}</TabTitleText>}
|
title={<TabTitleText>{t("credentials")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<Credentials clientId={id} form={form} save={save} />
|
<Credentials clientId={id} form={form} save={save} />
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={2}
|
eventKey="clientScopes"
|
||||||
title={<TabTitleText>{t("clientScopes")}</TabTitleText>}
|
title={<TabTitleText>{t("clientScopes")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<Tabs
|
<KeycloakTabs paramName="subtab" isSecondary>
|
||||||
activeKey={activeTab2}
|
|
||||||
isSecondary
|
|
||||||
onSelect={(_, key) => setActiveTab2(key as number)}
|
|
||||||
>
|
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={30}
|
eventKey="setup"
|
||||||
title={<TabTitleText>{t("setup")}</TabTitleText>}
|
title={<TabTitleText>{t("setup")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<ClientScopes clientId={id} protocol={client!.protocol!} />
|
<ClientScopes clientId={id} protocol={client!.protocol!} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={31}
|
eventKey="evaluate"
|
||||||
title={<TabTitleText>{t("evaluate")}</TabTitleText>}
|
title={<TabTitleText>{t("evaluate")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<EvaluateScopes clientId={id} protocol={client!.protocol!} />
|
<EvaluateScopes clientId={id} protocol={client!.protocol!} />
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</KeycloakTabs>
|
||||||
</Tab>
|
</Tab>
|
||||||
{client && client.serviceAccountsEnabled && (
|
{client && client.serviceAccountsEnabled && (
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={3}
|
eventKey="serviceAccount"
|
||||||
title={<TabTitleText>{t("serviceAccount")}</TabTitleText>}
|
title={<TabTitleText>{t("serviceAccount")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<ServiceAccount clientId={id} />
|
<ServiceAccount clientId={id} />
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
</Tabs>
|
</KeycloakTabs>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
53
src/components/keycloak-tabs/KeycloakTabs.tsx
Normal file
53
src/components/keycloak-tabs/KeycloakTabs.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React, { Children, isValidElement } from "react";
|
||||||
|
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||||
|
import { TabProps, Tabs, TabsProps } from "@patternfly/react-core";
|
||||||
|
|
||||||
|
type KeycloakTabsProps = Omit<TabsProps, "ref" | "activeKey" | "onSelect"> & {
|
||||||
|
paramName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUrl = (
|
||||||
|
path: string,
|
||||||
|
params: { [index: string]: string }
|
||||||
|
): string => {
|
||||||
|
let url = path;
|
||||||
|
for (const key in params) {
|
||||||
|
const value = params[key];
|
||||||
|
if (url.indexOf(key) !== -1) {
|
||||||
|
url = url.replace(new RegExp(`:${key}\\??`), value || "");
|
||||||
|
} else {
|
||||||
|
url += `/${value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KeycloakTabs = ({
|
||||||
|
paramName = "tab",
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: KeycloakTabsProps) => {
|
||||||
|
const match = useRouteMatch();
|
||||||
|
const params = match.params as { [index: string]: string };
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const firstTab = Children.toArray(children)[0];
|
||||||
|
const tab =
|
||||||
|
params[paramName] ||
|
||||||
|
(isValidElement<TabProps>(firstTab) && firstTab.props.eventKey) ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
activeKey={tab}
|
||||||
|
onSelect={(_, key) =>
|
||||||
|
history.push(
|
||||||
|
createUrl(match.path, { ...params, [paramName]: key as string })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,7 +5,6 @@ import {
|
||||||
Label,
|
Label,
|
||||||
PageSection,
|
PageSection,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
|
||||||
TabTitleText,
|
TabTitleText,
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
@ -19,13 +18,13 @@ import { RealmContext } from "../context/realm-context/RealmContext";
|
||||||
import { InfoCircleIcon } from "@patternfly/react-icons";
|
import { InfoCircleIcon } from "@patternfly/react-icons";
|
||||||
import { AdminEvents } from "./AdminEvents";
|
import { AdminEvents } from "./AdminEvents";
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
|
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
||||||
|
|
||||||
export const EventsSection = () => {
|
export const EventsSection = () => {
|
||||||
const { t } = useTranslation("events");
|
const { t } = useTranslation("events");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { realm } = useContext(RealmContext);
|
const { realm } = useContext(RealmContext);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
|
||||||
const [key, setKey] = useState("");
|
const [key, setKey] = useState("");
|
||||||
const refresh = () => setKey(`${new Date().getTime()}`);
|
const refresh = () => setKey(`${new Date().getTime()}`);
|
||||||
|
|
||||||
|
@ -55,13 +54,9 @@ export const EventsSection = () => {
|
||||||
<>
|
<>
|
||||||
<ViewHeader titleKey="events:title" subKey="events:eventExplain" />
|
<ViewHeader titleKey="events:title" subKey="events:eventExplain" />
|
||||||
<PageSection variant="light">
|
<PageSection variant="light">
|
||||||
<Tabs
|
<KeycloakTabs isBox>
|
||||||
activeKey={activeTab}
|
|
||||||
onSelect={(_, key) => setActiveTab(key as number)}
|
|
||||||
isBox
|
|
||||||
>
|
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={0}
|
eventKey="userEvents"
|
||||||
title={<TabTitleText>{t("userEvents")}</TabTitleText>}
|
title={<TabTitleText>{t("userEvents")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<KeycloakDataTable
|
<KeycloakDataTable
|
||||||
|
@ -110,12 +105,12 @@ export const EventsSection = () => {
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={1}
|
eventKey="adminEvents"
|
||||||
title={<TabTitleText>{t("adminEvents")}</TabTitleText>}
|
title={<TabTitleText>{t("adminEvents")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<AdminEvents />
|
<AdminEvents />
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</KeycloakTabs>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
PageSection,
|
PageSection,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
|
||||||
TabTitleText,
|
TabTitleText,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -20,6 +19,7 @@ import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
import { RealmRoleForm } from "./RealmRoleForm";
|
import { RealmRoleForm } from "./RealmRoleForm";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
|
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
||||||
|
|
||||||
const arrayToAttributes = (attributeArray: KeyValueType[]) => {
|
const arrayToAttributes = (attributeArray: KeyValueType[]) => {
|
||||||
const initValue: { [index: string]: string[] } = {};
|
const initValue: { [index: string]: string[] } = {};
|
||||||
|
@ -50,7 +50,6 @@ export const RealmRoleTabs = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
|
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
@ -145,24 +144,20 @@ export const RealmRoleTabs = () => {
|
||||||
/>
|
/>
|
||||||
<PageSection variant="light">
|
<PageSection variant="light">
|
||||||
{id && (
|
{id && (
|
||||||
<Tabs
|
<KeycloakTabs isBox>
|
||||||
activeKey={activeTab}
|
|
||||||
onSelect={(_, key) => setActiveTab(key as number)}
|
|
||||||
isBox
|
|
||||||
>
|
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={0}
|
eventKey="details"
|
||||||
title={<TabTitleText>{t("details")}</TabTitleText>}
|
title={<TabTitleText>{t("details")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<RealmRoleForm form={form} save={save} editMode={true} />
|
<RealmRoleForm form={form} save={save} editMode={true} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
eventKey={1}
|
eventKey="attributes"
|
||||||
title={<TabTitleText>{t("attributes")}</TabTitleText>}
|
title={<TabTitleText>{t("attributes")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<RoleAttributes form={form} save={save} />
|
<RoleAttributes form={form} save={save} />
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</KeycloakTabs>
|
||||||
)}
|
)}
|
||||||
{!id && <RealmRoleForm form={form} save={save} editMode={false} />}
|
{!id && <RealmRoleForm form={form} save={save} editMode={false} />}
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|
|
@ -63,6 +63,12 @@ export const routes: RoutesFn = (t: TFunction) => [
|
||||||
breadcrumb: t("clients:clientSettings"),
|
breadcrumb: t("clients:clientSettings"),
|
||||||
access: "view-clients",
|
access: "view-clients",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/:realm/clients/:id/:tab?/:subtab?",
|
||||||
|
component: ClientDetails,
|
||||||
|
breadcrumb: null,
|
||||||
|
access: "view-clients",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:realm/client-scopes/new",
|
path: "/:realm/client-scopes/new",
|
||||||
component: ClientScopeForm,
|
component: ClientScopeForm,
|
||||||
|
@ -75,6 +81,12 @@ export const routes: RoutesFn = (t: TFunction) => [
|
||||||
breadcrumb: t("client-scopes:clientScopeDetails"),
|
breadcrumb: t("client-scopes:clientScopeDetails"),
|
||||||
access: "view-clients",
|
access: "view-clients",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/:realm/client-scopes/:id/:tab",
|
||||||
|
component: ClientScopeForm,
|
||||||
|
breadcrumb: null,
|
||||||
|
access: "view-clients",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:realm/client-scopes/:scopeId/oidc-role-name-mapper",
|
path: "/:realm/client-scopes/:scopeId/oidc-role-name-mapper",
|
||||||
component: RoleMappingForm,
|
component: RoleMappingForm,
|
||||||
|
@ -117,6 +129,12 @@ export const routes: RoutesFn = (t: TFunction) => [
|
||||||
breadcrumb: t("roles:roleDetails"),
|
breadcrumb: t("roles:roleDetails"),
|
||||||
access: "view-realm",
|
access: "view-realm",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/:realm/roles/:id/:tab",
|
||||||
|
component: RealmRoleTabs,
|
||||||
|
breadcrumb: null,
|
||||||
|
access: "view-realm",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:realm/users",
|
path: "/:realm/users",
|
||||||
component: UsersSection,
|
component: UsersSection,
|
||||||
|
@ -144,6 +162,12 @@ export const routes: RoutesFn = (t: TFunction) => [
|
||||||
breadcrumb: t("events:title"),
|
breadcrumb: t("events:title"),
|
||||||
access: "view-events",
|
access: "view-events",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/:realm/events/:tab",
|
||||||
|
component: EventsSection,
|
||||||
|
breadcrumb: null,
|
||||||
|
access: "view-events",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:realm/realm-settings",
|
path: "/:realm/realm-settings",
|
||||||
component: RealmSettingsSection,
|
component: RealmSettingsSection,
|
||||||
|
|
Loading…
Reference in a new issue