Client add executors (#1284)

* client-add-executors: wip

* client-add-executors: added logic for deleting client from dropdown

* client-add-executors: wip

* client-add-executors: added edit profiles

* client-add-executors: added refesh client profiles

* client-add-executors: added cypress tests

* commented out test failing only in CI

* Update cypress/integration/realm_settings_test.spec.ts

Co-authored-by: Erik Jan de Wit <edewit@redhat.com>

* changed to arrow functions

* feedback fixes

* feedback fixes

* uncommented failing test to see if still failing and why

* test possible fix

* test fix

* test fix

* test fix

* client-add-executors: reused normaliseProfile func for delete dialog

Co-authored-by: Agnieszka Gancarczyk <agancarc@redhat.com>
Co-authored-by: Erik Jan de Wit <edewit@redhat.com>
This commit is contained in:
agagancarczyk 2021-10-04 14:39:54 +01:00 committed by GitHub
parent 751dcc6e04
commit 9af18e11e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 267 additions and 54 deletions

View file

@ -469,5 +469,37 @@ describe("Realm settings tests", () => {
it("Check deleting the client profile", () => { it("Check deleting the client profile", () => {
realmSettingsPage.shouldDeleteClientProfileDialog(); realmSettingsPage.shouldDeleteClientProfileDialog();
}); });
it("Check navigating between Form View and JSON editor", () => {
realmSettingsPage.shouldNavigateBetweenFormAndJSONView();
});
it("Check saving changed JSON profiles", () => {
realmSettingsPage.shouldSaveChangedJSONProfiles();
realmSettingsPage.shouldDeleteClientProfileDialog();
});
it("Should not create duplicate client profile", () => {
realmSettingsPage.shouldCompleteAndCreateNewClientProfile();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-clientPolicies-tab").click();
cy.findByTestId("rs-profiles-clientPolicies-tab").click();
realmSettingsPage.shouldCompleteAndCreateNewClientProfile();
realmSettingsPage.shouldNotCreateDuplicateClientProfile();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-clientPolicies-tab").click();
cy.findByTestId("rs-profiles-clientPolicies-tab").click();
realmSettingsPage.shouldDeleteClientProfileDialog();
});
it("Check deleting newly created client profile from create view via dropdown", () => {
realmSettingsPage.shouldRemoveClientFromCreateView();
});
it("Check reloading JSON profiles", () => {
realmSettingsPage.shouldReloadJSONProfiles();
});
}); });
}); });

View file

