Adds realm roles section to app and storybook (#79)

* add Realm Roles page

* add section for no realm roles

* update role-model, fixed UI to match designs

* fix paths

* add storybook demo and role actions kebab

* fix build and clean up

* fix formatting

* fix lint

* fix test and update snapshot

* fix storybook demo

* update snapshot after rebase

* PR feedback from Stan

* add back pf addons

* Update src/realm-roles/RealmRolesSection.tsx

Co-authored-by: Stan Silvert <ssilvert@redhat.com>

* fix format

* localize NoRealmRoles component

* fix formatting

* changes to PR suggested by Sarah

Co-authored-by: Stan Silvert <ssilvert@redhat.com>
This commit is contained in:
Eugenia 2020-09-18 04:04:55 -04:00 committed by GitHub
parent 43a4132beb
commit 9fd54eb964
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 458 additions and 122 deletions

View file

@ -29,66 +29,52 @@ export const App = () => {
<RealmContextProvider>
<Help>
<Page header={<Header />} isManagedSidebar sidebar={<PageNav />}>
<PageSection variant="light">
<Switch>
<Route exact path="/add-realm" component={NewRealmForm}></Route>
<Switch>
<Route exact path="/add-realm" component={NewRealmForm}></Route>
<Route exact path="/clients" component={ClientsSection}></Route>
<Route
exact
path="/add-client"
component={NewClientForm}
></Route>
<Route
exact
path="/import-client"
component={ImportForm}
></Route>
<Route exact path="/clients" component={ClientsSection}></Route>
<Route exact path="/add-client" component={NewClientForm}></Route>
<Route exact path="/import-client" component={ImportForm}></Route>
<Route
exact
path="/client-scopes"
component={ClientScopesSection}
></Route>
<Route
exact
path="/realm-roles"
component={RealmRolesSection}
></Route>
<Route exact path="/users" component={UsersSection}></Route>
<Route exact path="/groups" component={GroupsSection}></Route>
<Route
exact
path="/sessions"
component={SessionsSection}
></Route>
<Route exact path="/events" component={EventsSection}></Route>
<Route
exact
path="/client-scopes"
component={ClientScopesSection}
></Route>
<Route
exact
path="/realm-roles"
component={RealmRolesSection}
></Route>
<Route exact path="/users" component={UsersSection}></Route>
<Route exact path="/groups" component={GroupsSection}></Route>
<Route exact path="/sessions" component={SessionsSection}></Route>
<Route exact path="/events" component={EventsSection}></Route>
<Route
exact
path="/realm-settings"
component={RealmSettingsSection}
></Route>
<Route
exact
path="/authentication"
component={AuthenticationSection}
></Route>
<Route
exact
path="/identity-providers"
component={IdentityProvidersSection}
></Route>
<Route
exact
path="/user-federation"
component={UserFederationSection}
></Route>
<Route
exact
path="/realm-settings"
component={RealmSettingsSection}
></Route>
<Route
exact
path="/authentication"
component={AuthenticationSection}
></Route>
<Route
exact
path="/identity-providers"
component={IdentityProvidersSection}
></Route>
<Route
exact
path="/user-federation"
component={UserFederationSection}
></Route>
<Route exact path="/" component={ClientsSection} />
<Route component={PageNotFoundSection} />
</Switch>
</PageSection>
<Route exact path="/" component={ClientsSection} />
<Route component={PageNotFoundSection} />
</Switch>
</Page>
</Help>
</RealmContextProvider>

View file

@ -1,5 +1,10 @@
import { PageSection } from "@patternfly/react-core";
import React from "react";
export const ClientScopesSection = () => {
return <>The Client Scopes Page</>;
return (
<>
<PageSection variant="light">The Client Scopes Page</PageSection>
</>
);
};

View file

@ -1,7 +1,7 @@
import React, { useState, useContext } from "react";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Button } from "@patternfly/react-core";
import { Button, PageSection } from "@patternfly/react-core";
import { DataLoader } from "../components/data-loader/DataLoader";
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
@ -28,35 +28,40 @@ export const ClientsSection = () => {
};
return (
<DataLoader loader={loader}>
{(clients) => (
<TableToolbar
count={clients!.length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(f, m) => {
setFirst(f);
setMax(m);
}}
toolbarItem={
<>
<Button onClick={() => history.push("/add-client")}>
{t("createClient")}
</Button>
<Button
onClick={() => history.push("/import-client")}
variant="link"
>
{t("importClient")}
</Button>
</>
}
>
<ClientList clients={clients} baseUrl={keycloak!.authServerUrl()!} />
</TableToolbar>
)}
</DataLoader>
<PageSection variant="light">
<DataLoader loader={loader}>
{(clients) => (
<TableToolbar
count={clients!.length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(f, m) => {
setFirst(f);
setMax(m);
}}
toolbarItem={
<>
<Button onClick={() => history.push("/add-client")}>
{t("createClient")}
</Button>
<Button
onClick={() => history.push("/import-client")}
variant="link"
>
{t("importClient")}
</Button>
</>
}
>
<ClientList
clients={clients}
baseUrl={keycloak!.authServerUrl()!}
/>
</TableToolbar>
)}
</DataLoader>
</PageSection>
);
};

