added delete and cypress test

This commit is contained in:
Erik Jan de Wit 2021-03-05 14:47:59 +01:00
parent 5a8762b3fa
commit 949a0e198b
10 changed files with 176 additions and 58 deletions

View file

@ -6,6 +6,7 @@ import CreateClientPage from "../support/pages/admin_console/manage/clients/Crea
import ModalUtils from "../support/util/ModalUtils";
import AdvancedTab from "../support/pages/admin_console/manage/clients/AdvancedTab";
import AdminClient from "../support/util/AdminClient";
import InitialAccessTokenTab from "../support/pages/admin_console/manage/clients/InitialAccessTokenTab";
let itemId = "client_crud";
const loginPage = new LoginPage();
@ -78,6 +79,27 @@ describe("Clients test", function () {
listingPage.itemExist(itemId, false);
});
it("Initial access token", () => {
const initialAccessTokenTab = new InitialAccessTokenTab();
listingPage.goToInitialAccessTokenTab();
initialAccessTokenTab.shouldBeEmpty();
initialAccessTokenTab.createNewToken(1, 1).save();
modalUtils.checkModalTitle("Initial access token details").closeModal();
initialAccessTokenTab.shouldNotBeEmpty();
initialAccessTokenTab.getFistId((id) => {
listingPage.deleteItem(id);
modalUtils
.checkModalTitle("Delete initial access token?")
.confirmModal();
masthead.checkNotificationMessage(
"initial access token created successfully"
);
});
});
});
describe("Advanced tab test", () => {

View file

@ -7,6 +7,7 @@ export default class ListingPage {
searchBtn: string;
createBtn: string;
importBtn: string;
initialAccessTokenTab = "initialAccessToken";
constructor() {
this.searchInput = '.pf-c-toolbar__item [type="search"]';
@ -34,15 +35,20 @@ export default class ListingPage {
return this;
}
goToInitialAccessTokenTab() {
cy.getId(this.initialAccessTokenTab).click();
return this;
}
searchItem(searchValue: string, wait = true) {
if (wait) {
const searchUrl = `/admin/realms/master/*${searchValue}*`;
cy.intercept(searchUrl).as("searchClients");
cy.intercept(searchUrl).as("search");
}
cy.get(this.searchInput).type(searchValue);
cy.get(this.searchBtn).click();
if (wait) {
cy.wait(["@searchClients"]);
cy.wait(["@search"]);
}
return this;
}

View file

@ -0,0 +1,38 @@
export default class InitialAccessTokenTab {
private emptyAction = "empty-primary-action";
private expirationInput = "expiration";
private countInput = "count";
private saveBtn = "save";
shouldBeEmpty() {
cy.getId(this.emptyAction).should("exist");
return this;
}
shouldNotBeEmpty() {
cy.getId(this.emptyAction).should("not.exist");
return this;
}
getFistId(callback: (id: string) => void) {
cy.get('tbody > tr > [data-label="ID"]')
.invoke("text")
.then((text) => {
callback(text);
});
return this;
}
createNewToken(expiration: number, count: number) {
cy.getId(this.emptyAction).click();
cy.getId(this.expirationInput).type(`${expiration}`);
cy.getId(this.countInput).type(`${count}`);
return this;
}
save() {
cy.getId(this.saveBtn).click();
return this;
}
}

View file

@ -24,7 +24,7 @@
"@patternfly/react-table": "4.23.0",
"file-saver": "^2.0.2",
"i18next": "^19.6.2",
"keycloak-admin": "1.14.9",
"keycloak-admin": "1.14.10",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"react": "^16.8.5",

View file

@ -81,7 +81,11 @@ export const CreateInitialAccessToken = () => {
defaultValue=""
control={control}
render={({ onChange, value }) => (
<TimeSelector value={value} onChange={onChange} />
<TimeSelector
data-testid="expiration"
value={value}
onChange={onChange}
/>
)}
/>
</FormGroup>
@ -102,6 +106,7 @@ export const CreateInitialAccessToken = () => {
control={control}
render={({ onChange, value }) => (
<NumberInput
data-testid="count"
inputName="count"
inputAriaLabel={t("count")}
min={1}
@ -116,10 +121,11 @@ export const CreateInitialAccessToken = () => {
/>
</FormGroup>
<ActionGroup>
<Button variant="primary" type="submit">
<Button variant="primary" type="submit" data-testid="save">
{t("common:save")}
</Button>
<Button
data-testid="cancel"
variant="link"
onClick={() =>
history.push(`/${realm}/clients/initialAccessToken`)

View file

@ -1,70 +1,109 @@
import React from "react";
import React, { useState } from "react";
import { useHistory, useRouteMatch } from "react-router-dom";
import moment from "moment";
import { useTranslation } from "react-i18next";
import { Button } from "@patternfly/react-core";
import { AlertVariant, Button, ButtonVariant } from "@patternfly/react-core";
import ClientInitialAccessPresentation from "keycloak-admin/lib/defs/clientInitialAccessPresentation";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../../components/alert/Alerts";
export const InitialAccessTokenList = () => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const { realm } = useRealm();
const history = useHistory();
const { url } = useRouteMatch();
const [token, setToken] = useState<ClientInitialAccessPresentation>();
const loader = async () =>
await adminClient.realms.getClientsInitialAccess({ realm });
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "clients:tokenDeleteConfirmTitle",
messageKey: t("tokenDeleteConfirm", token),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.realms.delClientsInitialAccess({
realm,
id: token!.id!,
});
addAlert(t("tokenDeleteSuccess"), AlertVariant.success);
setToken(undefined);
} catch (error) {
addAlert(t("tokenDeleteError", { error }), AlertVariant.danger);
}
},
});
return (
<KeycloakDataTable
ariaLabelKey="clients:initialAccessToken"
searchPlaceholderKey="clients:searchInitialAccessToken"
loader={loader}
toolbarItem={
<>
<Button onClick={() => history.push(`${url}/create`)}>
{t("common:create")}
</Button>
</>
}
columns={[
{
name: "id",
displayKey: "clients:id",
},
{
name: "timestamp",
displayKey: "clients:timestamp",
cellRenderer: (row) => moment(row.timestamp! * 1000).format("LLL"),
},
{
name: "expiration",
displayKey: "clients:expires",
cellRenderer: (row) =>
moment(row.timestamp! * 1000 + row.expiration! * 1000).fromNow(),
},
{
name: "count",
displayKey: "clients:count",
},
{
name: "remainingCount",
displayKey: "clients:remainingCount",
},
]}
emptyState={
<ListEmptyState
message={t("noTokens")}
instructions={t("noTokensInstructions")}
primaryActionText={t("common:create")}
onPrimaryAction={() => history.push(`${url}/create`)}
/>
}
/>
<>
<DeleteConfirm />
<KeycloakDataTable
key={token?.id}
ariaLabelKey="clients:initialAccessToken"
searchPlaceholderKey="clients:searchInitialAccessToken"
loader={loader}
toolbarItem={
<>
<Button onClick={() => history.push(`${url}/create`)}>
{t("common:create")}
</Button>
</>
}
actions={[
{
title: t("common:delete"),
onRowClick: (token) => {
setToken(token);
toggleDeleteDialog();
},
},
]}
columns={[
{
name: "id",
displayKey: "clients:id",
},
{
name: "timestamp",
displayKey: "clients:timestamp",
cellRenderer: (row) => moment(row.timestamp! * 1000).format("LLL"),
},
{
name: "expiration",
displayKey: "clients:expires",
cellRenderer: (row) =>
moment(row.timestamp! * 1000 + row.expiration! * 1000).fromNow(),
},
{
name: "count",
displayKey: "clients:count",
},
{
name: "remainingCount",
displayKey: "clients:remainingCount",
},
]}
emptyState={
<ListEmptyState
message={t("noTokens")}
instructions={t("noTokensInstructions")}
primaryActionText={t("common:create")}
onPrimaryAction={() => history.push(`${url}/create`)}
/>
}
/>
</>
);
};

View file

@ -75,6 +75,10 @@
"clientDeleteConfirm": "If you delete this client, all associated data will be removed.",
"searchInitialAccessToken": "Search token",
"createToken": "Create initial access token",
"tokenDeleteConfirm": "Are you sure you want to permanently delete the initial access token {{id}}",
"tokenDeleteConfirmTitle": "Delete initial access token?",
"tokenDeleteSuccess": "initial access token created successfully",
"tokenDeleteError": "Could not delete initial access token: '{{error}}'",
"id": "ID",
"timestamp": "Created date",
"expires": "Expires",

View file

@ -5,13 +5,14 @@ import {
Split,
SplitItem,
TextInput,
TextInputProps,
} from "@patternfly/react-core";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export type Unit = "seconds" | "minutes" | "hours" | "days";
export type TimeSelectorProps = {
export type TimeSelectorProps = TextInputProps & {
value: number;
units?: Unit[];
onChange: (time: number | string) => void;
@ -21,6 +22,7 @@ export const TimeSelector = ({
value,
units = ["seconds", "minutes", "hours", "days"],
onChange,
...rest
}: TimeSelectorProps) => {
const { t } = useTranslation("common");
@ -73,6 +75,7 @@ export const TimeSelector = ({
<Split hasGutter>
<SplitItem>
<TextInput
{...rest}
type="number"
id={`kc-time-${new Date().getTime()}`}
min="0"

View file

@ -26,7 +26,7 @@ export const UsersInRoleTab = () => {
name: role.name!,
first: first!,
max: max!,
} as any);
});
return usersWithRole;
};

View file

@ -13477,10 +13477,10 @@ junk@^3.1.0:
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
keycloak-admin@1.14.9:
version "1.14.9"
resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.9.tgz#f068e8580714c3b92987901cfe6c83bfba669a2a"
integrity sha512-VaS6unTYLWuEkqb+FAY+c0Oo+ta505OxhOntCHLcgysMwRmL5pz6taVK8lUZ1ir/6QVto62adVQjmN5fMkpSWg==
keycloak-admin@1.14.10:
version "1.14.10"
resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.10.tgz#e44903826896262b3655303db46795b84a5f9b08"
integrity sha512-WhEA+FkcPikN/Oqh7L0puVkPU1cm3bB+15VOoPdESZknQ9poS0Ohz3Rg1flRfmMdqoMgcy+prigUPtHy6gOAUg==
dependencies:
axios "^0.21.0"
camelize "^1.0.0"