From ec23856cc0dfeeb5b61beab39a94225003d5ff56 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 27 Sep 2022 20:40:39 +0000 Subject: [PATCH] fix(package.json): fix broken prettier config, add other useful scripts (#478) * fix(package.json): quote prettier targets * split format into format:apply * add check-only format script * don't re-exec npm directly, use execpath allow ex. yarn to call back to yarn * reorder license check/apply * use npm-run-all * linting also runs format check * also lint .ts * add script to apply lint fixes * ci checks code format, not only license * correct yarn.lock after rebase fix * run format:apply * rename script * alias to check, not apply * simplify alias --- .github/workflows/ci.yaml | 2 +- package.json | 19 +- src/app/About/AboutCryostatModal.tsx | 25 +- src/app/About/AboutDescription.tsx | 121 ++-- src/app/AppLayout/AppLayout.tsx | 265 ++++---- src/app/AppLayout/AuthModal.tsx | 54 +- src/app/AppLayout/JmxAuthForm.tsx | 15 +- src/app/AppLayout/SslErrorModal.tsx | 64 +- .../AllTargetsArchivedRecordingsTable.tsx | 569 ++++++++-------- src/app/Archives/ArchiveUploadModal.tsx | 111 ++-- src/app/Archives/Archives.tsx | 24 +- src/app/BreadcrumbPage/BreadcrumbPage.tsx | 59 +- src/app/CreateRecording/CreateRecording.tsx | 39 +- .../CreateRecording/CustomRecordingForm.tsx | 502 ++++++++------ .../CreateRecording/SnapshotRecordingForm.tsx | 49 +- src/app/Dashboard/Dashboard.tsx | 8 +- src/app/DurationPicker/DurationPicker.tsx | 72 +-- src/app/ErrorView/ErrorView.tsx | 38 +- src/app/Events/EventTemplates.tsx | 376 ++++++----- src/app/Events/EventTypes.tsx | 142 ++-- src/app/Events/Events.tsx | 51 +- src/app/LoadingView/LoadingView.tsx | 29 +- src/app/Login/BasicAuthForm.tsx | 97 +-- src/app/Login/ConnectionError.tsx | 6 +- src/app/Login/Login.tsx | 37 +- src/app/Login/NoopAuthForm.tsx | 11 +- .../Login/OpenShiftPlaceholderAuthForm.tsx | 50 +- src/app/Modal/CancelUploadModal.tsx | 15 +- src/app/Modal/DeleteWarningModal.tsx | 16 +- src/app/Modal/DeleteWarningUtils.tsx | 98 +-- src/app/NotFound/NotFound.tsx | 39 +- src/app/NotFound/NotFoundCard.tsx | 24 +- src/app/Notifications/NotificationCenter.tsx | 167 ++--- src/app/Notifications/Notifications.tsx | 43 +- src/app/RecordingMetadata/BulkEditLabels.tsx | 31 +- src/app/RecordingMetadata/ClickableLabel.tsx | 49 +- src/app/RecordingMetadata/LabelCell.tsx | 70 +- src/app/RecordingMetadata/RecordingLabel.tsx | 8 +- .../RecordingLabelFields.tsx | 167 ++--- src/app/Recordings/ActiveRecordingsTable.tsx | 515 +++++++++------ .../Recordings/ArchivedRecordingsTable.tsx | 462 +++++++------ src/app/Recordings/Filters/DateTimePicker.tsx | 41 +- src/app/Recordings/Filters/DurationFilter.tsx | 30 +- src/app/Recordings/Filters/LabelFilter.tsx | 73 ++- src/app/Recordings/Filters/NameFilter.tsx | 14 +- .../Filters/RecordingStateFilter.tsx | 22 +- src/app/Recordings/RecordingActions.tsx | 57 +- src/app/Recordings/RecordingFilters.tsx | 152 +++-- src/app/Recordings/Recordings.tsx | 18 +- src/app/Recordings/RecordingsTable.tsx | 143 ++-- src/app/Recordings/ReportFrame.tsx | 38 +- src/app/Rules/CreateRule.tsx | 301 ++++++--- src/app/Rules/RuleDeleteWarningModal.tsx | 32 +- src/app/Rules/Rules.tsx | 274 +++++--- src/app/Rules/RulesUploadModal.tsx | 175 ++--- .../SecurityPanel/CertificateUploadModal.tsx | 50 +- .../Credentials/CreateJmxCredentialModal.tsx | 43 +- .../Credentials/MatchedTargetsTable.tsx | 97 ++- .../Credentials/StoreJmxCredentials.tsx | 232 ++++--- src/app/Settings/AutoRefresh.tsx | 66 +- src/app/Settings/DeletionDialogControl.tsx | 82 ++- src/app/Settings/NotificationControl.tsx | 72 ++- src/app/Settings/Settings.tsx | 54 +- src/app/Settings/WebSocketDebounce.tsx | 62 +- src/app/Shared/MatchExpressionEvaluator.tsx | 132 ++-- .../Shared/Redux/RecordingFilterActions.tsx | 115 ++-- .../Shared/Redux/RecordingFilterReducer.tsx | 339 +++++----- src/app/Shared/Redux/ReduxStore.tsx | 21 +- src/app/Shared/Services/Api.service.tsx | 610 +++++++++--------- src/app/Shared/Services/Login.service.tsx | 101 ++- .../Services/NotificationChannel.service.tsx | 228 ++++--- src/app/Shared/Services/Report.service.tsx | 20 +- src/app/Shared/Services/Services.tsx | 11 +- src/app/Shared/Services/Settings.service.tsx | 10 +- src/app/Shared/Services/Target.service.tsx | 7 +- src/app/Shared/Services/Targets.service.tsx | 50 +- src/app/TargetSelect/CreateTargetModal.tsx | 119 ++-- src/app/TargetSelect/TargetSelect.tsx | 270 ++++---- src/app/TargetView/NoTargetSelected.tsx | 45 +- src/app/TargetView/TargetView.tsx | 33 +- .../FormSelectTemplateSelector.tsx | 62 +- src/app/TemplateSelector/TemplateSelector.tsx | 25 +- src/app/utils/LocalStorage.ts | 10 +- src/app/utils/utils.ts | 8 +- src/index.tsx | 28 +- src/test/About/About.test.tsx | 32 +- ...AllTargetsArchivedRecordingsTable.test.tsx | 116 ++-- src/test/Archives/Archives.test.tsx | 37 +- src/test/Common.tsx | 103 ++- src/test/Events/EventTemplates.test.tsx | 244 +++---- .../BulkEditLabels.test.tsx | 50 +- .../ClickableLabel.test.tsx | 60 +- .../RecordingMetadata.tsx/LabelCell.test.tsx | 64 +- .../Recordings/ActiveRecordingsTable.test.tsx | 432 ++++++------- .../ArchivedRecordingsTable.test.tsx | 101 +-- .../Filters/DateTimePicker.test.tsx | 156 ++--- .../Filters/DurationFilter.test.tsx | 110 ++-- .../Recordings/Filters/LabelFilter.test.tsx | 86 +-- .../Recordings/Filters/NameFilter.test.tsx | 76 +-- .../Filters/RecordingStateFilter.test.tsx | 83 ++- src/test/Recordings/RecordingFilters.test.tsx | 273 ++++---- src/test/Recordings/Recordings.test.tsx | 77 ++- src/test/Recordings/RecordingsTable.test.tsx | 95 ++- src/test/Rules/CreateRule.test.tsx | 41 +- src/test/Rules/Rules.test.tsx | 83 +-- .../Credentials/StoreJmxCredentials.test.tsx | 102 ++- src/test/Targets/TargetSelect.test.tsx | 137 ++-- yarn.lock | 334 +++++++++- 108 files changed, 6524 insertions(+), 5279 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bb12a92d4..adb22e3ca 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - uses: bahmutov/npm-install@v1 - - run: yarn license:check + - run: yarn format:check # - run: yarn lint - run: yarn build - run: yarn test:ci diff --git a/package.json b/package.json index fe72e0447..e86926513 100644 --- a/package.json +++ b/package.json @@ -7,18 +7,26 @@ "license": "UPL", "private": true, "scripts": { - "build": "webpack --config webpack.prod.js && npm run test", + "build": "npm-run-all -l build:notests test", "build:notests": "webpack --config webpack.prod.js", "clean": "rimraf dist", "start:dev": "webpack serve --hot --color --progress --config webpack.dev.js", "test": "jest --maxWorkers=50% --coverage=true", "test:ci": "jest --maxWorkers=50%", - "eslint": "eslint --ext .tsx,.js ./src/", + "eslint": "npm-run-all eslint:check", + "eslint:check": "eslint --ext .ts,.tsx,.js ./src/", + "eslint:apply": "eslint --ext .ts,.tsx,.js --fix ./src/", "license:check": "license-check-and-add check -f license-config.json", - "lint": "npm run license-check-and-add && npm run eslint", - "format": "prettier --check --write ./src/**/*.{tsx,ts} && license-check-and-add add -f license-config.json", + "license:apply": "license-check-and-add add -f license-config.json", + "lint": "npm-run-all -l -p format eslint", + "format": "npm-run-all format:check", + "format:check": "npm-run-all -l license:check prettier:check", + "format:apply": "npm-run-all -l -p license:apply prettier:apply", + "prettier:check": "prettier --check './src/**/*.{tsx,ts}'", + "prettier:apply": "prettier --write './src/**/*.{tsx,ts}'", "build:bundle-profile": "webpack --config webpack.prod.js --profile --json > stats.json", - "bundle-profile:analyze": "npm run build:bundle-profile && webpack-bundle-analyzer ./stats.json", + "build:bundle-analyze": "webpack-bundle-analyzer ./stats.json", + "build:bundle-profile-analyze": "npm-run-all -l build:bundle-profile build:bundle-analyze", "yarn:install": "yarn install", "yarn:frzinstall": "yarn install --frozen-lockfile" }, @@ -54,6 +62,7 @@ "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.3.0", "nanoid": "^3.1.31", + "npm-run-all": "^4.1.5", "prettier": "^2.4.1", "prop-types": "^15.7.2", "raw-loader": "^4.0.2", diff --git a/src/app/About/AboutCryostatModal.tsx b/src/app/About/AboutCryostatModal.tsx index 684afcee4..a3684bc60 100644 --- a/src/app/About/AboutCryostatModal.tsx +++ b/src/app/About/AboutCryostatModal.tsx @@ -36,22 +36,23 @@ * SOFTWARE. */ -import { AboutModal } from "@patternfly/react-core" -import React from "react" +import { AboutModal } from '@patternfly/react-core'; +import React from 'react'; import cryostatLogoWhite from '@app/assets/logo-cryostat-3.svg'; -import { AboutDescription, CRYOSTAT_TRADEMARK } from "./AboutDescription"; +import { AboutDescription, CRYOSTAT_TRADEMARK } from './AboutDescription'; -export const AboutCryostatModal = ({isOpen, onClose}) => { - - return(<> - { + return ( + <> + - - - ); -} + + + + ); +}; diff --git a/src/app/About/AboutDescription.tsx b/src/app/About/AboutDescription.tsx index eb504ea37..39a4cd3bc 100644 --- a/src/app/About/AboutDescription.tsx +++ b/src/app/About/AboutDescription.tsx @@ -36,10 +36,10 @@ * SOFTWARE. */ -import { Text, TextContent, TextList, TextListItem, TextVariants } from "@patternfly/react-core" -import React from "react" -import { ServiceContext } from "@app/Shared/Services/Services"; -import { NotificationsContext } from "@app/Notifications/Notifications"; +import { Text, TextContent, TextList, TextListItem, TextVariants } from '@patternfly/react-core'; +import React from 'react'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { NotificationsContext } from '@app/Notifications/Notifications'; export const CRYOSTAT_TRADEMARK = 'Copyright The Cryostat Authors, The Universal Permissive License (UPL), Version 1.0'; @@ -51,7 +51,7 @@ export const AboutDescription = () => { React.useEffect(() => { const sub = serviceContext.api.cryostatVersion().subscribe(setCryostatVersion); return () => sub.unsubscribe(); - }, [serviceContext]) + }, [serviceContext]); const cryostatCommitHash = React.useMemo(() => { if (!cryostatVersion) { @@ -60,60 +60,69 @@ export const AboutDescription = () => { const expr = /^(?[a-zA-Z0-9-_.]+-[0-9]+-[a-z0-9]+)(?:-dirty)?$/; const result = cryostatVersion.match(expr); if (!result) { - notificationsContext.warning('Cryostat Version Parse Failure', `Could not parse Cryostat version string '${cryostatVersion}'.`); + notificationsContext.warning( + 'Cryostat Version Parse Failure', + `Could not parse Cryostat version string '${cryostatVersion}'.` + ); return 'main'; } return result.groups?.describe || 'main'; }, [cryostatVersion, notificationsContext]); - return(<> + return ( + <> - - - Version - - - {cryostatVersion} - - - Homepage - - - cryostat.io - - - Bugs - - - - Known Issues -  |  - - File a Report - - - - - Mailing List - - - Google Groups - - - Open Source License - - - License - - - - ); -} + + Version + + + {cryostatVersion} + + + Homepage + + + cryostat.io + + + Bugs + + + + Known Issues + +  |  + + File a Report + + + + Mailing List + + + Google Groups + + + Open Source License + + + License + + + + + + ); +}; diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index 9f28a40ef..3ecab1663 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -40,12 +40,32 @@ import * as _ from 'lodash'; import { ServiceContext } from '@app/Shared/Services/Services'; import { NotificationCenter } from '@app/Notifications/NotificationCenter'; import { IAppRoute, navGroups, routes } from '@app/routes'; -import { Alert, AlertGroup, AlertVariant, AlertActionCloseButton, - Brand, Button, Dropdown, DropdownGroup, DropdownItem, DropdownToggle, Nav, NavGroup, NavItem, NavList, NotificationBadge, Page, PageHeader, - PageHeaderTools, PageHeaderToolsGroup, PageHeaderToolsItem, PageSidebar, SkipToContent +import { + Alert, + AlertGroup, + AlertVariant, + AlertActionCloseButton, + Brand, + Button, + Dropdown, + DropdownGroup, + DropdownItem, + DropdownToggle, + Nav, + NavGroup, + NavItem, + NavList, + NotificationBadge, + Page, + PageHeader, + PageHeaderTools, + PageHeaderToolsGroup, + PageHeaderToolsItem, + PageSidebar, + SkipToContent, } from '@patternfly/react-core'; import { BellIcon, CaretDownIcon, CogIcon, HelpIcon, UserIcon } from '@patternfly/react-icons'; -import { map, } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { matchPath, NavLink, useHistory, useLocation } from 'react-router-dom'; import { Notification, Notifications, NotificationsContext } from '@app/Notifications/Notifications'; import { AuthModal } from './AuthModal'; @@ -59,12 +79,12 @@ interface IAppLayout { children: React.ReactNode; } -const AppLayout: React.FunctionComponent = ({children}) => { +const AppLayout: React.FunctionComponent = ({ children }) => { const serviceContext = React.useContext(ServiceContext); const notificationsContext = React.useContext(NotificationsContext); const routerHistory = useHistory(); const logoProps = { - href: '/' + href: '/', }; const [isNavOpen, setIsNavOpen] = React.useState(true); const [isMobileView, setIsMobileView] = React.useState(true); @@ -94,26 +114,41 @@ const AppLayout: React.FunctionComponent = ({children}) => { }, []); React.useEffect(() => { - const sub = notificationsContext.unreadNotifications().subscribe(s => setUnreadNotificationsCount(s.length)); + const sub = notificationsContext.unreadNotifications().subscribe((s) => setUnreadNotificationsCount(s.length)); return () => sub.unsubscribe(); - }, [notificationsContext, notificationsContext.unreadNotifications, unreadNotificationsCount, setUnreadNotificationsCount]); + }, [ + notificationsContext, + notificationsContext.unreadNotifications, + unreadNotificationsCount, + setUnreadNotificationsCount, + ]); React.useEffect(() => { const sub = notificationsContext .unreadNotifications() - .pipe(map((notifications: Notification[]) => - _.filter(notifications, n => n.variant === AlertVariant.danger || n.variant === AlertVariant.warning) - )) - .subscribe(s => setErrorNotificationsCount(s.length)); + .pipe( + map((notifications: Notification[]) => + _.filter(notifications, (n) => n.variant === AlertVariant.danger || n.variant === AlertVariant.warning) + ) + ) + .subscribe((s) => setErrorNotificationsCount(s.length)); return () => sub.unsubscribe(); - }, [notificationsContext, notificationsContext.unreadNotifications, unreadNotificationsCount, setUnreadNotificationsCount]); + }, [ + notificationsContext, + notificationsContext.unreadNotifications, + unreadNotificationsCount, + setUnreadNotificationsCount, + ]); const dismissAuthModal = () => { setShowAuthModal(false); }; - const handleMarkNotificationRead = React.useCallback(key => { - notificationsContext.setRead(key, true); - }, [notificationsContext]); + const handleMarkNotificationRead = React.useCallback( + (key) => { + notificationsContext.setRead(key, true); + }, + [notificationsContext] + ); React.useEffect(() => { const sub = serviceContext.target.sslFailure().subscribe(() => { @@ -124,19 +159,19 @@ const AppLayout: React.FunctionComponent = ({children}) => { const dismissSslErrorModal = () => { setShowSslErrorModal(false); - } + }; const onNavToggleMobile = () => { setIsNavOpenMobile(!isNavOpenMobile); }; const onNavToggle = () => { setIsNavOpen(!isNavOpen); - } + }; const onPageResize = (props: { mobileView: boolean; windowSize: number }) => { setIsMobileView(props.mobileView); }; const mobileOnSelect = (selected) => { - if(isMobileView) setIsNavOpenMobile(false) + if (isMobileView) setIsNavOpenMobile(false); }; const handleSettingsButtonClick = () => { routerHistory.push('/settings'); @@ -152,20 +187,18 @@ const AppLayout: React.FunctionComponent = ({children}) => { }; React.useEffect(() => { - const sub = serviceContext.login.getSessionState().subscribe(sessionState => { + const sub = serviceContext.login.getSessionState().subscribe((sessionState) => { setShowUserIcon(sessionState === SessionState.USER_SESSION); }); return () => sub.unsubscribe(); }, [serviceContext.target]); const handleLogout = React.useCallback(() => { - const sub = serviceContext.login.setLoggedOut().subscribe(); - return () => sub.unsubscribe(); + const sub = serviceContext.login.setLoggedOut().subscribe(); + return () => sub.unsubscribe(); }, [serviceContext.login]); - const handleUserInfoToggle = React.useCallback(() => - setShowUserInfoDropdown(v => !v), - [setShowUserInfoDropdown]); + const handleUserInfoToggle = React.useCallback(() => setShowUserInfoDropdown((v) => !v), [setShowUserInfoDropdown]); React.useEffect(() => { const sub = serviceContext.login.getUsername().subscribe(setUsername); @@ -175,40 +208,34 @@ const AppLayout: React.FunctionComponent = ({children}) => { const userInfoItems = [ Logout - + , ]; const UserInfoToggle = ( - {username || } + {username || } ); - const HeaderTools = (<> - - - - 0 ? 'attention' : unreadNotificationsCount === 0 ? 'read' : 'unread'} - onClick={handleNotificationCenterToggle} aria-label='Notifications' + const HeaderTools = ( + <> + + + + 0 ? 'attention' : unreadNotificationsCount === 0 ? 'read' : 'unread'} + onClick={handleNotificationCenterToggle} + aria-label="Notifications" > - - - - - - - - ) -} \ No newline at end of file + const handleClick = () => { + routerHistory.push('/security'); + props.onDismiss(); + }; + + return ( + + + To add the SSL certificate for this target, go to   + + + + ); +}; diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 44b07769b..5f9b469f3 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -40,7 +40,18 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { Target } from '@app/Shared/Services/Target.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge, Checkbox, EmptyState, EmptyStateIcon, Title } from '@patternfly/react-core'; +import { + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + SearchInput, + Badge, + Checkbox, + EmptyState, + EmptyStateIcon, + Title, +} from '@patternfly/react-core'; import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; @@ -50,60 +61,66 @@ import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; import { LoadingView } from '@app/LoadingView/LoadingView'; import _ from 'lodash'; -export interface AllTargetsArchivedRecordingsTableProps { } +export interface AllTargetsArchivedRecordingsTableProps {} -export const AllTargetsArchivedRecordingsTable: React.FunctionComponent = () => { - const context = React.useContext(ServiceContext); +export const AllTargetsArchivedRecordingsTable: React.FunctionComponent = + () => { + const context = React.useContext(ServiceContext); - const [targets, setTargets] = React.useState([] as Target[]); - const [counts, setCounts] = React.useState([] as number[]); - const [search, setSearch] = React.useState(''); - const [searchedTargets, setSearchedTargets] = React.useState([] as Target[]); - const [expandedTargets, setExpandedTargets] = React.useState([] as Target[]); - const [hideEmptyTargets, setHideEmptyTargets] = React.useState(true); - const [isLoading, setIsLoading] = React.useState(false); - const addSubscription = useSubscriptions(); + const [targets, setTargets] = React.useState([] as Target[]); + const [counts, setCounts] = React.useState([] as number[]); + const [search, setSearch] = React.useState(''); + const [searchedTargets, setSearchedTargets] = React.useState([] as Target[]); + const [expandedTargets, setExpandedTargets] = React.useState([] as Target[]); + const [hideEmptyTargets, setHideEmptyTargets] = React.useState(true); + const [isLoading, setIsLoading] = React.useState(false); + const addSubscription = useSubscriptions(); - const searchedTargetsRef = React.useRef(searchedTargets); + const searchedTargetsRef = React.useRef(searchedTargets); - const tableColumns: string[] = [ - 'Target', - 'Count' - ]; + const tableColumns: string[] = ['Target', 'Count']; - const updateCount = React.useCallback((connectUrl: string, delta: number) => { - for(let i = 0; i < targets.length; i++) { - if(targets[i].connectUrl === connectUrl) { - setCounts(old => { - let updated = [...old]; - updated[i] += delta; - return updated; - }); - break; - } - } - }, [targets, setCounts]); + const updateCount = React.useCallback( + (connectUrl: string, delta: number) => { + for (let i = 0; i < targets.length; i++) { + if (targets[i].connectUrl === connectUrl) { + setCounts((old) => { + let updated = [...old]; + updated[i] += delta; + return updated; + }); + break; + } + } + }, + [targets, setCounts] + ); - const handleTargetsAndCounts = React.useCallback((targetNodes) => { - let updatedTargets: Target[] = []; - let updatedCounts: number[] = []; - for (const node of targetNodes) { - const target: Target = { - connectUrl: node.target.serviceUri, - alias: node.target.alias, - } - updatedTargets.push(target); - updatedCounts.push(node.recordings.archived.aggregate.count as number); - } - setTargets(updatedTargets); - setCounts(updatedCounts); - setIsLoading(false); - },[setTargets, setCounts, setIsLoading]); + const handleTargetsAndCounts = React.useCallback( + (targetNodes) => { + let updatedTargets: Target[] = []; + let updatedCounts: number[] = []; + for (const node of targetNodes) { + const target: Target = { + connectUrl: node.target.serviceUri, + alias: node.target.alias, + }; + updatedTargets.push(target); + updatedCounts.push(node.recordings.archived.aggregate.count as number); + } + setTargets(updatedTargets); + setCounts(updatedCounts); + setIsLoading(false); + }, + [setTargets, setCounts, setIsLoading] + ); - const refreshTargetsAndCounts = React.useCallback(() => { - setIsLoading(true); - addSubscription( - context.api.graphql(` + const refreshTargetsAndCounts = React.useCallback(() => { + setIsLoading(true); + addSubscription( + context.api + .graphql( + ` query { targetNodes { target { @@ -118,17 +135,19 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent v.data.targetNodes) - ) - .subscribe(handleTargetsAndCounts) - ); - }, [addSubscription, context, context.api, setIsLoading, handleTargetsAndCounts]); + }` + ) + .pipe(map((v) => v.data.targetNodes)) + .subscribe(handleTargetsAndCounts) + ); + }, [addSubscription, context, context.api, setIsLoading, handleTargetsAndCounts]); - const getCountForNewTarget = React.useCallback((target: Target) => { - addSubscription( - context.api.graphql(` + const getCountForNewTarget = React.useCallback( + (target: Target) => { + addSubscription( + context.api + .graphql( + ` query { targetNodes(filter: { name: "${target.connectUrl}" }) { recordings { @@ -139,122 +158,144 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent setCounts(old => old.concat(v.data.targetNodes[0].recordings.archived.aggregate.count as number))) + }` + ) + .subscribe((v) => + setCounts((old) => old.concat(v.data.targetNodes[0].recordings.archived.aggregate.count as number)) + ) + ); + }, + [addSubscription, context, context.api, setCounts] ); - },[addSubscription, context, context.api, setCounts]); - const handleLostTarget = React.useCallback((target: Target) => { - let idx; - setTargets(old => { - for (idx = 0; idx < old.length; idx++) { - if (_.isEqual(target, old[idx])) break; - } - return old.filter(o => !_.isEqual(o, target)); - }); - setExpandedTargets(old => old.filter(o => !_.isEqual(o, target))); - setCounts(old => { - let updated = [...old]; - updated.splice(idx, 1); - return updated; - }); - }, [setTargets, setExpandedTargets, setCounts]); + const handleLostTarget = React.useCallback( + (target: Target) => { + let idx; + setTargets((old) => { + for (idx = 0; idx < old.length; idx++) { + if (_.isEqual(target, old[idx])) break; + } + return old.filter((o) => !_.isEqual(o, target)); + }); + setExpandedTargets((old) => old.filter((o) => !_.isEqual(o, target))); + setCounts((old) => { + let updated = [...old]; + updated.splice(idx, 1); + return updated; + }); + }, + [setTargets, setExpandedTargets, setCounts] + ); - const handleTargetNotification = React.useCallback((evt: TargetDiscoveryEvent) => { - const target: Target = { - connectUrl: evt.serviceRef.connectUrl, - alias: evt.serviceRef.alias, - } - if (evt.kind === 'FOUND') { - setTargets(old => old.concat(target)); - getCountForNewTarget(target); - } else if (evt.kind === 'LOST') { - handleLostTarget(target); - } - }, [setTargets, getCountForNewTarget, handleLostTarget]); + const handleTargetNotification = React.useCallback( + (evt: TargetDiscoveryEvent) => { + const target: Target = { + connectUrl: evt.serviceRef.connectUrl, + alias: evt.serviceRef.alias, + }; + if (evt.kind === 'FOUND') { + setTargets((old) => old.concat(target)); + getCountForNewTarget(target); + } else if (evt.kind === 'LOST') { + handleLostTarget(target); + } + }, + [setTargets, getCountForNewTarget, handleLostTarget] + ); - React.useEffect(() => { - refreshTargetsAndCounts(); - }, []); + React.useEffect(() => { + refreshTargetsAndCounts(); + }, []); - React.useEffect(() => { - searchedTargetsRef.current = searchedTargets; - }); + React.useEffect(() => { + searchedTargetsRef.current = searchedTargets; + }); - React.useEffect(() => { - if (!context.settings.autoRefreshEnabled()) { - return; - } - const id = window.setInterval(() => refreshTargetsAndCounts(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); - return () => window.clearInterval(id); - }, [context.target, context.settings, refreshTargetsAndCounts]); + React.useEffect(() => { + if (!context.settings.autoRefreshEnabled()) { + return; + } + const id = window.setInterval( + () => refreshTargetsAndCounts(), + context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits() + ); + return () => window.clearInterval(id); + }, [context.target, context.settings, refreshTargetsAndCounts]); - React.useEffect(() => { - let updatedSearchedTargets; - if (!search) { - updatedSearchedTargets = targets; - } else { - const searchText = search.trim().toLowerCase(); - updatedSearchedTargets = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) - } + React.useEffect(() => { + let updatedSearchedTargets; + if (!search) { + updatedSearchedTargets = targets; + } else { + const searchText = search.trim().toLowerCase(); + updatedSearchedTargets = targets.filter( + (t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText) + ); + } - if (!_.isEqual(searchedTargetsRef.current, updatedSearchedTargets)) { - setSearchedTargets(updatedSearchedTargets); - } - }, [search, targets]); + if (!_.isEqual(searchedTargetsRef.current, updatedSearchedTargets)) { + setSearchedTargets(updatedSearchedTargets); + } + }, [search, targets]); - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.TargetJvmDiscovery) - .pipe(concatMap(v => of(handleTargetNotification(v.message.event)))) - .subscribe(() => {} /* do nothing - callback will have already handled updating state */) - ); - }, [addSubscription, context, context.notificationChannel, handleTargetNotification]); + React.useEffect(() => { + addSubscription( + context.notificationChannel + .messages(NotificationCategory.TargetJvmDiscovery) + .pipe(concatMap((v) => of(handleTargetNotification(v.message.event)))) + .subscribe(() => {} /* do nothing - callback will have already handled updating state */) + ); + }, [addSubscription, context, context.notificationChannel, handleTargetNotification]); - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved) - .subscribe(v => { - updateCount(v.message.target, 1); - }) - ); - }, [addSubscription, context, context.notificationChannel, updateCount]); + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved).subscribe((v) => { + updateCount(v.message.target, 1); + }) + ); + }, [addSubscription, context, context.notificationChannel, updateCount]); + + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted).subscribe((v) => { + updateCount(v.message.target, -1); + }) + ); + }, [addSubscription, context, context.notificationChannel, updateCount]); - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted) - .subscribe(v => { - updateCount(v.message.target, -1); - }) + const toggleExpanded = React.useCallback( + (target) => { + const idx = expandedTargets.indexOf(target); + setExpandedTargets((expandedTargets) => + idx >= 0 + ? [...expandedTargets.slice(0, idx), ...expandedTargets.slice(idx + 1, expandedTargets.length)] + : [...expandedTargets, target] + ); + }, + [expandedTargets] ); - }, [addSubscription, context, context.notificationChannel, updateCount]); - const toggleExpanded = React.useCallback((target) => { - const idx = expandedTargets.indexOf(target); - setExpandedTargets(expandedTargets => idx >= 0 ? [...expandedTargets.slice(0, idx), ...expandedTargets.slice(idx + 1, expandedTargets.length)] : [...expandedTargets, target]); - }, [expandedTargets]); + const isHidden = React.useMemo(() => { + let isHidden: boolean[] = []; + targets.map((target, idx) => { + isHidden.push(!searchedTargets.includes(target) || (hideEmptyTargets && counts[idx] === 0)); + }); + return isHidden; + }, [targets, searchedTargets, hideEmptyTargets, counts]); - const isHidden = React.useMemo(() => { - let isHidden: boolean[] = []; - targets.map((target, idx) => { - isHidden.push(!searchedTargets.includes(target) || (hideEmptyTargets && counts[idx] === 0)) - }) - return isHidden; - },[targets, searchedTargets, hideEmptyTargets, counts]) + const targetRows = React.useMemo(() => { + return targets.map((target, idx) => { + let isExpanded: boolean = expandedTargets.includes(target); - const targetRows = React.useMemo(() => { - return targets.map((target, idx) => { - let isExpanded: boolean = expandedTargets.includes(target); - - const handleToggle = () => { - if (counts[idx] !== 0 || isExpanded) { - toggleExpanded(target); - } - }; + const handleToggle = () => { + if (counts[idx] !== 0 || isExpanded) { + toggleExpanded(target); + } + }; - return ( - - + - - {(target.alias == target.connectUrl) || !target.alias ? - `${target.connectUrl}` - : - `${target.alias} (${target.connectUrl})`} - - - - {counts[idx]} - - - - ); - }); - }, [targets, expandedTargets, counts, isHidden]); + + {target.alias == target.connectUrl || !target.alias + ? `${target.connectUrl}` + : `${target.alias} (${target.connectUrl})`} + + + {counts[idx]} + + + ); + }); + }, [targets, expandedTargets, counts, isHidden]); - const recordingRows = React.useMemo(() => { - return targets.map((target, idx) => { - let isExpanded: boolean = expandedTargets.includes(target); - - return ( - - - {isExpanded ? - - - - : - null} - - - ); - }); - }, [targets, expandedTargets, isHidden]); + const recordingRows = React.useMemo(() => { + return targets.map((target, idx) => { + let isExpanded: boolean = expandedTargets.includes(target); + + return ( + + + {isExpanded ? ( + + + + ) : null} + + + ); + }); + }, [targets, expandedTargets, isHidden]); - const rowPairs = React.useMemo(() => { - let rowPairs: JSX.Element[] = []; - for (let i = 0; i < targetRows.length; i++) { - rowPairs.push(targetRows[i]); - rowPairs.push(recordingRows[i]); + const rowPairs = React.useMemo(() => { + let rowPairs: JSX.Element[] = []; + for (let i = 0; i < targetRows.length; i++) { + rowPairs.push(targetRows[i]); + rowPairs.push(recordingRows[i]); + } + return rowPairs; + }, [targetRows, recordingRows]); + + const noTargets = React.useMemo(() => { + return isHidden.reduce((a, b) => a && b, true); + }, [isHidden]); + + let view: JSX.Element; + if (isLoading) { + view = ; + } else if (noTargets) { + view = ( + <> + + + + No Targets + + + + ); + } else { + view = ( + <> + + + + + {tableColumns.map((key) => ( + + {key} + + ))} + + + {rowPairs} + + + ); } - return rowPairs; - }, [targetRows, recordingRows]); - const noTargets = React.useMemo(() => { return ( - isHidden.reduce((a, b) => a && b, true) + <> + + + + + setSearch('')} + /> + + + + + setHideEmptyTargets((old) => !old)} + isChecked={hideEmptyTargets} + id={`all-archives-hide-check`} + aria-label={`all-archives-hide-check`} + /> + + + + + {view} + ); - }, [isHidden]); - - let view: JSX.Element; - if (isLoading) { - view = (); - } else if (noTargets) { - view = (<> - - - - No Targets - - - ); - } else { - view = (<> - - - - - {tableColumns.map((key) => ( - {key} - ))} - - - - {rowPairs} - - - ) - } - - return (<> - - - - - setSearch('')} - /> - - - - - setHideEmptyTargets(old => !old)} - isChecked={hideEmptyTargets} - id={`all-archives-hide-check`} - aria-label={`all-archives-hide-check`} - /> - - - - - {view} - ); -}; + }; diff --git a/src/app/Archives/ArchiveUploadModal.tsx b/src/app/Archives/ArchiveUploadModal.tsx index b21345125..556446d6e 100644 --- a/src/app/Archives/ArchiveUploadModal.tsx +++ b/src/app/Archives/ArchiveUploadModal.tsx @@ -48,7 +48,7 @@ export interface ArchiveUploadModalProps { onClose: () => void; } -export const ArchiveUploadModal: React.FunctionComponent = props => { +export const ArchiveUploadModal: React.FunctionComponent = (props) => { const context = React.useContext(ServiceContext); const notifications = React.useContext(NotificationsContext); const [uploadFile, setUploadFile] = React.useState(undefined as File | undefined); @@ -67,12 +67,15 @@ export const ArchiveUploadModal: React.FunctionComponent { - setRejected(false); - setUploadFile(file); - setFilename(filename); - setShowCancelPrompt(false); - }, [setRejected, setUploadFile, setFilename, setShowCancelPrompt]); + const handleFileChange = React.useCallback( + (file, filename) => { + setRejected(false); + setUploadFile(file); + setFilename(filename); + setShowCancelPrompt(false); + }, + [setRejected, setUploadFile, setFilename, setShowCancelPrompt] + ); const handleReject = React.useCallback(() => { setRejected(true); @@ -94,9 +97,7 @@ export const ArchiveUploadModal: React.FunctionComponent { @@ -105,51 +106,49 @@ export const ArchiveUploadModal: React.FunctionComponent - - + + - setShowCancelPrompt(false)} - /> - - - - - - - - - - - ); + setShowCancelPrompt(false)} + /> +
+ + + + + + + +
+
+ + ); }; diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index 2c7309d93..d6ab1f460 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -62,38 +62,36 @@ export const Archives = () => { the backend issues a notification with the "target" field set to the empty string, signalling that these recordings are not associated with any target. We can then match on the empty string when performing notification handling in the ArchivedRecordingsTable. - */ - const target: Target = { + */ + const target: Target = { connectUrl: UPLOADS_SUBDIRECTORY, alias: '', - } + }; const cardBody = React.useMemo(() => { return archiveEnabled ? ( - setActiveTab(Number(idx))}> - + setActiveTab(Number(idx))}> + - - + + ) : ( - + Archives Unavailable ); - }, [archiveEnabled, activeTab]) + }, [archiveEnabled, activeTab]); return ( - + - - { cardBody } - + {cardBody} ); diff --git a/src/app/BreadcrumbPage/BreadcrumbPage.tsx b/src/app/BreadcrumbPage/BreadcrumbPage.tsx index 2f3062855..6f23de9b4 100644 --- a/src/app/BreadcrumbPage/BreadcrumbPage.tsx +++ b/src/app/BreadcrumbPage/BreadcrumbPage.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 @@ -50,25 +50,28 @@ export interface BreadcrumbTrail { } export const BreadcrumbPage: React.FunctionComponent = (props) => { - return (<> - - - { - (props.breadcrumbs || []).map( - ({ title, path }) => (<>{title})}>) - } - {props.pageTitle} - - - { - React.Children.map(props.children, child => ( - - {child} - - )) - } - - - ); - -} + return ( + <> + + + {(props.breadcrumbs || []).map(({ title, path }) => ( + ( + <> + {title} + + )} + > + ))} + {props.pageTitle} + + + {React.Children.map(props.children, (child) => ( + {child} + ))} + + + + ); +}; diff --git a/src/app/CreateRecording/CreateRecording.tsx b/src/app/CreateRecording/CreateRecording.tsx index 90f556e36..10d3c99d8 100644 --- a/src/app/CreateRecording/CreateRecording.tsx +++ b/src/app/CreateRecording/CreateRecording.tsx @@ -61,9 +61,9 @@ export interface EventTemplate { type: TemplateType; } -export type TemplateType = "TARGET" | "CUSTOM"; +export type TemplateType = 'TARGET' | 'CUSTOM'; -const Comp: React.FunctionComponent< RouteComponentProps<{}, StaticContext, CreateRecordingProps>> = (props) => { +const Comp: React.FunctionComponent> = (props) => { const context = React.useContext(ServiceContext); const history = useHistory(); const addSubscription = useSubscriptions(); @@ -72,25 +72,27 @@ const Comp: React.FunctionComponent< RouteComponentProps<{}, StaticContext, Crea const handleCreateRecording = (recordingAttributes: RecordingAttributes): void => { addSubscription( - context.api.createRecording(recordingAttributes) - .pipe(first()) - .subscribe(success => { - if (success) { - history.push('/recordings'); - } - }) + context.api + .createRecording(recordingAttributes) + .pipe(first()) + .subscribe((success) => { + if (success) { + history.push('/recordings'); + } + }) ); }; const handleCreateSnapshot = (): void => { addSubscription( - context.api.createSnapshot() - .pipe(first()) - .subscribe(success => { - if (success) { - history.push('/recordings'); - } - }) + context.api + .createSnapshot() + .pipe(first()) + .subscribe((success) => { + if (success) { + history.push('/recordings'); + } + }) ); }; @@ -100,7 +102,8 @@ const Comp: React.FunctionComponent< RouteComponentProps<{}, StaticContext, Crea setActiveTab(Number(idx))}> - - + diff --git a/src/app/CreateRecording/CustomRecordingForm.tsx b/src/app/CreateRecording/CustomRecordingForm.tsx index a1ae67d71..6293d934b 100644 --- a/src/app/CreateRecording/CustomRecordingForm.tsx +++ b/src/app/CreateRecording/CustomRecordingForm.tsx @@ -38,7 +38,23 @@ import * as React from 'react'; import { NotificationsContext } from '@app/Notifications/Notifications'; import { ServiceContext } from '@app/Shared/Services/Services'; -import { ActionGroup, Button, Checkbox, ExpandableSection, Form, FormGroup, FormSelect, FormSelectOption, Split, SplitItem, Text, TextInput, TextVariants, Tooltip, ValidatedOptions } from '@patternfly/react-core'; +import { + ActionGroup, + Button, + Checkbox, + ExpandableSection, + Form, + FormGroup, + FormSelect, + FormSelectOption, + Split, + SplitItem, + Text, + TextInput, + TextVariants, + Tooltip, + ValidatedOptions, +} from '@patternfly/react-core'; import { HelpIcon } from '@patternfly/react-icons'; import { useHistory } from 'react-router-dom'; import { concatMap } from 'rxjs/operators'; @@ -63,15 +79,19 @@ export const CustomRecordingForm = (props) => { const history = useHistory(); const addSubscription = useSubscriptions(); - const [recordingName, setRecordingName] = React.useState(props.recordingName || props?.location?.state?.recordingName || ''); + const [recordingName, setRecordingName] = React.useState( + props.recordingName || props?.location?.state?.recordingName || '' + ); const [nameValid, setNameValid] = React.useState(ValidatedOptions.default); const [continuous, setContinuous] = React.useState(false); const [duration, setDuration] = React.useState(30); const [durationUnit, setDurationUnit] = React.useState(1000); const [durationValid, setDurationValid] = React.useState(ValidatedOptions.success); const [templates, setTemplates] = React.useState([] as EventTemplate[]); - const [template, setTemplate] = React.useState(props.template || props?.location?.state?.template || null); - const [templateType, setTemplateType] = React.useState(props.templateType || props?.location?.state?.templateType || null as TemplateType | null); + const [template, setTemplate] = React.useState(props.template || props?.location?.state?.template || null); + const [templateType, setTemplateType] = React.useState( + props.templateType || props?.location?.state?.templateType || (null as TemplateType | null) + ); const [maxAge, setMaxAge] = React.useState(0); const [maxAgeUnits, setMaxAgeUnits] = React.useState(1); const [maxSize, setMaxSize] = React.useState(0); @@ -80,26 +100,38 @@ export const CustomRecordingForm = (props) => { const [labels, setLabels] = React.useState([] as RecordingLabel[]); const [labelsValid, setLabelsValid] = React.useState(ValidatedOptions.default); - const handleContinuousChange = React.useCallback((checked) => { - setContinuous(checked); - setDuration(0); - setDurationValid(checked ? ValidatedOptions.success : ValidatedOptions.error) - }, [setContinuous, setDuration, setDurationValid]); + const handleContinuousChange = React.useCallback( + (checked) => { + setContinuous(checked); + setDuration(0); + setDurationValid(checked ? ValidatedOptions.success : ValidatedOptions.error); + }, + [setContinuous, setDuration, setDurationValid] + ); - const handleDurationChange = React.useCallback((evt) => { - setDuration(Number(evt)); - setDurationValid(DurationPattern.test(evt) ? ValidatedOptions.success : ValidatedOptions.error); - }, [setDurationValid, setDuration]); + const handleDurationChange = React.useCallback( + (evt) => { + setDuration(Number(evt)); + setDurationValid(DurationPattern.test(evt) ? ValidatedOptions.success : ValidatedOptions.error); + }, + [setDurationValid, setDuration] + ); - const handleDurationUnitChange = React.useCallback((evt) => { - setDurationUnit(Number(evt)); - }, [setDurationUnit]); + const handleDurationUnitChange = React.useCallback( + (evt) => { + setDurationUnit(Number(evt)); + }, + [setDurationUnit] + ); - const handleTemplateChange = React.useCallback((template) => { - const parts: string[] = template.split(','); - setTemplate(parts[0]); - setTemplateType(parts[1]); - }, [setTemplate, setTemplateType]); + const handleTemplateChange = React.useCallback( + (template) => { + const parts: string[] = template.split(','); + setTemplate(parts[0]); + setTemplateType(parts[1]); + }, + [setTemplate, setTemplateType] + ); const getEventString = React.useCallback(() => { var str = ''; @@ -114,48 +146,69 @@ export const CustomRecordingForm = (props) => { const getFormattedLabels = React.useCallback(() => { let obj = {}; - - labels.forEach(l => { - if(!!l.key && !!l.value) { - obj[l.key] = l.value; - } - }); + + labels.forEach((l) => { + if (!!l.key && !!l.value) { + obj[l.key] = l.value; + } + }); return obj; - }, [labels]) + }, [labels]); - const handleRecordingNameChange = React.useCallback((name) => { - setNameValid(RecordingNamePattern.test(name) ? ValidatedOptions.success : ValidatedOptions.error); - setRecordingName(name); - }, [setNameValid, setRecordingName]); + const handleRecordingNameChange = React.useCallback( + (name) => { + setNameValid(RecordingNamePattern.test(name) ? ValidatedOptions.success : ValidatedOptions.error); + setRecordingName(name); + }, + [setNameValid, setRecordingName] + ); - const handleMaxAgeChange = React.useCallback((evt) => { - setMaxAge(Number(evt)); - }, [setMaxAge]); + const handleMaxAgeChange = React.useCallback( + (evt) => { + setMaxAge(Number(evt)); + }, + [setMaxAge] + ); - const handleMaxAgeUnitChange = React.useCallback((evt) => { - setMaxAgeUnits(Number(evt)); - }, [setMaxAgeUnits]); + const handleMaxAgeUnitChange = React.useCallback( + (evt) => { + setMaxAgeUnits(Number(evt)); + }, + [setMaxAgeUnits] + ); - const handleMaxSizeChange = React.useCallback((evt) => { - setMaxSize(Number(evt)); - }, [setMaxSize]); + const handleMaxSizeChange = React.useCallback( + (evt) => { + setMaxSize(Number(evt)); + }, + [setMaxSize] + ); - const handleMaxSizeUnitChange = React.useCallback((evt) => { - setMaxSizeUnits(Number(evt)); - }, [setMaxSizeUnits]); + const handleMaxSizeUnitChange = React.useCallback( + (evt) => { + setMaxSizeUnits(Number(evt)); + }, + [setMaxSizeUnits] + ); - const handleToDiskChange = React.useCallback((checked, evt) => { - setToDisk(evt.target.checked); - }, [setToDisk]); + const handleToDiskChange = React.useCallback( + (checked, evt) => { + setToDisk(evt.target.checked); + }, + [setToDisk] + ); - const setRecordingOptions = React.useCallback((options: RecordingOptions) => { - // toDisk is not set, and defaults to true because of https://github.com/cryostatio/cryostat/issues/263 - setMaxAge(options.maxAge || 0); - setMaxAgeUnits(1); - setMaxSize(options.maxSize || 0); - setMaxSizeUnits(1); - }, [setMaxAge, setMaxAgeUnits, setMaxSize, setMaxSizeUnits]); + const setRecordingOptions = React.useCallback( + (options: RecordingOptions) => { + // toDisk is not set, and defaults to true because of https://github.com/cryostatio/cryostat/issues/263 + setMaxAge(options.maxAge || 0); + setMaxAgeUnits(1); + setMaxSize(options.maxSize || 0); + setMaxSizeUnits(1); + }, + [setMaxAge, setMaxAgeUnits, setMaxSize, setMaxSizeUnits] + ); const handleSubmit = React.useCallback(() => { const notificationMessages: string[] = []; @@ -171,131 +224,171 @@ export const CustomRecordingForm = (props) => { const options: RecordingOptions = { toDisk: toDisk, - maxAge: toDisk? continuous? maxAge * maxAgeUnits : undefined : undefined, - maxSize: toDisk? maxSize * maxSizeUnits : undefined - } + maxAge: toDisk ? (continuous ? maxAge * maxAgeUnits : undefined) : undefined, + maxSize: toDisk ? maxSize * maxSizeUnits : undefined, + }; const recordingAttributes: RecordingAttributes = { name: recordingName, events: getEventString(), - duration: continuous ? undefined : duration * (durationUnit/1000), + duration: continuous ? undefined : duration * (durationUnit / 1000), options: options, - metadata: { labels: getFormattedLabels() } - } + metadata: { labels: getFormattedLabels() }, + }; props.onSubmit(recordingAttributes); - }, [getEventString, getFormattedLabels, continuous, - duration, durationUnit, maxAge, maxAgeUnits, maxSize, maxSizeUnits, - nameValid, notifications, notifications.warning, recordingName, toDisk]); + }, [ + getEventString, + getFormattedLabels, + continuous, + duration, + durationUnit, + maxAge, + maxAgeUnits, + maxSize, + maxSizeUnits, + nameValid, + notifications, + notifications.warning, + recordingName, + toDisk, + ]); React.useEffect(() => { addSubscription( - context.target.target() - .pipe(concatMap(target => context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/templates`))) - .subscribe(setTemplates) - ) + context.target + .target() + .pipe( + concatMap((target) => + context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/templates`) + ) + ) + .subscribe(setTemplates) + ); }, [addSubscription, context, context.target, setTemplates]); React.useEffect(() => { addSubscription( - context.target.target() - .pipe(concatMap(target => context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/recordingOptions`))) - .subscribe(setRecordingOptions) - ) + context.target + .target() + .pipe( + concatMap((target) => + context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/recordingOptions`) + ) + ) + .subscribe(setRecordingOptions) + ); }, [addSubscription, context, context.target, setRecordingOptions]); - const isFormInvalid : boolean = React.useMemo(() => { - return nameValid !== ValidatedOptions.success || durationValid !== ValidatedOptions.success || !template || !templateType || labelsValid !== ValidatedOptions.success; + const isFormInvalid: boolean = React.useMemo(() => { + return ( + nameValid !== ValidatedOptions.success || + durationValid !== ValidatedOptions.success || + !template || + !templateType || + labelsValid !== ValidatedOptions.success + ); }, [nameValid, durationValid, template, templateType, labelsValid]); - return (<> - - JDK Flight Recordings are compact records of events which have occurred within the target JVM. - Many event types are built-in to the JVM itself, while others are user-defined. - -
- - + + JDK Flight Recordings are compact records of events which have occurred within the target JVM. Many event types + are built-in to the JVM itself, while others are user-defined. + + + - - - - - - - - - - Unique key-value pairs containing information about the recording.}> - - - } - > - - - - - - - A value of 0 for maximum size or age means unbounded. - + > + + + + + + + + + + Unique key-value pairs containing information about the recording.}> + + + } > - + - - - + + + + A value of 0 for maximum size or age means unbounded. + + + + + + { > - + - - - - - - - - - - - - - - - - - -
- - - - - - ); -} + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/app/CreateRecording/SnapshotRecordingForm.tsx b/src/app/CreateRecording/SnapshotRecordingForm.tsx index 863eb4912..0816c3127 100644 --- a/src/app/CreateRecording/SnapshotRecordingForm.tsx +++ b/src/app/CreateRecording/SnapshotRecordingForm.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 @@ -46,19 +46,24 @@ export interface SnapshotRecordingFormProps { export const SnapshotRecordingForm = (props) => { const history = useHistory(); - return (<> -
- - A Snapshot recording is one which contains all information about all - events that have been captured in the current session by other,  - non-Snapshot recordings. Snapshots do not themselves define which - events are enabled, their thresholds, or any other options. A Snapshot - is only ever in the STOPPED state from the moment it is created. - - - - - -
- ); -} + return ( + <> +
+ + A Snapshot recording is one which contains all information about all events that have been captured in the + current session by other,  non-Snapshot recordings. Snapshots do not themselves define which + events are enabled, their thresholds, or any other options. A Snapshot is only ever in the STOPPED state from + the moment it is created. + + + + + +
+ + ); +}; diff --git a/src/app/Dashboard/Dashboard.tsx b/src/app/Dashboard/Dashboard.tsx index 66ca64b6d..b3071dd04 100644 --- a/src/app/Dashboard/Dashboard.tsx +++ b/src/app/Dashboard/Dashboard.tsx @@ -39,9 +39,5 @@ import * as React from 'react'; import { TargetView } from '@app/TargetView/TargetView'; export const Dashboard = () => { - - return ( - - ); - -} + return ; +}; diff --git a/src/app/DurationPicker/DurationPicker.tsx b/src/app/DurationPicker/DurationPicker.tsx index 468438818..ea49df93f 100644 --- a/src/app/DurationPicker/DurationPicker.tsx +++ b/src/app/DurationPicker/DurationPicker.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 @@ -47,33 +47,33 @@ export interface DurationPickerProps { } export const DurationPicker: React.FunctionComponent = (props) => { - - return (<> - - - props.onPeriodChange(Number(v))} - isDisabled={!props.enabled} - min="0" - /> - - - props.onUnitScalarChange(Number(v))} - aria-label="Duration Picker Units Input" - isDisabled={!props.enabled} - > - - - - - - - ); - -} + return ( + <> + + + props.onPeriodChange(Number(v))} + isDisabled={!props.enabled} + min="0" + /> + + + props.onUnitScalarChange(Number(v))} + aria-label="Duration Picker Units Input" + isDisabled={!props.enabled} + > + + + + + + + + ); +}; diff --git a/src/app/ErrorView/ErrorView.tsx b/src/app/ErrorView/ErrorView.tsx index f0e92cac4..c33d4655c 100644 --- a/src/app/ErrorView/ErrorView.tsx +++ b/src/app/ErrorView/ErrorView.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 @@ -40,19 +40,19 @@ import { Bullseye, Text } from '@patternfly/react-core'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; export interface ErrorViewProps { - message: string; + message: string; } export const ErrorView: React.FunctionComponent = (props) => { - return (<> -
- - - - - - Error: {props.message} - - - ) -} + return ( + <> +
+ + + + + Error: {props.message} + + + ); +}; diff --git a/src/app/Events/EventTemplates.tsx b/src/app/Events/EventTemplates.tsx index c0f51764f..cbc295632 100644 --- a/src/app/Events/EventTemplates.tsx +++ b/src/app/Events/EventTemplates.tsx @@ -41,9 +41,33 @@ import { EventTemplate } from '@app/Shared/Services/Api.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { NO_TARGET } from '@app/Shared/Services/Target.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { ActionGroup, Button, FileUpload, Form, FormGroup, Modal, ModalVariant, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, TextInput } from '@patternfly/react-core'; +import { + ActionGroup, + Button, + FileUpload, + Form, + FormGroup, + Modal, + ModalVariant, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + TextInput, +} from '@patternfly/react-core'; import { UploadIcon } from '@patternfly/react-icons'; -import { Table, TableBody, TableHeader, TableVariant, IAction, IRowData, IExtraData, ISortBy, SortByDirection, sortable } from '@patternfly/react-table'; +import { + Table, + TableBody, + TableHeader, + TableVariant, + IAction, + IRowData, + IExtraData, + ISortBy, + SortByDirection, + sortable, +} from '@patternfly/react-table'; import { useHistory } from 'react-router-dom'; import { concatMap, filter, first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; @@ -71,10 +95,10 @@ export const EventTemplates = () => { const addSubscription = useSubscriptions(); const tableColumns = [ - { title: 'Name', transforms: [ sortable ] }, + { title: 'Name', transforms: [sortable] }, 'Description', - { title: 'Provider', transforms: [ sortable ] }, - { title: 'Type', transforms: [ sortable ] }, + { title: 'Provider', transforms: [sortable] }, + { title: 'Type', transforms: [sortable] }, ]; React.useEffect(() => { @@ -83,7 +107,12 @@ export const EventTemplates = () => { filtered = templates; } else { const ft = filterText.trim().toLowerCase(); - filtered = templates.filter((t: EventTemplate) => t.name.toLowerCase().includes(ft) || t.description.toLowerCase().includes(ft) || t.provider.toLowerCase().includes(ft)); + filtered = templates.filter( + (t: EventTemplate) => + t.name.toLowerCase().includes(ft) || + t.description.toLowerCase().includes(ft) || + t.provider.toLowerCase().includes(ft) + ); } const { index, direction } = sortBy; if (typeof index === 'number') { @@ -95,26 +124,39 @@ export const EventTemplates = () => { setFilteredTemplates([...filtered]); }, [filterText, templates, sortBy]); - const handleTemplates = React.useCallback((templates) => { - setTemplates(templates); - setIsLoading(false); - setErrorMessage(''); - }, [setTemplates, setIsLoading, setErrorMessage]); + const handleTemplates = React.useCallback( + (templates) => { + setTemplates(templates); + setIsLoading(false); + setErrorMessage(''); + }, + [setTemplates, setIsLoading, setErrorMessage] + ); - const handleError = React.useCallback((error) => { - setIsLoading(false); - setErrorMessage(error.message); - }, [setIsLoading, setErrorMessage]); + const handleError = React.useCallback( + (error) => { + setIsLoading(false); + setErrorMessage(error.message); + }, + [setIsLoading, setErrorMessage] + ); const refreshTemplates = React.useCallback(() => { - setIsLoading(true) + setIsLoading(true); addSubscription( - context.target.target() - .pipe( - filter(target => target !== NO_TARGET), - first(), - concatMap(target => context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/templates`)), - ).subscribe(value => handleTemplates(value), err => handleError(err)) + context.target + .target() + .pipe( + filter((target) => target !== NO_TARGET), + first(), + concatMap((target) => + context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/templates`) + ) + ) + .subscribe( + (value) => handleTemplates(value), + (err) => handleError(err) + ) ); }, [addSubscription, context, context.target, context.api, setIsLoading, handleTemplates, handleError]); @@ -123,48 +165,65 @@ export const EventTemplates = () => { context.target.target().subscribe(() => { setFilterText(''); refreshTemplates(); - })); + }) + ); }, [context, context.target, addSubscription, refreshTemplates]); React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.TemplateUploaded) - .subscribe(v => setTemplates(old => old.concat(v.message.template))) + context.notificationChannel + .messages(NotificationCategory.TemplateUploaded) + .subscribe((v) => setTemplates((old) => old.concat(v.message.template))) ); }, [addSubscription, context, context.notificationChannel, setTemplates]); React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.TemplateDeleted) - .subscribe(v => setTemplates(old => old.filter(o => (o.name != v.message.template.name || o.type != v.message.template.type)))) - ) + context.notificationChannel + .messages(NotificationCategory.TemplateDeleted) + .subscribe((v) => + setTemplates((old) => + old.filter((o) => o.name != v.message.template.name || o.type != v.message.template.type) + ) + ) + ); }, [addSubscription, context, context.notificationChannel, setTemplates]); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { return; } - const id = window.setInterval(() => refreshTemplates(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + const id = window.setInterval( + () => refreshTemplates(), + context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits() + ); return () => window.clearInterval(id); }, []); React.useEffect(() => { const sub = context.target.authFailure().subscribe(() => { - setErrorMessage("Auth failure"); + setErrorMessage('Auth failure'); }); return () => sub.unsubscribe(); }, [context.target]); const displayTemplates = React.useMemo( - () => filteredTemplates.map((t: EventTemplate) => ([ t.name, t.description, t.provider, t.type.charAt(0).toUpperCase() + t.type.slice(1).toLowerCase() ])), + () => + filteredTemplates.map((t: EventTemplate) => [ + t.name, + t.description, + t.provider, + t.type.charAt(0).toUpperCase() + t.type.slice(1).toLowerCase(), + ]), [filteredTemplates] ); - + const handleDelete = (rowData) => { addSubscription( - context.api.deleteCustomEventTemplate(rowData[0]) - .pipe(first()) - .subscribe(() => {} /* do nothing - notification will handle updating state */) + context.api + .deleteCustomEventTemplate(rowData[0]) + .pipe(first()) + .subscribe(() => {} /* do nothing - notification will handle updating state */) ); }; @@ -175,37 +234,41 @@ export const EventTemplates = () => { let actions = [ { title: 'Create Recording...', - onClick: (event, rowId, rowData) => history.push({ pathname: '/recordings/create', state: { template: rowData[0], templateType: String(rowData[3]).toUpperCase() } }), + onClick: (event, rowId, rowData) => + history.push({ + pathname: '/recordings/create', + state: { template: rowData[0], templateType: String(rowData[3]).toUpperCase() }, + }), }, ] as IAction[]; const template: EventTemplate = filteredTemplates[extraData.rowIndex]; - if ((template.name !== 'ALL')||(template.type !== 'TARGET')) { + if (template.name !== 'ALL' || template.type !== 'TARGET') { actions = actions.concat([ - { - title: 'Download', - onClick: (event, rowId) => context.api.downloadTemplate(filteredTemplates[rowId]), - } + { + title: 'Download', + onClick: (event, rowId) => context.api.downloadTemplate(filteredTemplates[rowId]), + }, ]); } if (template.type === 'CUSTOM') { actions = actions.concat([ - { - isSeparator: true, + { + isSeparator: true, + }, + { + title: 'Delete', + onClick: (event, rowId, rowData) => { + handleDeleteButton(rowData); }, - { - title: 'Delete', - onClick: (event, rowId, rowData) => { - handleDeleteButton(rowData); - }, - } + }, ]); } return actions; }; const handleModalToggle = () => { - setModalOpen(v => { + setModalOpen((v) => { if (v) { setUploadFile(undefined); setUploadFilename(''); @@ -228,16 +291,17 @@ export const EventTemplates = () => { } setUploading(true); addSubscription( - context.api.addCustomEventTemplate(uploadFile) - .pipe(first()) - .subscribe(success => { - setUploading(false); - if (success) { - setUploadFile(undefined); - setUploadFilename(''); - setModalOpen(false); - } - }) + context.api + .addCustomEventTemplate(uploadFile) + .pipe(first()) + .subscribe((success) => { + setUploading(false); + if (success) { + setUploadFile(undefined); + setUploadFilename(''); + setModalOpen(false); + } + }) ); }; @@ -255,15 +319,17 @@ export const EventTemplates = () => { setSortBy({ index, direction }); }; - const handleDeleteButton = React.useCallback((rowData) => { - if (context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteEventTemplates)) { - setRowDeleteData(rowData); - setWarningModalOpen(true); - } - else { - handleDelete(rowData); - } - }, [context, context.settings, setWarningModalOpen, setRowDeleteData, handleDelete]); + const handleDeleteButton = React.useCallback( + (rowData) => { + if (context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteEventTemplates)) { + setRowDeleteData(rowData); + setWarningModalOpen(true); + } else { + handleDelete(rowData); + } + }, + [context, context.settings, setWarningModalOpen, setRowDeleteData, handleDelete] + ); const handleWarningModalAccept = React.useCallback(() => { handleDelete(rowDeleteData); @@ -273,87 +339,105 @@ export const EventTemplates = () => { setWarningModalOpen(false); }, [setWarningModalOpen]); - const toolbar: JSX.Element = (<> - - - - - - - - - - - - - - - - ); - + const toolbar: JSX.Element = ( + <> + + + + + + + + + + + + + + + + + ); + if (errorMessage != '') { - return (); + return ; } else if (isLoading) { - return (<> - { toolbar } - - ); + return ( + <> + {toolbar} + + + ); } else { - return (<> - { toolbar } - - - -
+ return ( + <> + {toolbar} + + + +
- -
- - + - - - - - - -
- ); + > + + + + + + + + + + ); } - -} +}; diff --git a/src/app/Events/EventTypes.tsx b/src/app/Events/EventTypes.tsx index 22c825897..4e57ee742 100644 --- a/src/app/Events/EventTypes.tsx +++ b/src/app/Events/EventTypes.tsx @@ -37,9 +37,16 @@ */ import * as React from 'react'; import { ServiceContext } from '@app/Shared/Services/Services'; -import {NO_TARGET} from '@app/Shared/Services/Target.service'; +import { NO_TARGET } from '@app/Shared/Services/Target.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Toolbar, ToolbarContent, ToolbarItem, ToolbarItemVariant, Pagination, TextInput } from '@patternfly/react-core'; +import { + Toolbar, + ToolbarContent, + ToolbarItem, + ToolbarItemVariant, + Pagination, + TextInput, +} from '@patternfly/react-core'; import { expandable, Table, TableBody, TableHeader, TableVariant } from '@patternfly/react-table'; import { concatMap, filter, first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; @@ -64,7 +71,7 @@ type Row = { parent?: number; isOpen?: boolean; fullWidth?: boolean; -} +}; export const EventTypes = () => { const context = React.useContext(ServiceContext); @@ -83,34 +90,46 @@ export const EventTypes = () => { const tableColumns = [ { title: 'Name', - cellFormatters: [expandable] + cellFormatters: [expandable], }, 'Type ID', 'Description', - 'Categories' + 'Categories', ]; - const handleTypes = React.useCallback((types) => { - setTypes(types); - setIsLoading(false); - setErrorMessage(''); - }, [setTypes, setIsLoading, setErrorMessage]); + const handleTypes = React.useCallback( + (types) => { + setTypes(types); + setIsLoading(false); + setErrorMessage(''); + }, + [setTypes, setIsLoading, setErrorMessage] + ); - const handleError = React.useCallback((error) => { - setIsLoading(false); - setErrorMessage(error.message); - }, [setIsLoading, setErrorMessage]); + const handleError = React.useCallback( + (error) => { + setIsLoading(false); + setErrorMessage(error.message); + }, + [setIsLoading, setErrorMessage] + ); const refreshEvents = React.useCallback(() => { - setIsLoading(true) + setIsLoading(true); addSubscription( - context.target.target() + context.target + .target() .pipe( - filter(target => target !== NO_TARGET), + filter((target) => target !== NO_TARGET), first(), - concatMap(target => context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/events`)), + concatMap((target) => + context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/events`) + ) + ) + .subscribe( + (value) => handleTypes(value), + (err) => handleError(err) ) - .subscribe(value => handleTypes(value), err => handleError(err)) ); }, [addSubscription, context.target, context.api]); @@ -119,12 +138,13 @@ export const EventTypes = () => { context.target.target().subscribe(() => { setFilterText(''); refreshEvents(); - })); + }) + ); }, [addSubscription, context, context.target, refreshEvents]); React.useEffect(() => { const sub = context.target.authFailure().subscribe(() => { - setErrorMessage("Auth failure"); + setErrorMessage('Auth failure'); }); return () => sub.unsubscribe(); }, [context.target]); @@ -138,7 +158,7 @@ export const EventTypes = () => { return types; } const includesSubstr = (a, b) => !!a && !!b && a.toLowerCase().includes(b.trim().toLowerCase()); - return types.filter(t => { + return types.filter((t) => { if (includesSubstr(t.name, filterText)) { return true; } @@ -151,7 +171,7 @@ export const EventTypes = () => { if (includesSubstr(getCategoryString(t), filterText)) { return true; } - return false + return false; }); }, [types, filterText]); @@ -161,13 +181,13 @@ export const EventTypes = () => { const rows: Row[] = []; page.forEach((t: EventType, idx: number) => { - rows.push({ cells: [ t.name, t.typeId, t.description, getCategoryString(t) ], isOpen: (idx === openRow) }); + rows.push({ cells: [t.name, t.typeId, t.description, getCategoryString(t)], isOpen: idx === openRow }); if (idx === openRow) { let child = ''; for (const opt in t.options) { child += `${opt}=[${t.options[opt].defaultValue}]\t`; } - rows.push({ parent: idx, fullWidth: true, cells: [ child ] }); + rows.push({ parent: idx, fullWidth: true, cells: [child] }); } }); @@ -184,7 +204,7 @@ export const EventTypes = () => { prevPerPage.current = perPage; setOpenRow(-1); setPerPage(perPage); - setCurrentPage(1 + Math.floor(offset/perPage)); + setCurrentPage(1 + Math.floor(offset / perPage)); }; const onCollapse = (event, rowKey, isOpen) => { @@ -201,34 +221,48 @@ export const EventTypes = () => { // TODO replace table with data list so collapsed event options can be custom formatted if (errorMessage != '') { - return () + return ; } else if (isLoading) { - return () + return ; } else { - return (<> - - - - - - - - - - - - - -
- ) + return ( + <> + + + + + + + + + + + + + +
+ + ); } - -} +}; diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index 418124f61..07146ba7c 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.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 @@ -46,23 +46,24 @@ export const Events = () => { const handleTabSelect = (evt, idx) => { setActiveTab(idx); - } + }; - return (<> - - - - - - - - - - - - - - - ); - -} + return ( + <> + + + + + + + + + + + + + + + + ); +}; diff --git a/src/app/LoadingView/LoadingView.tsx b/src/app/LoadingView/LoadingView.tsx index 9b0bcf5e1..e871b46ea 100644 --- a/src/app/LoadingView/LoadingView.tsx +++ b/src/app/LoadingView/LoadingView.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 @@ -39,11 +39,12 @@ import * as React from 'react'; import { Bullseye, Spinner } from '@patternfly/react-core'; export const LoadingView: React.FunctionComponent = () => { - return (<> -
- - - - ) -} - + return ( + <> +
+ + + + + ); +}; diff --git a/src/app/Login/BasicAuthForm.tsx b/src/app/Login/BasicAuthForm.tsx index 2917fcfc0..e4d885b09 100644 --- a/src/app/Login/BasicAuthForm.tsx +++ b/src/app/Login/BasicAuthForm.tsx @@ -50,50 +50,63 @@ export const BasicAuthForm: React.FunctionComponent = (props) => { const [rememberMe, setRememberMe] = React.useState(true); React.useEffect(() => { - const sub = context.login.getToken().pipe(map(Base64.decode)).subscribe(creds => { - if (!creds.includes(':')) { - setUsername(creds); - return; - } - let parts: string[] = creds.split(':'); - setUsername(parts[0]); - setPassword(parts[1]); - }); + const sub = context.login + .getToken() + .pipe(map(Base64.decode)) + .subscribe((creds) => { + if (!creds.includes(':')) { + setUsername(creds); + return; + } + let parts: string[] = creds.split(':'); + setUsername(parts[0]); + setPassword(parts[1]); + }); return () => sub.unsubscribe(); }, [context, context.api, setUsername, setPassword]); - const handleUserChange = React.useCallback((evt) => { - setUsername(evt); - }, [setUsername]); + const handleUserChange = React.useCallback( + (evt) => { + setUsername(evt); + }, + [setUsername] + ); - const handlePasswordChange = React.useCallback((evt) => { - setPassword(evt); - }, [setPassword]); + const handlePasswordChange = React.useCallback( + (evt) => { + setPassword(evt); + }, + [setPassword] + ); - const handleRememberMeToggle = React.useCallback((evt) => { - setRememberMe(evt) - }, [setRememberMe]); + const handleRememberMeToggle = React.useCallback( + (evt) => { + setRememberMe(evt); + }, + [setRememberMe] + ); - const handleSubmit = React.useCallback((evt) => { - props.onSubmit(evt, `${username}:${password}`, AuthMethod.BASIC, rememberMe); - }, [props, props.onSubmit, username, password, context.login, rememberMe]); + const handleSubmit = React.useCallback( + (evt) => { + props.onSubmit(evt, `${username}:${password}`, AuthMethod.BASIC, rememberMe); + }, + [props, props.onSubmit, username, password, context.login, rememberMe] + ); // FIXME Patternfly Form component onSubmit is not triggered by Enter keydown when the Form contains // multiple FormGroups. This key handler is a workaround to allow keyboard-driven use of the form - const handleKeyDown = React.useCallback((evt) => { - if (evt.key === 'Enter') { - handleSubmit(evt); - } - }, [handleSubmit]); + const handleKeyDown = React.useCallback( + (evt) => { + if (evt.key === 'Enter') { + handleSubmit(evt); + } + }, + [handleSubmit] + ); return (
- + = (props) => { onKeyDown={handleKeyDown} /> - + = (props) => { - + ); - -} +}; export const BasicAuthDescriptionText = () => { - return ( - The Cryostat server is configured with Basic authentication. - ); -} + return The Cryostat server is configured with Basic authentication.; +}; diff --git a/src/app/Login/ConnectionError.tsx b/src/app/Login/ConnectionError.tsx index 1fd48e14e..72fbcf18e 100644 --- a/src/app/Login/ConnectionError.tsx +++ b/src/app/Login/ConnectionError.tsx @@ -41,12 +41,10 @@ import { ExclamationCircleIcon } from '@patternfly/react-icons'; export const ConnectionError = () => ( - + Unable to connect - - Check your connection and reload the page. - + Check your connection and reload the page. ); diff --git a/src/app/Login/Login.tsx b/src/app/Login/Login.tsx index 4d90515b9..9bf4ee6c9 100644 --- a/src/app/Login/Login.tsx +++ b/src/app/Login/Login.tsx @@ -50,19 +50,21 @@ export const Login = () => { const notifications = React.useContext(NotificationsContext); const [authMethod, setAuthMethod] = React.useState(''); - const handleSubmit = React.useCallback((evt, token, authMethod, rememberMe) => { - setAuthMethod(authMethod); + const handleSubmit = React.useCallback( + (evt, token, authMethod, rememberMe) => { + setAuthMethod(authMethod); - const sub = serviceContext.login.checkAuth(token, authMethod, rememberMe) - .subscribe(authSuccess => { - if(!authSuccess) { + const sub = serviceContext.login.checkAuth(token, authMethod, rememberMe).subscribe((authSuccess) => { + if (!authSuccess) { notifications.danger('Authentication Failure', `${authMethod} authentication failed`); } }); - () => sub.unsubscribe(); + () => sub.unsubscribe(); - evt.preventDefault(); - }, [serviceContext, serviceContext.login, setAuthMethod]); + evt.preventDefault(); + }, + [serviceContext, serviceContext.login, setAuthMethod] + ); React.useEffect(() => { const sub = serviceContext.login.getAuthMethod().subscribe(setAuthMethod); @@ -70,7 +72,7 @@ export const Login = () => { }, [serviceContext, serviceContext.login, setAuthMethod]); const loginForm = React.useMemo(() => { - switch(authMethod) { + switch (authMethod) { case AuthMethod.BASIC: return ; case AuthMethod.BEARER: @@ -83,7 +85,7 @@ export const Login = () => { }, [authMethod]); const descriptionText = React.useMemo(() => { - switch(authMethod) { + switch (authMethod) { case AuthMethod.BASIC: return ; case AuthMethod.BEARER: @@ -97,16 +99,13 @@ export const Login = () => { - Login + + Login + - - {loginForm} - - - {descriptionText} - + {loginForm} + {descriptionText} ); - -} +}; diff --git a/src/app/Login/NoopAuthForm.tsx b/src/app/Login/NoopAuthForm.tsx index 4de76c686..431c8b5f7 100644 --- a/src/app/Login/NoopAuthForm.tsx +++ b/src/app/Login/NoopAuthForm.tsx @@ -40,18 +40,13 @@ import * as React from 'react'; import { FormProps } from './FormProps'; export const NoopAuthForm: React.FunctionComponent = (props) => { - React.useEffect(() => { const noopEvt = { - preventDefault: () => {} + preventDefault: () => {}, } as Event; props.onSubmit(noopEvt, '', AuthMethod.NONE, false); }, [props.onSubmit]); - return ( - <> - - ); - -} + return <>; +}; diff --git a/src/app/Login/OpenShiftPlaceholderAuthForm.tsx b/src/app/Login/OpenShiftPlaceholderAuthForm.tsx index 8945ba598..3e030c100 100644 --- a/src/app/Login/OpenShiftPlaceholderAuthForm.tsx +++ b/src/app/Login/OpenShiftPlaceholderAuthForm.tsx @@ -50,57 +50,57 @@ export const OpenShiftPlaceholderAuthForm: React.FunctionComponent = const [showPermissionDenied, setShowPermissionDenied] = React.useState(false); React.useEffect(() => { - const sub = combineLatest([serviceContext.login.getSessionState(), notifications.problemsNotifications()]) - .subscribe(parts => { + const sub = combineLatest([ + serviceContext.login.getSessionState(), + notifications.problemsNotifications(), + ]).subscribe((parts) => { const sessionState = parts[0]; const errors = parts[1]; - const missingCryostatPermissions = errors.find(error => error.title.includes('401')) !== undefined; + const missingCryostatPermissions = errors.find((error) => error.title.includes('401')) !== undefined; setShowPermissionDenied(sessionState === SessionState.NO_USER_SESSION && missingCryostatPermissions); }); return () => sub.unsubscribe(); }, [setShowPermissionDenied]); - - const handleSubmit = React.useCallback((evt) => { - // Triggers a redirect to OpenShift Container Platform login page - props.onSubmit(evt, 'anInvalidToken', AuthMethod.BEARER, true); - }, [props, props.onSubmit, serviceContext.login]); + const handleSubmit = React.useCallback( + (evt) => { + // Triggers a redirect to OpenShift Container Platform login page + props.onSubmit(evt, 'anInvalidToken', AuthMethod.BEARER, true); + }, + [props, props.onSubmit, serviceContext.login] + ); const permissionDenied = ( - + Access Permissions Needed - {`To continue, add permissions to your current account or login with a + {`To continue, add permissions to your current account or login with a different account. For more info, see the User Authentication section of the `} + component={TextVariants.a} + target="_blank" + href="https://github.com/cryostatio/cryostat-operator#user-authentication" + > Cryostat Operator README. - + ); - return ( - <> - { showPermissionDenied && permissionDenied } - - ); -} + return <>{showPermissionDenied && permissionDenied}; +}; export const OpenShiftAuthDescriptionText = () => { return ( - - The Cryostat server is configured to use OpenShift OAuth authentication. - + The Cryostat server is configured to use OpenShift OAuth authentication. ); -} +}; diff --git a/src/app/Modal/CancelUploadModal.tsx b/src/app/Modal/CancelUploadModal.tsx index ab4c7be3d..dd32792bd 100644 --- a/src/app/Modal/CancelUploadModal.tsx +++ b/src/app/Modal/CancelUploadModal.tsx @@ -40,11 +40,11 @@ import * as React from 'react'; import { Button, Modal } from '@patternfly/react-core'; export interface CancelUploadModalProps { - visible: boolean; - onYes: () => void; - onNo: () => void; - title: string; - message: string; + visible: boolean; + onYes: () => void; + onNo: () => void; + title: string; + message: string; } export const CancelUploadModal: React.FunctionComponent = (props) => { @@ -61,11 +61,10 @@ export const CancelUploadModal: React.FunctionComponent , + , ]} > {props.message} ); -} - +}; diff --git a/src/app/Modal/DeleteWarningModal.tsx b/src/app/Modal/DeleteWarningModal.tsx index e67988d86..2ae4e82f2 100644 --- a/src/app/Modal/DeleteWarningModal.tsx +++ b/src/app/Modal/DeleteWarningModal.tsx @@ -51,14 +51,14 @@ export interface DeleteWarningProps { export const DeleteWarningModal = ({ warningType, visible, onAccept, onClose }: DeleteWarningProps): JSX.Element => { const context = React.useContext(ServiceContext); const [doNotAsk, setDoNotAsk] = useState(false); - + const realWarningType = getFromWarningMap(warningType); const onAcceptClose = React.useCallback(() => { onAccept(); onClose(); if (doNotAsk && !!realWarningType) { - context.settings.setDeletionDialogsEnabledFor(realWarningType.id, false); + context.settings.setDeletionDialogsEnabledFor(realWarningType.id, false); } }, [onAccept, onClose, doNotAsk, context, context.settings]); @@ -72,7 +72,7 @@ export const DeleteWarningModal = ({ warningType, visible, onAccept, onClose }: isOpen={visible} showClose onClose={onClose} - actions={[ + actions={[ - + , ]} > - - + + ); }; diff --git a/src/app/Modal/DeleteWarningUtils.tsx b/src/app/Modal/DeleteWarningUtils.tsx index d45c99bd8..0ada2aa49 100644 --- a/src/app/Modal/DeleteWarningUtils.tsx +++ b/src/app/Modal/DeleteWarningUtils.tsx @@ -36,70 +36,70 @@ * SOFTWARE. */ export enum DeleteWarningType { - DeleteActiveRecordings='DeleteActiveRecordings', - DeleteArchivedRecordings='DeleteArchivedRecordings', - DeleteAutomatedRules='DeleteAutomatedRules', - DeleteEventTemplates='DeleteEventTemplates', - DeleteJMXCredentials='DeleteJMXCredentials', + DeleteActiveRecordings = 'DeleteActiveRecordings', + DeleteArchivedRecordings = 'DeleteArchivedRecordings', + DeleteAutomatedRules = 'DeleteAutomatedRules', + DeleteEventTemplates = 'DeleteEventTemplates', + DeleteJMXCredentials = 'DeleteJMXCredentials', } export interface DeleteWarning { - id: DeleteWarningType; - title: string; - label: string; - description: string; - ariaLabel: string; + id: DeleteWarningType; + title: string; + label: string; + description: string; + ariaLabel: string; } export const DeleteActiveRecordings: DeleteWarning = { - id: DeleteWarningType.DeleteActiveRecordings, - title: 'Permanently delete Active Recording?', - label: 'Delete Active Recording', - description: `Recording and report data will be lost.`, - ariaLabel: "Recording delete warning" -} + id: DeleteWarningType.DeleteActiveRecordings, + title: 'Permanently delete Active Recording?', + label: 'Delete Active Recording', + description: `Recording and report data will be lost.`, + ariaLabel: 'Recording delete warning', +}; export const DeleteArchivedRecordings: DeleteWarning = { - id: DeleteWarningType.DeleteArchivedRecordings, - title: 'Permanently delete Archived Recording?', - label: 'Delete Archived Recording', - description: `Recording and report data will be lost.`, - ariaLabel: "Recording delete warning" -} + id: DeleteWarningType.DeleteArchivedRecordings, + title: 'Permanently delete Archived Recording?', + label: 'Delete Archived Recording', + description: `Recording and report data will be lost.`, + ariaLabel: 'Recording delete warning', +}; export const DeleteAutomatedRules: DeleteWarning = { - id: DeleteWarningType.DeleteAutomatedRules, - title: 'Permanently delete Automated Rule?', - label: 'Delete Automated Rule', - description: `Rule data will be lost.`, - ariaLabel: "Automated rule delete warning" -} + id: DeleteWarningType.DeleteAutomatedRules, + title: 'Permanently delete Automated Rule?', + label: 'Delete Automated Rule', + description: `Rule data will be lost.`, + ariaLabel: 'Automated rule delete warning', +}; export const DeleteEventTemplates: DeleteWarning = { - id: DeleteWarningType.DeleteEventTemplates, - title: 'Permanently delete Event Template?', - label: 'Delete Event Template', - description: `Custom event template data will be lost.`, - ariaLabel: "Event template delete warning" -} + id: DeleteWarningType.DeleteEventTemplates, + title: 'Permanently delete Event Template?', + label: 'Delete Event Template', + description: `Custom event template data will be lost.`, + ariaLabel: 'Event template delete warning', +}; export const DeleteJMXCredentials: DeleteWarning = { - id: DeleteWarningType.DeleteJMXCredentials, - title: 'Permanently delete JMX Credentials?', - label: 'Delete JMX Credentials', - description: `Credential data for this target will be lost.`, - ariaLabel: "JMX Credentials delete warning" -} + id: DeleteWarningType.DeleteJMXCredentials, + title: 'Permanently delete JMX Credentials?', + label: 'Delete JMX Credentials', + description: `Credential data for this target will be lost.`, + ariaLabel: 'JMX Credentials delete warning', +}; -export const DeleteWarningKinds : DeleteWarning[] = [ - DeleteActiveRecordings, - DeleteArchivedRecordings, - DeleteAutomatedRules, - DeleteEventTemplates, - DeleteJMXCredentials +export const DeleteWarningKinds: DeleteWarning[] = [ + DeleteActiveRecordings, + DeleteArchivedRecordings, + DeleteAutomatedRules, + DeleteEventTemplates, + DeleteJMXCredentials, ]; export const getFromWarningMap = (warning: DeleteWarningType): DeleteWarning | undefined => { - const wt = DeleteWarningKinds.find(t => t.id === warning); - return wt; -} + const wt = DeleteWarningKinds.find((t) => t.id === warning); + return wt; +}; diff --git a/src/app/NotFound/NotFound.tsx b/src/app/NotFound/NotFound.tsx index 4c9088c28..aa8d69b8d 100644 --- a/src/app/NotFound/NotFound.tsx +++ b/src/app/NotFound/NotFound.tsx @@ -44,9 +44,9 @@ import { EmptyStateIcon, EmptyStateBody, EmptyStateSecondaryActions, - Title + Title, } from '@patternfly/react-core'; -import '@app/app.css' +import '@app/app.css'; import { MapMarkedAltIcon } from '@patternfly/react-icons'; import { IAppRoute, routes, flatten } from '@app/routes'; @@ -54,29 +54,30 @@ const NotFound: React.FunctionComponent = () => { const cards = flatten(routes) .filter((route: IAppRoute): boolean => !!route.description) .sort((a: IAppRoute, b: IAppRoute): number => a.title.localeCompare(b.title)) - .map((route: IAppRoute) => + .map((route: IAppRoute) => ( ); + /> + )); - return (<> - - - - 404: We couldn't find that page - - - One of the following pages might have what you're looking for. - - - { cards } - - - - ); + return ( + <> + + + + 404: We couldn't find that page + + One of the following pages might have what you're looking for. + {cards} + + + + ); }; export { NotFound }; diff --git a/src/app/NotFound/NotFoundCard.tsx b/src/app/NotFound/NotFoundCard.tsx index 487b44b3b..af49fcf1e 100644 --- a/src/app/NotFound/NotFoundCard.tsx +++ b/src/app/NotFound/NotFoundCard.tsx @@ -40,14 +40,16 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; import { Card, CardTitle, CardBody, CardFooter } from '@patternfly/react-core'; -export const NotFoundCard = ({title, bodyText, linkText, linkPath}) => { - return(<> - - {title} - {bodyText} - - {linkText} - - - ); -} \ No newline at end of file +export const NotFoundCard = ({ title, bodyText, linkText, linkPath }) => { + return ( + <> + + {title} + {bodyText} + + {linkText} + + + + ); +}; diff --git a/src/app/Notifications/NotificationCenter.tsx b/src/app/Notifications/NotificationCenter.tsx index 289f51d11..eaabb3566 100644 --- a/src/app/Notifications/NotificationCenter.tsx +++ b/src/app/Notifications/NotificationCenter.tsx @@ -36,11 +36,23 @@ * SOFTWARE. */ import * as React from 'react'; -import { Dropdown, DropdownItem, DropdownPosition, KebabToggle, - NotificationDrawer, NotificationDrawerBody, NotificationDrawerGroup, NotificationDrawerGroupList, NotificationDrawerHeader, - NotificationDrawerList, NotificationDrawerListItem, - NotificationDrawerListItemBody, NotificationDrawerListItemHeader, Text, - TextVariants } from '@patternfly/react-core'; +import { + Dropdown, + DropdownItem, + DropdownPosition, + KebabToggle, + NotificationDrawer, + NotificationDrawerBody, + NotificationDrawerGroup, + NotificationDrawerGroupList, + NotificationDrawerHeader, + NotificationDrawerList, + NotificationDrawerListItem, + NotificationDrawerListItemBody, + NotificationDrawerListItemHeader, + Text, + TextVariants, +} from '@patternfly/react-core'; import { Notification, NotificationsContext } from './Notifications'; import { combineLatest } from 'rxjs'; @@ -55,63 +67,68 @@ export interface NotificationDrawerCategory { unreadCount: number; } -export const NotificationCenter: React.FunctionComponent = props => { +export const NotificationCenter: React.FunctionComponent = (props) => { const context = React.useContext(NotificationsContext); const [totalUnreadNotificationsCount, setTotalUnreadNotificationsCount] = React.useState(0); const [isHeaderDropdownOpen, setHeaderDropdownOpen] = React.useState(false); const PROBLEMS_CATEGORY_IDX = 2; const [drawerCategories, setDrawerCategories] = React.useState([ - {title: "Completed Actions", isExpanded: true, notifications: [] as Notification[], unreadCount: 0}, - {title: "Cryostat Status", isExpanded: false, notifications: [] as Notification[], unreadCount: 0}, - {title: "Problems", isExpanded: false, notifications: [] as Notification[], unreadCount: 0} + { title: 'Completed Actions', isExpanded: true, notifications: [] as Notification[], unreadCount: 0 }, + { title: 'Cryostat Status', isExpanded: false, notifications: [] as Notification[], unreadCount: 0 }, + { title: 'Problems', isExpanded: false, notifications: [] as Notification[], unreadCount: 0 }, ] as NotificationDrawerCategory[]); const countUnreadNotifications = (notifications: Notification[]) => { - return notifications.filter(n => !n.read).length; - } + return notifications.filter((n) => !n.read).length; + }; React.useEffect(() => { - const sub = combineLatest([context.actionsNotifications(), context.cryostatStatusNotifications(), context.problemsNotifications()]) - .subscribe(notificationLists => { - setDrawerCategories(drawerCategories => { - - return drawerCategories.map((category: NotificationDrawerCategory, idx) => { - category.notifications = notificationLists[idx]; - category.unreadCount = countUnreadNotifications(notificationLists[idx]); - return category; + const sub = combineLatest([ + context.actionsNotifications(), + context.cryostatStatusNotifications(), + context.problemsNotifications(), + ]).subscribe((notificationLists) => { + setDrawerCategories((drawerCategories) => { + return drawerCategories.map((category: NotificationDrawerCategory, idx) => { + category.notifications = notificationLists[idx]; + category.unreadCount = countUnreadNotifications(notificationLists[idx]); + return category; }); }); }); return () => sub.unsubscribe(); - },[context, context.notifications, setDrawerCategories]); + }, [context, context.notifications, setDrawerCategories]); React.useEffect(() => { - const sub = context.unreadNotifications().subscribe(s => { - setTotalUnreadNotificationsCount(s.length)}); + const sub = context.unreadNotifications().subscribe((s) => { + setTotalUnreadNotificationsCount(s.length); + }); return () => sub.unsubscribe(); }, [context, context.unreadNotifications, setTotalUnreadNotificationsCount]); const handleToggleDropdown = React.useCallback(() => { - setHeaderDropdownOpen(v => !v); + setHeaderDropdownOpen((v) => !v); }, [setHeaderDropdownOpen]); - const handleToggleExpandCategory = React.useCallback((categoryIdx) => { - setDrawerCategories(drawerCategories => { - - return drawerCategories.map((category: NotificationDrawerCategory, idx) => { - category.isExpanded = (idx === categoryIdx) ? !category.isExpanded : false; - return category; + const handleToggleExpandCategory = React.useCallback( + (categoryIdx) => { + setDrawerCategories((drawerCategories) => { + return drawerCategories.map((category: NotificationDrawerCategory, idx) => { + category.isExpanded = idx === categoryIdx ? !category.isExpanded : false; + return category; + }); }); - }); - }, [setDrawerCategories]); + }, + [setDrawerCategories] + ); // Expands the Problems tab when unread errors/warnings are present React.useEffect(() => { - if(drawerCategories[PROBLEMS_CATEGORY_IDX].unreadCount === 0) { + if (drawerCategories[PROBLEMS_CATEGORY_IDX].unreadCount === 0) { return; } - setDrawerCategories(drawerCategories => { + setDrawerCategories((drawerCategories) => { return drawerCategories.map((category: NotificationDrawerCategory, idx) => { category.isExpanded = idx === PROBLEMS_CATEGORY_IDX; return category; @@ -127,9 +144,12 @@ export const NotificationCenter: React.FunctionComponent { - context.setRead(key); - }, [context, context.setRead]); + const markRead = React.useCallback( + (key?: string) => { + context.setRead(key); + }, + [context, context.setRead] + ); const timestampToDateTimeString = (timestamp?: number): string => { if (!timestamp) { @@ -137,7 +157,7 @@ export const NotificationCenter: React.FunctionComponent @@ -148,45 +168,44 @@ export const NotificationCenter: React.FunctionComponent, ]; - return (<> - - - )} - isOpen={isHeaderDropdownOpen} - position={DropdownPosition.right} - dropdownItems={drawerDropdownItems} - /> - - - - { drawerCategories.map(({title, isExpanded, notifications, unreadCount}, idx) => ( - handleToggleExpandCategory(idx)} - key={idx} - > - - { - notifications.map(({ key, title, message, variant, timestamp, read }) => ( - markRead(key)} isRead={read} > + return ( + <> + + + } + isOpen={isHeaderDropdownOpen} + position={DropdownPosition.right} + dropdownItems={drawerDropdownItems} + /> + + + + {drawerCategories.map(({ title, isExpanded, notifications, unreadCount }, idx) => ( + handleToggleExpandCategory(idx)} + key={idx} + > + + {notifications.map(({ key, title, message, variant, timestamp, read }) => ( + markRead(key)} isRead={read}> - + {message} - )) - } - - - ))} - - - - ); - + ))} + + + ))} + + + + + ); }; diff --git a/src/app/Notifications/Notifications.tsx b/src/app/Notifications/Notifications.tsx index 251b325fa..7debc4476 100644 --- a/src/app/Notifications/Notifications.tsx +++ b/src/app/Notifications/Notifications.tsx @@ -53,9 +53,10 @@ export interface Notification { } export class Notifications { - private readonly _notifications: Notification[] = []; - private readonly _notifications$: BehaviorSubject = new BehaviorSubject(this._notifications); + private readonly _notifications$: BehaviorSubject = new BehaviorSubject( + this._notifications + ); notify(notification: Notification): void { if (!notification.key) { @@ -93,34 +94,25 @@ export class Notifications { } unreadNotifications(): Observable { - return this.notifications() - .pipe( - map(a => a.filter(n => !n.read)) - ); + return this.notifications().pipe(map((a) => a.filter((n) => !n.read))); } actionsNotifications(): Observable { - return this.notifications() - .pipe( - map(a => a.filter(n => this.isActionNotification(n))) - ); + return this.notifications().pipe(map((a) => a.filter((n) => this.isActionNotification(n)))); } cryostatStatusNotifications(): Observable { - return this.notifications() - .pipe( - map(a => a.filter(n => - (this.isWsClientActivity(n) || this.isJvmDiscovery(n)) - && !Notifications.isProblemNotification(n) - )) + return this.notifications().pipe( + map((a) => + a.filter( + (n) => (this.isWsClientActivity(n) || this.isJvmDiscovery(n)) && !Notifications.isProblemNotification(n) + ) + ) ); } problemsNotifications(): Observable { - return this.notifications() - .pipe( - map(a => a.filter(Notifications.isProblemNotification)) - ); + return this.notifications().pipe(map((a) => a.filter(Notifications.isProblemNotification))); } setRead(key?: string, read: boolean = true): void { @@ -150,21 +142,19 @@ export class Notifications { } private isActionNotification(n: Notification): boolean { - return !this.isWsClientActivity(n) - && !this.isJvmDiscovery(n) - && !Notifications.isProblemNotification(n); + return !this.isWsClientActivity(n) && !this.isJvmDiscovery(n) && !Notifications.isProblemNotification(n); } private isWsClientActivity(n: Notification): boolean { - return (n.category === NotificationCategory.WsClientActivity); + return n.category === NotificationCategory.WsClientActivity; } private isJvmDiscovery(n: Notification): boolean { - return (n.category === NotificationCategory.TargetJvmDiscovery); + return n.category === NotificationCategory.TargetJvmDiscovery; } static isProblemNotification(n: Notification): boolean { - return (n.variant === AlertVariant.warning) || (n.variant === AlertVariant.danger); + return n.variant === AlertVariant.warning || n.variant === AlertVariant.danger; } } @@ -173,4 +163,3 @@ const NotificationsInstance = new Notifications(); const NotificationsContext = React.createContext(NotificationsInstance); export { NotificationsContext, NotificationsInstance }; - diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index 63dfdd9be..21009dd02 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -63,13 +63,16 @@ export const BulkEditLabels: React.FunctionComponent = (pro const [valid, setValid] = React.useState(ValidatedOptions.default); const addSubscription = useSubscriptions(); - const getIdxFromRecording = React.useCallback((r: ArchivedRecording): number => { - if (props.isTargetRecording) { - return (r as ActiveRecording).id; - } else { - return hashCode(r.name); - } - }, [hashCode, props.isTargetRecording]); + const getIdxFromRecording = React.useCallback( + (r: ArchivedRecording): number => { + if (props.isTargetRecording) { + return (r as ActiveRecording).id; + } else { + return hashCode(r.name); + } + }, + [hashCode, props.isTargetRecording] + ); const handleUpdateLabels = React.useCallback(() => { const tasks: Observable[] = []; @@ -140,8 +143,8 @@ export const BulkEditLabels: React.FunctionComponent = (pro .pipe( filter((target) => target !== NO_TARGET), concatMap((target) => - props.isTargetRecording ? - context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/recordings`) + props.isTargetRecording + ? context.api.doGet(`targets/${encodeURIComponent(target.connectUrl)}/recordings`) : context.api.graphql(` query { targetNodes(filter: { name: "${target.connectUrl}" }) { @@ -158,9 +161,11 @@ export const BulkEditLabels: React.FunctionComponent = (pro } } } - }`), + }`) + ), + map((v) => + props.isTargetRecording ? v : (v.data.targetNodes[0].recordings.archived.data as ArchivedRecording[]) ), - map(v => props.isTargetRecording ? v : v.data.targetNodes[0].recordings.archived.data as ArchivedRecording[]), first() ) .subscribe((value) => setRecordings(value)) @@ -231,7 +236,7 @@ export const BulkEditLabels: React.FunctionComponent = (pro - + {editing ? ( @@ -258,7 +263,7 @@ export const BulkEditLabels: React.FunctionComponent = (pro ) : ( - {!!props.labels && props.labels.map((label, idx) => ( - - - handleKeyChange(idx, key)} - validated={validKeys[idx]} - /> - Key - - - - Keys must be unique. Labels should not contain whitespace. - - - - - - handleValueChange(idx, value)} - validated={validValues[idx]} - /> - Value - - - + , ]; if (props.archiveEnabled) { - arr.push(( - - )); + arr.push( + + ); } arr = [ ...arr, - , - , - - ] - return <> - { - arr.map((btn, idx) => ( - - { btn } - - )) - } - ; - }, [props.checkedIndices, - props.handleCreateRecording, - props.handleArchiveRecordings, - props.handleEditLabels, - props.handleStopRecordings, - handleDeleteButton]); + , + , + , + ]; + return ( + <> + {arr.map((btn, idx) => ( + {btn} + ))} + + ); + }, [ + props.checkedIndices, + props.handleCreateRecording, + props.handleArchiveRecordings, + props.handleEditLabels, + props.handleStopRecordings, + handleDeleteButton, + ]); const deleteActiveWarningModal = React.useMemo(() => { - return + return ( + + ); }, [warningModalOpen, props.handleDeleteRecordings, handleWarningModalClose]); return ( - - - { buttons } - - { deleteActiveWarningModal } + isArchived={false} + recordings={props.recordings} + filters={props.targetRecordingFilters} + updateFilters={props.updateFilters} + /> + {buttons} + {deleteActiveWarningModal} ); diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index f8de2eca9..7f6e555d4 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -40,7 +40,17 @@ import { ArchivedRecording, UPLOADS_SUBDIRECTORY } from '@app/Shared/Services/Ap import { ServiceContext } from '@app/Shared/Services/Services'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Button, Checkbox, Drawer, DrawerContent, DrawerContentBody, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; +import { + Button, + Checkbox, + Drawer, + DrawerContent, + DrawerContentBody, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from '@patternfly/react-core'; import { Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import { PlusIcon } from '@patternfly/react-icons'; import { RecordingActions } from './RecordingActions'; @@ -59,7 +69,13 @@ import { filterRecordings, RecordingFilters } from './RecordingFilters'; import { ArchiveUploadModal } from '@app/Archives/ArchiveUploadModal'; import { useDispatch, useSelector } from 'react-redux'; import { TargetRecordingFilters, UpdateFilterOptions } from '@app/Shared/Redux/RecordingFilterReducer'; -import { addFilterIntent, addTargetIntent, deleteAllFiltersIntent, deleteCategoryFiltersIntent, deleteFilterIntent } from '@app/Shared/Redux/RecordingFilterActions'; +import { + addFilterIntent, + addTargetIntent, + deleteAllFiltersIntent, + deleteCategoryFiltersIntent, + deleteFilterIntent, +} from '@app/Shared/Redux/RecordingFilterActions'; import { RootState, StateDispatch } from '@app/Shared/Redux/ReduxStore'; import { hashCode } from '@app/utils/utils'; @@ -74,7 +90,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent(); - const [targetConnectURL, setTargetConnectURL] = React.useState(""); + const [targetConnectURL, setTargetConnectURL] = React.useState(''); const [recordings, setRecordings] = React.useState([] as ArchivedRecording[]); const [filteredRecordings, setFilteredRecordings] = React.useState([] as ArchivedRecording[]); const [headerChecked, setHeaderChecked] = React.useState(false); @@ -85,40 +101,49 @@ export const ArchivedRecordingsTable: React.FunctionComponent { - const filters = state.recordingFilters.list.filter((targetFilter: TargetRecordingFilters) => targetFilter.target === targetConnectURL); - return filters.length > 0? filters[0].archived.filters: emptyArchivedRecordingFilters; + const filters = state.recordingFilters.list.filter( + (targetFilter: TargetRecordingFilters) => targetFilter.target === targetConnectURL + ); + return filters.length > 0 ? filters[0].archived.filters : emptyArchivedRecordingFilters; }) as RecordingFiltersCategories; - const tableColumns: string[] = [ - 'Name', - 'Labels', - ]; + const tableColumns: string[] = ['Name', 'Labels']; - const handleHeaderCheck = React.useCallback((event, checked) => { - setHeaderChecked(checked); - setCheckedIndices(checked ? filteredRecordings.map((r) => hashCode(r.name)) : []); - }, [setHeaderChecked, setCheckedIndices, filteredRecordings]); + const handleHeaderCheck = React.useCallback( + (event, checked) => { + setHeaderChecked(checked); + setCheckedIndices(checked ? filteredRecordings.map((r) => hashCode(r.name)) : []); + }, + [setHeaderChecked, setCheckedIndices, filteredRecordings] + ); - const handleRowCheck = React.useCallback((checked, index) => { - if (checked) { - setCheckedIndices(ci => ([...ci, index])); - } else { - setHeaderChecked(false); - setCheckedIndices(ci => ci.filter(v => v !== index)); - } - }, [setCheckedIndices, setHeaderChecked]); + const handleRowCheck = React.useCallback( + (checked, index) => { + if (checked) { + setCheckedIndices((ci) => [...ci, index]); + } else { + setHeaderChecked(false); + setCheckedIndices((ci) => ci.filter((v) => v !== index)); + } + }, + [setCheckedIndices, setHeaderChecked] + ); const handleEditLabels = React.useCallback(() => { setShowDetailsPanel(true); }, [setShowDetailsPanel]); - const handleRecordings = React.useCallback((recordings) => { - setRecordings(recordings); - setIsLoading(false); - }, [setRecordings, setIsLoading]); + const handleRecordings = React.useCallback( + (recordings) => { + setRecordings(recordings); + setIsLoading(false); + }, + [setRecordings, setIsLoading] + ); - const queryTargetRecordings = React.useCallback((connectUrl: string) => { - return context.api.graphql(` + const queryTargetRecordings = React.useCallback( + (connectUrl: string) => { + return context.api.graphql(` query { archivedRecordings(filter: { sourceTarget: "${connectUrl}" }) { data { @@ -130,8 +155,10 @@ export const ArchivedRecordingsTable: React.FunctionComponent { return context.api.graphql(` @@ -146,31 +173,27 @@ export const ArchivedRecordingsTable: React.FunctionComponent { setIsLoading(true); if (props.isUploadsTable) { addSubscription( queryUploadedRecordings() - .pipe( - map(v => v.data.archivedRecordings.data as ArchivedRecording[]) - ) - .subscribe(handleRecordings) + .pipe(map((v) => v.data.archivedRecordings.data as ArchivedRecording[])) + .subscribe(handleRecordings) ); } else { addSubscription( props.target - .pipe( - filter(target => target !== NO_TARGET), - first(), - concatMap(target => - queryTargetRecordings(target.connectUrl) - ), - map(v => v.data.archivedRecordings.data as ArchivedRecording[]), - ) - .subscribe(handleRecordings) + .pipe( + filter((target) => target !== NO_TARGET), + first(), + concatMap((target) => queryTargetRecordings(target.connectUrl)), + map((v) => v.data.archivedRecordings.data as ArchivedRecording[]) + ) + .subscribe(handleRecordings) ); } }, [addSubscription, context, context.api, setIsLoading, handleRecordings]); @@ -179,17 +202,20 @@ export const ArchivedRecordingsTable: React.FunctionComponent { - if (deleted) { - if (deleteOptions && deleteOptions.all) { - dispatch(deleteCategoryFiltersIntent(target, filterKey, true)); + const updateFilters = React.useCallback( + (target, { filterValue, filterKey, deleted = false, deleteOptions }) => { + if (deleted) { + if (deleteOptions && deleteOptions.all) { + dispatch(deleteCategoryFiltersIntent(target, filterKey, true)); + } else { + dispatch(deleteFilterIntent(target, filterKey, filterValue, true)); + } } else { - dispatch(deleteFilterIntent(target, filterKey, filterValue, true)); + dispatch(addFilterIntent(target, filterKey, filterValue, true)); } - } else { - dispatch(addFilterIntent(target, filterKey, filterValue, true)); - } - }, [dispatch, deleteCategoryFiltersIntent, deleteFilterIntent, addFilterIntent]); + }, + [dispatch, deleteCategoryFiltersIntent, deleteFilterIntent, addFilterIntent] + ); React.useEffect(() => { addSubscription( @@ -207,16 +233,15 @@ export const ArchivedRecordingsTable: React.FunctionComponent { + ]).subscribe((parts) => { const currentTarget = parts[0]; const event = parts[1]; if (currentTarget.connectUrl != event.message.target) { return; } - setRecordings(old => old.concat(event.message.recording)); + setRecordings((old) => old.concat(event.message.recording)); }) ); }, [addSubscription, context, context.notificationChannel, setRecordings]); @@ -226,16 +251,15 @@ export const ArchivedRecordingsTable: React.FunctionComponent { - const currentTarget = parts[0]; - const event = parts[1]; - if (currentTarget.connectUrl != event.message.target) { - return; - } + ]).subscribe((parts) => { + const currentTarget = parts[0]; + const event = parts[1]; + if (currentTarget.connectUrl != event.message.target) { + return; + } - setRecordings((old) => old.filter((r) => r.name !== event.message.recording.name)); - setCheckedIndices(old => old.filter((idx) => idx !== hashCode(event.message.recording.name))); + setRecordings((old) => old.filter((r) => r.name !== event.message.recording.name)); + setCheckedIndices((old) => old.filter((idx) => idx !== hashCode(event.message.recording.name))); }) ); }, [addSubscription, context, context.notificationChannel, setRecordings, setCheckedIndices]); @@ -244,18 +268,18 @@ export const ArchivedRecordingsTable: React.FunctionComponent { + context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), + ]).subscribe((parts) => { const currentTarget = parts[0]; const event = parts[1]; if (currentTarget.connectUrl != event.message.target) { return; } - setRecordings(old => old.map( - o => o.name == event.message.recordingName - ? { ...o, metadata: { labels: event.message.metadata.labels } } - : o)); + setRecordings((old) => + old.map((o) => + o.name == event.message.recordingName ? { ...o, metadata: { labels: event.message.metadata.labels } } : o + ) + ); }) ); }, [addSubscription, context, context.notificationChannel, setRecordings]); @@ -268,7 +292,10 @@ export const ArchivedRecordingsTable: React.FunctionComponent refreshRecordingList(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + const id = window.setInterval( + () => refreshRecordingList(), + context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits() + ); return () => window.clearInterval(id); }, [context, context.settings, refreshRecordingList]); @@ -286,32 +313,30 @@ export const ArchivedRecordingsTable: React.FunctionComponent { const tasks: Observable[] = []; addSubscription( - props.target.subscribe(t => { + props.target.subscribe((t) => { filteredRecordings.forEach((r: ArchivedRecording) => { if (checkedIndices.includes(hashCode(r.name))) { context.reports.delete(r); - tasks.push( - context.api.deleteArchivedRecording(t.connectUrl, r.name).pipe(first()) - ); + tasks.push(context.api.deleteArchivedRecording(t.connectUrl, r.name).pipe(first())); } }); }) ); - addSubscription( - forkJoin(tasks).subscribe() - ); + addSubscription(forkJoin(tasks).subscribe()); }, [filteredRecordings, checkedIndices, context.reports, context.api, addSubscription]); const toggleExpanded = React.useCallback( (id: string) => { setExpandedRows((expandedRows) => { const idx = expandedRows.indexOf(id); - return idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id] + return idx >= 0 + ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] + : [...expandedRows, id]; }); - }, [setExpandedRows] + }, + [setExpandedRows] ); - const RecordingRow = (props) => { const parsedLabels = React.useMemo(() => { return parseLabels(props.recording.metadata.labels); @@ -326,13 +351,16 @@ export const ArchivedRecordingsTable: React.FunctionComponent { - handleRowCheck(checked, props.index); - }, [handleRowCheck, props.index]); + const handleCheck = React.useCallback( + (checked) => { + handleRowCheck(checked, props.index); + }, + [handleRowCheck, props.index] + ); const parentRow = React.useMemo(() => { - return( - + return ( + + key={`archived-table-row-${props.index}_1`} + id={`archived-ex-toggle-${props.index}`} + aria-controls={`archived-ex-expand-${props.index}`} + expand={{ + rowIndex: props.index, + isExpanded: isExpanded, + onToggle: handleToggle, + }} + /> {props.recording.name} @@ -388,23 +414,19 @@ export const ArchivedRecordingsTable: React.FunctionComponent { return ( - - - - + + + + - ) + ); }, [props.recording, props.recording.name, props.index, isExpanded, tableColumns]); return ( @@ -415,35 +437,47 @@ export const ArchivedRecordingsTable: React.FunctionComponent ( - setShowUploadModal(true)} - isUploadsTable={props.isUploadsTable}/> - ), [ - targetConnectURL, - checkedIndices, - targetRecordingFilters, - recordings, - filteredRecordings, - updateFilters, - handleClearFilters, - handleEditLabels, - handleDeleteRecordings, - setShowUploadModal, - props.isUploadsTable - ]); + const RecordingsToolbar = React.useMemo( + () => ( + setShowUploadModal(true)} + isUploadsTable={props.isUploadsTable} + /> + ), + [ + targetConnectURL, + checkedIndices, + targetRecordingFilters, + recordings, + filteredRecordings, + updateFilters, + handleClearFilters, + handleEditLabels, + handleDeleteRecordings, + setShowUploadModal, + props.isUploadsTable, + ] + ); const recordingRows = React.useMemo(() => { - return filteredRecordings.map((r) => ) + return filteredRecordings.map((r) => ( + + )); }, [filteredRecordings, expandedRows, checkedIndices]); const handleModalClose = React.useCallback(() => { @@ -451,56 +485,55 @@ export const ArchivedRecordingsTable: React.FunctionComponent ( - - ), [checkedIndices, setShowDetailsPanel]); + const LabelsPanel = React.useMemo( + () => ( + + ), + [checkedIndices, setShowDetailsPanel] + ); return ( - - + + {recordingRows} - {props.isUploadsTable ? - - : - null - } + {props.isUploadsTable ? : null} ); }; export interface ArchivedRecordingsToolbarProps { - target: string, - checkedIndices: number[], - targetRecordingFilters: RecordingFiltersCategories, - recordings: ArchivedRecording[], - filteredRecordings: ArchivedRecording[], - updateFilters: (target: string, updateFilterOptions: UpdateFilterOptions) => void, - handleClearFilters: () => void, - handleEditLabels: () => void, - handleDeleteRecordings: () => void, - handleShowUploadModal: () => void, - isUploadsTable: boolean + target: string; + checkedIndices: number[]; + targetRecordingFilters: RecordingFiltersCategories; + recordings: ArchivedRecording[]; + filteredRecordings: ArchivedRecording[]; + updateFilters: (target: string, updateFilterOptions: UpdateFilterOptions) => void; + handleClearFilters: () => void; + handleEditLabels: () => void; + handleDeleteRecordings: () => void; + handleShowUploadModal: () => void; + isUploadsTable: boolean; } const ArchivedRecordingsToolbar: React.FunctionComponent = (props) => { @@ -509,7 +542,8 @@ const ArchivedRecordingsToolbar: React.FunctionComponent context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteArchivedRecordings), - [context, context.settings, context.settings.deletionDialogsEnabledFor]); + [context, context.settings, context.settings.deletionDialogsEnabledFor] + ); const handleWarningModalClose = React.useCallback(() => { setWarningModalOpen(false); @@ -524,42 +558,54 @@ const ArchivedRecordingsToolbar: React.FunctionComponent { - return + return ( + + ); }, [warningModalOpen, props.handleDeleteRecordings, handleWarningModalClose]); return ( - - - + + + + + + + + + + + {deleteArchivedWarningModal} + {props.isUploadsTable ? ( + - - - - + - { deleteArchivedWarningModal } - {props.isUploadsTable ? - - - - - - : - null - } - - + ) : null} + + ); }; diff --git a/src/app/Recordings/Filters/DateTimePicker.tsx b/src/app/Recordings/Filters/DateTimePicker.tsx index 8b7748030..9e4455ee8 100644 --- a/src/app/Recordings/Filters/DateTimePicker.tsx +++ b/src/app/Recordings/Filters/DateTimePicker.tsx @@ -36,15 +36,7 @@ * SOFTWARE. */ -import { - Button, - ButtonVariant, - DatePicker, - Flex, - FlexItem, - Text, - TimePicker, -} from '@patternfly/react-core'; +import { Button, ButtonVariant, DatePicker, Flex, FlexItem, Text, TimePicker } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import React from 'react'; @@ -53,7 +45,7 @@ export interface DateTimePickerProps { } export const DateTimePicker: React.FunctionComponent = (props) => { - const [selectedDate, setSelectedDate] = React.useState(); + const [selectedDate, setSelectedDate] = React.useState(); const [selectedHour, setSelectedHour] = React.useState(0); const [selectedMinute, setSelectedMinute] = React.useState(0); const [isTimeOpen, setIsTimeOpen] = React.useState(false); @@ -73,9 +65,12 @@ export const DateTimePicker: React.FunctionComponent = (pro [setSelectedHour, setSelectedMinute] ); - const onTimeToggle = React.useCallback((opened) => { - setIsTimeOpen(opened); - }, [setIsTimeOpen]); + const onTimeToggle = React.useCallback( + (opened) => { + setIsTimeOpen(opened); + }, + [setIsTimeOpen] + ); const handleSubmit = React.useCallback(() => { selectedDate!.setUTCHours(selectedHour, selectedMinute); @@ -91,10 +86,11 @@ export const DateTimePicker: React.FunctionComponent = (pro + appendTo={elementToAppend} + onChange={onDateChange} + aria-label="Date Picker" + placeholder="YYYY-MM-DD" + /> = (pro aria-label="Time Picker" className="time-picker" menuAppendTo={elementToAppend} - onChange={onTimeChange} - /> + onChange={onTimeChange} + /> UTC - diff --git a/src/app/Recordings/Filters/DurationFilter.tsx b/src/app/Recordings/Filters/DurationFilter.tsx index 15561ffe4..0980ea09f 100644 --- a/src/app/Recordings/Filters/DurationFilter.tsx +++ b/src/app/Recordings/Filters/DurationFilter.tsx @@ -47,19 +47,27 @@ export interface DurationFilterProps { export const DurationFilter: React.FunctionComponent = (props) => { const [duration, setDuration] = React.useState(30); - const isContinuous = React.useMemo(() => props.durations && props.durations.includes("continuous"), - [props.durations]); + const isContinuous = React.useMemo( + () => props.durations && props.durations.includes('continuous'), + [props.durations] + ); - const handleContinousCheckBoxChange = React.useCallback((checked, envt) => { - props.onContinuousDurationSelect(checked); - }, [props.onContinuousDurationSelect]); + const handleContinousCheckBoxChange = React.useCallback( + (checked, envt) => { + props.onContinuousDurationSelect(checked); + }, + [props.onContinuousDurationSelect] + ); - const handleEnterKey = React.useCallback((e) => { - if (e.key && e.key !== 'Enter') { - return; - } - props.onDurationInput(duration) - }, [props.onDurationInput, duration]); + const handleEnterKey = React.useCallback( + (e) => { + if (e.key && e.key !== 'Enter') { + return; + } + props.onDurationInput(duration); + }, + [props.onDurationInput, duration] + ); return ( diff --git a/src/app/Recordings/Filters/LabelFilter.tsx b/src/app/Recordings/Filters/LabelFilter.tsx index 5d6539ca4..1fdb344c6 100644 --- a/src/app/Recordings/Filters/LabelFilter.tsx +++ b/src/app/Recordings/Filters/LabelFilter.tsx @@ -50,41 +50,46 @@ export interface LabelFilterProps { export const getLabelDisplay = (label: RecordingLabel) => `${label.key}:${label.value}`; export const LabelFilter: React.FunctionComponent = (props) => { - const [isExpanded, setIsExpanded] = React.useState(false); + const [isExpanded, setIsExpanded] = React.useState(false); - const onSelect = React.useCallback( - (_, selection, isPlaceholder) => { - if (!isPlaceholder) { - setIsExpanded(false); - props.onSubmit(selection); - } - }, [props.onSubmit, setIsExpanded] - ); + const onSelect = React.useCallback( + (_, selection, isPlaceholder) => { + if (!isPlaceholder) { + setIsExpanded(false); + props.onSubmit(selection); + } + }, + [props.onSubmit, setIsExpanded] + ); - const labels = React.useMemo(() => { - const labels = new Set(); - props.recordings.forEach((r) => { - if (!r || !r.metadata || !r.metadata.labels) return; - parseLabels(r.metadata.labels).map((label) => labels.add(getLabelDisplay(label))); - }); - return Array.from(labels).filter((l) => !props.filteredLabels.includes(l)).sort(); - }, [props.recordings, parseLabels, getLabelDisplay]); + const labels = React.useMemo(() => { + const labels = new Set(); + props.recordings.forEach((r) => { + if (!r || !r.metadata || !r.metadata.labels) return; + parseLabels(r.metadata.labels).map((label) => labels.add(getLabelDisplay(label))); + }); + return Array.from(labels) + .filter((l) => !props.filteredLabels.includes(l)) + .sort(); + }, [props.recordings, parseLabels, getLabelDisplay]); - return ( - - ); + return ( + + ); }; diff --git a/src/app/Recordings/Filters/NameFilter.tsx b/src/app/Recordings/Filters/NameFilter.tsx index 7337d595b..c5ba266ba 100644 --- a/src/app/Recordings/Filters/NameFilter.tsx +++ b/src/app/Recordings/Filters/NameFilter.tsx @@ -55,13 +55,15 @@ export const NameFilter: React.FunctionComponent = (props) => { setIsExpanded(false); props.onSubmit(selection); } - }, [props.onSubmit, setIsExpanded] + }, + [props.onSubmit, setIsExpanded] ); const nameOptions = React.useMemo(() => { - return props.recordings.map((r) => r.name).filter((n) => !props.filteredNames.includes(n)).map((option, index) => ( - - )); + return props.recordings + .map((r) => r.name) + .filter((n) => !props.filteredNames.includes(n)) + .map((option, index) => ); }, [props.recordings, props.filteredNames]); return ( @@ -72,9 +74,9 @@ export const NameFilter: React.FunctionComponent = (props) => { isOpen={isExpanded} typeAheadAriaLabel="Filter by name..." placeholderText="Filter by name..." - aria-label='Filter by name' + aria-label="Filter by name" > - { nameOptions } + {nameOptions} ); }; diff --git a/src/app/Recordings/Filters/RecordingStateFilter.tsx b/src/app/Recordings/Filters/RecordingStateFilter.tsx index 075ca2bfa..7fd4b2898 100644 --- a/src/app/Recordings/Filters/RecordingStateFilter.tsx +++ b/src/app/Recordings/Filters/RecordingStateFilter.tsx @@ -36,7 +36,6 @@ * SOFTWARE. */ - import React from 'react'; import { Select, SelectOption, SelectVariant } from '@patternfly/react-core'; import { RecordingState } from '@app/Shared/Services/Api.service'; @@ -53,8 +52,10 @@ export const RecordingStateFilter: React.FunctionComponent { setIsOpen(false); props.onSelectToggle(selection); - }, [setIsOpen, props.onSelectToggle]); - + }, + [setIsOpen, props.onSelectToggle] + ); + return ( + aria-label="Filter by state" + placeholderText="Filter by state" + > + {Object.values(RecordingState).map((rs) => ( + + ))} + ); }; diff --git a/src/app/Recordings/RecordingActions.tsx b/src/app/Recordings/RecordingActions.tsx index e7c1a283d..b6551c681 100644 --- a/src/app/Recordings/RecordingActions.tsx +++ b/src/app/Recordings/RecordingActions.tsx @@ -35,15 +35,15 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import {NotificationsContext} from '@app/Notifications/Notifications'; -import {ActiveRecording} from '@app/Shared/Services/Api.service'; -import {ServiceContext} from '@app/Shared/Services/Services'; +import { NotificationsContext } from '@app/Notifications/Notifications'; +import { ActiveRecording } from '@app/Shared/Services/Api.service'; +import { ServiceContext } from '@app/Shared/Services/Services'; import { Target } from '@app/Shared/Services/Target.service'; -import {useSubscriptions} from '@app/utils/useSubscriptions'; +import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Td } from '@patternfly/react-table'; import * as React from 'react'; -import {Observable} from 'rxjs'; -import {first} from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; export interface RecordingActionsProps { index: number; @@ -60,7 +60,8 @@ export const RecordingActions: React.FunctionComponent = const addSubscription = useSubscriptions(); React.useEffect(() => { - const sub = context.api.grafanaDatasourceUrl() + const sub = context.api + .grafanaDatasourceUrl() .pipe(first()) .subscribe(() => setGrafanaEnabled(true)); return () => sub.unsubscribe(); @@ -69,14 +70,18 @@ export const RecordingActions: React.FunctionComponent = const grafanaUpload = React.useCallback(() => { notifications.info('Upload Started', `Recording "${props.recording.name}" uploading...`); addSubscription( - props.uploadFn() - .pipe(first()) - .subscribe(success => { - if (success) { - notifications.success('Upload Success', `Recording "${props.recording.name}" uploaded`); - context.api.grafanaDashboardUrl().pipe(first()).subscribe(url => window.open(url, '_blank')); - } - }) + props + .uploadFn() + .pipe(first()) + .subscribe((success) => { + if (success) { + notifications.success('Upload Success', `Recording "${props.recording.name}" uploaded`); + context.api + .grafanaDashboardUrl() + .pipe(first()) + .subscribe((url) => window.open(url, '_blank')); + } + }) ); }, [addSubscription, notifications, props.uploadFn, context.api]); @@ -91,21 +96,19 @@ export const RecordingActions: React.FunctionComponent = const actionItems = React.useMemo(() => { const actionItems = [ { - title: "Download Recording", - onClick: handleDownloadRecording + title: 'Download Recording', + onClick: handleDownloadRecording, }, { - title: "View Report ...", - onClick: handleViewReport - } + title: 'View Report ...', + onClick: handleViewReport, + }, ]; if (grafanaEnabled) { - actionItems.push( - { - title: "View in Grafana ...", - onClick: grafanaUpload - } - ); + actionItems.push({ + title: 'View in Grafana ...', + onClick: grafanaUpload, + }); } return actionItems; }, [handleDownloadRecording, handleViewReport, grafanaEnabled, grafanaUpload]); @@ -114,7 +117,7 @@ export const RecordingActions: React.FunctionComponent = ); diff --git a/src/app/Recordings/RecordingFilters.tsx b/src/app/Recordings/RecordingFilters.tsx index 3ec5d912a..1b626550f 100644 --- a/src/app/Recordings/RecordingFilters.tsx +++ b/src/app/Recordings/RecordingFilters.tsx @@ -62,12 +62,12 @@ import { updateCategoryIntent } from '@app/Shared/Redux/RecordingFilterActions'; import { StateDispatch, RootState } from '@app/Shared/Redux/ReduxStore'; export interface RecordingFiltersCategories { - Name: string[], - Label: string[], - State?: RecordingState[], - StartedBeforeDate?: string[], - StartedAfterDate?: string[], - DurationSeconds?: string[], + Name: string[]; + Label: string[]; + State?: RecordingState[]; + StartedBeforeDate?: string[]; + StartedAfterDate?: string[]; + DurationSeconds?: string[]; } export const emptyActiveRecordingFilters = { @@ -89,8 +89,8 @@ export const emptyArchivedRecordingFilters = { export const allowedArchivedRecordingFilters = Object.keys(emptyArchivedRecordingFilters); export interface RecordingFiltersProps { - target: string, - isArchived: boolean, + target: string; + isArchived: boolean; recordings: ArchivedRecording[]; filters: RecordingFiltersCategories; updateFilters: (target: string, updateFilterOptions: UpdateFilterOptions) => void; @@ -100,9 +100,11 @@ export const RecordingFilters: React.FunctionComponent = const dispatch = useDispatch(); const currentCategory = useSelector((state: RootState) => { - const targetRecordingFilters = state.recordingFilters.list.filter((targetFilter) => targetFilter.target === props.target); - if (!targetRecordingFilters.length) return "Name"; // Target is not yet loaded - return (props.isArchived? targetRecordingFilters[0].archived: targetRecordingFilters[0].active).selectedCategory; + const targetRecordingFilters = state.recordingFilters.list.filter( + (targetFilter) => targetFilter.target === props.target + ); + if (!targetRecordingFilters.length) return 'Name'; // Target is not yet loaded + return (props.isArchived ? targetRecordingFilters[0].archived : targetRecordingFilters[0].active).selectedCategory; }); const [isCategoryDropdownOpen, setIsCategoryDropdownOpen] = React.useState(false); @@ -114,8 +116,9 @@ export const RecordingFilters: React.FunctionComponent = const onCategorySelect = React.useCallback( (category) => { setIsCategoryDropdownOpen(false); - dispatch(updateCategoryIntent(props.target, category, props.isArchived)) - },[dispatch, updateCategoryIntent, setIsCategoryDropdownOpen, props.target, props.isArchived] + dispatch(updateCategoryIntent(props.target, category, props.isArchived)); + }, + [dispatch, updateCategoryIntent, setIsCategoryDropdownOpen, props.target, props.isArchived] ); const onDelete = React.useCallback( @@ -124,7 +127,8 @@ export const RecordingFilters: React.FunctionComponent = ); const onDeleteGroup = React.useCallback( - (category) => props.updateFilters(props.target, { filterKey: category, deleted: true, deleteOptions: {all: true} }), + (category) => + props.updateFilters(props.target, { filterKey: category, deleted: true, deleteOptions: { all: true } }), [props.updateFilters, props.target] ); @@ -134,7 +138,7 @@ export const RecordingFilters: React.FunctionComponent = ); const onLabelInput = React.useCallback( - (inputLabel) => props.updateFilters(props.target, { filterKey: currentCategory!, filterValue: inputLabel }), + (inputLabel) => props.updateFilters(props.target, { filterKey: currentCategory!, filterValue: inputLabel }), [props.updateFilters, currentCategory, props.target] ); @@ -144,50 +148,59 @@ export const RecordingFilters: React.FunctionComponent = ); const onStartedAfterInput = React.useCallback( - (searchDate) => props.updateFilters(props.target, { filterKey: currentCategory!, filterValue: searchDate }), + (searchDate) => props.updateFilters(props.target, { filterKey: currentCategory!, filterValue: searchDate }), [props.updateFilters, currentCategory, props.target] ); const onDurationInput = React.useCallback( - (duration) => props.updateFilters(props.target,{ filterKey: currentCategory!, filterValue: `${duration.toString()} s` }), + (duration) => + props.updateFilters(props.target, { filterKey: currentCategory!, filterValue: `${duration.toString()} s` }), [props.updateFilters, currentCategory, props.target] ); const onRecordingStateSelectToggle = React.useCallback( (searchState) => { const deleted = props.filters.State && props.filters.State.includes(searchState); - props.updateFilters(props.target,{ filterKey: currentCategory!, filterValue: searchState, deleted: deleted}) + props.updateFilters(props.target, { filterKey: currentCategory!, filterValue: searchState, deleted: deleted }); }, [props.updateFilters, currentCategory, props.target] ); const onContinuousDurationSelect = React.useCallback( - (cont) => props.updateFilters(props.target,{ filterKey: currentCategory!, filterValue: 'continuous', deleted: !cont }), + (cont) => + props.updateFilters(props.target, { filterKey: currentCategory!, filterValue: 'continuous', deleted: !cont }), [props.updateFilters, currentCategory, props.target] ); const categoryDropdown = React.useMemo(() => { - return ( - {currentCategory} - - } - isOpen={isCategoryDropdownOpen} - dropdownItems={ - (!props.isArchived? allowedActiveRecordingFilters: allowedArchivedRecordingFilters).map((cat) => ( - onCategorySelect(cat)}> - {cat} - - )) - } - /> + aria-label={'Category Dropdown'} + position={DropdownPosition.left} + toggle={ + + {currentCategory} + + } + isOpen={isCategoryDropdownOpen} + dropdownItems={(!props.isArchived ? allowedActiveRecordingFilters : allowedArchivedRecordingFilters).map( + (cat) => ( + onCategorySelect(cat)}> + {cat} + + ) + )} + /> ); - }, [props.isArchived, allowedActiveRecordingFilters, allowedArchivedRecordingFilters, isCategoryDropdownOpen, currentCategory, onCategoryToggle, onCategorySelect]); + }, [ + props.isArchived, + allowedActiveRecordingFilters, + allowedArchivedRecordingFilters, + isCategoryDropdownOpen, + currentCategory, + onCategoryToggle, + onCategorySelect, + ]); const filterDropdownItems = React.useMemo( () => [ @@ -197,22 +210,29 @@ export const RecordingFilters: React.FunctionComponent = , - ...(!props.isArchived? - [ - - - , - - - , - - - , - - - , - ]: [] - ) + ...(!props.isArchived + ? [ + + + , + + + , + + + , + + + , + ] + : []), ], [ props.isArchived, @@ -227,16 +247,14 @@ export const RecordingFilters: React.FunctionComponent = onStartedAfterInput, onStartedBeforeInput, onContinuousDurationSelect, - onDurationInput + onDurationInput, ] ); return ( } breakpoint="xl"> - - {categoryDropdown} - + {categoryDropdown} {Object.keys(props.filters).map((filterKey, i) => ( !!filters.State && filters.State.includes(r.state)); } if (!!filters.DurationSeconds && !!filters.DurationSeconds.length) { - filtered = filtered.filter( - (r) => { + filtered = filtered.filter((r) => { if (!filters.DurationSeconds) return true; - return filters.DurationSeconds.includes(`${r.duration / 1000} s`) || - (filters.DurationSeconds.includes('continuous') && r.continuous); - }); + return ( + filters.DurationSeconds.includes(`${r.duration / 1000} s`) || + (filters.DurationSeconds.includes('continuous') && r.continuous) + ); + }); } if (!!filters.StartedBeforeDate && !!filters.StartedBeforeDate.length) { filtered = filtered.filter((rec) => { @@ -296,11 +315,10 @@ export const filterRecordings = (recordings: any[], filters: RecordingFiltersCat }); } if (!!filters.Label.length) { - filtered = filtered.filter((r) => - Object.entries(r.metadata.labels) - .filter(([k,v]) => filters.Label.includes(`${k}:${v}`)).length - ); + filtered = filtered.filter( + (r) => Object.entries(r.metadata.labels).filter(([k, v]) => filters.Label.includes(`${k}:${v}`)).length + ); } return filtered; -} +}; diff --git a/src/app/Recordings/Recordings.tsx b/src/app/Recordings/Recordings.tsx index 03eee355a..a8f346cf0 100644 --- a/src/app/Recordings/Recordings.tsx +++ b/src/app/Recordings/Recordings.tsx @@ -54,18 +54,20 @@ export const Recordings = () => { const cardBody = React.useMemo(() => { return archiveEnabled ? ( - setActiveTab(Number(idx))}> - + setActiveTab(Number(idx))}> + - - + + ) : ( <> - Active Recordings - + + Active Recordings + + ); }, [archiveEnabled, activeTab]); @@ -73,9 +75,7 @@ export const Recordings = () => { return ( - - { cardBody } - + {cardBody} ); diff --git a/src/app/Recordings/RecordingsTable.tsx b/src/app/Recordings/RecordingsTable.tsx index 5a6ba00ab..50ebc6110 100644 --- a/src/app/Recordings/RecordingsTable.tsx +++ b/src/app/Recordings/RecordingsTable.tsx @@ -36,7 +36,14 @@ * SOFTWARE. */ import * as React from 'react'; -import { Title, EmptyState, EmptyStateIcon, EmptyStateBody, Button, EmptyStateSecondaryActions } from '@patternfly/react-core'; +import { + Title, + EmptyState, + EmptyStateIcon, + EmptyStateBody, + Button, + EmptyStateSecondaryActions, +} from '@patternfly/react-core'; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import { TableComposable, Thead, Tr, Th, OuterScrollContainer, InnerScrollContainer } from '@patternfly/react-table'; import { LoadingView } from '@app/LoadingView/LoadingView'; @@ -59,76 +66,84 @@ export interface RecordingsTableProps { export const RecordingsTable: React.FunctionComponent = (props) => { let view: JSX.Element; if (props.errorMessage != '') { - view = () + view = ; } else if (props.isLoading) { - view = () + view = ; } else if (props.isEmpty) { - view = (<> - - - - No {props.tableTitle} - - - ); + view = ( + <> + + + + No {props.tableTitle} + + + + ); } else if (props.isEmptyFilterResult) { - view = (<> - - - - No {props.tableTitle} found - - - No results match this filter criteria. - Remove all filters or clear all filters to show results. - - - - - - ); + view = ( + <> + + + + No {props.tableTitle} found + + + No results match this filter criteria. Remove all filters or clear all filters to show results. + + + + + + + ); } else { - view = (<> - - - - - - {props.tableColumns.map((key, idx) => ( - {key} - ))} - - - - { props.children } - - ); + view = ( + <> + + + + + + {props.tableColumns.map((key, idx) => ( + {key} + ))} + + + + {props.children} + + + ); } if (props.isNestedTable) { - return (<> - - { props.toolbar } - - { view } - - - ); + return ( + <> + + {props.toolbar} + {view} + + + ); } - return (<> - { props.toolbar } - { view } - ); + return ( + <> + {props.toolbar} + {view} + + ); }; diff --git a/src/app/Recordings/ReportFrame.tsx b/src/app/Recordings/ReportFrame.tsx index 18bea116c..b53e0847a 100644 --- a/src/app/Recordings/ReportFrame.tsx +++ b/src/app/Recordings/ReportFrame.tsx @@ -57,24 +57,30 @@ export const ReportFrame: React.FunctionComponent = React.memo if (!props.isExpanded) { return; } - const sub = context.reports.report(recording).pipe( - first() - ).subscribe(report => setReport(report), err => { - if (isGenerationError(err)) { - err.messageDetail.pipe(first()).subscribe(detail => setReport(detail)); - } else if (isHttpError(err)) { - setReport(err.message); - } else { - setReport(JSON.stringify(err)); - } - }); - return () => sub.unsubscribe(); + const sub = context.reports + .report(recording) + .pipe(first()) + .subscribe( + (report) => setReport(report), + (err) => { + if (isGenerationError(err)) { + err.messageDetail.pipe(first()).subscribe((detail) => setReport(detail)); + } else if (isHttpError(err)) { + setReport(err.message); + } else { + setReport(JSON.stringify(err)); + } + } + ); + return () => sub.unsubscribe(); }, [context, context.reports, recording, isExpanded, setReport, props, props.isExpanded, props.recording]); const onLoad = () => setLoaded(true); - return (<> - { !loaded && } -