View file

@ -1,5 +1,10 @@
import { PageSection } from "@patternfly/react-core";
import React from "react";
export const EventsSection = () => {
return <>The Events Page</>;
return (
<>
<PageSection variant="light">The Events Page</PageSection>
</>
);
};

View file

@ -1,7 +1,7 @@
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { Button } from "@patternfly/react-core";
import { Button, PageSection } from "@patternfly/react-core";
import { HttpClientContext } from "../http-service/HttpClientContext";
import { GroupsList } from "./GroupsList";
@ -24,30 +24,32 @@ export const GroupsSection = () => {
return (
<>
<DataLoader loader={loader}>
{(groups) => (
<TableToolbar
count={groups!.length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(f, m) => {
setFirst(f);
setMax(m);
}}
toolbarItem={
<>
<Button onClick={() => history.push("/add-group")}>
{t("Create group")}
</Button>
</>
}
>
<GroupsList list={groups} />
</TableToolbar>
)}
</DataLoader>
<PageSection variant="light">
<DataLoader loader={loader}>
{(groups) => (
<TableToolbar
count={groups!.length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(f, m) => {
setFirst(f);
setMax(m);
}}
toolbarItem={
<>
<Button onClick={() => history.push("/add-group")}>
{t("Create group")}
</Button>
</>
}
>
<GroupsList list={groups} />
</TableToolbar>
)}
</DataLoader>
</PageSection>
</>
);
};

View file

