initial version dashboard section (#283)

* initial version dashboard section

* changed to new design

* changed text to "realm info"
This commit is contained in:
Erik Jan de Wit 2021-01-15 02:44:16 +01:00 committed by GitHub
parent e4ab191136
commit 324d9ff061
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 236 additions and 26 deletions

1
public/icon.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Guides" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#4d4d4d;}.cls-4{fill:#e1e1e1;}.cls-5{fill:#c8c8c8;}.cls-6{fill:#c2c2c2;}.cls-7{fill:#c7c7c7;}.cls-8{fill:#cecece;}.cls-9{fill:#d3d3d3;}.cls-10{fill:#c6c6c6;}.cls-11{fill:#d5d5d5;}.cls-12{fill:#d0d0d0;}.cls-13{fill:#bfbfbf;}.cls-14{fill:#d9d9d9;}.cls-15{fill:#d4d4d4;}.cls-16{fill:#d8d8d8;}.cls-17{fill:#e2e2e2;}.cls-18{fill:#e4e4e4;}.cls-19{fill:#dedede;}.cls-20{fill:#c5c5c5;}.cls-21{fill:#d1d1d1;}.cls-22{fill:#ddd;}.cls-23{fill:#e3e3e3;}.cls-24{fill:#00b8e3;}.cls-25{fill:#33c6e9;}.cls-26{fill:#008aaa;}</style><clipPath id="clip-path"><rect class="cls-1" x="0.02" y="-0.02" width="127.98" height="127.98"/></clipPath></defs><title>keycloak_deliverables</title><g class="cls-2"><path class="cls-3" d="M109.62,38a1,1,0,0,1-.83-0.47l-14.44-25A1,1,0,0,0,93.51,12H34.59a1,1,0,0,0-.83.47l-15,26a0,0,0,0,0,0,0L4.31,63.5a1,1,0,0,0,0,1l14.45,25,15,26a1,1,0,0,0,.83.47H93.51a1,1,0,0,0,.84-0.47l14.45-25a1,1,0,0,1,.83-0.47h18a1.08,1.08,0,0,0,1.08-1.08V39.06A1.08,1.08,0,0,0,127.61,38h-18Z"/><path class="cls-1" d="M127.61,38H19.6a1,1,0,0,0-.83.47s0,0,0,0l-0.53.92L11.62,50.85,4.31,63.5a1,1,0,0,0,0,1L5.9,67.21,18.76,89.49a1,1,0,0,0,.84.48h108a1.07,1.07,0,0,0,1.06-1.07V39.06A1.08,1.08,0,0,0,127.61,38Z"/><path class="cls-4" d="M22,61.35L5.9,67.21,4.31,64.46a1,1,0,0,1,0-1l7.31-12.66Z"/><polygon class="cls-5" points="118.06 66.03 128.69 64.51 128.69 76.85 118.06 66.03"/><path class="cls-6" d="M118.05,66l10.64,10.82V88.9A1.07,1.07,0,0,1,127.63,90H115.25Z"/><polygon class="cls-7" points="118.06 66.03 115.25 89.97 100.37 89.97 95.86 79.11 118.06 66.03"/><polygon class="cls-8" points="118.06 66.03 128.69 53.08 128.69 64.51 118.06 66.03"/><path class="cls-9" d="M128.69,39.06v14L118.05,66l-8-28h17.58A1.08,1.08,0,0,1,128.69,39.06Z"/><polygon class="cls-10" points="100.37 89.97 92.52 89.97 90.48 87.05 95.86 79.11 100.37 89.97"/><polygon class="cls-11" points="118.06 66.03 88.61 53.58 104.1 37.98 110.03 37.98 118.06 66.03"/><path class="cls-12" d="M88.61,53.58l7.25,25.52L118.05,66Z"/><polygon class="cls-13" points="92.52 89.97 90.39 89.97 90.48 87.05 92.52 89.97"/><polygon class="cls-14" points="104.1 37.98 88.61 53.58 85.82 39.63 91.8 37.98 104.1 37.98"/><path class="cls-15" d="M88.61,53.58L52.88,61.82,90.48,87.05Z"/><path class="cls-12" d="M88.61,53.58l1.87,33.47,5.37-7.95Z"/><path class="cls-14" d="M85.82,39.63L52.88,61.82l35.74-8.24Z"/><polygon class="cls-16" points="52.88 61.82 37.38 89.97 28.56 89.97 22.04 61.35 52.88 61.82"/><path class="cls-17" d="M37,38L22,61.35l-3.81-22,0.53-.92s0,0,0,0A1,1,0,0,1,19.6,38H37Z"/><path class="cls-16" d="M28.56,90h-9a1,1,0,0,1-.84-0.48L5.9,67.21,22,61.35Z"/><polygon class="cls-18" points="22.04 61.35 11.62 50.84 18.23 39.39 22.04 61.35"/><polygon class="cls-19" points="69.2 37.98 58.89 37.98 43.11 37.98 52.88 61.82 85.82 39.63 75.89 37.98 69.2 37.98"/><polygon class="cls-19" points="39.03 37.98 37 37.98 22.04 61.35 52.88 61.82 43.11 37.98 39.03 37.98"/><polygon class="cls-20" points="83.31 89.97 89.06 89.97 90.39 89.97 90.48 87.05 83.31 89.97"/><polygon class="cls-12" points="90.48 87.05 52.88 61.82 59.65 89.97 69.2 89.97 83.31 89.97 90.48 87.05"/><polygon class="cls-21" points="37.38 89.97 39.03 89.97 58.89 89.97 59.65 89.97 52.88 61.82 37.38 89.97"/><polygon class="cls-22" points="85.92 37.98 85.82 39.63 91.8 37.98 89.06 37.98 85.92 37.98"/><polygon class="cls-23" points="75.89 37.98 85.82 39.63 84.9 37.98 75.89 37.98"/><polygon class="cls-17" points="84.9 37.98 85.82 39.63 85.92 37.98 84.9 37.98"/><path class="cls-24" d="M58.8,38.43L44.27,63.59a0.85,0.85,0,0,0-.1.41H34L54,29.46a0.78,0.78,0,0,1,.3.29l0,0,4.52,7.85A0.87,0.87,0,0,1,58.8,38.43Z"/><path class="cls-25" d="M58.78,90.45l-4.51,7.82a0.88,0.88,0,0,1-.31.29L34,64H44.16a0.77,0.77,0,0,0,.1.39,0.09,0.09,0,0,0,0,0l14.5,25.14A0.85,0.85,0,0,1,58.78,90.45Z"/><path class="cls-26" d="M54,29.46L34,64h0l-5,8.66L24.25,64.4a0.77,0.77,0,0,1-.1-0.39,0.85,0.85,0,0,1,.1-0.41l4.83-8.38L43.78,29.79a0.85,0.85,0,0,1,.74-0.44h9A0.89,0.89,0,0,1,54,29.46Z"/><path class="cls-24" d="M54,98.55a0.89,0.89,0,0,1-.43.11h-9a0.85,0.85,0,0,1-.74-0.44L30.35,75,29,72.67,34,64Z"/><path class="cls-26" d="M94.05,64L74.1,98.55a0.93,0.93,0,0,1-.3-0.29l0,0L69.27,90.4a0.87,0.87,0,0,1,0-.8L83.79,64.44A0.84,0.84,0,0,0,83.91,64H94.05Z"/><path class="cls-24" d="M103.92,64a0.84,0.84,0,0,1-.12.44L84.27,98.26a0.86,0.86,0,0,1-.73.4h-9a0.93,0.93,0,0,1-.44-0.11L94.05,64l5-8.65,4.75,8.23A0.84,0.84,0,0,1,103.92,64Z"/><path class="cls-24" d="M94.05,64H83.91a0.84,0.84,0,0,0-.12-0.43L69.29,38.44a0.85,0.85,0,0,1,0-.87l4.52-7.82a0.93,0.93,0,0,1,.3-0.29Z"/><path class="cls-25" d="M99.05,55.34h0l-5,8.65L74.1,29.46a0.93,0.93,0,0,1,.44-0.11h9a0.86,0.86,0,0,1,.73.4Z"/></g></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -1,6 +1,6 @@
@import "@patternfly/patternfly/patternfly.min.css";
@import "@patternfly/patternfly/patternfly-addons.css";
.pf-c-brand {
.keycloak__pageheader_brand {
height: 35px;
}

View file

@ -1,6 +1,6 @@
@import "@patternfly/patternfly/patternfly.min.css";
.pf-c-brand {
.keycloak__pageheader_brand {
height: 16px;
}

View file

@ -16,8 +16,9 @@ import {
import { HelpIcon } from "@patternfly/react-icons";
import { WhoAmIContext } from "./context/whoami/WhoAmI";
import { HelpHeader } from "./components/help-enabler/HelpHeader";
import { Link } from "react-router-dom";
import { Link, useHistory } from "react-router-dom";
import { useAdminClient } from "./context/auth/AdminClient";
import { useRealm } from "./context/realm-context/RealmContext";
export const Header = () => {
const adminClient = useAdminClient();
@ -55,7 +56,19 @@ export const Header = () => {
const ServerInfoDropdownItem = () => {
const { t } = useTranslation();
return <DropdownItem key="server info">{t("serverInfo")}</DropdownItem>;
const history = useHistory();
const { setRealm } = useRealm();
return (
<DropdownItem
key="server info"
onClick={() => {
history.push("/master/");
setRealm("master");
}}
>
{t("realmInfo")}
</DropdownItem>
);
};
const HelpDropdownItem = () => {
@ -160,7 +173,11 @@ export const Header = () => {
showNavToggle
logo={
<Link to="/">
<Brand src="/logo.svg" alt="Logo" />
<Brand
src="/logo.svg"
alt="Logo"
className="keycloak__pageheader_brand"
/>
</Link>
}
logoComponent="div"

View file

@ -26,8 +26,7 @@ export const PageNav: React.FunctionComponent = () => {
const history = useHistory();
let initialItem = history.location.pathname;
if (initialItem === "/") initialItem = "/client-list";
const initialItem = history.location.pathname;
const [activeItem, setActiveItem] = useState(initialItem);
@ -56,7 +55,7 @@ export const PageNav: React.FunctionComponent = () => {
<NavItem
id={"nav-item" + path.replace("/", "-")}
to={`/${realm}${path}`}
isActive={activeItem === path}
isActive={activeItem.substr(realm.length + 1) === path}
>
{t(title)}
</NavItem>
@ -90,6 +89,9 @@ export const PageNav: React.FunctionComponent = () => {
)}
</DataLoader>
</NavList>
<NavGroup title="">
<LeftNav title="home" path="/" />
</NavGroup>
{showManage && (
<NavGroup title={t("manage")}>
<LeftNav title="clients" path="/clients" />

View file

@ -27,6 +27,7 @@ import {
useAdminClient,
asyncStateFetch,
} from "../../context/auth/AdminClient";
import { toUpperCase } from "../../util";
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { AddScopeDialog } from "./AddScopeDialog";
@ -42,9 +43,6 @@ export type ClientScopesProps = {
protocol: string;
};
const firstUpperCase = (name: string) =>
name.charAt(0).toUpperCase() + name.slice(1);
const castAdminClient = (adminClient: KeycloakAdminClient) =>
(adminClient.clients as unknown) as {
[index: string]: Function;
@ -67,7 +65,7 @@ const removeScope = async (
clientScope: ClientScopeRepresentation,
type: ClientScopeType
) => {
const typeToName = firstUpperCase(type);
const typeToName = toUpperCase(type);
await castAdminClient(adminClient)[`del${typeToName}ClientScope`]({
id: clientId,
clientScopeId: clientScope.id!,
@ -80,7 +78,7 @@ const addScope = async (
clientScope: ClientScopeRepresentation,
type: ClientScopeType
) => {
const typeToName = firstUpperCase(type);
const typeToName = toUpperCase(type);
await castAdminClient(adminClient)[`add${typeToName}ClientScope`]({
id: clientId,
clientScopeId: clientScope.id!,

View file

@ -33,6 +33,7 @@
"signOut": "Sign out",
"manageAccount": "Manage account",
"serverInfo": "Server info",
"realmInfo": "Realm info",
"help": "Help",
"helpLabel": "More help for {{label}}",
"documentation": "Documentation",

View file

@ -15,12 +15,12 @@ import {
} from "@patternfly/react-core";
import { CheckIcon } from "@patternfly/react-icons";
import { toUpperCase } from "../../util";
import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { useRealm } from "../../context/realm-context/RealmContext";
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
import "./realm-selector.css";
import { toUpperCase } from "../../util";
type RealmSelectorProps = {
realmList: RealmRepresentation[];
@ -34,7 +34,6 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
const [filteredItems, setFilteredItems] = useState(realmList);
const history = useHistory();
const { t } = useTranslation("common");
const RealmText = ({ value }: { value: string }) => (
<Split className="keycloak__realm_selector__list-item-split">
<SplitItem isFilled>{toUpperCase(value)}</SplitItem>

169
src/dashboard/Dashboard.tsx Normal file
View file

@ -0,0 +1,169 @@
import {
Brand,
Button,
Card,
CardBody,
CardTitle,
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
EmptyState,
EmptyStateBody,
Grid,
GridItem,
Label,
List,
ListItem,
ListVariant,
PageSection,
Text,
TextContent,
Title,
} from "@patternfly/react-core";
import React from "react";
import { useTranslation } from "react-i18next";
import _ from "lodash";
import { useRealm } from "../context/realm-context/RealmContext";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import "./dashboard.css";
import { toUpperCase } from "../util";
const EmptyDashboard = () => {
const { t } = useTranslation("dashboard");
const { realm, setRealm } = useRealm();
return (
<PageSection variant="light">
<EmptyState variant="large">
<Brand
src="/icon.svg"
alt="Keycloak icon"
className="keycloak__dashboard_icon"
/>
<Title headingLevel="h4" size="3xl">
{t("welcome")}
</Title>
<Title headingLevel="h4" size="4xl">
{realm}
</Title>
<EmptyStateBody>{t("introduction")}</EmptyStateBody>
<Button variant="link" onClick={() => setRealm("master")}>
{t("common:providerInfo")}
</Button>
</EmptyState>
</PageSection>
);
};
const Dashboard = () => {
const { t } = useTranslation("dashboard");
const { realm } = useRealm();
const serverInfo = useServerInfo();
const enabledFeatures = _.xor(
serverInfo.profileInfo?.disabledFeatures,
serverInfo.profileInfo?.experimentalFeatures,
serverInfo.profileInfo?.previewFeatures
);
const isExperimentalFeature = (feature: string) => {
return serverInfo.profileInfo?.experimentalFeatures?.includes(feature);
};
const isPreviewFeature = (feature: string) => {
return serverInfo.profileInfo?.previewFeatures?.includes(feature);
};
return (
<>
<PageSection variant="light">
<TextContent className="pf-u-mr-sm">
<Text component="h1">{toUpperCase(realm)} realm</Text>
</TextContent>
</PageSection>
<PageSection>
<Grid hasGutter>
<GridItem span={2}>
<Card className="keycloak__dashboard_card">
<CardTitle>{t("serverInfo")}</CardTitle>
<CardBody>
<DescriptionList>
<DescriptionListGroup>
<DescriptionListTerm>{t("version")}</DescriptionListTerm>
<DescriptionListDescription>
{serverInfo.systemInfo?.version}
</DescriptionListDescription>
<DescriptionListTerm>{t("product")}</DescriptionListTerm>
<DescriptionListDescription>
{serverInfo.profileInfo?.name}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</CardBody>
</Card>
</GridItem>
<GridItem span={10}>
<Card className="keycloak__dashboard_card">
<CardTitle>{t("profile")}</CardTitle>
<CardBody>
<DescriptionList>
<DescriptionListGroup>
<DescriptionListTerm>
{t("enabledFeatures")}
</DescriptionListTerm>
<DescriptionListDescription>
<List variant={ListVariant.inline}>
{enabledFeatures.map((feature) => (
<ListItem key={feature}>
{feature}{" "}
{isExperimentalFeature(feature) ? (
<Label color="orange">{t("experimental")}</Label>
) : (
<></>
)}
{isPreviewFeature(feature) ? (
<Label>{t("preview")}</Label>
) : (
<></>
)}
</ListItem>
))}
</List>
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
{t("disabledFeatures")}
</DescriptionListTerm>
<DescriptionListDescription>
<List variant={ListVariant.inline}>
{serverInfo.profileInfo?.disabledFeatures?.map(
(feature) => (
<ListItem key={feature}>{feature}</ListItem>
)
)}
</List>
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</CardBody>
</Card>
</GridItem>
</Grid>
</PageSection>
</>
);
};
export const DashboardSection = () => {
const { realm } = useRealm();
const isMasterRealm = realm === "master";
return (
<>
{!isMasterRealm && <EmptyDashboard />}
{isMasterRealm && <Dashboard />}
</>
);
};

View file

@ -0,0 +1,8 @@
.keycloak__dashboard_icon {
max-width: 114px;
}
.keycloak__dashboard_card {
height: 100%;
}

View file

@ -0,0 +1,14 @@
{
"dashboard": {
"welcome": "Welcome to",
"introduction": "If you want to leave this page and mange this realm, please click the corresponding menu items in the left navigation bar.",
"serverInfo": "Server info",
"version": "Version",
"product": "Product",
"profile": "Profile",
"enabledFeatures": "Enabled features",
"experimental": "Experimental",
"preview": "Preview",
"disabledFeatures": "Disabled features"
}
}

View file

@ -4,6 +4,7 @@ import { initReactI18next } from "react-i18next";
import common from "./common-messages.json";
import help from "./common-help.json";
import dashboard from "./dashboard/messages.json";
import clients from "./clients/messages.json";
import clientsHelp from "./clients/help.json";
import clientScopes from "./client-scopes/messages.json";
@ -26,6 +27,7 @@ const initOptions = {
en: {
...common,
...help,
...dashboard,
...clients,
...clientsHelp,
...clientScopes,

View file

@ -17,7 +17,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
import { formattedLinkTableCell } from "../components/external-link/FormattedLink";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { emptyFormatter } from "../util";
import { emptyFormatter, toUpperCase } from "../util";
export const RealmRolesSection = () => {
const { t } = useTranslation("roles");
@ -28,9 +28,9 @@ export const RealmRolesSection = () => {
const [selectedRole, setSelectedRole] = useState<RoleRepresentation>();
const loader = async (first?: number, max?: number, search?: string) => {
const loader = async (to?: number, max?: number, search?: string) => {
const params: { [name: string]: string | number } = {
first: first!,
to: to!,
max: max!,
search: search!,
};
@ -48,9 +48,7 @@ export const RealmRolesSection = () => {
const boolFormatter = (): IFormatter => (data?: IFormatterValueType) => {
const boolVal = data?.toString();
return (boolVal
? boolVal.charAt(0).toUpperCase() + boolVal.slice(1)
: undefined) as string;
return (boolVal ? toUpperCase(boolVal) : undefined) as string;
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({

View file

@ -1,9 +1,11 @@
import { TFunction } from "i18next";
import { BreadcrumbsRoute } from "use-react-router-breadcrumbs";
import { AccessType } from "keycloak-admin/lib/defs/whoAmIRepresentation";
import { AuthenticationSection } from "./authentication/AuthenticationSection";
import { ClientScopeForm } from "./client-scopes/form/ClientScopeForm";
import { ClientScopesSection } from "./client-scopes/ClientScopesSection";
import { DashboardSection } from "./dashboard/Dashboard";
import { NewClientForm } from "./clients/add/NewClientForm";
import { ClientsSection } from "./clients/ClientsSection";
import { ImportForm } from "./clients/import/ImportForm";
@ -22,7 +24,6 @@ import { ClientDetails } from "./clients/ClientDetails";
import { UserFederationKerberosSettings } from "./user-federation/UserFederationKerberosSettings";
import { UserFederationLdapSettings } from "./user-federation/UserFederationLdapSettings";
import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm";
import { BreadcrumbsRoute } from "use-react-router-breadcrumbs";
import { RealmRoleTabs } from "./realm-roles/RealmRoleTabs";
export type RouteDef = BreadcrumbsRoute & {
@ -218,13 +219,13 @@ export const routes: RoutesFn = (t: TFunction) => [
},
{
path: "/:realm/",
component: ClientsSection,
component: DashboardSection,
breadcrumb: t("common:home"),
access: "anyone",
},
{
path: "/",
component: ClientsSection,
component: DashboardSection,
breadcrumb: t("common:home"),
access: "anyone",
},

View file

@ -50,8 +50,8 @@ export const exportClient = (client: ClientRepresentation): void => {
);
};
export const toUpperCase = (realmName: string) =>
realmName.charAt(0).toUpperCase() + realmName.slice(1);
export const toUpperCase = (name: string) =>
name.charAt(0).toUpperCase() + name.slice(1);
export const convertToFormValues = (
obj: any,