keycloak-scim/js/apps/admin-ui/src/realm-settings/PartialImport.tsx
Erik Jan de Wit 5949fd43d0
remove all use of deprecated Select and Dropdown (#29270)
* removed deprecated select

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* some more deprecation removal

working towards fixing: #28197

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* changed to use new api

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* more deprecation removal

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed merge error

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* small fix

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed merge error

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* no more default text for SelectOption

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* changed to use id

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed dropdown in keycloakCard and test fixes

Signed-off-by: mfrances <mfrances@redhat.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed lint error

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix dropdown/select related test failures

Signed-off-by: mfrances <mfrances@redhat.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* i18n label

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* removed

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed merge error

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: mfrances <mfrances@redhat.com>
Co-authored-by: mfrances <mfrances@redhat.com>
2024-05-30 13:45:58 +02:00

504 lines
14 KiB
TypeScript

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";
import {
Alert,
Button,
ButtonVariant,
Checkbox,
DataList,
DataListCell,
DataListItem,
DataListItemCells,
DataListItemRow,
Divider,
Label,
Modal,
ModalVariant,
SelectOption,
Stack,
StackItem,
Text,
TextContent,
} from "@patternfly/react-core";
import { FormEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts";
import { JsonFileUpload } from "../components/json-file-upload/JsonFileUpload";
import { KeycloakSelect } from "../components/select/KeycloakSelect";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useRealm } from "../context/realm-context/RealmContext";
export type PartialImportProps = {
open: boolean;
toggleDialog: () => void;
};
// An imported JSON file can either be an array of realm objects
// or a single realm object.
type ImportedMultiRealm = RealmRepresentation | RealmRepresentation[];
type NonRoleResource = "users" | "clients" | "groups" | "identityProviders";
type RoleResource = "realmRoles" | "clientRoles";
type Resource = NonRoleResource | RoleResource;
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 { adminClient } = useAdminClient();
const { t } = useTranslation();
const { realm } = useRealm();
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<RealmRepresentation>({});
const [importResponse, setImportResponse] = useState<PartialImportResponse>();
const { addError } = useAlerts();
const [resourcesToImport, setResourcesToImport] = useState(INITIAL_RESOURCES);
const isAnyResourceChecked = Object.values(resourcesToImport).some(
(checked) => checked,
);
const resetResourcesToImport = () => {
setResourcesToImport(INITIAL_RESOURCES);
};
const resetInputState = () => {
setImportedFile(undefined);
setTargetRealm({});
setCollisionOption("FAIL");
resetResourcesToImport();
};
// when dialog opens or closes, clear state
useEffect(() => {
setImportInProgress(false);
setImportResponse(undefined);
resetInputState();
}, [props.open]);
const handleFileChange = (value: ImportedMultiRealm) => {
resetInputState();
setImportedFile(value);
if (!Array.isArray(value)) {
setTargetRealm(value);
} else if (value.length > 0) {
setTargetRealm(value[0]);
}
};
const handleRealmSelect = (realm: string | number | object) => {
setTargetRealm(realm as RealmRepresentation);
setIsRealmSelectOpen(false);
resetResourcesToImport();
};
const handleResourceCheckBox = (
checked: boolean,
event: FormEvent<HTMLInputElement>,
) => {
const resource = event.currentTarget.name as Resource;
setResourcesToImport({
...resourcesToImport,
[resource]: checked,
});
};
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 = (option: string | number | object) => {
setCollisionOption(option as CollisionOption);
setIsCollisionSelectOpen(false);
};
const collisionOptions = () => {
return [
<SelectOption key="fail" value="FAIL">
{t("FAIL")}
</SelectOption>,
<SelectOption key="skip" value="SKIP">
{t("SKIP")}
</SelectOption>,
<SelectOption key="overwrite" value="OVERWRITE">
{t("OVERWRITE")}
</SelectOption>,
];
};
const targetHasResources = () => {
return (
targetHasResource("users") ||
targetHasResource("groups") ||
targetHasResource("clients") ||
targetHasResource("identityProviders") ||
targetHasRealmRoles() ||
targetHasClientRoles()
);
};
const targetHasResource = (resource: NonRoleResource) => {
const value = targetRealm[resource];
return value !== undefined && value.length > 0;
};
const targetHasRealmRoles = () => {
const value = targetRealm.roles?.realm;
return value !== undefined && value.length > 0;
};
const targetHasClientRoles = () => {
const value = targetRealm.roles?.client;
return value !== undefined && Object.keys(value).length > 0;
};
const itemCount = (resource: Resource) => {
if (!isFileSelected) return 0;
if (resource === "realmRoles") {
return targetRealm.roles?.realm?.length ?? 0;
}
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,
) => {
return (
<DataListItem aria-labelledby={`${resource}-list-item`}>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key={resource}>
<Checkbox
id={`${resource}-checkbox`}
label={`${itemCount(resource)} ${resourceDisplayName}`}
aria-labelledby={`${resource}-checkbox`}
name={resource}
isChecked={resourcesToImport[resource]}
onChange={(event, checked: boolean) =>
handleResourceCheckBox(checked, event)
}
data-testid={resource + "-checkbox"}
/>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
);
};
const jsonForImport = () => {
const jsonToImport: PartialImportRealmRepresentation = {
ifResourceExists: collisionOption,
id: targetRealm.id,
realm: targetRealm.realm,
};
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("importFail", error);
}
setImportInProgress(false);
}
const importModal = () => {
return (
<Modal
variant={ModalVariant.medium}
title={t("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("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>
<Divider />
</StackItem>
{Array.isArray(importedFile) && importedFile.length > 1 && (
<StackItem>
<Text>{t("selectRealm")}:</Text>
<KeycloakSelect
toggleId="realm-selector"
isOpen={isRealmSelectOpen}
typeAheadAriaLabel={t("realmSelector")}
aria-label={"realmSelector"}
onToggle={() => setIsRealmSelectOpen(!isRealmSelectOpen)}
onSelect={(value) => handleRealmSelect(value)}
placeholderText={targetRealm.realm || targetRealm.id}
>
{realmSelectOptions(importedFile)}
</KeycloakSelect>
</StackItem>
)}
<StackItem>
<Text>{t("chooseResources")}:</Text>
<DataList aria-label={t("resourcesToImport")} isCompact>
{targetHasResource("users") &&
resourceDataListItem("users", t("users"))}
{targetHasResource("groups") &&
resourceDataListItem("groups", t("groups"))}
{targetHasResource("clients") &&
resourceDataListItem("clients", t("clients"))}
{targetHasResource("identityProviders") &&
resourceDataListItem(
"identityProviders",
t("identityProviders"),
)}
{targetHasRealmRoles() &&
resourceDataListItem("realmRoles", t("realmRoles"))}
{targetHasClientRoles() &&
resourceDataListItem("clientRoles", t("clientRoles"))}
</DataList>
</StackItem>
<StackItem>
<Text>{t("selectIfResourceExists")}:</Text>
<KeycloakSelect
isOpen={isCollisionSelectOpen}
direction="up"
onToggle={() => {
setIsCollisionSelectOpen(!isCollisionSelectOpen);
}}
onSelect={handleCollisionSelect}
placeholderText={t(collisionOption)}
>
{collisionOptions()}
</KeycloakSelect>
</StackItem>
</>
)}
</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>
);
default:
return "";
}
};
const TypeRenderer = (importRecord: PartialImportResult) => {
const typeMap = new Map([
["CLIENT", t("clients")],
["REALM_ROLE", t("realmRoles")],
["USER", t("users")],
["CLIENT_ROLE", t("clientRoles")],
["IDP", t("identityProviders")],
["GROUP", t("groups")],
]);
return <span>{typeMap.get(importRecord.resourceType)}</span>;
};
const importCompletedModal = () => {
return (
<Modal
variant={ModalVariant.medium}
title={t("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("close")}
</Button>,
]}
>
<Alert
variant="success"
component="p"
isInline
title={importCompleteMessage()}
/>
<KeycloakDataTable
loader={loader}
isPaginated
ariaLabelKey="partialImport"
columns={[
{
name: "action",
displayKey: "action",
cellRenderer: ActionLabel,
},
{
name: "resourceType",
displayKey: "type",
cellRenderer: TypeRenderer,
},
{
name: "resourceName",
displayKey: "name",
},
{
name: "id",
displayKey: "id",
},
]}
/>
</Modal>
);
};
if (!importResponse) {
return importModal();
}
return importCompletedModal();
};