@ -5,13 +5,14 @@ import { initReactI18next } from "react-i18next";
import common from "./common-messages.json";
import clients from "./clients/messages.json";
import realm from "./realm/messages.json";
import roles from "./realm-roles/messages.json";
import help from "./help.json";
const initOptions = {
ns: ["common", "help", "clients", "realm"],
ns: ["common", "help", "clients", "realm", "roles"],
defaultNS: "common",
resources: {
en: { ...common, ...help, ...clients, ...realm },
en: { ...common, ...help, ...clients, ...realm, ...roles },
},
lng: "en",
fallbackLng: "en",

19
src/model/role-model.ts Normal file
View file

@ -0,0 +1,19 @@
// Generated using typescript-generator version 2.0.400 on 2020-09-11 12:02:07.
export interface RoleRepresentation {
id?: string;
name?: string;
description?: string;
scopeParamRequired?: boolean;
composite?: boolean;
composites?: Composites;
clientRole?: boolean;
containerId?: string;
attributes?: { [index: string]: string[] };
}
export interface Composites {
realm?: string[];
client?: { [index: string]: string[] };
application?: { [index: string]: string[] };
}

View file

@ -0,0 +1,35 @@
import React from "react";
import {
Button,
PageSection,
EmptyState,
EmptyStateVariant,
EmptyStateIcon,
Title,
EmptyStateBody,
} from "@patternfly/react-core";
import { useHistory } from "react-router-dom";
import { PlusCircleIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next";
export const NoRealmRolesPage = () => {
const { t } = useTranslation("realm");
const history = useHistory();
return (
<>
<PageSection>
<EmptyState variant={EmptyStateVariant.large}>
<EmptyStateIcon icon={PlusCircleIcon} />
<Title headingLevel="h4" size="lg">
{t("noRealmRoles")}
</Title>
<EmptyStateBody>{t("emptyStateText")}</EmptyStateBody>
<Button variant="primary" onClick={() => history.push("/add-role")}>
{t("createRealm")}
</Button>
</EmptyState>
</PageSection>
</>
);
};

View file

@ -1,5 +1,78 @@
import React from "react";
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useState, useContext, useEffect } from "react";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
Button,
Divider,
Page,
PageSection,
PageSectionVariants,
Text,
TextContent,
} from "@patternfly/react-core";
import { DataLoader } from "../components/data-loader/DataLoader";
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
import { HttpClientContext } from "../http-service/HttpClientContext";
import { RoleRepresentation } from "../model/role-model";
import { RolesList } from "./RoleList";
import { RealmContext } from "../components/realm-context/RealmContext";
export const RealmRolesSection = () => {
return <>The Realm Roles Page</>;
const { t } = useTranslation("roles");
const history = useHistory();
const [max, setMax] = useState(10);
const [, setRoles] = useState([] as RoleRepresentation[]);
const [first, setFirst] = useState(0);
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const loader = async () => {
return await httpClient
.doGet(`/admin/realms/${realm}/roles`)
.then((r) => r.data as RoleRepresentation[]);
};
useEffect(() => {
loader().then((result) => setRoles(result || []));
}, []);
return (
<DataLoader loader={loader}>
{(roles) => (
<>
<PageSection variant="light">
<TextContent>
<Text component="h1">Realm roles</Text>
<Text component="p">{t("roleExplain")}</Text>
</TextContent>
</PageSection>
<Divider component="li" key={1} />
<PageSection padding={{ default: "noPadding" }}>
<TableToolbar
count={roles!.length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(f, m) => {
setFirst(f);
setMax(m);
}}
toolbarItem={
<>
<Button onClick={() => history.push("/add-role")}>
{t("createRole")}
</Button>
</>
}
>
<RolesList roles={roles} />
</TableToolbar>
</PageSection>
</>
)}
</DataLoader>
);
};

View file

@ -0,0 +1,79 @@
import React from "react";
import { useTranslation } from "react-i18next";
import {
Table,
TableBody,
TableHeader,
TableVariant,
IFormatter,
IFormatterValueType,
} from "@patternfly/react-table";
import { ExternalLink } from "../components/external-link/ExternalLink";
import { RoleRepresentation } from "../model/role-model";
type RolesListProps = {
roles?: RoleRepresentation[];
};
const columns: (keyof RoleRepresentation)[] = [
"name",
"composite",
"description",
];
export const RolesList = ({ roles }: RolesListProps) => {
const { t } = useTranslation("roles");
const emptyFormatter = (): IFormatter => (data?: IFormatterValueType) => {
return data ? data : "—";
};
const externalLink = (): IFormatter => (data?: IFormatterValueType) => {
return (data ? (
<ExternalLink href={data.toString()} />
) : undefined) as object;
};
const boolFormatter = (): IFormatter => (data?: IFormatterValueType) => {
const boolVal = data?.toString();
return (boolVal
? boolVal.charAt(0).toUpperCase() + boolVal.slice(1)
: undefined) as string;
};
const data = roles!.map((c) => {
return { cells: columns.map((col) => c[col]) };
});
return (
<Table
variant={TableVariant.compact}
cells={[
{
title: t("roleName"),
cellFormatters: [externalLink(), emptyFormatter()],
},
{
title: t("composite"),
cellFormatters: [boolFormatter(), emptyFormatter()],
},
{ title: t("description"), cellFormatters: [emptyFormatter()] },
]}
rows={data}
actions={[
{
title: t("common:Export"),
},
{
title: t("common:Delete"),
},
]}
aria-label="Roles list"
>
<TableHeader />
<TableBody />
</Table>
);
};

