Change the input fields based on access rights (#184)
* poc of possible way to change form dynamically * fixed detail routs * Update src/components/form-access/FormAccess.tsx Co-authored-by: Stan Silvert <ssilvert@redhat.com> * Update src/components/form-access/FormAccess.tsx Co-authored-by: Stan Silvert <ssilvert@redhat.com> * Update src/components/form-access/FormAccess.tsx Co-authored-by: Stan Silvert <ssilvert@redhat.com> * added more form access and logic for Controller * render switches for boolean types * better render of `<Controller` wrapped components * added isDisabled property * added test * small refactor * fixed types * TextArea doesn't support isDisabled * when field is disabled, disable button as well * added jsdoc Co-authored-by: Stan Silvert <ssilvert@redhat.com>
This commit is contained in:
parent
889f8078d2
commit
2543893373
14 changed files with 472 additions and 25 deletions
|
@ -7,6 +7,8 @@ import "@testing-library/jest-dom/extend-expect";
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import 'mutationobserver-shim';
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
|
@ -22,3 +24,6 @@ import { configure } from 'enzyme';
|
|||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
global.MutationObserver = window.MutationObserver;
|
|
@ -68,6 +68,7 @@
|
|||
"grunt": "^1.2.1",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"jest": "^25.4.0",
|
||||
"mutationobserver-shim": "^0.3.7",
|
||||
"postcss": "^7.0.32",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-import": "^12.0.1",
|
||||
|
|
|
@ -67,6 +67,7 @@ export const PageNav: React.FunctionComponent = () => {
|
|||
"view-realm",
|
||||
"query-groups",
|
||||
"query-users",
|
||||
"query-clients",
|
||||
"view-events"
|
||||
);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import { FormGroup, TextInput } from "@patternfly/react-core";
|
||||
import { UseFormMethods } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
|
||||
type ClientDescriptionProps = {
|
||||
form: UseFormMethods;
|
||||
|
@ -11,7 +12,7 @@ export const ClientDescription = ({ form }: ClientDescriptionProps) => {
|
|||
const { t } = useTranslation("clients");
|
||||
const { register, errors } = form;
|
||||
return (
|
||||
<>
|
||||
<FormAccess role="manage-clients" unWrap>
|
||||
<FormGroup
|
||||
label={t("clientID")}
|
||||
fieldId="kc-client-id"
|
||||
|
@ -37,6 +38,6 @@ export const ClientDescription = ({ form }: ClientDescriptionProps) => {
|
|||
name="description"
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -32,6 +32,7 @@ import { ViewHeader } from "../components/view-header/ViewHeader";
|
|||
import { exportClient } from "../util";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { useDownloadDialog } from "../components/download-dialog/DownloadDialog";
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
|
||||
export const ClientSettings = () => {
|
||||
const { t } = useTranslation("clients");
|
||||
|
@ -163,7 +164,7 @@ export const ClientSettings = () => {
|
|||
<Form isHorizontal>
|
||||
<ClientDescription form={form} />
|
||||
</Form>
|
||||
<Form isHorizontal>
|
||||
<FormAccess isHorizontal role="manage-clients">
|
||||
<FormGroup label={t("rootUrl")} fieldId="kc-root-url">
|
||||
<TextInput
|
||||
type="text"
|
||||
|
@ -183,8 +184,8 @@ export const ClientSettings = () => {
|
|||
ref={form.register}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
<Form isHorizontal>
|
||||
</FormAccess>
|
||||
<FormAccess isHorizontal role="manage-clients">
|
||||
<FormGroup
|
||||
label={t("consentRequired")}
|
||||
fieldId="kc-consent"
|
||||
|
@ -241,7 +242,7 @@ export const ClientSettings = () => {
|
|||
</Button>
|
||||
<Button variant="link">{t("common:cancel")}</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
</FormAccess>
|
||||
</ScrollForm>
|
||||
</PageSection>
|
||||
</>
|
||||
|
|
|
@ -6,10 +6,11 @@ import {
|
|||
Checkbox,
|
||||
Grid,
|
||||
GridItem,
|
||||
Form,
|
||||
} from "@patternfly/react-core";
|
||||
import { UseFormMethods, Controller } from "react-hook-form";
|
||||
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
|
||||
type CapabilityConfigProps = {
|
||||
form: UseFormMethods;
|
||||
};
|
||||
|
@ -17,7 +18,7 @@ type CapabilityConfigProps = {
|
|||
export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
return (
|
||||
<Form isHorizontal>
|
||||
<FormAccess isHorizontal role="manage-clients">
|
||||
<FormGroup
|
||||
hasNoPaddingTop
|
||||
label={t("clientAuthentication")}
|
||||
|
@ -132,6 +133,6 @@ export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
|
|||
</GridItem>
|
||||
</Grid>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
||||
|
|
117
src/components/form-access/FormAccess.tsx
Normal file
117
src/components/form-access/FormAccess.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
import React, {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
ReactElement,
|
||||
} from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import {
|
||||
ActionGroup,
|
||||
Form,
|
||||
FormGroup,
|
||||
FormProps,
|
||||
Grid,
|
||||
GridItem,
|
||||
TextArea,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
import { AccessType } from "../../context/whoami/who-am-i-model";
|
||||
|
||||
export type FormAccessProps = FormProps & {
|
||||
/**
|
||||
* One of the AccessType's that the user needs to have to view this form. Also see {@link useAccess}.
|
||||
* @type {AccessType}
|
||||
*/
|
||||
role: AccessType;
|
||||
|
||||
/**
|
||||
* An override property if fine grained access has been setup for this form.
|
||||
* @type {boolean}
|
||||
*/
|
||||
fineGrainedAccess?: boolean;
|
||||
|
||||
/**
|
||||
* Set unWrap when you don't want this component to wrap your "children" in a {@link Form} component.
|
||||
* @type {boolean}
|
||||
*/
|
||||
unWrap?: boolean;
|
||||
children: ReactElement[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Use this in place of a patternfly Form component and add the `role` and `fineGrainedAccess` properties.
|
||||
* @param {FormAccessProps} param0 - all properties of Form + role and fineGrainedAccess
|
||||
*/
|
||||
export const FormAccess = ({
|
||||
children,
|
||||
role,
|
||||
fineGrainedAccess = false,
|
||||
unWrap = false,
|
||||
...rest
|
||||
}: FormAccessProps) => {
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
const recursiveCloneChildren = (
|
||||
children: ReactElement[],
|
||||
newProps: any
|
||||
): ReactElement[] => {
|
||||
return Children.map(children, (child) => {
|
||||
if (!isValidElement(child)) {
|
||||
return child;
|
||||
}
|
||||
|
||||
if (child.props) {
|
||||
const element = child as ReactElement;
|
||||
if (child.type === Controller) {
|
||||
return cloneElement(child, {
|
||||
...element.props,
|
||||
render: (props: any) => {
|
||||
const renderElement = element.props.render(props);
|
||||
return cloneElement(renderElement, {
|
||||
value: props.value,
|
||||
onChange: props.onChange,
|
||||
...newProps,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
const children = recursiveCloneChildren(
|
||||
element.props.children,
|
||||
newProps
|
||||
);
|
||||
if (child.type === TextArea) {
|
||||
return cloneElement(child, {
|
||||
readOnly: newProps.isDisabled,
|
||||
children,
|
||||
} as any);
|
||||
}
|
||||
return cloneElement(
|
||||
child,
|
||||
child.type === FormGroup ||
|
||||
child.type === GridItem ||
|
||||
child.type === Grid ||
|
||||
child.type === ActionGroup
|
||||
? { children }
|
||||
: { ...newProps, children }
|
||||
);
|
||||
}
|
||||
return child;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!unWrap && (
|
||||
<Form {...rest}>
|
||||
{recursiveCloneChildren(children, {
|
||||
isDisabled: !hasAccess(role) && !fineGrainedAccess,
|
||||
})}
|
||||
</Form>
|
||||
)}
|
||||
{unWrap &&
|
||||
recursiveCloneChildren(children, {
|
||||
isDisabled: !hasAccess(role) && !fineGrainedAccess,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
62
src/components/form-access/__tests__/FormAccess.test.tsx
Normal file
62
src/components/form-access/__tests__/FormAccess.test.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { FormGroup, Switch, TextInput } from "@patternfly/react-core";
|
||||
|
||||
import { WhoAmI, WhoAmIContext } from "../../../context/whoami/WhoAmI";
|
||||
import whoami from "../../../context/whoami/__tests__/mock-whoami.json";
|
||||
import { RealmContext } from "../../../context/realm-context/RealmContext";
|
||||
import { AccessContextProvider } from "../../../context/access/Access";
|
||||
|
||||
import { FormAccess } from "../FormAccess";
|
||||
|
||||
describe("<FormAccess />", () => {
|
||||
const Form = ({ realm }: { realm: string }) => {
|
||||
const { register, control } = useForm();
|
||||
return (
|
||||
<WhoAmIContext.Provider value={new WhoAmI("master", whoami)}>
|
||||
<RealmContext.Provider value={{ realm, setRealm: () => {} }}>
|
||||
<AccessContextProvider>
|
||||
<FormAccess role="manage-clients">
|
||||
<FormGroup label="test" fieldId="field">
|
||||
<TextInput
|
||||
type="text"
|
||||
id="field"
|
||||
name="fieldName"
|
||||
ref={register()}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Controller
|
||||
name="consentRequired"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="kc-consent"
|
||||
label={"on"}
|
||||
labelOff="off"
|
||||
isChecked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormAccess>
|
||||
</AccessContextProvider>
|
||||
</RealmContext.Provider>
|
||||
</WhoAmIContext.Provider>
|
||||
);
|
||||
};
|
||||
it("render normal form", () => {
|
||||
const comp = mount(<Form realm="master" />);
|
||||
expect(comp).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("render form disabled for test realm", () => {
|
||||
const container = mount(<Form realm="test" />);
|
||||
|
||||
const disabled = container.find("input#field").props().disabled;
|
||||
expect(disabled).toBe(true);
|
||||
|
||||
expect(container.find("input#kc-consent").props().disabled).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,242 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FormAccess /> render normal form 1`] = `
|
||||
<Form
|
||||
realm="master"
|
||||
>
|
||||
<AccessContextProvider>
|
||||
<FormAccess
|
||||
role="manage-clients"
|
||||
>
|
||||
<Form>
|
||||
<form
|
||||
className="pf-c-form"
|
||||
noValidate={true}
|
||||
>
|
||||
<FormGroup
|
||||
fieldId="field"
|
||||
key=".0"
|
||||
label="test"
|
||||
>
|
||||
<div
|
||||
className="pf-c-form__group"
|
||||
>
|
||||
<div
|
||||
className="pf-c-form__group-label"
|
||||
>
|
||||
<label
|
||||
className="pf-c-form__label"
|
||||
htmlFor="field"
|
||||
>
|
||||
<span
|
||||
className="pf-c-form__label-text"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="pf-c-form__group-control"
|
||||
>
|
||||
<ForwardRef
|
||||
id="field"
|
||||
isDisabled={false}
|
||||
key=".0"
|
||||
name="fieldName"
|
||||
type="text"
|
||||
>
|
||||
<TextInputBase
|
||||
aria-label={null}
|
||||
className=""
|
||||
id="field"
|
||||
innerRef={[Function]}
|
||||
isDisabled={false}
|
||||
isLeftTruncated={false}
|
||||
isReadOnly={false}
|
||||
isRequired={false}
|
||||
name="fieldName"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
validated="default"
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label={null}
|
||||
className="pf-c-form-control"
|
||||
disabled={false}
|
||||
id="field"
|
||||
name="fieldName"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
readOnly={false}
|
||||
required={false}
|
||||
type="text"
|
||||
/>
|
||||
</TextInputBase>
|
||||
</ForwardRef>
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<Controller
|
||||
control={
|
||||
Object {
|
||||
"defaultValuesRef": Object {
|
||||
"current": Object {},
|
||||
},
|
||||
"fieldArrayDefaultValuesRef": Object {
|
||||
"current": Object {},
|
||||
},
|
||||
"fieldArrayNamesRef": Object {
|
||||
"current": Set {},
|
||||
},
|
||||
"fieldsRef": Object {
|
||||
"current": Object {
|
||||
"consentRequired": Object {
|
||||
"ref": Object {
|
||||
"focus": undefined,
|
||||
"name": "consentRequired",
|
||||
},
|
||||
},
|
||||
"fieldName": Object {
|
||||
"ref": <input
|
||||
aria-invalid="false"
|
||||
class="pf-c-form-control"
|
||||
id="field"
|
||||
name="fieldName"
|
||||
type="text"
|
||||
value=""
|
||||
/>,
|
||||
},
|
||||
},
|
||||
},
|
||||
"fieldsWithValidationRef": Object {
|
||||
"current": Object {},
|
||||
},
|
||||
"formStateRef": Object {
|
||||
"current": Object {
|
||||
"dirtyFields": Object {},
|
||||
"errors": Object {},
|
||||
"isDirty": false,
|
||||
"isSubmitSuccessful": false,
|
||||
"isSubmitted": false,
|
||||
"isSubmitting": false,
|
||||
"isValid": false,
|
||||
"submitCount": 0,
|
||||
"touched": Object {},
|
||||
},
|
||||
},
|
||||
"getValues": [Function],
|
||||
"isWatchAllRef": Object {
|
||||
"current": false,
|
||||
},
|
||||
"mode": Object {
|
||||
"isOnAll": false,
|
||||
"isOnBlur": false,
|
||||
"isOnChange": false,
|
||||
"isOnSubmit": true,
|
||||
"isOnTouch": false,
|
||||
},
|
||||
"reValidateMode": Object {
|
||||
"isReValidateOnBlur": false,
|
||||
"isReValidateOnChange": true,
|
||||
},
|
||||
"readFormStateRef": Object {
|
||||
"current": Object {
|
||||
"dirtyFields": false,
|
||||
"isDirty": false,
|
||||
"isSubmitting": false,
|
||||
"isValid": false,
|
||||
"touched": false,
|
||||
},
|
||||
},
|
||||
"register": [Function],
|
||||
"removeFieldEventListener": [Function],
|
||||
"renderWatchedInputs": [Function],
|
||||
"resetFieldArrayFunctionRef": Object {
|
||||
"current": Object {},
|
||||
},
|
||||
"setValue": [Function],
|
||||
"shallowFieldsStateRef": Object {
|
||||
"current": Object {},
|
||||
},
|
||||
"shouldUnregister": true,
|
||||
"trigger": [Function],
|
||||
"unregister": [Function],
|
||||
"updateFormState": [Function],
|
||||
"useWatchFieldsRef": Object {
|
||||
"current": Object {},
|
||||
},
|
||||
"useWatchRenderFunctionsRef": Object {
|
||||
"current": Object {},
|
||||
},
|
||||
"validFieldsRef": Object {
|
||||
"current": Object {},
|
||||
},
|
||||
"validateResolver": undefined,
|
||||
"watchFieldsRef": Object {
|
||||
"current": Set {},
|
||||
},
|
||||
"watchInternal": [Function],
|
||||
}
|
||||
}
|
||||
defaultValue={false}
|
||||
key=".1"
|
||||
name="consentRequired"
|
||||
render={[Function]}
|
||||
>
|
||||
<Switch
|
||||
aria-label=""
|
||||
id="kc-consent"
|
||||
isChecked={false}
|
||||
isDisabled={false}
|
||||
label="on"
|
||||
labelOff="off"
|
||||
onChange={[Function]}
|
||||
value={false}
|
||||
>
|
||||
<label
|
||||
className="pf-c-switch"
|
||||
data-ouia-component-id="OUIA-Generated-Switch-1"
|
||||
data-ouia-component-type="PF4/Switch"
|
||||
data-ouia-safe={true}
|
||||
htmlFor="kc-consent"
|
||||
>
|
||||
<input
|
||||
aria-label=""
|
||||
aria-labelledby="kc-consent-on"
|
||||
checked={false}
|
||||
className="pf-c-switch__input"
|
||||
disabled={false}
|
||||
id="kc-consent"
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
value={false}
|
||||
/>
|
||||
<span
|
||||
className="pf-c-switch__toggle"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pf-c-switch__label pf-m-on"
|
||||
id="kc-consent-on"
|
||||
>
|
||||
on
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pf-c-switch__label pf-m-off"
|
||||
id="kc-consent-off"
|
||||
>
|
||||
off
|
||||
</span>
|
||||
</label>
|
||||
</Switch>
|
||||
</Controller>
|
||||
</form>
|
||||
</Form>
|
||||
</FormAccess>
|
||||
</AccessContextProvider>
|
||||
</Form>
|
||||
`;
|
|
@ -6,6 +6,7 @@ import {
|
|||
SplitItem,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
TextInputProps,
|
||||
} from "@patternfly/react-core";
|
||||
import { MinusIcon, PlusIcon } from "@patternfly/react-icons";
|
||||
|
||||
|
@ -23,12 +24,16 @@ export function toValue(formValue: MultiLine[]): string[] {
|
|||
return formValue.map((field) => field.value);
|
||||
}
|
||||
|
||||
export type MultiLineInputProps = {
|
||||
export type MultiLineInputProps = Omit<TextInputProps, "form"> & {
|
||||
form: UseFormMethods;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const MultiLineInput = ({ name, form }: MultiLineInputProps) => {
|
||||
export const MultiLineInput = ({
|
||||
name,
|
||||
form,
|
||||
...rest
|
||||
}: MultiLineInputProps) => {
|
||||
const { register, control } = form;
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name,
|
||||
|
@ -49,6 +54,7 @@ export const MultiLineInput = ({ name, form }: MultiLineInputProps) => {
|
|||
ref={register()}
|
||||
name={`${name}[${index}].value`}
|
||||
defaultValue={value}
|
||||
{...rest}
|
||||
/>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
|
@ -57,6 +63,7 @@ export const MultiLineInput = ({ name, form }: MultiLineInputProps) => {
|
|||
variant={ButtonVariant.link}
|
||||
onClick={() => append({})}
|
||||
tabIndex={-1}
|
||||
isDisabled={rest.isDisabled}
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
|
|
|
@ -20,7 +20,7 @@ test("can not create realm", () => {
|
|||
|
||||
test("getRealmAccess", () => {
|
||||
const whoami = new WhoAmI("master", whoamiMock);
|
||||
expect(Object.keys(whoami.getRealmAccess()).length).toEqual(2);
|
||||
expect(Object.keys(whoami.getRealmAccess()).length).toEqual(3);
|
||||
expect(whoami.getRealmAccess()["master"].length).toEqual(18);
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
"locale": "en",
|
||||
"createRealm": false,
|
||||
"realm_access": {
|
||||
"test": [
|
||||
"query-clients",
|
||||
"view-clients"
|
||||
],
|
||||
"aaa": [
|
||||
"view-identity-providers",
|
||||
"view-realm",
|
||||
|
|
|
@ -37,18 +37,18 @@ export const routes: RoutesFn = (t: TFunction) => [
|
|||
breadcrumb: t("realm:createRealm"),
|
||||
access: "manage-realm",
|
||||
},
|
||||
{
|
||||
path: "/clients",
|
||||
component: ClientsSection,
|
||||
breadcrumb: t("clients:clientList"),
|
||||
access: "query-clients",
|
||||
},
|
||||
{
|
||||
path: "/clients/:id",
|
||||
component: ClientSettings,
|
||||
breadcrumb: t("clients:clientSettings"),
|
||||
access: "view-clients",
|
||||
},
|
||||
{
|
||||
path: "/clients",
|
||||
component: ClientsSection,
|
||||
breadcrumb: t("clients:clientList"),
|
||||
access: "query-clients",
|
||||
},
|
||||
{
|
||||
path: "/add-client",
|
||||
component: NewClientForm,
|
||||
|
@ -61,12 +61,6 @@ export const routes: RoutesFn = (t: TFunction) => [
|
|||
breadcrumb: t("clients:importClient"),
|
||||
access: "manage-clients",
|
||||
},
|
||||
{
|
||||
path: "/client-scopes",
|
||||
component: ClientScopesSection,
|
||||
breadcrumb: t("client-scopes:clientScopeList"),
|
||||
access: "view-clients",
|
||||
},
|
||||
{
|
||||
path: "/client-scopes/new",
|
||||
component: ClientScopeForm,
|
||||
|
@ -91,6 +85,12 @@ export const routes: RoutesFn = (t: TFunction) => [
|
|||
breadcrumb: t("client-scopes:clientScopeDetails"),
|
||||
access: "view-clients",
|
||||
},
|
||||
{
|
||||
path: "/client-scopes",
|
||||
component: ClientScopesSection,
|
||||
breadcrumb: t("client-scopes:clientScopeList"),
|
||||
access: "view-clients",
|
||||
},
|
||||
{
|
||||
path: "/roles",
|
||||
component: RealmRolesSection,
|
||||
|
|
|
@ -13365,6 +13365,11 @@ multicast-dns@^6.0.1:
|
|||
dns-packet "^1.3.1"
|
||||
thunky "^1.0.2"
|
||||
|
||||
mutationobserver-shim@^0.3.7:
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/mutationobserver-shim/-/mutationobserver-shim-0.3.7.tgz#8bf633b0c0b0291a1107255ed32c13088a8c5bf3"
|
||||
integrity sha512-oRIDTyZQU96nAiz2AQyngwx1e89iApl2hN5AOYwyxLUB47UYsU3Wv9lJWqH5y/QdiYkc5HQLi23ZNB3fELdHcQ==
|
||||
|
||||
mute-stream@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||
|
@ -15237,7 +15242,7 @@ prepend-http@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||
|
||||
prettier@2.1.2:
|
||||
prettier@^2.0.5:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5"
|
||||
integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==
|
||||
|
|
Loading…
Reference in a new issue