diff --git a/client/src/features/admin/AddConnectedServiceButton.tsx b/client/src/features/admin/AddConnectedServiceButton.tsx index 69938d1ca..341d043d7 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 0f1b806a7..b7a4d4b0d 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 000000000..4cdebce53 --- /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 52557eb72..a0e5eaa6f 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 000000000..a6dc63aac --- /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 cb42c983a..04abb19dd 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 19ca9cd66..3e97d537d 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;