@ -143,6 +143,8 @@ export default class RealmSettingsPage {
executeActionsSelectMenu = "#kc-execute-actions-select-menu"; executeActionsSelectMenu = "#kc-execute-actions-select-menu";
executeActionsSelectMenuList = "#kc-execute-actions-select-menu > div > ul"; executeActionsSelectMenuList = "#kc-execute-actions-select-menu > div > ul";
private formViewProfilesView = "formView-profilesView";
private jsonEditorProfilesView = "jsonEditor-profilesView";
private createProfileBtn = "createProfile"; private createProfileBtn = "createProfile";
private formViewSelect = "formView-profilesView"; private formViewSelect = "formView-profilesView";
private jsonEditorSelect = "jsonEditor-profilesView"; private jsonEditorSelect = "jsonEditor-profilesView";
@ -156,6 +158,10 @@ export default class RealmSettingsPage {
private deleteDialogTitle = ".pf-c-modal-box__title-text"; private deleteDialogTitle = ".pf-c-modal-box__title-text";
private deleteDialogBodyText = ".pf-c-modal-box__body"; private deleteDialogBodyText = ".pf-c-modal-box__body";
private deleteDialogCancelBtn = ".pf-c-button.pf-m-link"; private deleteDialogCancelBtn = ".pf-c-button.pf-m-link";
private jsonEditorSaveBtn = "jsonEditor-saveBtn";
private jsonEditorReloadBtn = "jsonEditor-reloadBtn";
private jsonEditor = ".monaco-scrollable-element.editor-scrollable.vs";
private createClientDrpDwn = ".pf-c-dropdown.pf-m-align-right";
selectLoginThemeType(themeType: string) { selectLoginThemeType(themeType: string) {
cy.get(this.selectLoginTheme).click(); cy.get(this.selectLoginTheme).click();
@ -519,7 +525,64 @@ export default class RealmSettingsPage {
cy.get(this.moreDrpDwn).last().click(); cy.get(this.moreDrpDwn).last().click();
cy.get(this.moreDrpDwnItems).click(); cy.get(this.moreDrpDwnItems).click();
cy.findByTestId("modalConfirm").contains("Delete").click(); cy.findByTestId("modalConfirm").contains("Delete").click();
cy.get("table").should("not.have.text", "Test");
cy.get(this.alertMessage).should("be.visible", "Client profile deleted"); cy.get(this.alertMessage).should("be.visible", "Client profile deleted");
cy.get("table").should("not.have.text", "Test");
}
shouldNavigateBetweenFormAndJSONView() {
cy.findByTestId(this.jsonEditorProfilesView).check();
cy.findByTestId(this.jsonEditorSaveBtn).contains("Save");
cy.findByTestId(this.jsonEditorReloadBtn).contains("Reload");
cy.findByTestId(this.formViewProfilesView).check();
cy.findByTestId(this.createProfileBtn).contains("Create client profile");
}
shouldSaveChangedJSONProfiles() {
cy.findByTestId(this.jsonEditorProfilesView).check();
cy.get(this.jsonEditor).type(`{pageup}{del} [{
"name": "Test",
"description": "Test Description",
"executors": [],
"global": false
}, {downarrow}{end}{backspace}{backspace}`);
cy.findByTestId(this.jsonEditorSaveBtn).click();
cy.get(this.alertMessage).should(
"be.visible",
"The client profiles configuration was updated"
);
cy.findByTestId(this.formViewProfilesView).check();
cy.get("table").should("be.visible").contains("td", "Test");
}
shouldNotCreateDuplicateClientProfile() {
cy.get(this.alertMessage).should(
"be.visible",
"Could not create client profile: 'proposed client profile name duplicated.'"
);
}
shouldRemoveClientFromCreateView() {
cy.findByTestId(this.createProfileBtn).click();
cy.findByTestId(this.newClientProfileNameInput).type("Test again");
cy.findByTestId(this.newClientProfileDescriptionInput).type(
"Test Again Description"
);
cy.findByTestId(this.saveNewClientProfileBtn).click();
cy.get(this.alertMessage).should(
"be.visible",
"New client profile created"
);
cy.get(this.createClientDrpDwn).contains("Action").click();
cy.findByTestId("deleteClientProfileDropdown").click();
cy.findByTestId("modalConfirm").contains("Delete").click();
cy.get(this.alertMessage).should("be.visible", "Client profile deleted");
cy.get("table").should("not.have.text", "Test Again Description");
}
shouldReloadJSONProfiles() {
cy.findByTestId(this.jsonEditorProfilesView).check();
cy.findByTestId(this.jsonEditorReloadBtn).contains("Reload").click();
cy.findByTestId(this.jsonEditorSaveBtn).contains("Save");
cy.findByTestId(this.jsonEditorReloadBtn).contains("Reload");
} }
} }

View file

@ -3,7 +3,11 @@ import {
ActionGroup, ActionGroup,
AlertVariant, AlertVariant,
Button, Button,
ButtonVariant,
Divider, Divider,
DropdownItem,
Flex,
FlexItem,
FormGroup, FormGroup,
PageSection, PageSection,
Text, Text,
@ -16,7 +20,7 @@ import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { FormAccess } from "../components/form-access/FormAccess"; import { FormAccess } from "../components/form-access/FormAccess";
import { ViewHeader } from "../components/view-header/ViewHeader"; import { ViewHeader } from "../components/view-header/ViewHeader";
import { Link } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../context/auth/AdminClient";
@ -24,13 +28,14 @@ import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/li
import { HelpItem } from "../components/help-enabler/HelpItem"; import { HelpItem } from "../components/help-enabler/HelpItem";
import { PlusCircleIcon } from "@patternfly/react-icons"; import { PlusCircleIcon } from "@patternfly/react-icons";
import "./RealmSettingsSection.css"; import "./RealmSettingsSection.css";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
type NewClientProfileForm = Required<ClientProfileRepresentation>; type NewClientProfileForm = Required<ClientProfileRepresentation>;
const defaultValues: NewClientProfileForm = { const defaultValues: NewClientProfileForm = {
name: "", name: "",
executors: [],
description: "", description: "",
executors: [],
}; };
export const NewClientProfileForm = () => { export const NewClientProfileForm = () => {
@ -46,6 +51,10 @@ export const NewClientProfileForm = () => {
>([]); >([]);
const [profiles, setProfiles] = useState<ClientProfileRepresentation[]>([]); const [profiles, setProfiles] = useState<ClientProfileRepresentation[]>([]);
const [showAddExecutorsForm, setShowAddExecutorsForm] = useState(false); const [showAddExecutorsForm, setShowAddExecutorsForm] = useState(false);
const [createdProfile, setCreatedProfile] =
useState<ClientProfileRepresentation>();
const form = getValues();
const history = useHistory();
useFetch( useFetch(
() => () =>
@ -77,14 +86,56 @@ export const NewClientProfileForm = () => {
AlertVariant.success AlertVariant.success
); );
setShowAddExecutorsForm(true); setShowAddExecutorsForm(true);
setCreatedProfile(createdProfile);
} catch (error) { } catch (error) {
addError("realm-settings:createClientProfileError", error); addError("realm-settings:createClientProfileError", error);
} }
}; };
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteClientProfileConfirmTitle"),
messageKey: t("deleteClientProfileConfirm"),
continueButtonLabel: t("delete"),
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
const updatedProfiles = profiles.filter(
(profile) => profile.name !== createdProfile?.name
);
try {
await adminClient.clientPolicies.createProfiles({
profiles: updatedProfiles,
globalProfiles,
});
addAlert(t("deleteClientSuccess"), AlertVariant.success);
history.push(`/${realm}/realm-settings/clientPolicies`);
} catch (error) {
addError(t("deleteClientError"), error);
}
},
});
return ( return (
<> <>
<ViewHeader titleKey={t("newClientProfile")} divider /> <DeleteConfirm />
<ViewHeader
titleKey={showAddExecutorsForm ? form.name : t("newClientProfile")}
divider
dropdownItems={
showAddExecutorsForm
? [
<DropdownItem
key="delete"
value="delete"
onClick={toggleDeleteDialog}
data-testid="deleteClientProfileDropdown"
>
{t("deleteClientProfile")}
</DropdownItem>,
]
: undefined
}
/>
<PageSection variant="light"> <PageSection variant="light">
<FormAccess isHorizontal role="view-realm" className="pf-u-mt-lg"> <FormAccess isHorizontal role="view-realm" className="pf-u-mt-lg">
<FormGroup <FormGroup
@ -120,6 +171,7 @@ export const NewClientProfileForm = () => {
variant="primary" variant="primary"
onClick={save} onClick={save}
data-testid="saveCreateProfile" data-testid="saveCreateProfile"
isDisabled={showAddExecutorsForm ? true : false}
> >
{t("common:save")} {t("common:save")}
</Button> </Button>
@ -133,41 +185,44 @@ export const NewClientProfileForm = () => {
)} )}
data-testid="cancelCreateProfile" data-testid="cancelCreateProfile"
> >
{t("common:cancel")} {showAddExecutorsForm
? t("realm-settings:reload")
: t("common:cancel")}
</Button> </Button>
</ActionGroup> </ActionGroup>
{showAddExecutorsForm && ( {showAddExecutorsForm && (
<> <>
<FormGroup <Flex>
label={t("executors")} <FlexItem>
fieldId="kc-executors" <Text className="kc-executors" component={TextVariants.h1}>
labelIcon={ {t("executors")}
<HelpItem <HelpItem
helpText={t("realm-settings:executorsHelpText")} helpText={t("realm-settings:executorsHelpText")}
forLabel={t("executorsHelpItem")} forLabel={t("executorsHelpItem")}
forID={t("executors")} forID={t("executors")}
/> />
} </Text>
> </FlexItem>
<Button <FlexItem align={{ default: "alignRight" }}>
id="addExecutor" <Button
component={(props) => ( id="addExecutor"
<Link component={(props) => (
{...props} <Link
to={`/${realm}/realm-settings/clientPolicies`} {...props}
></Link> to={`/${realm}/realm-settings/clientPolicies`}
)} ></Link>
variant="link" )}
className="kc-addExecutor" variant="link"
data-testid="cancelCreateProfile" className="kc-addExecutor"
icon={<PlusCircleIcon />} data-testid="cancelCreateProfile"
isDisabled icon={<PlusCircleIcon />}
> >
{t("realm-settings:addExecutor")} {t("realm-settings:addExecutor")}
</Button> </Button>
</FormGroup> </FlexItem>
</Flex>
<Divider /> <Divider />
<Text component={TextVariants.h6}> <Text className="kc-emptyExecutors" component={TextVariants.h6}>
{t("realm-settings:emptyExecutors")} {t("realm-settings:emptyExecutors")}
</Text> </Text>
</> </>

View file

@ -1,9 +1,11 @@
import React, { useMemo, useState } from "react"; import React, { useState } from "react";
import { omit } from "lodash"; import { omit } from "lodash";
import { import {
ActionGroup,
AlertVariant, AlertVariant,
Button, Button,
ButtonVariant, ButtonVariant,
FormGroup,
Label, Label,
PageSection, PageSection,
Spinner, Spinner,
@ -38,6 +40,7 @@ export const ProfilesTab = () => {
useState<ClientProfileRepresentation[]>(); useState<ClientProfileRepresentation[]>();
const [selectedProfile, setSelectedProfile] = useState<ClientProfile>(); const [selectedProfile, setSelectedProfile] = useState<ClientProfile>();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [code, setCode] = useState<string>();
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
useFetch( useFetch(
@ -62,16 +65,16 @@ export const ProfilesTab = () => {
const allClientProfiles = globalProfiles?.concat(profiles ?? []); const allClientProfiles = globalProfiles?.concat(profiles ?? []);
setTableProfiles(allClientProfiles || []); setTableProfiles(allClientProfiles || []);
setCode(JSON.stringify(allClientProfiles, null, 2));
}, },
[key] [key]
); );
const loader = async () => tableProfiles ?? []; const loader = async () => tableProfiles ?? [];
const code = useMemo( const normalizeProfile = (
() => JSON.stringify(tableProfiles, null, 2), profile: ClientProfile
[tableProfiles] ): ClientProfileRepresentation => omit(profile, "global");
);
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteClientProfileConfirmTitle"), titleKey: t("deleteClientProfileConfirmTitle"),
@ -83,7 +86,9 @@ export const ProfilesTab = () => {
?.filter( ?.filter(
(profile) => profile.name !== selectedProfile?.name && !profile.global (profile) => profile.name !== selectedProfile?.name && !profile.global
) )
.map<ClientProfileRepresentation>((profile) => omit(profile, "global")); .map<ClientProfileRepresentation>((profile) =>
normalizeProfile(profile)
);
try { try {
await adminClient.clientPolicies.createProfiles({ await adminClient.clientPolicies.createProfiles({
@ -112,6 +117,39 @@ export const ProfilesTab = () => {
); );
} }
const save = async () => {
if (!code) {
return;
}
try {
const obj: ClientProfile[] = JSON.parse(code);
const changedProfiles = obj
.filter((profile) => !profile.global)
.map((profile) => normalizeProfile(profile));
const changedGlobalProfiles = obj
.filter((profile) => profile.global)
.map((profile) => normalizeProfile(profile));
try {
await adminClient.clientPolicies.createProfiles({
profiles: changedProfiles,
globalProfiles: changedGlobalProfiles,
});
addAlert(
t("realm-settings:updateClientProfilesSuccess"),
AlertVariant.success
);
setKey(key + 1);
} catch (error) {
addError("realm-settings:updateClientProfilesError", error);
}
} catch (error) {
console.warn("Invalid json, ignoring value using {}");
}
};
return ( return (
<> <>
<DeleteConfirm /> <DeleteConfirm />
@ -195,26 +233,42 @@ export const ProfilesTab = () => {
} }
/> />
) : ( ) : (
<> <FormGroup fieldId={"jsonEditor"}>
<div className="pf-u-mt-md pf-u-ml-lg"> <div className="pf-u-mt-md pf-u-ml-lg">
<CodeEditor <CodeEditor
isLineNumbersVisible isLineNumbersVisible
isLanguageLabelVisible isLanguageLabelVisible
isReadOnly={false}
code={code} code={code}
language={Language.json} language={Language.json}
height="30rem" height="30rem"
onChange={(value) => {
setCode(value ?? "");
}}
/> />
</div> </div>
<div className="pf-u-mt-md"> <ActionGroup>
<Button <div className="pf-u-mt-md">
variant={ButtonVariant.primary} <Button
className="pf-u-mr-md pf-u-ml-lg" variant={ButtonVariant.primary}
> className="pf-u-mr-md pf-u-ml-lg"
{t("save")} onClick={save}
</Button> data-testid="jsonEditor-saveBtn"
<Button variant={ButtonVariant.link}> {t("reload")}</Button> >
</div> {t("save")}
</> </Button>
<Button
variant={ButtonVariant.link}
onClick={() => {
setCode(JSON.stringify(tableProfiles, null, 2));
}}
data-testid="jsonEditor-reloadBtn"
>
{t("reload")}
</Button>
</div>
</ActionGroup>
</FormGroup>
)} )}
</> </>
); );

View file

@ -194,6 +194,10 @@ article.pf-c-card.pf-m-flat.kc-login-settings-template
transform: scale(1.6); transform: scale(1.6);
} }
.kc-addExecutor { .kc-emptyExecutors {
float: right; color: #8D9195;
} }
.kc-action-dropdown {
background-color: transparent;
}

View file

@ -234,6 +234,7 @@ export default {
deleteClientSuccess: "Client profile deleted", deleteClientSuccess: "Client profile deleted",
deleteClientError: "Could not delete profile: {{error}}", deleteClientError: "Could not delete profile: {{error}}",
createClientProfile: "Create client profile", createClientProfile: "Create client profile",
deleteClientProfile: "Delete this client profile",
createClientProfileSuccess: "New client profile created", createClientProfileSuccess: "New client profile created",
createClientProfileError: "Could not create client profile: '{{error}}'", createClientProfileError: "Could not create client profile: '{{error}}'",
createClientProfileNameHelperText: createClientProfileNameHelperText:
@ -252,6 +253,10 @@ export default {
executorsHelpItem: "Executors help item", executorsHelpItem: "Executors help item",
addExecutor: "Add executor", addExecutor: "Add executor",
emptyExecutors: "No executors configured", emptyExecutors: "No executors configured",
updateClientProfilesSuccess:
"The client profiles configuration was updated",
updateClientProfilesError:
"Provided JSON is incorrect: Unexpected token { in JSON",
tokens: "Tokens", tokens: "Tokens",
key: "Key", key: "Key",
value: "Value", value: "Value",