View file

@ -0,0 +1,77 @@
[
{
"name":"Admin",
"composite":true,
"description": "Lorem ipsum dolor sit amet"
},
{
"name":"Author",
"composite":false,
"description": "Lorem ipsum dolor sit amet"
},
{
"name":"Billing",
"composite":true,
"description": "Lorem ipsum dolor sit"
},
{
"name":"Contributor",
"composite":true,
"description": "Lorem ipsum dolor sit, consecte"
},
{
"name":"Editor",
"composite":true,
"description": "Lorem ipsum dolor sit amet"
},
{
"name":"Engineer",
"composite":true,
"description": "Lorem ipsum dolor sit amet"
},
{
"name":"Member",
"composite":false,
"description": "Lorem ipsum dolor sit amet"
},
{
"name":"Moderator",
"composite":true,
"description": "Lorem ipsum dolor sit amet"
},
{
"name":"Owner",
"composite":true,
"description": "Lorem ipsum dolor sit amet"
},
{
"name":"Reader",
"composite":true,
"description": "Lorem ipsum dolor sit amet"
},
{
"name":"Subscriber",
"composite":true,
"description": "Lorem ipsum dolor sit "
},
{
"name":"Teenager",
"composite":true,
"description": "Lorem ipsum dolor sit amet, consecte occaecat"
},
{
"name":"User",
"composite":true,
"description": "Lorem ipsum dolor sit amet, consecte"
},
{
"name":"Writer",
"composite":true,
"description": "Lorem ipsum dolor"
},
{
"name":"Zara",
"composite":true,
"description": "Lorem ipsum dolor sit amet"
}
]

View file

@ -0,0 +1,22 @@
{
"roles": {
"createRole": "Create role",
"importRole": "Import role",
"roleID": "Role ID",
"type": "Type",
"homeURL": "Home URL",
"roleExplain": "Realm-level roles are a global namespace to define your roles.",
"roleName": "Role name",
"composite": "Composite",
"description": "Description",
"roleList": "Role list",
"generalSettings": "General Settings",
"capabilityConfig": "Capability config",
"roleImportError": "Could not import role",
"roleImportSuccess": "Role imported succeful",
"roleDeletedSucess": "The role has been deleted",
"roleDeleteError": "Could not delete role:",
"roleAuthentication": "Role authentication"
}
}

View file

@ -3,6 +3,9 @@
"uploadFile":"Upload JSON file",
"realmName":"Realm name",
"enabled":"Enabled",
"create":"Create"
"create":"Create",
"createRealm": "Create realm",
"noRealmRoles": "No realm roles",
"emptyStateText": "There aren't any realm roles in this realm. Create a realm role to get started."
}
}

View file

@ -1,5 +1,13 @@
import { PageSection } from "@patternfly/react-core";
import React from "react";
export const SessionsSection = () => {
return <>The Sessions Page</>;
return (
<>
<>
<PageSection variant="light">The Sessions Page</PageSection>
</>
;
</>
);
};

View file

@ -4,7 +4,7 @@ import { Page } from "@patternfly/react-core";
import { NewRealmForm } from "../realm/add/NewRealmForm";
export default {
title: "New reaml form",
title: "New realm form",
component: NewRealmForm,
} as Meta;

View file

@ -0,0 +1,11 @@
import React from "react";
import { Meta } from "@storybook/react";
import { RolesList } from "../realm-roles/RoleList";
import rolesMock from "../realm-roles/__tests__/mock-roles.json";
export default {
title: "Roles List",
component: RolesList,
} as Meta;
export const RolesListExample = () => <RolesList roles={rolesMock} />;

View file

@ -1,5 +1,10 @@
import { PageSection } from "@patternfly/react-core";
import React from "react";
export const UsersSection = () => {
return <>The Users Page</>;
return (
<>
<PageSection variant="light">The Users Page</PageSection>
</>
);
};