Adds functionality to delete realm role (#160)

* added function to refresh the dataloader data

* 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

update snapshot after rebase

PR feedback from Stan

add back pf addons

localize NoRealmRoles component

changes to PR suggested by Sarah

adds new role form

rename fields

cleaning up

address PR feedback from Stan

add logic to delete realm role

format

rebase with erik branch

resolves merge conflicts

fix check types error

fix invalid state

fix check-types

fix ts error

* fix format

* remove log stmt

* address PR feedback from Erik and Sarah

* remove utility class

* address Erik comment

* fix formatting

* fix bug and address PR feedback

* move msg

* fix lint

* fix format

* delete details file (on another branch)

Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Eugenia 2020-10-14 16:50:10 -04:00 committed by GitHub
parent 35584207c8
commit caec08f95f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 137 additions and 61 deletions

View file

@ -69,7 +69,7 @@ export const PageNav: React.FunctionComponent = () => {
<NavGroup title={t("manage")}>
{makeNavItem("clients", "clients")}
{makeNavItem("clientScopes", "client-scopes")}
{makeNavItem("realmRoles", "realm-roles")}
{makeNavItem("realmRoles", "roles")}
{makeNavItem("users", "users")}
{makeNavItem("groups", "groups")}
{makeNavItem("sessions", "sessions")}

View file

@ -1,46 +1,69 @@
import React, { useContext } from "react";
import React, { useContext, useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Button, PageSection } from "@patternfly/react-core";
import { Bullseye, Button, PageSection, Spinner } from "@patternfly/react-core";
import { DataLoader } from "../components/data-loader/DataLoader";
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { RoleRepresentation } from "../model/role-model";
import { RolesList } from "./RoleList";
import { RealmContext } from "../context/realm-context/RealmContext";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { PaginatingTableToolbar } from "../components/table-toolbar/PaginatingTableToolbar";
export const RealmRolesSection = () => {
const [max, setMax] = useState(10);
const [first, setFirst] = useState(0);
const { t } = useTranslation("roles");
const history = useHistory();
const httpClient = useContext(HttpClientContext)!;
const [roles, setRoles] = useState<RoleRepresentation[]>();
const { realm } = useContext(RealmContext);
const loader = async () => {
const params: { [name: string]: string | number } = { first, max };
const result = await httpClient.doGet<RoleRepresentation[]>(
`/admin/realms/${realm}/roles`
`/admin/realms/${realm}/roles`,
{ params: params }
);
return result.data;
setRoles(result.data);
};
useEffect(() => {
loader();
}, [first, max]);
return (
<>
<ViewHeader titleKey="roles:title" subKey="roles:roleExplain" />
<PageSection padding={{ default: "noPadding" }}>
<TableToolbar
toolbarItem={
<>
<Button onClick={() => history.push("/add-role")}>
{t("createRole")}
</Button>
</>
}
>
<DataLoader loader={loader}>
{(roles) => <RolesList roles={roles.data} />}
</DataLoader>
</TableToolbar>
<PageSection variant="light">
{!roles && (
<Bullseye>
<Spinner />
</Bullseye>
)}
{roles && (
<PaginatingTableToolbar
count={roles!.length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(first, max) => {
setFirst(first);
setMax(max);
}}
toolbarItem={
<>
<Button onClick={() => history.push("/add-role")}>
{t("createRole")}
</Button>
</>
}
>
<RolesList roles={roles} refresh={loader} />
</PaginatingTableToolbar>
)}
</PageSection>
</>
);

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import {
@ -12,9 +12,15 @@ import {
import { ExternalLink } from "../components/external-link/ExternalLink";
import { RoleRepresentation } from "../model/role-model";
import { AlertVariant, ButtonVariant } from "@patternfly/react-core";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { useAlerts } from "../components/alert/Alerts";
import { RealmContext } from "../context/realm-context/RealmContext";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
type RolesListProps = {
roles?: RoleRepresentation[];
refresh: () => void;
};
const columns: (keyof RoleRepresentation)[] = [
@ -23,8 +29,12 @@ const columns: (keyof RoleRepresentation)[] = [
"description",
];
export const RolesList = ({ roles }: RolesListProps) => {
export const RolesList = ({ roles, refresh }: RolesListProps) => {
const { t } = useTranslation("roles");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const { addAlert } = useAlerts();
const [selectedRowId, setSelectedRowId] = useState(-1);
const emptyFormatter = (): IFormatter => (data?: IFormatterValueType) => {
return data ? data : "—";
@ -43,37 +53,66 @@ export const RolesList = ({ roles }: RolesListProps) => {
? boolVal.charAt(0).toUpperCase() + boolVal.slice(1)
: undefined) as string;
};
const data = roles!.map((c) => {
return { cells: columns.map((col) => c[col]) };
const data = roles!.map((column) => {
return { cells: columns.map((col) => column[col]), role: column };
});
let selectedRoleName;
if (selectedRowId === data.length) {
selectedRoleName = data[selectedRowId - 1].role.name;
} else if (selectedRowId != -1) {
selectedRoleName = data[selectedRowId].role.name;
}
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleDeleteConfirm",
messageKey: t("roles:roleDeleteConfirmDialog", { selectedRoleName }),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await httpClient.doDelete(
`/admin/realms/${realm}/roles/${data[selectedRowId].role.name}`
);
refresh();
addAlert(t("roleDeletedSuccess"), AlertVariant.success);
} catch (error) {
addAlert(`${t("roleDeleteError")} ${error}`, AlertVariant.danger);
}
},
});
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>
<>
<DeleteConfirm />
<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:Delete"),
onClick: (_, rowId) => {
setSelectedRowId(rowId);
toggleDeleteDialog();
},
},
]}
aria-label="Roles list"
>
<TableHeader />
<TableBody />
</Table>
</>
);
};

View file

@ -18,7 +18,7 @@ import {
import { RoleRepresentation } from "../../model/role-model";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { useAlerts } from "../../components/alert/Alerts";
import { Controller, useForm } from "react-hook-form";
import { Controller, useForm, FieldErrors } from "react-hook-form";
import { RealmContext } from "../../context/realm-context/RealmContext";
export const NewRoleForm = () => {
@ -40,6 +40,8 @@ export const NewRoleForm = () => {
}
};
console.log(errors);
return (
<>
<PageSection variant="light">
@ -56,16 +58,18 @@ export const NewRoleForm = () => {
type="text"
id="kc-role-name"
name="name"
ref={register()}
ref={register({ required: true })}
/>
</FormGroup>
<FormGroup
label={t("description")}
fieldId="kc-role-description"
helperTextInvalid="Max length 255"
validated={
errors ? ValidatedOptions.error : ValidatedOptions.default
Object.keys(errors).length != 0
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={"Max length 255"}
>
<Controller
name="description"
@ -75,6 +79,12 @@ export const NewRoleForm = () => {
render={({ onChange, value }) => (
<TextArea
type="text"
validated={
errors.description &&
errors.description.type === "maxLength"
? "error"
: "default"
}
id="kc-role-description"
value={value}
onChange={onChange}

View file

@ -17,7 +17,9 @@
"roleCreated": "Role created",
"roleCreateError": "Could not create role:",
"roleImportSuccess": "Role import successful",
"roleDeletedSucess": "The role has been deleted",
"roleDeleteConfirm": "Delete role?",
"roleDeleteConfirmDialog": "This action will permanently delete the role {{selectedRoleName}} and cannot be undone.",
"roleDeletedSuccess": "The role has been deleted",
"roleDeleteError": "Could not delete role:",
"roleAuthentication": "Role authentication"
}

View file

@ -60,7 +60,7 @@ export const NewRealmForm = () => {
type="text"
id="kc-realm-name"
name="realm"
ref={register()}
ref={register({ required: true })}
/>
</FormGroup>
<FormGroup label={t("enabled")} fieldId="kc-realm-enabled-switch">

View file

@ -55,7 +55,7 @@ export const routes = (t: TFunction) => [
breadcrumb: t("client-scopes:createClientScope"),
},
{
path: "/realm-roles",
path: "/roles",
component: RealmRolesSection,
breadcrumb: t("roles:roleList"),
},

View file

@ -8,4 +8,6 @@ export default {
component: RolesList,
} as Meta;
export const RolesListExample = () => <RolesList roles={rolesMock} />;
export const RolesListExample = () => (
<RolesList roles={rolesMock} refresh={() => {}} />
);