From ef6d8d3c59b0a1a05906eaaaa42976f9fedcb55c Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 25 Sep 2024 15:27:32 +0200 Subject: [PATCH 01/13] refactor: change buttons color --- .../connectedServices/ConnectedServicesPage.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/src/features/connectedServices/ConnectedServicesPage.tsx b/client/src/features/connectedServices/ConnectedServicesPage.tsx index 2842768142..9fcf9123b4 100644 --- a/client/src/features/connectedServices/ConnectedServicesPage.tsx +++ b/client/src/features/connectedServices/ConnectedServicesPage.tsx @@ -109,7 +109,7 @@ function ConnectedServiceCard({ provider }: ConnectedServiceCardProps) {
{display_name} - +
@@ -129,20 +129,22 @@ function ConnectedServiceCard({ provider }: ConnectedServiceCardProps) { } interface ConnectButtonParams { - id: string; connectionStatus?: ConnectionStatus; + id: string; } -function ConnectButton({ id, connectionStatus }: ConnectButtonParams) { +function ConnectButton({ connectionStatus, id }: ConnectButtonParams) { const hereUrl = window.location.href; const authorizeUrl = `/ui-server/api/data/oauth2/providers/${id}/authorize`; const url = `${authorizeUrl}?next_url=${encodeURIComponent(hereUrl)}`; const text = connectionStatus === "connected" ? "Reconnect" : "Connect"; + const color = + connectionStatus === "connected" ? "btn-outline-primary" : "btn-primary"; return ( - + {text} ); From 178518efa8495169e78c7b64ad42e44634236b6a Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 25 Sep 2024 17:11:19 +0200 Subject: [PATCH 02/13] feat: handle connected services in the admin panel (part 1) --- client/.eslintrc.json | 1 + .../admin/AddConnectedServiceButton.tsx | 316 ++++++++++++++++++ client/src/features/admin/AdminPage.tsx | 2 + .../admin/ConnectedServicesSection.tsx | 132 ++++++++ .../connectedServices.api.ts | 29 ++ .../connectedServices.types.ts | 7 + 6 files changed, 487 insertions(+) create mode 100644 client/src/features/admin/AddConnectedServiceButton.tsx create mode 100644 client/src/features/admin/ConnectedServicesSection.tsx diff --git a/client/.eslintrc.json b/client/.eslintrc.json index 09552ec567..edcb56f461 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -203,6 +203,7 @@ "papermill", "pathname", "pdfjs", + "pkce", "plaintext", "poller", "popups", diff --git a/client/src/features/admin/AddConnectedServiceButton.tsx b/client/src/features/admin/AddConnectedServiceButton.tsx new file mode 100644 index 0000000000..8611980adf --- /dev/null +++ b/client/src/features/admin/AddConnectedServiceButton.tsx @@ -0,0 +1,316 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; +import { useCallback, useEffect, useState } from "react"; +import { PlusLg, XLg } from "react-bootstrap-icons"; +import { Control, Controller, FieldErrors, useForm } from "react-hook-form"; +import { + Button, + Form, + Input, + Label, + Modal, + ModalBody, + ModalFooter, + ModalHeader, +} from "reactstrap"; +import { + ConnectedServiceForm, + Provider, +} from "../connectedServices/connectedServices.types"; +import { useCreateProviderMutation } from "../connectedServices/connectedServices.api"; +import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; +import { Loader } from "../../components/Loader"; + +export default function AddConnectedServiceButton() { + const [isOpen, setIsOpen] = useState(false); + const toggle = useCallback(() => { + setIsOpen((open) => !open); + }, []); + + return ( + <> + + + + ); +} + +interface AddConnectedServiceModalProps { + isOpen: boolean; + toggle: () => void; +} +function AddConnectedServiceModal({ + isOpen, + toggle, +}: AddConnectedServiceModalProps) { + const [createProvider, result] = useCreateProviderMutation(); + + const { + control, + formState: { errors }, + handleSubmit, + reset, + } = useForm({ + defaultValues: { + id: "", + kind: "", + client_id: "", + client_secret: "", + display_name: "", + scope: "api", + url: "", + }, + }); + const onSubmit = useCallback( + (data: Provider) => { + createProvider({ + id: data.id, + kind: data.kind, + client_id: data.client_id, + client_secret: data.client_secret, + display_name: data.display_name, + scope: data.scope, + url: data.url, + use_pkce: data.use_pkce, + }); + }, + [createProvider] + ); + + useEffect(() => { + if (!result.isSuccess) { + return; + } + toggle(); + }, [result.isSuccess, toggle]); + + useEffect(() => { + if (!isOpen) { + reset(); + result.reset(); + } + }, [isOpen, reset, result]); + + return ( + +
+ Add provider + + {result.error && } + + + + + + + +
+
+ ); +} + +interface ConnectedServiceFormContentProps { + control: Control; + errors: FieldErrors; +} +function ConnectedServiceFormContent({ + control, + errors, +}: ConnectedServiceFormContentProps) { + return ( + <> +
+ + ( + + )} + rules={{ required: true }} + /> +
+ +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a kind
+
+ +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide an id
+
+ +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a client secret
+
+ +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a display name
+
+ +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a scope
+
+ +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a URL
+
+ + ); +} diff --git a/client/src/features/admin/AdminPage.tsx b/client/src/features/admin/AdminPage.tsx index ef013906d9..28ff6428e4 100644 --- a/client/src/features/admin/AdminPage.tsx +++ b/client/src/features/admin/AdminPage.tsx @@ -62,6 +62,7 @@ import { ResourcePoolUser } from "./adminComputeResources.types"; import { useGetKeycloakUserQuery } from "./adminKeycloak.api"; import { KeycloakUser } from "./adminKeycloak.types"; import useKeycloakRealm from "./useKeycloakRealm.hook"; +import ConnectedServicesSection from "./ConnectedServicesSection"; export default function AdminPage() { return ( @@ -70,6 +71,7 @@ export default function AdminPage() { + ); } diff --git a/client/src/features/admin/ConnectedServicesSection.tsx b/client/src/features/admin/ConnectedServicesSection.tsx new file mode 100644 index 0000000000..f00c100b09 --- /dev/null +++ b/client/src/features/admin/ConnectedServicesSection.tsx @@ -0,0 +1,132 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; +import { + Card, + CardBody, + CardText, + CardTitle, + Col, + Container, + Row, +} from "reactstrap"; +import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; +import { Loader } from "../../components/Loader"; +import { useGetProvidersQuery } from "../connectedServices/connectedServices.api"; +import { + Provider, + ProviderList, +} from "../connectedServices/connectedServices.types"; +import AddConnectedServiceButton from "./AddConnectedServiceButton"; + +export default function ConnectedServicesSection() { + return ( +
+

Connected Services - Renku 2.0

+ +
+ ); +} + +function ConnectedServices() { + const { data: providers, isLoading, error } = useGetProvidersQuery(); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( +
+
+ +
+
+ +
+
+ ); +} + +interface ConnectedServicesListProps { + providers?: ProviderList; +} +function ConnectedServicesList({ providers }: ConnectedServicesListProps) { + if (!providers || providers.length === 0) { + return

No connected services

; + } + return ( + + + {providers.map((provider) => ( + + ))} + + + ); +} + +interface ConnectedServiceProps { + provider: Provider; +} +function ConnectedService({ provider }: ConnectedServiceProps) { + return ( + + + + + {provider.display_name} + + + + ID: {provider.id} + + + Kind: {provider.kind} + + + URL: {provider.url} + + + + Client ID: {provider.client_id} + + + Client secret: {provider.client_secret} + + + + Scope: {provider.scope} + + + Use PKCE: {provider.use_pkce.toString()} + + + {/*
+ + +
*/} +
+
+ + ); +} diff --git a/client/src/features/connectedServices/connectedServices.api.ts b/client/src/features/connectedServices/connectedServices.api.ts index 7b112b29e2..84392b65fa 100644 --- a/client/src/features/connectedServices/connectedServices.api.ts +++ b/client/src/features/connectedServices/connectedServices.api.ts @@ -23,6 +23,7 @@ import { ConnectionList, ConnectedAccount, GetConnectedAccountParams, + Provider, } from "./connectedServices.types"; const connectedServicesApi = createApi({ @@ -32,6 +33,33 @@ const connectedServicesApi = createApi({ }), tagTypes: ["Provider", "Connection", "ConnectedAccount"], endpoints: (builder) => ({ + createProvider: builder.mutation({ + query: ({ + id, + kind, + client_id, + client_secret, + display_name, + scope, + url, + }) => { + return { + url: "providers", + method: "POST", + body: { + id, + kind, + client_id, + client_secret, + display_name, + scope, + url, + use_pkce: false, + }, + }; + }, + invalidatesTags: ["Provider"], + }), getProviders: builder.query({ query: () => { return { @@ -84,6 +112,7 @@ const connectedServicesApi = createApi({ export default connectedServicesApi; export const { + useCreateProviderMutation, useGetConnectedAccountQuery, useGetConnectionsQuery, useGetProvidersQuery, diff --git a/client/src/features/connectedServices/connectedServices.types.ts b/client/src/features/connectedServices/connectedServices.types.ts index b932de1f2b..29924d1f30 100644 --- a/client/src/features/connectedServices/connectedServices.types.ts +++ b/client/src/features/connectedServices/connectedServices.types.ts @@ -18,8 +18,13 @@ export interface Provider { id: string; + kind: string; + client_id: string; + client_secret: string; display_name: string; + scope: string; url: string; + use_pkce: boolean; } export type ProviderList = Provider[]; @@ -42,3 +47,5 @@ export interface ConnectedAccount { export interface GetConnectedAccountParams { connectionId: string; } + +export type ConnectedServiceForm = Provider; From ea1315418b3ea601a9a331b1418c2843d5fd36fc Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 27 Sep 2024 11:48:30 +0200 Subject: [PATCH 03/13] feat: continue the Connected service admin section * Finish the component to add a new service. * Polish the card to show existing services. * Add the delete button. --- .../admin/AddConnectedServiceButton.tsx | 137 +++++++++++------- .../admin/ConnectedServicesSection.tsx | 106 ++++++++++---- .../admin/DeleteConnectedServiceButton.tsx | 122 ++++++++++++++++ .../connectedServices.api.ts | 20 ++- .../connectedServices.types.ts | 11 ++ 5 files changed, 310 insertions(+), 86 deletions(-) create mode 100644 client/src/features/admin/DeleteConnectedServiceButton.tsx diff --git a/client/src/features/admin/AddConnectedServiceButton.tsx b/client/src/features/admin/AddConnectedServiceButton.tsx index 8611980adf..69938d1ca3 100644 --- a/client/src/features/admin/AddConnectedServiceButton.tsx +++ b/client/src/features/admin/AddConnectedServiceButton.tsx @@ -77,8 +77,9 @@ function AddConnectedServiceModal({ client_id: "", client_secret: "", display_name: "", - scope: "api", + scope: "", url: "", + use_pkce: false, }, }); const onSubmit = useCallback( @@ -188,13 +189,20 @@ function ConnectedServiceFormContent({ control={control} name="kind" render={({ field }) => ( - + <> + + + + )} rules={{ required: true }} /> @@ -202,114 +210,143 @@ function ConnectedServiceFormContent({
-
-
- ( + )} + /> + +
+ +
+ + ( + )} rules={{ required: true }} /> -
Please provide a display name
+
Please provide an id
-
-
); diff --git a/client/src/features/admin/ConnectedServicesSection.tsx b/client/src/features/admin/ConnectedServicesSection.tsx index f00c100b09..78a3c86c91 100644 --- a/client/src/features/admin/ConnectedServicesSection.tsx +++ b/client/src/features/admin/ConnectedServicesSection.tsx @@ -20,9 +20,11 @@ import cx from "classnames"; import { Card, CardBody, + CardFooter, + CardHeader, CardText, - CardTitle, Col, + Collapse, Container, Row, } from "reactstrap"; @@ -34,6 +36,9 @@ import { ProviderList, } from "../connectedServices/connectedServices.types"; import AddConnectedServiceButton from "./AddConnectedServiceButton"; +import ChevronFlippedIcon from "../../components/icons/ChevronFlippedIcon"; +import { useCallback, useState } from "react"; +import DeleteConnectedServiceButton from "./DeleteConnectedServiceButton"; export default function ConnectedServicesSection() { return ( @@ -89,43 +94,80 @@ interface ConnectedServiceProps { provider: Provider; } function ConnectedService({ provider }: ConnectedServiceProps) { + const [isOpen, setIsOpen] = useState(false); + const toggle = useCallback(() => { + setIsOpen((isOpen) => !isOpen); + }, []); + return ( - + - - + + + + + + + ID: {provider.id} + + + Kind: {provider.kind} + + + URL: {provider.url} + - - Scope: {provider.scope} - - - Use PKCE: {provider.use_pkce.toString()} - + + Client ID: {provider.client_id} + + + Client secret: {provider.client_secret} + - {/*
- + + Scope: {provider.scope} + + + Use PKCE: {provider.use_pkce.toString()} + + + -
*/} -
+ {/* */} + +
); diff --git a/client/src/features/admin/DeleteConnectedServiceButton.tsx b/client/src/features/admin/DeleteConnectedServiceButton.tsx new file mode 100644 index 0000000000..9b24690f44 --- /dev/null +++ b/client/src/features/admin/DeleteConnectedServiceButton.tsx @@ -0,0 +1,122 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; + +import { useCallback, useEffect, useState } from "react"; +import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "reactstrap"; +import { TrashFill, XLg } from "react-bootstrap-icons"; + +import { Provider } from "../connectedServices/connectedServices.types"; +import { useDeleteProviderMutation } from "../connectedServices/connectedServices.api"; +import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; + +interface DeleteConnectedServiceButtonProps { + provider: Provider; +} + +export default function DeleteConnectedServiceButton({ + provider, +}: DeleteConnectedServiceButtonProps) { + const [isOpen, setIsOpen] = useState(false); + const toggle = useCallback(() => { + setIsOpen((open) => !open); + }, []); + + return ( + <> + + + + ); +} + +interface DeleteConnectedServiceModalProps { + provider: Provider; + isOpen: boolean; + toggle: () => void; +} + +function DeleteConnectedServiceModal({ + provider, + isOpen, + toggle, +}: DeleteConnectedServiceModalProps) { + const [deleteSessionEnvironment, result] = useDeleteProviderMutation(); + + const onDelete = useCallback(() => { + deleteSessionEnvironment(provider.id); + }, [deleteSessionEnvironment, provider.id]); + + useEffect(() => { + if (!result.isSuccess) { + return; + } + toggle(); + }, [result.isSuccess, toggle]); + + useEffect(() => { + if (!isOpen) { + result.reset(); + } + }, [isOpen, result]); + + return ( + + Are you sure? + + {result.error && } + +

+ Please confirm that you want to delete the {provider.display_name}{" "} + service. +

+
+ + + + +
+ ); +} diff --git a/client/src/features/connectedServices/connectedServices.api.ts b/client/src/features/connectedServices/connectedServices.api.ts index 84392b65fa..cb42c983ab 100644 --- a/client/src/features/connectedServices/connectedServices.api.ts +++ b/client/src/features/connectedServices/connectedServices.api.ts @@ -24,6 +24,7 @@ import { ConnectedAccount, GetConnectedAccountParams, Provider, + ConnectedServiceParams, } from "./connectedServices.types"; const connectedServicesApi = createApi({ @@ -33,7 +34,7 @@ const connectedServicesApi = createApi({ }), tagTypes: ["Provider", "Connection", "ConnectedAccount"], endpoints: (builder) => ({ - createProvider: builder.mutation({ + createProvider: builder.mutation({ query: ({ id, kind, @@ -42,6 +43,7 @@ const connectedServicesApi = createApi({ display_name, scope, url, + use_pkce, }) => { return { url: "providers", @@ -50,16 +52,25 @@ const connectedServicesApi = createApi({ id, kind, client_id, - client_secret, + ...(client_secret && { client_secret }), display_name, - scope, + scope: scope || "", url, - use_pkce: false, + use_pkce, }, }; }, invalidatesTags: ["Provider"], }), + deleteProvider: builder.mutation({ + query: (providerId) => { + return { + url: `providers/${providerId}`, + method: "DELETE", + }; + }, + invalidatesTags: ["Provider"], + }), getProviders: builder.query({ query: () => { return { @@ -113,6 +124,7 @@ const connectedServicesApi = createApi({ export default connectedServicesApi; export const { useCreateProviderMutation, + useDeleteProviderMutation, useGetConnectedAccountQuery, useGetConnectionsQuery, useGetProvidersQuery, diff --git a/client/src/features/connectedServices/connectedServices.types.ts b/client/src/features/connectedServices/connectedServices.types.ts index 29924d1f30..19ca9cd66c 100644 --- a/client/src/features/connectedServices/connectedServices.types.ts +++ b/client/src/features/connectedServices/connectedServices.types.ts @@ -48,4 +48,15 @@ export interface GetConnectedAccountParams { connectionId: string; } +export interface ConnectedServiceParams { + id: string; + kind: string; + client_id: string; + client_secret?: string; + display_name: string; + scope?: string; + url: string; + use_pkce: boolean; +} + export type ConnectedServiceForm = Provider; From 0e4a418e7e13ba319f878f2ef0044315ce0b1a59 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 27 Sep 2024 14:11:19 +0200 Subject: [PATCH 04/13] refactor: use consistent style and spacing in the admin panel sections --- client/src/features/admin/AdminPage.tsx | 4 ++-- client/src/features/admin/ConnectedServicesSection.tsx | 4 ++-- client/src/features/admin/IncidentsAndMaintenanceSection.tsx | 2 +- client/src/features/admin/SessionEnvironmentsSection.tsx | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/src/features/admin/AdminPage.tsx b/client/src/features/admin/AdminPage.tsx index 28ff6428e4..0f1b806a78 100644 --- a/client/src/features/admin/AdminPage.tsx +++ b/client/src/features/admin/AdminPage.tsx @@ -78,8 +78,8 @@ export default function AdminPage() { function ComputeResourcesSection() { return ( -
-

Compute Resources

+
+

Compute Resources

); diff --git a/client/src/features/admin/ConnectedServicesSection.tsx b/client/src/features/admin/ConnectedServicesSection.tsx index 78a3c86c91..52557eb725 100644 --- a/client/src/features/admin/ConnectedServicesSection.tsx +++ b/client/src/features/admin/ConnectedServicesSection.tsx @@ -42,8 +42,8 @@ import DeleteConnectedServiceButton from "./DeleteConnectedServiceButton"; export default function ConnectedServicesSection() { return ( -
-

Connected Services - Renku 2.0

+
+

Connected Services - Renku 2.0

); diff --git a/client/src/features/admin/IncidentsAndMaintenanceSection.tsx b/client/src/features/admin/IncidentsAndMaintenanceSection.tsx index b4d733b09b..b814fb8465 100644 --- a/client/src/features/admin/IncidentsAndMaintenanceSection.tsx +++ b/client/src/features/admin/IncidentsAndMaintenanceSection.tsx @@ -62,7 +62,7 @@ export default function IncidentsAndMaintenanceSection() { return (
-

Incidents And Maintenance

+

Incidents And Maintenance

-

Session Environments - Renku 1.0

+
+

Session Environments - Renku 2.0

); From f0af031b6e840a89ea9cb804717caf7375d5fc8d Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 27 Sep 2024 14:11:29 +0200 Subject: [PATCH 05/13] fix typo --- client/src/features/admin/DeleteConnectedServiceButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/features/admin/DeleteConnectedServiceButton.tsx b/client/src/features/admin/DeleteConnectedServiceButton.tsx index 9b24690f44..9094337fb8 100644 --- a/client/src/features/admin/DeleteConnectedServiceButton.tsx +++ b/client/src/features/admin/DeleteConnectedServiceButton.tsx @@ -114,7 +114,7 @@ function DeleteConnectedServiceModal({ role="button" > - Delete session environment + Delete service From 13bfef7e9642aceb8bd470f01e9d3c25976f2a26 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 27 Sep 2024 15:04:32 +0200 Subject: [PATCH 06/13] feat: add update button and finalize the feature --- .../admin/AddConnectedServiceButton.tsx | 230 ++---------------- client/src/features/admin/AdminPage.tsx | 2 +- .../admin/ConnectedServiceFormContent.tsx | 201 +++++++++++++++ .../admin/ConnectedServicesSection.tsx | 7 +- .../admin/UpdateConnectedServiceButton.tsx | 175 +++++++++++++ .../connectedServices.api.ts | 20 +- .../connectedServices.types.ts | 13 +- 7 files changed, 434 insertions(+), 214 deletions(-) create mode 100644 client/src/features/admin/ConnectedServiceFormContent.tsx create mode 100644 client/src/features/admin/UpdateConnectedServiceButton.tsx diff --git a/client/src/features/admin/AddConnectedServiceButton.tsx b/client/src/features/admin/AddConnectedServiceButton.tsx index 69938d1ca3..341d043d7a 100644 --- a/client/src/features/admin/AddConnectedServiceButton.tsx +++ b/client/src/features/admin/AddConnectedServiceButton.tsx @@ -19,7 +19,7 @@ import cx from "classnames"; import { useCallback, useEffect, useState } from "react"; import { PlusLg, XLg } from "react-bootstrap-icons"; -import { Control, Controller, FieldErrors, useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { Button, Form, @@ -32,11 +32,12 @@ import { } from "reactstrap"; import { ConnectedServiceForm, - Provider, + CreateProviderParams, } from "../connectedServices/connectedServices.types"; import { useCreateProviderMutation } from "../connectedServices/connectedServices.api"; import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; import { Loader } from "../../components/Loader"; +import ConnectedServiceFormContent from "./ConnectedServiceFormContent"; export default function AddConnectedServiceButton() { const [isOpen, setIsOpen] = useState(false); @@ -73,7 +74,7 @@ function AddConnectedServiceModal({ } = useForm({ defaultValues: { id: "", - kind: "", + kind: "gitlab", client_id: "", client_secret: "", display_name: "", @@ -83,7 +84,7 @@ function AddConnectedServiceModal({ }, }); const onSubmit = useCallback( - (data: Provider) => { + (data: CreateProviderParams) => { createProvider({ id: data.id, kind: data.kind, @@ -130,6 +131,26 @@ function AddConnectedServiceModal({ {result.error && } +
+ + ( + + )} + rules={{ required: true }} + /> +
+
@@ -150,204 +171,3 @@ function AddConnectedServiceModal({ ); } - -interface ConnectedServiceFormContentProps { - control: Control; - errors: FieldErrors; -} -function ConnectedServiceFormContent({ - control, - errors, -}: ConnectedServiceFormContentProps) { - return ( - <> -
- - ( - - )} - rules={{ required: true }} - /> -
- -
- - ( - <> - - - - - )} - rules={{ required: true }} - /> -
Please provide a kind
-
- -
- - ( - - )} - rules={{ required: true }} - /> -
Please provide a display name
-
- -
- - ( - - )} - rules={{ required: true }} - /> -
Please provide a URL
-
- -
- ( - - )} - /> - -
- -
- - ( - - )} - rules={{ required: true }} - /> -
Please provide an id
-
- -
- - ( - - )} - /> -
- Please provide a valid client secret or leave it empty -
-
- -
- - ( - - )} - /> -
- Please provide a valid scope or leave it empty -
-
- - ); -} diff --git a/client/src/features/admin/AdminPage.tsx b/client/src/features/admin/AdminPage.tsx index 0f1b806a78..b7a4d4b0de 100644 --- a/client/src/features/admin/AdminPage.tsx +++ b/client/src/features/admin/AdminPage.tsx @@ -70,8 +70,8 @@ export default function AdminPage() {

Admin Panel

- + ); } diff --git a/client/src/features/admin/ConnectedServiceFormContent.tsx b/client/src/features/admin/ConnectedServiceFormContent.tsx new file mode 100644 index 0000000000..4cdebce531 --- /dev/null +++ b/client/src/features/admin/ConnectedServiceFormContent.tsx @@ -0,0 +1,201 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; +import { Control, Controller, FieldErrors } from "react-hook-form"; +import { Input, Label } from "reactstrap"; + +import { ConnectedServiceForm } from "../connectedServices/connectedServices.types"; + +export interface ConnectedServiceFormContentProps { + control: Control; + errors: FieldErrors; +} +export default function ConnectedServiceFormContent({ + control, + errors, +}: ConnectedServiceFormContentProps) { + return ( + <> +
+ + ( + <> + + + + + + )} + rules={{ required: true }} + /> +
Please provide a kind
+
+ +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a display name
+
+ +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a URL
+
+ +
+ ( + + )} + /> + +
+ +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a client id
+
+ +
+ + ( + + )} + /> +
+ Please provide a valid client secret or leave it empty +
+
+ +
+ + ( + + )} + /> +
+ Please provide a valid scope or leave it empty +
+
+ + ); +} diff --git a/client/src/features/admin/ConnectedServicesSection.tsx b/client/src/features/admin/ConnectedServicesSection.tsx index 52557eb725..a0e5eaa6fc 100644 --- a/client/src/features/admin/ConnectedServicesSection.tsx +++ b/client/src/features/admin/ConnectedServicesSection.tsx @@ -39,6 +39,7 @@ import AddConnectedServiceButton from "./AddConnectedServiceButton"; import ChevronFlippedIcon from "../../components/icons/ChevronFlippedIcon"; import { useCallback, useState } from "react"; import DeleteConnectedServiceButton from "./DeleteConnectedServiceButton"; +import UpdateConnectedServiceButton from "./UpdateConnectedServiceButton"; export default function ConnectedServicesSection() { return ( @@ -126,6 +127,7 @@ function ConnectedService({ provider }: ConnectedServiceProps) { + @@ -137,14 +139,12 @@ function ConnectedService({ provider }: ConnectedServiceProps) { URL: {provider.url} - Client ID: {provider.client_id} Client secret: {provider.client_secret} - Scope: {provider.scope} @@ -152,6 +152,7 @@ function ConnectedService({ provider }: ConnectedServiceProps) { Use PKCE: {provider.use_pkce.toString()} + + - {/* */} diff --git a/client/src/features/admin/UpdateConnectedServiceButton.tsx b/client/src/features/admin/UpdateConnectedServiceButton.tsx new file mode 100644 index 0000000000..a6dc63aac6 --- /dev/null +++ b/client/src/features/admin/UpdateConnectedServiceButton.tsx @@ -0,0 +1,175 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; +import { useCallback, useEffect, useState } from "react"; +import { CheckLg, PencilSquare, XLg } from "react-bootstrap-icons"; +import { useForm } from "react-hook-form"; +import { + Button, + Form, + Modal, + ModalBody, + ModalFooter, + ModalHeader, +} from "reactstrap"; + +import { Loader } from "../../components/Loader"; +import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; +import { useUpdateProviderMutation } from "../connectedServices/connectedServices.api"; +import { + ConnectedServiceForm, + Provider, + UpdateProviderParams, +} from "../connectedServices/connectedServices.types"; +import ConnectedServiceFormContent from "./ConnectedServiceFormContent"; + +interface UpdateConnectedServiceButtonProps { + provider: Provider; +} +export default function UpdateConnectedServiceButton({ + provider, +}: UpdateConnectedServiceButtonProps) { + const [isOpen, setIsOpen] = useState(false); + const toggle = useCallback(() => { + setIsOpen((open) => !open); + }, []); + + return ( + <> + + + + ); +} + +interface UpdateConnectedServiceModalProps { + provider: Provider; + isOpen: boolean; + toggle: () => void; +} + +function UpdateConnectedServiceModal({ + provider, + isOpen, + toggle, +}: UpdateConnectedServiceModalProps) { + const [updateProvider, result] = useUpdateProviderMutation(); + + const { + control, + formState: { errors, isDirty }, + handleSubmit, + reset, + } = useForm({ + defaultValues: { + kind: "", + client_id: "", + client_secret: "", + display_name: "", + scope: "", + url: "", + use_pkce: false, + }, + }); + const onSubmit = useCallback( + (data: UpdateProviderParams) => { + updateProvider({ + id: provider.id, + kind: data.kind, + client_id: data.client_id, + client_secret: data.client_secret, + display_name: data.display_name, + scope: data.scope, + url: data.url, + use_pkce: data.use_pkce, + }); + }, + [provider.id, updateProvider] + ); + + useEffect(() => { + if (!result.isSuccess) { + return; + } + toggle(); + }, [result.isSuccess, toggle]); + + useEffect(() => { + if (!isOpen) { + result.reset(); + } + }, [isOpen, result]); + + useEffect(() => { + reset({ + kind: provider.kind, + client_id: provider.client_id, + client_secret: provider.client_secret, + display_name: provider.display_name, + scope: provider.scope, + url: provider.url, + use_pkce: provider.use_pkce, + }); + }, [provider, reset]); + + return ( + +
+ Update provider + + {result.error && } + + + + + + + +
+
+ ); +} diff --git a/client/src/features/connectedServices/connectedServices.api.ts b/client/src/features/connectedServices/connectedServices.api.ts index cb42c983ab..04abb19dd2 100644 --- a/client/src/features/connectedServices/connectedServices.api.ts +++ b/client/src/features/connectedServices/connectedServices.api.ts @@ -19,12 +19,13 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; import { - ProviderList, - ConnectionList, ConnectedAccount, + ConnectionList, + CreateProviderParams, GetConnectedAccountParams, Provider, - ConnectedServiceParams, + ProviderList, + UpdateProviderParams, } from "./connectedServices.types"; const connectedServicesApi = createApi({ @@ -34,7 +35,7 @@ const connectedServicesApi = createApi({ }), tagTypes: ["Provider", "Connection", "ConnectedAccount"], endpoints: (builder) => ({ - createProvider: builder.mutation({ + createProvider: builder.mutation({ query: ({ id, kind, @@ -118,6 +119,16 @@ const connectedServicesApi = createApi({ ] : [], }), + updateProvider: builder.mutation({ + query: ({ id, ...params }) => { + return { + url: `providers/${id}`, + method: "PATCH", + body: params, + }; + }, + invalidatesTags: (_result, _error, { id }) => [{ id, type: "Provider" }], + }), }), }); @@ -128,4 +139,5 @@ export const { useGetConnectedAccountQuery, useGetConnectionsQuery, useGetProvidersQuery, + useUpdateProviderMutation, } = connectedServicesApi; diff --git a/client/src/features/connectedServices/connectedServices.types.ts b/client/src/features/connectedServices/connectedServices.types.ts index 19ca9cd66c..3e97d537d5 100644 --- a/client/src/features/connectedServices/connectedServices.types.ts +++ b/client/src/features/connectedServices/connectedServices.types.ts @@ -48,7 +48,7 @@ export interface GetConnectedAccountParams { connectionId: string; } -export interface ConnectedServiceParams { +export interface CreateProviderParams { id: string; kind: string; client_id: string; @@ -59,4 +59,15 @@ export interface ConnectedServiceParams { use_pkce: boolean; } +export interface UpdateProviderParams { + id: string; + kind?: string; + client_id?: string; + client_secret?: string; + display_name?: string; + scope?: string; + url?: string; + use_pkce?: boolean; +} + export type ConnectedServiceForm = Provider; From ae60afd13815492728cb6026b822353552cd6c23 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 2 Oct 2024 15:51:38 +0200 Subject: [PATCH 07/13] fixes from review (part 1) * Add provider button text * Connected serivces page button color * italics text --- .../admin/AddConnectedServiceButton.tsx | 2 +- .../admin/ConnectedServicesSection.tsx | 24 ++++++------------- .../ConnectedServicesPage.tsx | 2 +- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/client/src/features/admin/AddConnectedServiceButton.tsx b/client/src/features/admin/AddConnectedServiceButton.tsx index 341d043d7a..c67f851bab 100644 --- a/client/src/features/admin/AddConnectedServiceButton.tsx +++ b/client/src/features/admin/AddConnectedServiceButton.tsx @@ -49,7 +49,7 @@ export default function AddConnectedServiceButton() { <> diff --git a/client/src/features/admin/ConnectedServicesSection.tsx b/client/src/features/admin/ConnectedServicesSection.tsx index a0e5eaa6fc..44eb8798bb 100644 --- a/client/src/features/admin/ConnectedServicesSection.tsx +++ b/client/src/features/admin/ConnectedServicesSection.tsx @@ -130,27 +130,17 @@ function ConnectedService({ provider }: ConnectedServiceProps) { + ID: {provider.id} + Kind: {provider.kind} + URL: {provider.url} - ID: {provider.id} + Client ID: {provider.client_id} - Kind: {provider.kind} - - - URL: {provider.url} - - - Client ID: {provider.client_id} - - - Client secret: {provider.client_secret} - - - Scope: {provider.scope} - - - Use PKCE: {provider.use_pkce.toString()} + Client secret: {provider.client_secret} + Scope: {provider.scope} + Use PKCE: {provider.use_pkce.toString()} + {text} ); From c901003cf471ef478784f9db2e0c78b14d9bcb3d Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 2 Oct 2024 15:58:26 +0200 Subject: [PATCH 08/13] refactor: use the same style already used in the nearby components --- .../src/features/admin/ConnectedServicesSection.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/src/features/admin/ConnectedServicesSection.tsx b/client/src/features/admin/ConnectedServicesSection.tsx index 44eb8798bb..a02421a067 100644 --- a/client/src/features/admin/ConnectedServicesSection.tsx +++ b/client/src/features/admin/ConnectedServicesSection.tsx @@ -143,21 +143,18 @@ function ConnectedService({ provider }: ConnectedServiceProps) { Use PKCE: {provider.use_pkce.toString()} - - + From 1aef2c328bdf4afe8c1fb86ad70175183d9a4b37 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 2 Oct 2024 16:22:35 +0200 Subject: [PATCH 09/13] fix: prevent updating the client secret when it's redacted --- client/src/features/admin/UpdateConnectedServiceButton.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/features/admin/UpdateConnectedServiceButton.tsx b/client/src/features/admin/UpdateConnectedServiceButton.tsx index a6dc63aac6..c8ec4cb840 100644 --- a/client/src/features/admin/UpdateConnectedServiceButton.tsx +++ b/client/src/features/admin/UpdateConnectedServiceButton.tsx @@ -127,11 +127,14 @@ function UpdateConnectedServiceModal({ reset({ kind: provider.kind, client_id: provider.client_id, - client_secret: provider.client_secret, display_name: provider.display_name, scope: provider.scope, url: provider.url, use_pkce: provider.use_pkce, + ...(provider.client_secret && + provider.client_secret !== "redacted" && { + client_secret: provider.client_secret, + }), }); }, [provider, reset]); From 54ea02f4203441ed8f7dd0ff3fd6db77069c0012 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 3 Oct 2024 09:16:19 +0200 Subject: [PATCH 10/13] fix --- client/src/features/admin/ConnectedServicesSection.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/features/admin/ConnectedServicesSection.tsx b/client/src/features/admin/ConnectedServicesSection.tsx index a02421a067..a210c4bbfc 100644 --- a/client/src/features/admin/ConnectedServicesSection.tsx +++ b/client/src/features/admin/ConnectedServicesSection.tsx @@ -20,7 +20,6 @@ import cx from "classnames"; import { Card, CardBody, - CardFooter, CardHeader, CardText, Col, From d640d8321630d9db9481f20da603778b80714d85 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 3 Oct 2024 10:58:18 +0200 Subject: [PATCH 11/13] add additional reset --- client/src/features/admin/UpdateConnectedServiceButton.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/features/admin/UpdateConnectedServiceButton.tsx b/client/src/features/admin/UpdateConnectedServiceButton.tsx index c8ec4cb840..eee83c77bb 100644 --- a/client/src/features/admin/UpdateConnectedServiceButton.tsx +++ b/client/src/features/admin/UpdateConnectedServiceButton.tsx @@ -115,7 +115,8 @@ function UpdateConnectedServiceModal({ return; } toggle(); - }, [result.isSuccess, toggle]); + reset(); + }, [result.isSuccess, reset, toggle]); useEffect(() => { if (!isOpen) { From ae3689871eb92bf0e48f789571c56d3a814f5def Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 3 Oct 2024 11:12:09 +0200 Subject: [PATCH 12/13] refactor: use password for client secret --- client/src/features/admin/ConnectedServiceFormContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/features/admin/ConnectedServiceFormContent.tsx b/client/src/features/admin/ConnectedServiceFormContent.tsx index 4cdebce531..5648ba00bc 100644 --- a/client/src/features/admin/ConnectedServiceFormContent.tsx +++ b/client/src/features/admin/ConnectedServiceFormContent.tsx @@ -165,7 +165,7 @@ export default function ConnectedServiceFormContent({ )} id="addConnectedServiceClientSecret" placeholder="Client Secret" - type="text" + type="password" {...field} /> )} From 78795df91d8826f62175f8e130d639a4204110d3 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 3 Oct 2024 11:12:40 +0200 Subject: [PATCH 13/13] show provider id in the update modal --- .../admin/UpdateConnectedServiceButton.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/client/src/features/admin/UpdateConnectedServiceButton.tsx b/client/src/features/admin/UpdateConnectedServiceButton.tsx index eee83c77bb..1ccb20e0fc 100644 --- a/client/src/features/admin/UpdateConnectedServiceButton.tsx +++ b/client/src/features/admin/UpdateConnectedServiceButton.tsx @@ -23,6 +23,8 @@ import { useForm } from "react-hook-form"; import { Button, Form, + Input, + Label, Modal, ModalBody, ModalFooter, @@ -157,6 +159,20 @@ function UpdateConnectedServiceModal({ {result.error && } +
+ + +
+