From 35303e7fe63917dcaf86723d01d06b617e0cc566 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Wed, 24 Apr 2024 19:19:04 -0400 Subject: [PATCH] feat(certs): replace cert upload flow with cert listing (#1240) --- src/app/AppLayout/SslErrorModal.tsx | 2 +- .../SecurityPanel/CertificateUploadModal.tsx | 162 ------------------ src/app/SecurityPanel/ImportCertificate.tsx | 81 +++++++-- src/app/SecurityPanel/SecurityPanel.tsx | 4 +- src/app/Topology/Shared/utils.tsx | 2 +- 5 files changed, 69 insertions(+), 182 deletions(-) delete mode 100644 src/app/SecurityPanel/CertificateUploadModal.tsx diff --git a/src/app/AppLayout/SslErrorModal.tsx b/src/app/AppLayout/SslErrorModal.tsx index 4ea2ee04d..d17ae9d8b 100644 --- a/src/app/AppLayout/SslErrorModal.tsx +++ b/src/app/AppLayout/SslErrorModal.tsx @@ -42,7 +42,7 @@ export const SslErrorModal: React.FC = ({ visible, onDismiss description="The connection failed because the SSL Certificate for the target is not trusted." > - To add the SSL certificate for this target, go to   + To view the trusted application certificates, go to   diff --git a/src/app/SecurityPanel/CertificateUploadModal.tsx b/src/app/SecurityPanel/CertificateUploadModal.tsx deleted file mode 100644 index e4b1f8a5d..000000000 --- a/src/app/SecurityPanel/CertificateUploadModal.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright The Cryostat Authors. - * - * 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 { FUpload, MultiFileUpload, UploadCallbacks } from '@app/Shared/Components/FileUploads'; -import { LoadingProps } from '@app/Shared/Components/types'; -import { ServiceContext } from '@app/Shared/Services/Services'; -import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; -import { portalRoot } from '@app/utils/utils'; -import { ActionGroup, Button, Form, FormGroup, Modal, ModalVariant } from '@patternfly/react-core'; -import * as React from 'react'; -import { forkJoin, Observable, of } from 'rxjs'; -import { catchError, defaultIfEmpty, tap } from 'rxjs/operators'; - -export interface CertificateUploadModalProps { - visible: boolean; - onClose: () => void; -} - -export const CertificateUploadModal: React.FC = ({ visible, onClose }) => { - const addSubscriptions = useSubscriptions(); - const context = React.useContext(ServiceContext); - const submitRef = React.useRef(null); // Use ref to refer to submit trigger div - const abortRef = React.useRef(null); // Use ref to refer to abort trigger div - - const [allOks, setAllOks] = React.useState(false); - const [uploading, setUploading] = React.useState(false); - const [numOfFiles, setNumOfFiles] = React.useState(0); - - const reset = React.useCallback(() => { - setUploading(false); - setNumOfFiles(0); - }, [setUploading, setNumOfFiles]); - - const onFilesChange = React.useCallback( - (fileUploads: FUpload[]) => { - setAllOks(!fileUploads.some((f) => !f.progress || f.progress.progressVariant !== 'success')); - setNumOfFiles(fileUploads.length); - }, - [setNumOfFiles, setAllOks], - ); - - const handleClose = React.useCallback(() => { - if (uploading) { - abortRef.current && abortRef.current.click(); - } else { - reset(); - onClose(); - } - }, [uploading, reset, onClose]); - - const handleSubmit = React.useCallback(() => { - submitRef.current && submitRef.current.click(); - }, []); - - const onFileSubmit = React.useCallback( - (fileUploads: FUpload[], { getProgressUpdateCallback, onSingleSuccess, onSingleFailure }: UploadCallbacks) => { - setUploading(true); - - const tasks: Observable[] = []; - fileUploads.forEach((fileUpload) => { - tasks.push( - context.api - .uploadSSLCertificate( - fileUpload.file, - getProgressUpdateCallback(fileUpload.file.name), - fileUpload.abortSignal, - ) - .pipe( - tap({ - next: () => { - onSingleSuccess(fileUpload.file.name); - }, - error: (err) => { - onSingleFailure(fileUpload.file.name, err); - }, - }), - catchError((_) => of(false)), - ), - ); - }); - - addSubscriptions( - forkJoin(tasks) - .pipe(defaultIfEmpty([true])) - .subscribe((oks) => { - setUploading(false); - setAllOks(oks.reduce((prev, curr, _) => prev && curr, true)); - }), - ); - }, - [setUploading, context.api, addSubscriptions, setAllOks], - ); - - const submitButtonLoadingProps = React.useMemo( - () => - ({ - spinnerAriaValueText: 'Submitting', - spinnerAriaLabel: 'submitting-ssl-certificates', - isLoading: uploading, - }) as LoadingProps, - [uploading], - ); - - return ( - -
- - - - - {allOks && numOfFiles ? ( - - ) : ( - <> - - - - )} - -
-
- ); -}; diff --git a/src/app/SecurityPanel/ImportCertificate.tsx b/src/app/SecurityPanel/ImportCertificate.tsx index 9ded56492..bb7509eda 100644 --- a/src/app/SecurityPanel/ImportCertificate.tsx +++ b/src/app/SecurityPanel/ImportCertificate.tsx @@ -15,34 +15,80 @@ */ import { JmxSslDescription } from '@app/Shared/Components/JmxSslDescription'; -import { Button, Popover, Text, TextContent, TextVariants } from '@patternfly/react-core'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; +import { + Button, + EmptyState, + EmptyStateBody, + EmptyStateVariant, + Label, + List, + ListItem, + Panel, + PanelMain, + PanelMainBody, + Popover, + Spinner, + Text, + TextContent, + TextVariants, + Title, +} from '@patternfly/react-core'; import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; import * as React from 'react'; -import { CertificateUploadModal } from './CertificateUploadModal'; import { SecurityCard } from './types'; +import { tap } from 'rxjs/operators'; export const CertificateImport: React.FC = () => { - const [showModal, setShowModal] = React.useState(false); + const context = React.useContext(ServiceContext); + const addSubscription = useSubscriptions(); + const [loading, setLoading] = React.useState(true); + const [certs, setCerts] = React.useState([] as string[]); - const handleModalClose = () => { - setShowModal(false); - }; + React.useEffect(() => { + setLoading(true); + addSubscription( + context.api + .doGet('tls/certs', 'v3') + .pipe(tap((_) => setLoading(false))) + .subscribe(setCerts), + ); + }, [setLoading, addSubscription, context.api, setCerts]); return ( - <> - - - + + + + {loading ? ( + + ) : certs.length ? ( + + {certs.map((cert) => ( + + + + ))} + + ) : ( + + + No certificates + + No additional certificates are loaded. + + )} + + + ); }; -export const ImportCertificate: SecurityCard = { +export const ListCertificates: SecurityCard = { key: 'ssl', title: ( - Import SSL Certificates + Imported SSL Certificates }>