From 007b073f25f1908046aecb6602b3cbca8b03bff8 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 24 Jun 2021 13:42:28 -0400 Subject: [PATCH] Custom targets (#213) * Add target deletion button * Add target creation button * Extract modal subcomponent * Handle null/undefined aliases or aliases equal to connectUrl * Remove unused imports * Support Enter to submit * Display notification on target creation completion * More informative notification if target deletion fails --- src/app/Shared/Services/Api.service.tsx | 100 ++++++++++++------- src/app/Shared/Services/Target.service.tsx | 14 +-- src/app/TargetSelect/CreateTargetModal.tsx | 108 +++++++++++++++++++++ src/app/TargetSelect/TargetSelect.tsx | 101 ++++++++++++++++--- 4 files changed, 266 insertions(+), 57 deletions(-) create mode 100644 src/app/TargetSelect/CreateTargetModal.tsx diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 03708dc6d..ce31feadc 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -1,8 +1,8 @@ /* * Copyright The Cryostat Authors - * + * * The Universal Permissive License (UPL), Version 1.0 - * + * * Subject to the condition set forth below, permission is hereby granted to any * person obtaining a copy of this software, associated documentation and/or data * (collectively the "Software"), free of charge and under any and all copyright @@ -10,23 +10,23 @@ * licensable by each licensor hereunder covering either (i) the unmodified * Software as contributed to or provided by such licensor, or (ii) the Larger * Works (as defined below), to deal in both - * + * * (a) the Software, and * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if * one is included with the Software (each a "Larger Work" to which the Software * is contributed by such licensors), - * + * * without restriction, including without limitation the rights to copy, create * derivative works of, display, perform, and distribute the Software and make, * use, sell, offer for sale, import, export, have made, and have sold the * Software and the Larger Work(s), and to sublicense the foregoing rights on * either these or other terms. - * + * * This license is subject to the following condition: * The above copyright notice and either this complete permission notice or at * a minimum a reference to the UPL must be included in all copies or * substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -38,7 +38,7 @@ import { from, Observable, ObservableInput, of, ReplaySubject, forkJoin, throwError } from 'rxjs'; import { fromFetch } from 'rxjs/fetch'; import { catchError, combineLatest, concatMap, first, flatMap, map, tap } from 'rxjs/operators'; -import { TargetService } from './Target.service'; +import { Target, TargetService } from './Target.service'; import { Notifications } from '@app/Notifications/Notifications'; type ApiVersion = "v1" | "v2"; @@ -150,38 +150,68 @@ export class ApiService { ); } + createTarget(target: Target): Observable { + const form = new window.FormData(); + form.append('connectUrl', target.connectUrl); + if (!!target.alias && !!target.alias.trim()) { + form.append('alias', target.alias); + } + return this.sendRequest( + 'v2', `targets`, + { + method: 'POST', + body: form, + } + ).pipe( + map(resp => resp.ok), + first(), + ); + } + + deleteTarget(target: Target): Observable { + return this.sendRequest( + 'v2', `targets/${encodeURIComponent(target.connectUrl)}`, + { + method: 'DELETE', + } + ).pipe( + map(resp => resp.ok), + first(), + ); + } + createRecording(recordingAttributes: RecordingAttributes): Observable { - const form = new window.FormData(); - form.append('recordingName', recordingAttributes.name); - form.append('events', recordingAttributes.events); - if (!!recordingAttributes.duration && recordingAttributes.duration > 0) { - form.append('duration', String(recordingAttributes.duration)); + const form = new window.FormData(); + form.append('recordingName', recordingAttributes.name); + form.append('events', recordingAttributes.events); + if (!!recordingAttributes.duration && recordingAttributes.duration > 0) { + form.append('duration', String(recordingAttributes.duration)); + } + if (!!recordingAttributes.options){ + if (recordingAttributes.options.toDisk != null) { + form.append('toDisk', String(recordingAttributes.options.toDisk)); } - if (!!recordingAttributes.options){ - if (recordingAttributes.options.toDisk != null) { - form.append('toDisk', String(recordingAttributes.options.toDisk)); - } - if (!!recordingAttributes.options.maxAge && recordingAttributes.options.maxAge >= 0) { - form.append('maxAge', String(recordingAttributes.options.maxAge)); - } - if (!!recordingAttributes.options.maxSize && recordingAttributes.options.maxSize >= 0) { - form.append('maxSize', String(recordingAttributes.options.maxSize)); - } + if (!!recordingAttributes.options.maxAge && recordingAttributes.options.maxAge >= 0) { + form.append('maxAge', String(recordingAttributes.options.maxAge)); + } + if (!!recordingAttributes.options.maxSize && recordingAttributes.options.maxSize >= 0) { + form.append('maxSize', String(recordingAttributes.options.maxSize)); } + } - return this.target.target().pipe(concatMap(target => - this.sendRequest('v1', `targets/${encodeURIComponent(target.connectUrl)}/recordings`, { - method: 'POST', - body: form, - }).pipe( - tap(resp => { - if (resp.ok) { - this.notifications.success('Recording created'); - } - }), - map(resp => resp.ok), - first(), - ))); + return this.target.target().pipe(concatMap(target => + this.sendRequest('v1', `targets/${encodeURIComponent(target.connectUrl)}/recordings`, { + method: 'POST', + body: form, + }).pipe( + tap(resp => { + if (resp.ok) { + this.notifications.success('Recording created'); + } + }), + map(resp => resp.ok), + first(), + ))); } createSnapshot(): Observable { diff --git a/src/app/Shared/Services/Target.service.tsx b/src/app/Shared/Services/Target.service.tsx index 068d8e3cb..eb496a905 100644 --- a/src/app/Shared/Services/Target.service.tsx +++ b/src/app/Shared/Services/Target.service.tsx @@ -1,8 +1,8 @@ /* * Copyright The Cryostat Authors - * + * * The Universal Permissive License (UPL), Version 1.0 - * + * * Subject to the condition set forth below, permission is hereby granted to any * person obtaining a copy of this software, associated documentation and/or data * (collectively the "Software"), free of charge and under any and all copyright @@ -10,23 +10,23 @@ * licensable by each licensor hereunder covering either (i) the unmodified * Software as contributed to or provided by such licensor, or (ii) the Larger * Works (as defined below), to deal in both - * + * * (a) the Software, and * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if * one is included with the Software (each a "Larger Work" to which the Software * is contributed by such licensors), - * + * * without restriction, including without limitation the rights to copy, create * derivative works of, display, perform, and distribute the Software and make, * use, sell, offer for sale, import, export, have made, and have sold the * Software and the Larger Work(s), and to sublicense the foregoing rights on * either these or other terms. - * + * * This license is subject to the following condition: * The above copyright notice and either this complete permission notice or at * a minimum a reference to the UPL must be included in all copies or * substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -52,7 +52,7 @@ class TargetService { private readonly _sslFailure: Subject = new Subject(); setTarget(target: Target): void { - if (target === NO_TARGET || (!!target.alias && !!target.connectUrl)) { + if (target === NO_TARGET || !!target.connectUrl) { this._target.next(target); } else { throw new Error("Malformed target"); diff --git a/src/app/TargetSelect/CreateTargetModal.tsx b/src/app/TargetSelect/CreateTargetModal.tsx new file mode 100644 index 000000000..70cb4a43a --- /dev/null +++ b/src/app/TargetSelect/CreateTargetModal.tsx @@ -0,0 +1,108 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { Target } from '@app/Shared/Services/Target.service'; +import { ActionGroup, Button, ButtonType, Form, FormGroup, Modal, ModalVariant, TextInput } from '@patternfly/react-core'; +import * as React from 'react'; + +export interface CreateTargetModalProps { + visible: boolean; + onSubmit: (target: Target) => void; + onDismiss: () => void; +} + +export const CreateTargetModal: React.FunctionComponent = (props) => { + const [connectUrl, setConnectUrl] = React.useState(''); + const [alias, setAlias] = React.useState(''); + + const createTarget = React.useCallback(() => { + props.onSubmit({ connectUrl, alias: alias.trim() || connectUrl }); + setConnectUrl(''); + setAlias(''); + }, [props.onSubmit, connectUrl, alias]); + + const handleKeyDown = React.useCallback((evt) => { + if (evt.key === 'Enter') { + createTarget(); + } + }, [createTarget]); + + return (<> + +
+ + + + + + +
+ + + +
+ ); +} diff --git a/src/app/TargetSelect/TargetSelect.tsx b/src/app/TargetSelect/TargetSelect.tsx index 329e83785..81793953c 100644 --- a/src/app/TargetSelect/TargetSelect.tsx +++ b/src/app/TargetSelect/TargetSelect.tsx @@ -1,8 +1,8 @@ /* * Copyright The Cryostat Authors - * + * * The Universal Permissive License (UPL), Version 1.0 - * + * * Subject to the condition set forth below, permission is hereby granted to any * person obtaining a copy of this software, associated documentation and/or data * (collectively the "Software"), free of charge and under any and all copyright @@ -10,23 +10,23 @@ * licensable by each licensor hereunder covering either (i) the unmodified * Software as contributed to or provided by such licensor, or (ii) the Larger * Works (as defined below), to deal in both - * + * * (a) the Software, and * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if * one is included with the Software (each a "Larger Work" to which the Software * is contributed by such licensors), - * + * * without restriction, including without limitation the rights to copy, create * derivative works of, display, perform, and distribute the Software and make, * use, sell, offer for sale, import, export, have made, and have sold the * Software and the Larger Work(s), and to sublicense the foregoing rights on * either these or other terms. - * + * * This license is subject to the following condition: * The above copyright notice and either this complete permission notice or at * a minimum a reference to the UPL must be included in all copies or * substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -41,9 +41,14 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { NotificationsContext } from '@app/Notifications/Notifications'; import { NO_TARGET, Target } from '@app/Shared/Services/Target.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Button, Card, CardActions, CardBody, CardHeader, CardHeaderMain, Grid, GridItem, Select, SelectOption, SelectVariant, Text, TextVariants } from '@patternfly/react-core'; -import { ContainerNodeIcon, Spinner2Icon } from '@patternfly/react-icons'; -import { filter, first } from 'rxjs/operators'; +import { Button, Card, CardActions, CardBody, CardHeader, CardHeaderMain, Grid, + GridItem, Select, SelectOption, SelectVariant, Text, TextVariants +} from '@patternfly/react-core'; +import { ContainerNodeIcon, PlusCircleIcon, Spinner2Icon, TrashIcon } from '@patternfly/react-icons'; +import { of } from 'rxjs'; +import { catchError, filter, first } from 'rxjs/operators'; + +import { CreateTargetModal } from './CreateTargetModal'; export interface TargetSelectProps { isCompact?: boolean; @@ -58,6 +63,7 @@ export const TargetSelect: React.FunctionComponent = (props) const [targets, setTargets] = React.useState([] as Target[]); const [expanded, setExpanded] = React.useState(false); const [isLoading, setLoading] = React.useState(true); + const [isModalOpen, setModalOpen] = React.useState(false); const addSubscription = useSubscriptions(); const refreshTargetList = React.useCallback(() => { @@ -138,6 +144,47 @@ export const TargetSelect: React.FunctionComponent = (props) setExpanded(false); }; + const showCreateTargetModal = React.useCallback(() => { + setModalOpen(true); + }, [setModalOpen]); + + const createTarget = React.useCallback((target: Target) => { + setLoading(true); + addSubscription( + context.api.createTarget(target) + .pipe(first(), catchError(() => of(false))) + .subscribe(success => { + setLoading(false); + setModalOpen(false); + if (success) { + notifications.info('Target Created'); + } else { + notifications.danger('Target Creation Failed'); + } + }) + ); + }, [context.api, notifications, setModalOpen]); + + const deleteTarget = React.useCallback(() => { + setLoading(true); + addSubscription( + context.api.deleteTarget(selected) + .pipe(first()) + .subscribe(() => { + setLoading(false); + }, () => { + setLoading(false); + let id: string; + if (selected.alias === selected.connectUrl) { + id = selected.alias; + } else { + id = `${selected.alias} [${selected.connectUrl}]` + } + notifications.danger('Target Deletion Failed', `The selected target (${id}) could not be deleted`); + }) + ); + }, [context.api, selected]); + return (<> @@ -149,6 +196,18 @@ export const TargetSelect: React.FunctionComponent = (props) +