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", () => {
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";
executeActionsSelectMenuList = "#kc-execute-actions-select-menu > div > ul";
private formViewProfilesView = "formView-profilesView";
private jsonEditorProfilesView = "jsonEditor-profilesView";
private createProfileBtn = "createProfile";
private formViewSelect = "formView-profilesView";
private jsonEditorSelect = "jsonEditor-profilesView";
@ -156,6 +158,10 @@ export default class RealmSettingsPage {
private deleteDialogTitle = ".pf-c-modal-box__title-text";
private deleteDialogBodyText = ".pf-c-modal-box__body";
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) {
cy.get(this.selectLoginTheme).click();
@ -519,7 +525,64 @@ export default class RealmSettingsPage {
cy.get(this.moreDrpDwn).last().click();
cy.get(this.moreDrpDwnItems).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("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,
AlertVariant,
Button,
ButtonVariant,
Divider,
DropdownItem,
Flex,
FlexItem,
FormGroup,
PageSection,
Text,
@ -16,7 +20,7 @@ import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { FormAccess } from "../components/form-access/FormAccess";
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 { useAlerts } from "../components/alert/Alerts";
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 { PlusCircleIcon } from "@patternfly/react-icons";
import "./RealmSettingsSection.css";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
type NewClientProfileForm = Required<ClientProfileRepresentation>;
const defaultValues: NewClientProfileForm = {
name: "",
executors: [],
description: "",
executors: [],
};
export const NewClientProfileForm = () => {
@ -46,6 +51,10 @@ export const NewClientProfileForm = () => {
>([]);
const [profiles, setProfiles] = useState<ClientProfileRepresentation[]>([]);
const [showAddExecutorsForm, setShowAddExecutorsForm] = useState(false);
const [createdProfile, setCreatedProfile] =
useState<ClientProfileRepresentation>();
const form = getValues();
const history = useHistory();
useFetch(
() =>
@ -77,14 +86,56 @@ export const NewClientProfileForm = () => {
AlertVariant.success
);
setShowAddExecutorsForm(true);
setCreatedProfile(createdProfile);
} catch (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 (
<>
<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">
<FormAccess isHorizontal role="view-realm" className="pf-u-mt-lg">
<FormGroup
@ -120,6 +171,7 @@ export const NewClientProfileForm = () => {
variant="primary"
onClick={save}
data-testid="saveCreateProfile"
isDisabled={showAddExecutorsForm ? true : false}
>
{t("common:save")}
</Button>
@ -133,41 +185,44 @@ export const NewClientProfileForm = () => {
)}
data-testid="cancelCreateProfile"
>
{t("common:cancel")}
{showAddExecutorsForm
? t("realm-settings:reload")
: t("common:cancel")}
</Button>
</ActionGroup>
{showAddExecutorsForm && (
<>
<FormGroup
label={t("executors")}
fieldId="kc-executors"
labelIcon={
<HelpItem
helpText={t("realm-settings:executorsHelpText")}
forLabel={t("executorsHelpItem")}
forID={t("executors")}
/>
}
>
<Button
id="addExecutor"
component={(props) => (
<Link
{...props}
to={`/${realm}/realm-settings/clientPolicies`}
></Link>
)}
variant="link"
className="kc-addExecutor"
data-testid="cancelCreateProfile"
icon={<PlusCircleIcon />}
isDisabled
>
{t("realm-settings:addExecutor")}
</Button>
</FormGroup>
<Flex>
<FlexItem>
<Text className="kc-executors" component={TextVariants.h1}>
{t("executors")}
<HelpItem
helpText={t("realm-settings:executorsHelpText")}
forLabel={t("executorsHelpItem")}
forID={t("executors")}
/>
</Text>
</FlexItem>
<FlexItem align={{ default: "alignRight" }}>
<Button
id="addExecutor"
component={(props) => (
<Link
{...props}
to={`/${realm}/realm-settings/clientPolicies`}
></Link>
)}
variant="link"
className="kc-addExecutor"
data-testid="cancelCreateProfile"
icon={<PlusCircleIcon />}
>
{t("realm-settings:addExecutor")}
</Button>
</FlexItem>
</Flex>
<Divider />
<Text component={TextVariants.h6}>
<Text className="kc-emptyExecutors" component={TextVariants.h6}>
{t("realm-settings:emptyExecutors")}
</Text>
</>

View file

@ -1,9 +1,11 @@
import React, { useMemo, useState } from "react";
import React, { useState } from "react";
import { omit } from "lodash";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
FormGroup,
Label,
PageSection,
Spinner,
@ -38,6 +40,7 @@ export const ProfilesTab = () => {
useState<ClientProfileRepresentation[]>();
const [selectedProfile, setSelectedProfile] = useState<ClientProfile>();
const [show, setShow] = useState(false);
const [code, setCode] = useState<string>();
const [key, setKey] = useState(0);
useFetch(
@ -62,16 +65,16 @@ export const ProfilesTab = () => {
const allClientProfiles = globalProfiles?.concat(profiles ?? []);
setTableProfiles(allClientProfiles || []);
setCode(JSON.stringify(allClientProfiles, null, 2));
},
[key]
);
const loader = async () => tableProfiles ?? [];
const code = useMemo(
() => JSON.stringify(tableProfiles, null, 2),
[tableProfiles]
);
const normalizeProfile = (
profile: ClientProfile
): ClientProfileRepresentation => omit(profile, "global");
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteClientProfileConfirmTitle"),
@ -83,7 +86,9 @@ export const ProfilesTab = () => {
?.filter(
(profile) => profile.name !== selectedProfile?.name && !profile.global
)
.map<ClientProfileRepresentation>((profile) => omit(profile, "global"));
.map<ClientProfileRepresentation>((profile) =>
normalizeProfile(profile)
);
try {
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 (
<>
<DeleteConfirm />
@ -195,26 +233,42 @@ export const ProfilesTab = () => {
}
/>
) : (
<>
<FormGroup fieldId={"jsonEditor"}>
<div className="pf-u-mt-md pf-u-ml-lg">
<CodeEditor
isLineNumbersVisible
isLanguageLabelVisible
isReadOnly={false}
code={code}
language={Language.json}
height="30rem"
onChange={(value) => {
setCode(value ?? "");
}}
/>
</div>
<div className="pf-u-mt-md">
<Button
variant={ButtonVariant.primary}
className="pf-u-mr-md pf-u-ml-lg"
>
{t("save")}
</Button>
<Button variant={ButtonVariant.link}> {t("reload")}</Button>
</div>
</>
<ActionGroup>
<div className="pf-u-mt-md">
<Button
variant={ButtonVariant.primary}
className="pf-u-mr-md pf-u-ml-lg"
onClick={save}
data-testid="jsonEditor-saveBtn"
>
{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);
}
.kc-addExecutor {
float: right;
.kc-emptyExecutors {
color: #8D9195;
}
.kc-action-dropdown {
background-color: transparent;
}

View file

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