Complete partial import functionality. (#1644)

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Stan Silvert 2021-12-10 05:53:21 -05:00 committed by GitHub
parent 242c1d8445
commit 6250ccdaef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 423 additions and 263 deletions

View file

@ -74,8 +74,6 @@
}
],
"client": {
"database-service": [],
"admin-client": [],
"realm-management": [
{
"id": "3b939f75-d013-4096-8462-48aa39261293",
@ -101,7 +99,7 @@
"secret": "password"
},
{
"clientId": "customer-portal",
"clientId": "customer-portal2",
"enabled": true,
"adminUrl": "/customer-portal",
"baseUrl": "/customer-portal",

View file

@ -59,12 +59,12 @@ describe("Partial import test", () => {
modal.importButton().should("be.disabled");
// verify resource counts
modal.userCount().contains("1 users");
modal.groupCount().contains("1 groups");
modal.clientCount().contains("1 clients");
modal.idpCount().contains("1 identity providers");
modal.realmRolesCount().contains("2 realm roles");
modal.clientRolesCount().contains("1 client roles");
modal.userCount().contains("1 Users");
modal.groupCount().contains("1 Groups");
modal.clientCount().contains("1 Clients");
modal.idpCount().contains("1 Identity providers");
modal.realmRolesCount().contains("2 Realm roles");
modal.clientRolesCount().contains("1 Client roles");
// import button should disable when switching realms
modal.usersCheckbox().click();
@ -72,23 +72,36 @@ describe("Partial import test", () => {
modal.selectRealm("realm2");
modal.importButton().should("be.disabled");
modal.clientCount().contains("2 clients");
modal.clientCount().contains("2 Clients");
modal.clientsCheckbox().click();
modal.importButton().click();
cy.contains("2 records added");
cy.contains("customer-portal");
cy.contains("customer-portal2");
});
it("Displays user options after realmless import", () => {
it("Displays user options after realmless import and does the import", () => {
modal.open();
modal.typeResourceFile("client-only.json");
modal.realmSelector().should("not.exist");
modal.clientCount().contains("1 clients");
modal.clientCount().contains("1 Clients");
modal.userCount().should("not.exist");
modal.groupCount().should("not.exist");
modal.idpCount().should("not.exist");
modal.realmRolesCount().should("not.exist");
modal.clientRolesCount().should("not.exist");
modal.usersCheckbox().should("not.exist");
modal.groupsCheckbox().should("not.exist");
modal.idpCheckbox().should("not.exist");
modal.realmRolesCheckbox().should("not.exist");
modal.clientRolesCheckbox().should("not.exist");
modal.clientsCheckbox().click();
modal.importButton().click();
cy.contains("One record added");
cy.contains("customer-portal");
});
// Unfortunately, the PatternFly FileUpload component does not create an id for the clear button. So we can't easily test that function right now.

View file

@ -30,36 +30,52 @@ export default class GroupModal {
return cy.findByTestId("cancel-button");
}
groupsCheckbox() {
return cy.findByTestId("groups-checkbox");
}
usersCheckbox() {
return cy.findByTestId("users-checkbox");
}
clientsCheckbox() {
return cy.findByTestId("clients-checkbox");
}
groupsCheckbox() {
return cy.findByTestId("groups-checkbox");
}
idpCheckbox() {
return cy.findByTestId("identityProviders-checkbox");
}
realmRolesCheckbox() {
return cy.findByTestId("realmRoles-checkbox");
}
clientRolesCheckbox() {
return cy.findByTestId("clientRoles-checkbox");
}
userCount() {
return cy.findByTestId("users-count");
return this.usersCheckbox().get("label");
}
clientCount() {
return cy.findByTestId("clients-count");
return this.clientsCheckbox().get("label");
}
groupCount() {
return cy.findByTestId("groups-count");
return this.groupsCheckbox().get("label");
}
idpCount() {
return cy.findByTestId("identityProviders-count");
return this.idpCheckbox().get("label");
}
realmRolesCount() {
return cy.findByTestId("realmRoles-count");
return this.realmRolesCheckbox().get("label");
}
clientRolesCount() {
return cy.findByTestId("clientRoles-count");
return this.clientRolesCheckbox().get("label");
}
realmSelector() {

14
package-lock.json generated
View file

@ -7,7 +7,7 @@
"name": "keycloak-admin-ui",
"license": "Apache",
"dependencies": {
"@keycloak/keycloak-admin-client": "^16.0.0-dev.59",
"@keycloak/keycloak-admin-client": "^16.0.0-dev.61",
"@patternfly/patternfly": "^4.159.1",
"@patternfly/react-code-editor": "^4.16.4",
"@patternfly/react-core": "^4.175.4",
@ -3407,9 +3407,9 @@
}
},
"node_modules/@keycloak/keycloak-admin-client": {
"version": "16.0.0-dev.59",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.59.tgz",
"integrity": "sha512-ygDXfVh7MRGbWNA/8zloWh5ULqhukZ+dhptGKuLmN1kxirzsc0P9//96/EYI3FX9rf+xiuF575dkOsR6sQx5Eg==",
"version": "16.0.0-dev.61",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.61.tgz",
"integrity": "sha512-xeJTOQevOeHe8bQfu3/6Y9Lsort3Ep/VgzieUKmBHdR25kkMyQEUtZGX/7nQKYofjONG2m/KWivNwf0OZp+rhg==",
"dependencies": {
"axios": "^0.24.0",
"camelize-ts": "^1.0.8",
@ -24119,9 +24119,9 @@
}
},
"@keycloak/keycloak-admin-client": {
"version": "16.0.0-dev.59",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.59.tgz",
"integrity": "sha512-ygDXfVh7MRGbWNA/8zloWh5ULqhukZ+dhptGKuLmN1kxirzsc0P9//96/EYI3FX9rf+xiuF575dkOsR6sQx5Eg==",
"version": "16.0.0-dev.61",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.61.tgz",
"integrity": "sha512-xeJTOQevOeHe8bQfu3/6Y9Lsort3Ep/VgzieUKmBHdR25kkMyQEUtZGX/7nQKYofjONG2m/KWivNwf0OZp+rhg==",
"requires": {
"axios": "^0.24.0",
"camelize-ts": "^1.0.8",

View file

@ -23,7 +23,7 @@
"prepare": "husky install"
},
"dependencies": {
"@keycloak/keycloak-admin-client": "^16.0.0-dev.59",
"@keycloak/keycloak-admin-client": "^16.0.0-dev.61",
"@patternfly/patternfly": "^4.159.1",
"@patternfly/react-code-editor": "^4.16.4",
"@patternfly/react-core": "^4.175.4",

View file

@ -81,6 +81,7 @@ export default {
clients: "Clients",
clientScopes: "Client scopes",
realmRoles: "Realm roles",
clientRoles: "Client roles",
users: "Users",
groups: "Groups",
sessions: "Sessions",

View file

@ -1,16 +1,17 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import _ from "lodash";
import {
Alert,
Button,
ButtonVariant,
Checkbox,
DataList,
DataListCell,
DataListItem,
DataListItemCells,
DataListItemRow,
DataListCheck,
Divider,
Label,
Modal,
ModalVariant,
Select,
@ -22,7 +23,20 @@ import {
TextContent,
} from "@patternfly/react-core";
import { useAlerts } from "../components/alert/Alerts";
import { JsonFileUpload } from "../components/json-file-upload/JsonFileUpload";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAdminClient } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import type {
PartialImportRealmRepresentation,
PartialImportResponse,
PartialImportResult,
} from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
export type PartialImportProps = {
open: boolean;
@ -31,22 +45,7 @@ export type PartialImportProps = {
// An imported JSON file can either be an array of realm objects
// or a single realm object.
type ImportedMultiRealm = [ImportedRealm?] | ImportedRealm;
// Realms in imported json can have a lot more properties,
// but these are the ones we care about.
type ImportedRealm = {
id?: string;
realm?: string;
users?: [];
clients?: [];
groups?: [];
identityProviders?: [];
roles?: {
realm?: [];
client?: { [index: string]: [] };
};
};
type ImportedMultiRealm = RealmRepresentation | RealmRepresentation[];
type NonRoleResource = "users" | "clients" | "groups" | "identityProviders";
type RoleResource = "realmRoles" | "clientRoles";
@ -56,42 +55,43 @@ type CollisionOption = "FAIL" | "SKIP" | "OVERWRITE";
type ResourceChecked = { [k in Resource]: boolean };
const INITIAL_RESOURCES: Readonly<ResourceChecked> = {
users: false,
clients: false,
groups: false,
identityProviders: false,
realmRoles: false,
clientRoles: false,
};
export const PartialImportDialog = (props: PartialImportProps) => {
const tRealm = useTranslation("realm-settings").t;
const { t } = useTranslation("partial-import");
const adminClient = useAdminClient();
const { realm } = useRealm();
const [isFileSelected, setIsFileSelected] = useState(false);
const [isMultiRealm, setIsMultiRealm] = useState(false);
const [importedFile, setImportedFile] = useState<ImportedMultiRealm>([]);
const [importedFile, setImportedFile] = useState<ImportedMultiRealm>();
const isFileSelected = !!importedFile;
const [isRealmSelectOpen, setIsRealmSelectOpen] = useState(false);
const [isCollisionSelectOpen, setIsCollisionSelectOpen] = useState(false);
const [importInProgress, setImportInProgress] = useState(false);
const [collisionOption, setCollisionOption] =
useState<CollisionOption>("FAIL");
const [targetRealm, setTargetRealm] = useState<ImportedRealm>({});
const [targetRealm, setTargetRealm] = useState<RealmRepresentation>({});
const [importResponse, setImportResponse] = useState<PartialImportResponse>();
const { addError } = useAlerts();
const allResourcesUnChecked: Readonly<ResourceChecked> = {
users: false,
clients: false,
groups: false,
identityProviders: false,
realmRoles: false,
clientRoles: false,
};
const [resourcesToImport, setResourcesToImport] = useState<ResourceChecked>(
_.cloneDeep(allResourcesUnChecked)
const [resourcesToImport, setResourcesToImport] = useState(INITIAL_RESOURCES);
const isAnyResourceChecked = Object.values(resourcesToImport).some(
(checked) => checked
);
const [isAnyResourceChecked, setIsAnyResourceChecked] = useState(false);
const resetResourcesToImport = () => {
setResourcesToImport(_.cloneDeep(allResourcesUnChecked));
setIsAnyResourceChecked(false);
setResourcesToImport(INITIAL_RESOURCES);
};
const resetInputState = () => {
setIsMultiRealm(false);
setImportedFile([]);
setImportedFile(undefined);
setTargetRealm({});
setCollisionOption("FAIL");
resetResourcesToImport();
@ -99,30 +99,24 @@ export const PartialImportDialog = (props: PartialImportProps) => {
// when dialog opens or closes, clear state
useEffect(() => {
setIsFileSelected(false);
setImportInProgress(false);
setImportResponse(undefined);
resetInputState();
}, [props.open]);
const handleFileChange = (value: object) => {
setIsFileSelected(!!value);
const handleFileChange = (value: ImportedMultiRealm) => {
resetInputState();
setImportedFile(value);
if (value instanceof Array && value.length > 0) {
setIsMultiRealm(value.length > 1);
setTargetRealm(value[0] || {});
} else {
setIsMultiRealm(false);
setTargetRealm((value as ImportedRealm) || {});
if (!Array.isArray(value)) {
setTargetRealm(value);
} else if (value.length > 0) {
setTargetRealm(value[0]);
}
};
const handleRealmSelect = (
event: React.ChangeEvent<Element> | React.MouseEvent<Element, MouseEvent>,
realm: string | SelectOptionObject
) => {
setTargetRealm(realm as ImportedRealm);
const handleRealmSelect = (realm: string | SelectOptionObject) => {
setTargetRealm(realm as RealmRepresentation);
setIsRealmSelectOpen(false);
resetResourcesToImport();
};
@ -131,30 +125,24 @@ export const PartialImportDialog = (props: PartialImportProps) => {
checked: boolean,
event: React.FormEvent<HTMLInputElement>
) => {
const resource: Resource = event.currentTarget.name as Resource;
const copyOfResourcesToImport = _.cloneDeep(resourcesToImport);
copyOfResourcesToImport[resource] = checked;
setResourcesToImport(copyOfResourcesToImport);
setIsAnyResourceChecked(resourcesChecked(copyOfResourcesToImport));
const resource = event.currentTarget.name as Resource;
setResourcesToImport({
...resourcesToImport,
[resource]: checked,
});
};
const realmSelectOptions = () => {
if (!isMultiRealm) return [];
const mapper = (realm: ImportedRealm) => {
return (
<SelectOption
key={realm.id}
value={realm}
data-testid={realm.id + "-select-option"}
>
{realm.realm || realm.id}
</SelectOption>
);
};
return (importedFile as [ImportedRealm]).map(mapper);
};
const realmSelectOptions = (realms: RealmRepresentation[]) =>
realms.map((realm) => (
<SelectOption
key={realm.id}
value={realm}
data-testid={realm.id + "-select-option"}
>
{realm.realm || realm.id}
</SelectOption>
));
const handleCollisionSelect = (
event: React.ChangeEvent<Element> | React.MouseEvent<Element, MouseEvent>,
@ -190,66 +178,41 @@ export const PartialImportDialog = (props: PartialImportProps) => {
};
const targetHasResource = (resource: NonRoleResource) => {
return (
targetRealm &&
targetRealm[resource] instanceof Array &&
targetRealm[resource]!.length > 0
);
};
const targetHasRoles = () => {
return (
targetRealm && Object.prototype.hasOwnProperty.call(targetRealm, "roles")
);
const value = targetRealm[resource];
return value !== undefined && value.length > 0;
};
const targetHasRealmRoles = () => {
return (
targetHasRoles() &&
targetRealm.roles!.realm instanceof Array &&
targetRealm.roles!.realm.length > 0
);
const value = targetRealm.roles?.realm;
return value !== undefined && value.length > 0;
};
const targetHasClientRoles = () => {
return (
targetHasRoles() &&
Object.prototype.hasOwnProperty.call(targetRealm.roles, "client") &&
Object.keys(targetRealm.roles!.client!).length > 0
);
const value = targetRealm.roles?.client;
return value !== undefined && Object.keys(value).length > 0;
};
const itemCount = (resource: Resource) => {
if (!isFileSelected) return 0;
if (targetHasRealmRoles() && resource === "realmRoles")
return targetRealm.roles!.realm!.length;
if (targetHasClientRoles() && resource == "clientRoles")
return clientRolesCount(targetRealm.roles!.client!);
if (!targetRealm[resource as NonRoleResource]) return 0;
return targetRealm[resource as NonRoleResource]!.length;
};
const clientRolesCount = (clientRoles: { [index: string]: [] }) => {
let total = 0;
for (const clientName in clientRoles) {
total += clientRoles[clientName].length;
}
return total;
};
const resourcesChecked = (resources: ResourceChecked) => {
let resource: Resource;
for (resource in resources) {
if (resources[resource]) return true;
if (resource === "realmRoles") {
return targetRealm.roles?.realm?.length ?? 0;
}
return false;
if (resource === "clientRoles") {
return targetHasClientRoles()
? clientRolesCount(targetRealm.roles!.client!)
: 0;
}
return targetRealm[resource]?.length ?? 0;
};
const clientRolesCount = (
clientRoles: Record<string, RoleRepresentation[]>
) =>
Object.values(clientRoles).reduce((total, role) => total + role.length, 0);
const resourceDataListItem = (
resource: Resource,
resourceDisplayName: string
@ -257,19 +220,18 @@ export const PartialImportDialog = (props: PartialImportProps) => {
return (
<DataListItem aria-labelledby={`${resource}-list-item`}>
<DataListItemRow>
<DataListCheck
aria-labelledby={`${resource}-checkbox`}
name={resource}
isChecked={resourcesToImport[resource]}
onChange={handleResourceCheckBox}
data-testid={resource + "-checkbox"}
/>
<DataListItemCells
dataListCells={[
<DataListCell key={resource}>
<span data-testid={resource + "-count"}>
{itemCount(resource)} {resourceDisplayName}
</span>
<Checkbox
id={`${resource}-checkbox`}
label={`${itemCount(resource)} ${resourceDisplayName}`}
aria-labelledby={`${resource}-checkbox`}
name={resource}
isChecked={resourcesToImport[resource]}
onChange={handleResourceCheckBox}
data-testid={resource + "-checkbox"}
/>
</DataListCell>,
]}
/>
@ -278,107 +240,263 @@ export const PartialImportDialog = (props: PartialImportProps) => {
);
};
return (
<Modal
variant={ModalVariant.medium}
title={tRealm("partialImport")}
isOpen={props.open}
onClose={props.toggleDialog}
actions={[
<Button
id="modal-import"
data-testid="import-button"
key="import"
isDisabled={!isAnyResourceChecked}
onClick={() => {
props.toggleDialog();
}}
>
{t("import")}
</Button>,
<Button
id="modal-cancel"
data-testid="cancel-button"
key="cancel"
variant={ButtonVariant.link}
onClick={() => {
props.toggleDialog();
}}
>
{t("common:cancel")}
</Button>,
]}
>
<Stack hasGutter>
<StackItem>
<TextContent>
<Text>{t("partialImportHeaderText")}</Text>
</TextContent>
</StackItem>
<StackItem>
<JsonFileUpload
id="partial-import-file"
allowEditingUploadedText
onChange={handleFileChange}
/>
</StackItem>
const jsonForImport = () => {
const jsonToImport: PartialImportRealmRepresentation = {
ifResourceExists: collisionOption,
id: targetRealm.id,
realm: targetRealm.realm,
};
{isFileSelected && targetHasResources() && (
<>
<StackItem>
<Divider />
</StackItem>
{isMultiRealm && (
if (resourcesToImport["users"]) jsonToImport.users = targetRealm.users;
if (resourcesToImport["groups"]) jsonToImport.groups = targetRealm.groups;
if (resourcesToImport["identityProviders"])
jsonToImport.identityProviders = targetRealm.identityProviders;
if (resourcesToImport["clients"])
jsonToImport.clients = targetRealm.clients;
if (resourcesToImport["realmRoles"] || resourcesToImport["clientRoles"]) {
jsonToImport.roles = targetRealm.roles;
if (!resourcesToImport["realmRoles"]) delete jsonToImport.roles?.realm;
if (!resourcesToImport["clientRoles"]) delete jsonToImport.roles?.client;
}
return jsonToImport;
};
async function doImport() {
if (importInProgress) return;
setImportInProgress(true);
try {
const importResults = await adminClient.realms.partialImport({
realm,
rep: jsonForImport(),
});
setImportResponse(importResults);
} catch (error) {
addError("partial-import:importFail", error);
}
setImportInProgress(false);
}
const importModal = () => {
return (
<Modal
variant={ModalVariant.medium}
title={tRealm("partialImport")}
isOpen={props.open}
onClose={props.toggleDialog}
actions={[
<Button
id="modal-import"
data-testid="import-button"
key="import"
isDisabled={!isAnyResourceChecked}
onClick={() => {
doImport();
}}
>
{t("import")}
</Button>,
<Button
id="modal-cancel"
data-testid="cancel-button"
key="cancel"
variant={ButtonVariant.link}
onClick={() => {
props.toggleDialog();
}}
>
{t("common:cancel")}
</Button>,
]}
>
<Stack hasGutter>
<StackItem>
<TextContent>
<Text>{t("partialImportHeaderText")}</Text>
</TextContent>
</StackItem>
<StackItem>
<JsonFileUpload
id="partial-import-file"
allowEditingUploadedText
onChange={handleFileChange}
/>
</StackItem>
{isFileSelected && targetHasResources() && (
<>
<StackItem>
<Text>{t("selectRealm")}:</Text>
<Divider />
</StackItem>
{Array.isArray(importedFile) && importedFile.length > 1 && (
<StackItem>
<Text>{t("selectRealm")}:</Text>
<Select
toggleId="realm-selector"
isOpen={isRealmSelectOpen}
onToggle={() => setIsRealmSelectOpen(!isRealmSelectOpen)}
onSelect={(_, value) => handleRealmSelect(value)}
placeholderText={targetRealm.realm || targetRealm.id}
>
{realmSelectOptions(importedFile)}
</Select>
</StackItem>
)}
<StackItem>
<Text>{t("chooseResources")}:</Text>
<DataList aria-label={t("resourcesToImport")} isCompact>
{targetHasResource("users") &&
resourceDataListItem("users", t("common:users"))}
{targetHasResource("groups") &&
resourceDataListItem("groups", t("common:groups"))}
{targetHasResource("clients") &&
resourceDataListItem("clients", t("common:clients"))}
{targetHasResource("identityProviders") &&
resourceDataListItem(
"identityProviders",
t("common:identityProviders")
)}
{targetHasRealmRoles() &&
resourceDataListItem("realmRoles", t("common:realmRoles"))}
{targetHasClientRoles() &&
resourceDataListItem(
"clientRoles",
t("common:clientRoles")
)}
</DataList>
</StackItem>
<StackItem>
<Text>{t("selectIfResourceExists")}:</Text>
<Select
toggleId="realm-selector"
isOpen={isRealmSelectOpen}
onToggle={() => setIsRealmSelectOpen(!isRealmSelectOpen)}
onSelect={handleRealmSelect}
placeholderText={targetRealm.realm || targetRealm.id}
isOpen={isCollisionSelectOpen}
direction="up"
onToggle={() => {
setIsCollisionSelectOpen(!isCollisionSelectOpen);
}}
onSelect={handleCollisionSelect}
placeholderText={t(collisionOption)}
>
{realmSelectOptions()}
{collisionOptions()}
</Select>
</StackItem>
)}
<StackItem>
<Text>{t("chooseResources")}:</Text>
<DataList aria-label="Resources to import" isCompact>
{targetHasResource("users") &&
resourceDataListItem("users", "users")}
{targetHasResource("groups") &&
resourceDataListItem("groups", "groups")}
{targetHasResource("clients") &&
resourceDataListItem("clients", "clients")}
{targetHasResource("identityProviders") &&
resourceDataListItem(
"identityProviders",
"identity providers"
)}
{targetHasRealmRoles() &&
resourceDataListItem("realmRoles", "realm roles")}
{targetHasClientRoles() &&
resourceDataListItem("clientRoles", "client roles")}
</DataList>
</StackItem>
<StackItem>
<Text>{t("selectIfResourceExists")}:</Text>
<Select
isOpen={isCollisionSelectOpen}
direction="up"
onToggle={() => {
setIsCollisionSelectOpen(!isCollisionSelectOpen);
}}
onSelect={handleCollisionSelect}
placeholderText={t(collisionOption)}
>
{collisionOptions()}
</Select>
</StackItem>
</>
)}
</Stack>
</Modal>
);
</>
)}
</Stack>
</Modal>
);
};
const importCompleteMessage = () => {
return `${t("importAdded", {
count: importResponse?.added,
})} ${t("importSkipped", {
count: importResponse?.skipped,
})} ${t("importOverwritten", {
count: importResponse?.overwritten,
})}`;
};
const loader = async (first = 0, max = 15) => {
if (!importResponse) {
return [];
}
const last = Math.min(first + max, importResponse.results.length);
return importResponse.results.slice(first, last);
};
const ActionLabel = (importRecord: PartialImportResult) => {
switch (importRecord.action) {
case "ADDED":
return (
<Label key={importRecord.id} color="green">
{t("added")}
</Label>
);
case "SKIPPED":
return (
<Label key={importRecord.id} color="orange">
{t("skipped")}
</Label>
);
case "OVERWRITTEN":
return (
<Label key={importRecord.id} color="purple">
{t("overwritten")}
</Label>
);
}
};
const TypeRenderer = (importRecord: PartialImportResult) => {
const typeMap = new Map([
["CLIENT", t("common:clients")],
["REALM_ROLE", t("common:realmRoles")],
["USER", t("common:users")],
["CLIENT_ROLE", t("common:clientRoles")],
["IDP", t("common:identityProviders")],
]);
return <span>{typeMap.get(importRecord.resourceType)}</span>;
};
const importCompletedModal = () => {
return (
<Modal
variant={ModalVariant.medium}
title={tRealm("partialImport")}
isOpen={props.open}
onClose={props.toggleDialog}
actions={[
<Button
id="modal-close"
data-testid="close-button"
key="close"
variant={ButtonVariant.primary}
onClick={() => {
props.toggleDialog();
}}
>
{t("common:close")}
</Button>,
]}
>
<Alert variant="success" isInline title={importCompleteMessage()} />
<KeycloakDataTable
loader={loader}
isPaginated
ariaLabelKey="realm-settings:partialImport"
columns={[
{
name: "action",
displayKey: "common:action",
cellRenderer: ActionLabel,
},
{
name: "resourceType",
displayKey: "common:type",
cellRenderer: TypeRenderer,
},
{
name: "resourceName",
displayKey: "common:name",
},
{
name: "id",
displayKey: "common:id",
},
]}
/>
</Modal>
);
};
if (!importResponse) {
return importModal();
}
return importCompletedModal();
};

View file

@ -743,9 +743,23 @@ export default {
selectIfResourceExists:
"If a resource already exists, specify what should be done",
import: "Import",
resourcesToImport: "Resources to import",
importFail: "Import failed: {{error}}",
FAIL: "Fail import",
SKIP: "Skip",
OVERWRITE: "Overwrite",
added: "Added",
skipped: "Skipped",
overwritten: "Overwritten",
importAdded_zero: "No records added.",
importAdded_one: "One record added.",
importAdded_other: "{{count}} records added.",
importOverwritten_zero: "No records overwritten.",
importOverwritten_one: "One record overwritten.",
importOverwritten_other: "{{count}} records overwritten.",
importSkipped_zero: "No records skipped.",
importSkipped_one: "One record skipped.",
importSkipped_other: "{{count}} records skipped.",
},
"partial-export": {
partialExportHeaderText: