diff --git a/package.json b/package.json index 8b8ac3cb0d..a1ba96bbe1 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@patternfly/react-core": "^4.40.4", "@patternfly/react-icons": "^4.7.2", "@patternfly/react-table": "^4.15.5", + "file-saver": "^2.0.2", "i18next": "^19.6.2", "i18next-http-backend": "^1.0.18", "keycloak-js": "^11.0.0", @@ -47,6 +48,7 @@ "@testing-library/react": "^10.4.6", "@types/dot": "^1.1.4", "@types/enzyme": "^3.10.5", + "@types/file-saver": "^2.0.1", "@types/jest": "^26.0.4", "@types/react": "^16.9.23", "@types/react-dom": "^16.9.5", diff --git a/src/clients/ClientList.test.tsx b/src/clients/ClientList.test.tsx index 6542b08191..96ed3ddd49 100644 --- a/src/clients/ClientList.test.tsx +++ b/src/clients/ClientList.test.tsx @@ -3,10 +3,14 @@ import { render } from "@testing-library/react"; import clientMock from "./mock-clients.json"; import { ClientList } from "./ClientList"; +import { I18nextProvider } from "react-i18next"; +import { i18n } from "../i18n"; test("renders ClientList", () => { const { getByText } = render( - + + + ); const headerElement = getByText(/Client ID/i); expect(headerElement).toBeInTheDocument(); diff --git a/src/clients/ClientList.tsx b/src/clients/ClientList.tsx index 3dfe10eb31..afba0f4ab3 100644 --- a/src/clients/ClientList.tsx +++ b/src/clients/ClientList.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import { Table, TableBody, @@ -7,10 +7,15 @@ import { IFormatter, IFormatterValueType, } from "@patternfly/react-table"; -import { Badge } from "@patternfly/react-core"; +import { Badge, AlertVariant } from "@patternfly/react-core"; +import { saveAs } from "file-saver"; import { ExternalLink } from "../components/external-link/ExternalLink"; import { ClientRepresentation } from "../model/client-model"; +import { HttpClientContext } from "../http-service/HttpClientContext"; +import { useAlerts } from "../components/alert/Alerts"; +import { AlertPanel } from "../components/alert/AlertPanel"; +import { useTranslation } from "react-i18next"; type ClientListProps = { clients?: ClientRepresentation[]; @@ -25,9 +30,15 @@ const columns: (keyof ClientRepresentation)[] = [ ]; export const ClientList = ({ baseUrl, clients }: ClientListProps) => { + const httpClient = useContext(HttpClientContext)!; + const { t } = useTranslation(); + const [add, alerts, hide] = useAlerts(); + + const convertClientId = (clientId: string) => + clientId.substring(0, clientId.indexOf("#")); const enabled = (): IFormatter => (data?: IFormatterValueType) => { const field = data!.toString(); - const value = field.substring(0, field.indexOf("#")); + const value = convertClientId(field); return field.indexOf("true") !== -1 ? ( <>{value} ) : ( @@ -61,25 +72,67 @@ export const ClientList = ({ baseUrl, clients }: ClientListProps) => { return r; }) .map((c) => { - return { cells: columns.map((col) => c[col]) }; + return { cells: columns.map((col) => c[col]), client: c }; }); return ( - - - -
+ <> + + { + const clientCopy = JSON.parse(JSON.stringify(data[rowId].client)); + clientCopy.clientId = convertClientId(clientCopy.clientId); + delete clientCopy.id; + + if (clientCopy.protocolMappers) { + for (let i = 0; i < clientCopy.protocolMappers.length; i++) { + delete clientCopy.protocolMappers[i].id; + } + } + + saveAs( + new Blob([JSON.stringify(clientCopy, null, 2)], { + type: "application/json", + }), + clientCopy.clientId + ".json" + ); + }, + }, + { + title: t("Delete"), + onClick: (_, rowId) => { + try { + httpClient.doDelete( + `/admin/realms/master/clients/${data[rowId].client.id}` + ); + add(t("The client has been deleted"), AlertVariant.success); + } catch (error) { + add( + `${t("Could not delete client:")} ${error}`, + AlertVariant.danger + ); + } + }, + }, + ]} + aria-label="Client list" + > + + +
+ ); }; diff --git a/src/http-service/http-client.ts b/src/http-service/http-client.ts index 88c7e3de4f..fe21d628a1 100644 --- a/src/http-service/http-client.ts +++ b/src/http-service/http-client.ts @@ -74,10 +74,13 @@ export class HttpClient { await this.makeConfig(config) ); - try { - response.data = await response.json(); - } catch (e) { - console.warn(e); + const contentType = response.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + try { + response.data = await response.json(); + } catch (e) { + console.warn(e); + } } if (!response.ok) { diff --git a/yarn.lock b/yarn.lock index 79540cbff0..e0caedf71b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4124,6 +4124,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/file-saver@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.1.tgz#e18eb8b069e442f7b956d313f4fadd3ef887354e" + integrity sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw== + "@types/glob-base@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@types/glob-base/-/glob-base-0.3.0.tgz#a581d688347e10e50dd7c17d6f2880a10354319d" @@ -9165,6 +9170,11 @@ file-loader@^6.0.0: loader-utils "^2.0.0" schema-utils "^2.7.1" +file-saver@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a" + integrity sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw== + file-selector@^0.1.8: version "0.1.12" resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0"