From af8cb6b5cbb9d344be96d86b99b42f1418ebc9f9 Mon Sep 17 00:00:00 2001 From: Max Cao Date: Wed, 15 Mar 2023 14:40:11 -0400 Subject: [PATCH 01/21] init commit --- src/app/AppLayout/AppLayout.tsx | 20 +++--- src/app/AppLayout/QuickStartDrawer.tsx | 71 +++++++++++++++++++ src/app/QuickStarts/QuickStarts.tsx | 31 +++----- src/app/QuickStarts/all-quickstarts.ts | 3 +- .../quickstarts/settings-quickstart.ts | 71 +++++++++++++++++++ 5 files changed, 162 insertions(+), 34 deletions(-) create mode 100644 src/app/AppLayout/QuickStartDrawer.tsx create mode 100644 src/app/QuickStarts/quickstarts/settings-quickstart.ts diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index 8932a0ddb..965b66358 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -40,6 +40,7 @@ import cryostatLogo from '@app/assets/cryostat_logo_hori_rgb_reverse.svg'; import build from '@app/build.json'; import { NotificationCenter } from '@app/Notifications/NotificationCenter'; import { Notification, NotificationsContext } from '@app/Notifications/Notifications'; +import { allQuickStarts } from '@app/QuickStarts/all-quickstarts'; import { IAppRoute, navGroups, routes } from '@app/routes'; import { selectTab } from '@app/Settings/Settings'; import { DynamicFeatureFlag, FeatureFlag } from '@app/Shared/FeatureFlag/FeatureFlag'; @@ -49,6 +50,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { FeatureLevel } from '@app/Shared/Services/Settings.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { openTabForUrl, portalRoot } from '@app/utils/utils'; +import { QuickStartCatalogPage, QuickStartDrawer } from '@patternfly/quickstarts'; import { Alert, AlertActionCloseButton, @@ -95,6 +97,7 @@ import * as React from 'react'; import { Link, matchPath, NavLink, useHistory, useLocation } from 'react-router-dom'; import { map } from 'rxjs/operators'; import { AuthModal } from './AuthModal'; +import { GlobalQuickStartDrawer } from './QuickStartDrawer'; import { SslErrorModal } from './SslErrorModal'; interface AppLayoutProps { children: React.ReactNode; @@ -340,10 +343,11 @@ const AppLayout: React.FC = ({ children }) => { About , - - + Quick Starts + }> , ], @@ -558,14 +562,8 @@ const AppLayout: React.FC = ({ children }) => { ); return ( - <> - + + {notificationsToDisplay.slice(0, visibleNotificationsCount).map(({ key, title, message, variant }) => ( = ({ children }) => { - + ); }; diff --git a/src/app/AppLayout/QuickStartDrawer.tsx b/src/app/AppLayout/QuickStartDrawer.tsx new file mode 100644 index 000000000..e250f64bb --- /dev/null +++ b/src/app/AppLayout/QuickStartDrawer.tsx @@ -0,0 +1,71 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import { + QuickStartContext, + QuickStartDrawer, + useLocalStorage, + useValuesForQuickStartContext, +} from '@patternfly/quickstarts'; +import { allQuickStarts } from '@app/QuickStarts/all-quickstarts'; +import { useTranslation } from 'react-i18next'; + +export interface GlobalQuickStartDrawerProps { + children: React.ReactNode; +} + +export const GlobalQuickStartDrawer: React.FC = ({ children }) => { + const { t, i18n } = useTranslation(); + + const [activeQuickStartID, setActiveQuickStartID] = useLocalStorage('quickstartId', ''); + const [allQuickStartStates, setAllQuickStartStates] = useLocalStorage('quickstarts', {}); + const valuesForQuickStartContext = useValuesForQuickStartContext({ + allQuickStarts, + activeQuickStartID, + setActiveQuickStartID, + allQuickStartStates, + setAllQuickStartStates, + language: i18n.language, + }); + + return ( + + {children} + + ); +}; diff --git a/src/app/QuickStarts/QuickStarts.tsx b/src/app/QuickStarts/QuickStarts.tsx index 5265498fe..7b8c7a8aa 100644 --- a/src/app/QuickStarts/QuickStarts.tsx +++ b/src/app/QuickStarts/QuickStarts.tsx @@ -49,32 +49,19 @@ import { allQuickStarts } from './all-quickstarts'; export interface QuickStartsProps {} -const QuickStarts: React.FunctionComponent = (_) => { - const { t, i18n } = useTranslation(); - const [activeQuickStartID, setActiveQuickStartID] = useLocalStorage('quickstartId', ''); - const [allQuickStartStates, setAllQuickStartStates] = useLocalStorage('quickstarts', {}); +const QuickStartsCatalogPage: React.FunctionComponent = (_) => { + const { t } = useTranslation(); - const drawerProps: QuickStartContainerProps = { - quickStarts: allQuickStarts, - activeQuickStartID, - allQuickStartStates, - setActiveQuickStartID, - setAllQuickStartStates, - language: i18n.language, - alwaysShowTaskReview: true, - }; return ( }> - - - + ); }; -export default withRouter(QuickStarts); +export default withRouter(QuickStartsCatalogPage); diff --git a/src/app/QuickStarts/all-quickstarts.ts b/src/app/QuickStarts/all-quickstarts.ts index b666c22f1..fb410b681 100644 --- a/src/app/QuickStarts/all-quickstarts.ts +++ b/src/app/QuickStarts/all-quickstarts.ts @@ -39,6 +39,7 @@ import { QuickStart } from '@patternfly/quickstarts'; import { AddCardQuickStart } from './quickstarts/add-card-quickstart'; // import { GenericQuickStart } from './quickstarts/generic-quickstart'; import { SampleQuickStart } from './quickstarts/my-quickstart'; +import { SettingsQuickStart } from './quickstarts/settings-quickstart'; // Add your quick start here e.g. [GenericQuickStart, SampleQuickStart, AddCardQuickStart] -export const allQuickStarts: QuickStart[] = [SampleQuickStart, AddCardQuickStart]; +export const allQuickStarts: QuickStart[] = [SampleQuickStart, AddCardQuickStart, SettingsQuickStart ]; diff --git a/src/app/QuickStarts/quickstarts/settings-quickstart.ts b/src/app/QuickStarts/quickstarts/settings-quickstart.ts new file mode 100644 index 000000000..d8258fbec --- /dev/null +++ b/src/app/QuickStarts/quickstarts/settings-quickstart.ts @@ -0,0 +1,71 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import cryostatLogo from '@app/assets/cryostat_icon_rgb_default.svg'; +import { CogIcon } from '@patternfly/react-icons'; +import build from '@app/build.json'; +import { QuickStart } from '@patternfly/quickstarts'; + +// TODO: Add quickstarts based on the following example: +export const SettingsQuickStart: QuickStart = { + apiVersion: 'v2.3.0', + metadata: { + name: 'settings-quickstart', + }, + spec: { + displayName: 'Using Settings', + durationMinutes: 1, + icon: , + description: `Learn about the settings page in ${build.productName} and how to use it.`, + introduction: '### This is a generic quickstart.', + tasks: [ + { + title: 'Get started', + description: `### We will press the notifications bell icon on the top right. +1. Press the bell icon.`, + }, + ], + conclusion: `You finished **Getting Started with ${build.productName}**! + +Learn more about [${build.productName}](https://cryostat.io) from our website. +`, + type: { + text: 'Featured', + color: 'blue', + }, + }, +}; From ed10e0412beac26e96d9bb50313dddba54c40ba3 Mon Sep 17 00:00:00 2001 From: Max Cao Date: Wed, 15 Mar 2023 18:06:26 -0400 Subject: [PATCH 02/21] init commit 2 --- package.json | 1 + src/app/AppLayout/AppLayout.tsx | 90 +++++++++++------ src/app/AppLayout/CryostatJoyride.tsx | 36 +++++++ .../Quickstart/dashboard-quickstarts.ts | 2 +- .../add-card-quickstart.ts | 0 ...kStarts.tsx => QuickStartsCatalogPage.tsx} | 4 +- src/app/QuickStarts/all-quickstarts.ts | 3 +- .../quickstarts/generic-quickstart.ts | 2 + ...-quickstart.ts => settings-quickstart.tsx} | 63 +++++++++++- src/app/routes.tsx | 2 +- yarn.lock | 98 +++++++++++++++++++ 11 files changed, 262 insertions(+), 39 deletions(-) create mode 100644 src/app/AppLayout/CryostatJoyride.tsx rename src/app/{QuickStarts/quickstarts => Dashboard/Quickstart/dashboard-quickstarts}/add-card-quickstart.ts (100%) rename src/app/QuickStarts/{QuickStarts.tsx => QuickStartsCatalogPage.tsx} (97%) rename src/app/QuickStarts/quickstarts/{settings-quickstart.ts => settings-quickstart.tsx} (58%) diff --git a/package.json b/package.json index ccb04ddd1..eac4ec4b4 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-i18next": "^12.1.5", + "react-joyride": "^2.5.3", "react-redux": "^8.0.5", "react-router-last-location": "^2.0.1", "showdown": "^2.1.0" diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index 965b66358..64cee9a28 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -99,6 +99,9 @@ import { map } from 'rxjs/operators'; import { AuthModal } from './AuthModal'; import { GlobalQuickStartDrawer } from './QuickStartDrawer'; import { SslErrorModal } from './SslErrorModal'; +import Joyride from 'react-joyride'; +import { CryostatJoyride } from './CryostatJoyride'; +import ReactJoyride from 'react-joyride'; interface AppLayoutProps { children: React.ReactNode; } @@ -561,36 +564,67 @@ const AppLayout: React.FC = ({ children }) => { [handleCloseNotificationCenter] ); + const steps = [ + { + target: "pf-c-page__main-sectionk", + content: "Welcome to Cryostat! This is a quick tour of the UI.", + }, + { + target: ".pf-c-page__header-brand-link", + content: "This is the Cryostat logo. Clicking it will take you to the home page.", + disableBeacon: true, + }, + { + target: ".pf-c-page__header-tools", + content: "This is the toolbar. It contains the settings cog, the help icon, and the user menu.", + disableBeacon: true, + }, + { + target: ".pf-c-page__header-tools", + content: "Clicking the settings cog will take you to the settings page.", + disableBeacon: true, + }, +]; + return ( + - - {notificationsToDisplay.slice(0, visibleNotificationsCount).map(({ key, title, message, variant }) => ( - } - timeout={true} - onTimeout={handleTimeout(key)} - > - {message?.toString()} - - ))} - - - {children} - - - + + + + {notificationsToDisplay.slice(0, visibleNotificationsCount).map(({ key, title, message, variant }) => ( + } + timeout={true} + onTimeout={handleTimeout(key)} + > + {message?.toString()} + + ))} + + + {children} + + + ); }; diff --git a/src/app/AppLayout/CryostatJoyride.tsx b/src/app/AppLayout/CryostatJoyride.tsx new file mode 100644 index 000000000..0e2c560c6 --- /dev/null +++ b/src/app/AppLayout/CryostatJoyride.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import ReactJoyride from "react-joyride"; + +const steps = [ + { + target: "pf-c-page__main-sectionk", + content: "Welcome to Cryostat! This is a quick tour of the UI.", + }, + { + target: ".pf-c-page__header-brand-link", + content: "This is the Cryostat logo. Clicking it will take you to the home page.", + disableBeacon: true, + }, + { + target: ".pf-c-page__header-tools", + content: "This is the toolbar. It contains the settings cog, the help icon, and the user menu.", + disableBeacon: true, + }, + { + target: ".pf-c-page__header-tools", + content: "Clicking the settings cog will take you to the settings page.", + disableBeacon: true, + }, +]; + +export const CryostatJoyride: React.FC = ({}) => { + return ( + + ); +}; diff --git a/src/app/Dashboard/Quickstart/dashboard-quickstarts.ts b/src/app/Dashboard/Quickstart/dashboard-quickstarts.ts index 6fd0b4009..ffa2300c6 100644 --- a/src/app/Dashboard/Quickstart/dashboard-quickstarts.ts +++ b/src/app/Dashboard/Quickstart/dashboard-quickstarts.ts @@ -35,8 +35,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { AddCardQuickStart } from '@app/QuickStarts/quickstarts/add-card-quickstart'; import { SampleQuickStart } from '@app/QuickStarts/quickstarts/my-quickstart'; import { QuickStart } from '@patternfly/quickstarts'; +import { AddCardQuickStart } from './dashboard-quickstarts/add-card-quickstart'; export const allQuickStarts: QuickStart[] = [SampleQuickStart, AddCardQuickStart]; diff --git a/src/app/QuickStarts/quickstarts/add-card-quickstart.ts b/src/app/Dashboard/Quickstart/dashboard-quickstarts/add-card-quickstart.ts similarity index 100% rename from src/app/QuickStarts/quickstarts/add-card-quickstart.ts rename to src/app/Dashboard/Quickstart/dashboard-quickstarts/add-card-quickstart.ts diff --git a/src/app/QuickStarts/QuickStarts.tsx b/src/app/QuickStarts/QuickStartsCatalogPage.tsx similarity index 97% rename from src/app/QuickStarts/QuickStarts.tsx rename to src/app/QuickStarts/QuickStartsCatalogPage.tsx index 7b8c7a8aa..9a6a07c53 100644 --- a/src/app/QuickStarts/QuickStarts.tsx +++ b/src/app/QuickStarts/QuickStartsCatalogPage.tsx @@ -47,9 +47,9 @@ import { useTranslation } from 'react-i18next'; import { withRouter } from 'react-router-dom'; import { allQuickStarts } from './all-quickstarts'; -export interface QuickStartsProps {} +export interface QuickStartsCatalogPageProps {} -const QuickStartsCatalogPage: React.FunctionComponent = (_) => { +const QuickStartsCatalogPage: React.FunctionComponent = (_) => { const { t } = useTranslation(); return ( diff --git a/src/app/QuickStarts/all-quickstarts.ts b/src/app/QuickStarts/all-quickstarts.ts index fb410b681..dd56b7a3d 100644 --- a/src/app/QuickStarts/all-quickstarts.ts +++ b/src/app/QuickStarts/all-quickstarts.ts @@ -36,10 +36,9 @@ * SOFTWARE. */ import { QuickStart } from '@patternfly/quickstarts'; -import { AddCardQuickStart } from './quickstarts/add-card-quickstart'; // import { GenericQuickStart } from './quickstarts/generic-quickstart'; import { SampleQuickStart } from './quickstarts/my-quickstart'; import { SettingsQuickStart } from './quickstarts/settings-quickstart'; // Add your quick start here e.g. [GenericQuickStart, SampleQuickStart, AddCardQuickStart] -export const allQuickStarts: QuickStart[] = [SampleQuickStart, AddCardQuickStart, SettingsQuickStart ]; +export const allQuickStarts: QuickStart[] = [SampleQuickStart, SettingsQuickStart ]; diff --git a/src/app/QuickStarts/quickstarts/generic-quickstart.ts b/src/app/QuickStarts/quickstarts/generic-quickstart.ts index c8bb49fa6..1264a9dc6 100644 --- a/src/app/QuickStarts/quickstarts/generic-quickstart.ts +++ b/src/app/QuickStarts/quickstarts/generic-quickstart.ts @@ -46,10 +46,12 @@ export const GenericQuickStart: QuickStart = { name: 'generic-quickstart', }, spec: { + version: 2.3, displayName: 'Getting Started with', durationMinutes: 1, icon: cryostatLogo, description: `Get started with ${build.productName}.`, + prerequisites: [''], introduction: '### This is a generic quickstart.', tasks: [ { diff --git a/src/app/QuickStarts/quickstarts/settings-quickstart.ts b/src/app/QuickStarts/quickstarts/settings-quickstart.tsx similarity index 58% rename from src/app/QuickStarts/quickstarts/settings-quickstart.ts rename to src/app/QuickStarts/quickstarts/settings-quickstart.tsx index d8258fbec..91afae47c 100644 --- a/src/app/QuickStarts/quickstarts/settings-quickstart.ts +++ b/src/app/QuickStarts/quickstarts/settings-quickstart.tsx @@ -39,6 +39,7 @@ import cryostatLogo from '@app/assets/cryostat_icon_rgb_default.svg'; import { CogIcon } from '@patternfly/react-icons'; import build from '@app/build.json'; import { QuickStart } from '@patternfly/quickstarts'; +import React from 'react'; // TODO: Add quickstarts based on the following example: export const SettingsQuickStart: QuickStart = { @@ -47,16 +48,68 @@ export const SettingsQuickStart: QuickStart = { name: 'settings-quickstart', }, spec: { + version: 2.3, displayName: 'Using Settings', - durationMinutes: 1, + durationMinutes: 5, icon: , description: `Learn about the settings page in ${build.productName} and how to use it.`, - introduction: '### This is a generic quickstart.', + prerequisites: [''], + introduction: ` +
+

+

Using Settings

+ Cryostat has a settings page that allows you to configure the application. This quick start will show you how to use the settings page. +

+ There are various settings that can be configured: +

+
    +
  • Connectivity
  • +
  • Languages & Region
  • +
  • Notification & Messages
  • +
  • Dashboard
  • +
  • Advanced
  • +
+ We will go over each of these settings in detail. +

+
+ `, tasks: [ { - title: 'Get started', - description: `### We will press the notifications bell icon on the top right. -1. Press the bell icon.`, + title: 'Navigate to the Settings page', + description: ` + 1. Press the Settings cog icon.`, + }, + { + title: 'Go to the Connectivity tab', + description: ` + 1. Here you can configure the WebSocket connection to the Cryostat backend. + 2. You can also configure Auto-Refresh period for content-views.`, + }, + { + title: 'Go to the Languages & Region tab', + description: ` + 1. Here you can configure the language and region settings for the Cryostat UI. + 2. You can also configure the date and time format.`, + + }, + { + title: 'Go to the Notification & Messages tab', + description: ` + 1. Here you can configure the notification settings for the Cryostat UI. + 2. You can also configure the message settings.`, + + }, + { + title: 'Go to the Dashboard tab', + description: ` + 1. Here you can configure the dashboard settings for the Cryostat UI. + 2. You can also configure the default dashboard.`, + }, + { + title: 'Go to the Advanced tab', + description: ` + 1. Here you can configure the advanced settings for the Cryostat UI. + 2. You can also configure the default dashboard.`, }, ], conclusion: `You finished **Getting Started with ${build.productName}**! diff --git a/src/app/routes.tsx b/src/app/routes.tsx index 48ac749b0..f3b8561bd 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -47,7 +47,7 @@ import DashboardSolo from './Dashboard/DashboardSolo'; import Events from './Events/Events'; import Login from './Login/Login'; import NotFound from './NotFound/NotFound'; -import QuickStarts from './QuickStarts/QuickStarts'; +import QuickStarts from './QuickStarts/QuickStartsCatalogPage'; import Recordings from './Recordings/Recordings'; import CreateRule from './Rules/CreateRule'; import Rules from './Rules/Rules'; diff --git a/yarn.lock b/yarn.lock index b9e7a072d..cfa42b88f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -610,6 +610,13 @@ __metadata: languageName: node linkType: hard +"@gilbarbara/deep-equal@npm:^0.1.1": + version: 0.1.2 + resolution: "@gilbarbara/deep-equal@npm:0.1.2" + checksum: 78d4e76d36cbee639c008a63be52c1ac803212ff2560e55f68d2b8b2a6ac5e746c1976854cf101483ca18a9911aed2349da147b7756be43e75efb95e3f24468b + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.8": version: 0.11.8 resolution: "@humanwhocodes/config-array@npm:0.11.8" @@ -3966,6 +3973,7 @@ __metadata: react-docgen-typescript-loader: ^3.7.2 react-dom: ^17.0.2 react-i18next: ^12.1.5 + react-joyride: ^2.5.3 react-redux: ^8.0.5 react-router-dom: ^5.3.4 react-router-last-location: ^2.0.1 @@ -5889,6 +5897,13 @@ __metadata: languageName: node linkType: hard +"exenv@npm:^1.2.2": + version: 1.2.2 + resolution: "exenv@npm:1.2.2" + checksum: a894f3b60ab8419e0b6eec99c690a009c8276b4c90655ccaf7d28faba2de3a6b93b3d92210f9dc5efd36058d44f04098f6bbccef99859151104bfd49939904dc + languageName: node + linkType: hard + "exit@npm:^0.1.2": version: 0.1.2 resolution: "exit@npm:0.1.2" @@ -7475,6 +7490,20 @@ __metadata: languageName: node linkType: hard +"is-lite@npm:^0.8.2": + version: 0.8.2 + resolution: "is-lite@npm:0.8.2" + checksum: 0ee62cb238c2a044f58d1cd139fb0b48026c407ec8625ee6572b417f164e17ec937f0a0785f466e320749a796c316a3b78dcb4b520f7ddd4b9de38ad5a23d70f + languageName: node + linkType: hard + +"is-lite@npm:^0.9.2": + version: 0.9.2 + resolution: "is-lite@npm:0.9.2" + checksum: 8c4d2c58cf99a8289715925c0c3175dadf63e5ad293ad395ce650430ce90afe533a84ad0ffdeed0ce277dabdae63acdd3dac5d9b629bd61c3c0c620e2376f26e + languageName: node + linkType: hard + "is-map@npm:^2.0.1, is-map@npm:^2.0.2": version: 2.0.2 resolution: "is-map@npm:2.0.2" @@ -10800,6 +10829,24 @@ __metadata: languageName: node linkType: hard +"react-floater@npm:^0.7.6": + version: 0.7.6 + resolution: "react-floater@npm:0.7.6" + dependencies: + deepmerge: ^4.2.2 + exenv: ^1.2.2 + is-lite: ^0.8.2 + popper.js: ^1.16.0 + prop-types: ^15.8.1 + react-proptype-conditional-require: ^1.0.4 + tree-changes: ^0.9.1 + peerDependencies: + react: 15 - 18 + react-dom: 15 - 18 + checksum: 8268e14fbdf9393b39300f3c90ea2de382782f1d959176579e30841095a73f9240ec05fce3ec89e8b4e58cbc46c9b043b2ad0b338f5c5d3b91168b16a4282ac1 + languageName: node + linkType: hard + "react-i18next@npm:^12.1.5": version: 12.1.5 resolution: "react-i18next@npm:12.1.5" @@ -10839,6 +10886,26 @@ __metadata: languageName: node linkType: hard +"react-joyride@npm:^2.5.3": + version: 2.5.3 + resolution: "react-joyride@npm:2.5.3" + dependencies: + deepmerge: ^4.2.2 + exenv: ^1.2.2 + is-lite: ^0.9.2 + prop-types: ^15.8.1 + react-floater: ^0.7.6 + react-is: ^16.13.1 + scroll: ^3.0.1 + scrollparent: ^2.0.1 + tree-changes: ^0.9.2 + peerDependencies: + react: 15 - 18 + react-dom: 15 - 18 + checksum: 696b1bbf5583c95dac4f26968752c2ea249abcc600730b2087c3d6f6e6873e0aa2af26d222fce8a4c4442fdf92ce58d6588b68b4fdded15aa5faa3fb1f087c34 + languageName: node + linkType: hard + "react-measure@npm:^2.3.0": version: 2.5.2 resolution: "react-measure@npm:2.5.2" @@ -10854,6 +10921,13 @@ __metadata: languageName: node linkType: hard +"react-proptype-conditional-require@npm:^1.0.4": + version: 1.0.4 + resolution: "react-proptype-conditional-require@npm:1.0.4" + checksum: 78f82d15b2c77c14fd8fbcbbed279850df3a856984aacd519ee6c2162e034b114b8ac47c00157b84ef7c98c0711d933a0177d9d54555629cf381f54341bb0e8f + languageName: node + linkType: hard + "react-redux@npm:^8.0.5": version: 8.0.5 resolution: "react-redux@npm:8.0.5" @@ -11490,6 +11564,20 @@ __metadata: languageName: node linkType: hard +"scroll@npm:^3.0.1": + version: 3.0.1 + resolution: "scroll@npm:3.0.1" + checksum: e6b045347adace30035073882e6ef2af7e1c81dd611faf3a578ca8cd0d1a3a9da54932dd97ed6fd99c9573351c758fa50e6d8ed4afb5bd3a33794b6c48d25922 + languageName: node + linkType: hard + +"scrollparent@npm:^2.0.1": + version: 2.0.1 + resolution: "scrollparent@npm:2.0.1" + checksum: 9ff2b29c1233431ebd0910e5f8db4a058836ee0292d1bb18042fbb4d2175c3ed73fa28a71a96074ac6848173185238ad1139bdefc3863be4808df981175cb807 + languageName: node + linkType: hard + "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0" @@ -12534,6 +12622,16 @@ __metadata: languageName: node linkType: hard +"tree-changes@npm:^0.9.1, tree-changes@npm:^0.9.2": + version: 0.9.3 + resolution: "tree-changes@npm:0.9.3" + dependencies: + "@gilbarbara/deep-equal": ^0.1.1 + is-lite: ^0.8.2 + checksum: 86d890b18e83f2a20e7257982aec62efa186abbb08de4cead1c8062c50793f5b3c5fd09f98d9f4b8784b921e830687c0ea9bdb42ef4abb2eb9d6782d8c56a673 + languageName: node + linkType: hard + "ts-jest@npm:^27.0.5": version: 27.1.5 resolution: "ts-jest@npm:27.1.5" From 623dc8076dd7e254fe8b13fca411ec9200988764 Mon Sep 17 00:00:00 2001 From: Max Cao Date: Thu, 16 Mar 2023 18:12:57 -0400 Subject: [PATCH 03/21] joyride/quickstarts Signed-off-by: Max Cao --- README.md | 6 + locales/en/public.json | 9 + src/app/About/AboutCryostatModal.tsx | 26 +-- src/app/AppLayout/AppLayout.tsx | 146 +++++++------- src/app/AppLayout/CryostatJoyride.tsx | 179 +++++++++++++++--- src/app/AppLayout/QuickStartDrawer.tsx | 16 +- src/app/BreadcrumbPage/BreadcrumbPage.tsx | 14 +- .../Dashboard/Quickstart/QuickStartsCard.tsx | 2 +- .../QuickStarts/QuickStartsCatalogPage.tsx | 8 +- src/app/QuickStarts/all-quickstarts.ts | 2 +- .../quickstarts/settings-quickstart.tsx | 5 +- src/app/app.css | 7 + src/app/assets/palette.svg | 7 + 13 files changed, 285 insertions(+), 142 deletions(-) create mode 100644 src/app/assets/palette.svg diff --git a/README.md b/README.md index 8179910dc..2eedef49d 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,9 @@ The extraction tool is [`i18next-parser`](https://www.npmjs.com/package/i18next- ). To workaround this, specify static values in `i18n.ts` file under any top-level directory below `src/app`. For example, `src/app/Settings/i18n.ts`. + +## COLOR PALETTE + +The color palette for Cryostat is defined in `src/app/app.css` in `:root`. The colors are defined as variables and can be used throughout the application. + +![Palette](./src/app/assets/palette.svg) \ No newline at end of file diff --git a/locales/en/public.json b/locales/en/public.json index c7fe21c0d..685ef0e42 100644 --- a/locales/en/public.json +++ b/locales/en/public.json @@ -17,6 +17,15 @@ "CARD_DESCRIPTION_FULL": "This is a do-nothing placeholder with all the config.", "CARD_TITLE": "All Placeholder" }, + "AppLayout": { + "APP_LAUNCHER": { + "ABOUT": "About", + "DOCUMENTATION": "Documentation", + "GUIDED_TOUR": "Guided tour", + "HELP": "Help", + "QUICKSTARTS": "Quick Starts" + } + }, "AutomatedAnalysisCard": { "CARD_DESCRIPTION": "Assess common application performance and configuration issues", "CARD_DESCRIPTION_FULL": "Creates a recording and periodically evaluates various common problems in application configuration and performance. Results are displayed with scores from 0-100 with colour coding and in groups. This card should be unique on a dashboard.", diff --git a/src/app/About/AboutCryostatModal.tsx b/src/app/About/AboutCryostatModal.tsx index dc0ed772d..aa2f97204 100644 --- a/src/app/About/AboutCryostatModal.tsx +++ b/src/app/About/AboutCryostatModal.tsx @@ -38,6 +38,7 @@ import bkgImg from '@app/assets/about_background.png'; import cryostatLogo from '@app/assets/cryostat_icon_rgb_reverse.svg'; import build from '@app/build.json'; +import { portalRoot } from '@app/utils/utils'; import { AboutModal } from '@patternfly/react-core'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -46,18 +47,17 @@ import { AboutDescription } from './AboutDescription'; export const AboutCryostatModal = ({ isOpen, onClose }) => { const { t } = useTranslation(); return ( - <> - - - - + + + ); }; diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index 64cee9a28..9f8d2492b 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -56,12 +56,15 @@ import { AlertActionCloseButton, AlertGroup, AlertVariant, + ApplicationLauncher, + ApplicationLauncherItem, Brand, Button, Dropdown, DropdownGroup, DropdownItem, DropdownToggle, + Icon, Label, Masthead, MastheadBrand, @@ -74,6 +77,8 @@ import { NavList, NotificationBadge, Page, + PageGroup, + PageSection, PageSidebar, PageToggleButton, SkipToContent, @@ -88,6 +93,7 @@ import { CaretDownIcon, CogIcon, ExternalLinkAltIcon, + HelpIcon, PlusCircleIcon, QuestionCircleIcon, UserIcon, @@ -99,9 +105,10 @@ import { map } from 'rxjs/operators'; import { AuthModal } from './AuthModal'; import { GlobalQuickStartDrawer } from './QuickStartDrawer'; import { SslErrorModal } from './SslErrorModal'; -import Joyride from 'react-joyride'; -import { CryostatJoyride } from './CryostatJoyride'; +import Joyride, { CallBackProps, STATUS } from 'react-joyride'; +import CryostatJoyride from './CryostatJoyride'; import ReactJoyride from 'react-joyride'; +import { useTranslation } from 'react-i18next'; interface AppLayoutProps { children: React.ReactNode; } @@ -111,6 +118,7 @@ const AppLayout: React.FC = ({ children }) => { const notificationsContext = React.useContext(NotificationsContext); const addSubscription = useSubscriptions(); const routerHistory = useHistory(); + const { t } = useTranslation(); const [isNavOpen, setIsNavOpen] = React.useState(true); const [isMobileView, setIsMobileView] = React.useState(true); @@ -128,6 +136,7 @@ const AppLayout: React.FC = ({ children }) => { const [unreadNotificationsCount, setUnreadNotificationsCount] = React.useState(0); const [errorNotificationsCount, setErrorNotificationsCount] = React.useState(0); const [activeLevel, setActiveLevel] = React.useState(FeatureLevel.PRODUCTION); + const [joyrideRun, setJoyrideRun] = React.useState(false); const location = useLocation(); React.useEffect(() => { @@ -332,39 +341,37 @@ const AppLayout: React.FC = ({ children }) => { openTabForUrl(build.discussionUrl); }, []); - const helpItems = React.useMemo( - () => [ - - Documentation - - , - - Help - - , - - About - , - - - Quick Starts - - }> - - , - ], - [handleOpenDocumentation, handleOpenDiscussion, handleOpenAboutModal] - ); - - const HelpToggle = React.useMemo( - () => ( - - - - ), - [handleHelpToggle] - ); + const handleOpenGuidedTour = React.useCallback(() => { + console.log('handleOpenGuidedTour'); + setJoyrideRun(true); + }, [setJoyrideRun]); + + const helpItems = React.useMemo(() => { + return [ + {t('AppLayout.APP_LAUNCHER.QUICKSTARTS')}} + >, + + {t('AppLayout.APP_LAUNCHER.DOCUMENTATION')} + + + + , + + {t('AppLayout.APP_LAUNCHER.GUIDED_TOUR')} + , + + {t('AppLayout.APP_LAUNCHER.HELP')} + + + + , + + {t('AppLayout.APP_LAUNCHER.ABOUT')} + , + ]; + }, [t, handleOpenDocumentation, handleOpenGuidedTour, handleOpenDiscussion, handleOpenAboutModal]); const levelBadge = React.useCallback((level: FeatureLevel) => { return ( @@ -408,19 +415,22 @@ const AppLayout: React.FC = ({ children }) => { `; + }, + }, + ], + }, }); return ( diff --git a/src/app/Joyride/JoyrideProvider.tsx b/src/app/Joyride/JoyrideProvider.tsx new file mode 100644 index 000000000..8a3dfd43a --- /dev/null +++ b/src/app/Joyride/JoyrideProvider.tsx @@ -0,0 +1,80 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import useSetState from '@app/utils/useSetState'; +import React from 'react'; +import { Step } from 'react-joyride'; + +export interface JoyrideState { + run: boolean; + stepIndex: number; + steps: Step[]; + tourActive: boolean; +} + +const defaultState = { + run: false, + stepIndex: 0, + steps: [] as Step[], + tourActive: false, +}; + +export const JoyrideContext = React.createContext({ + state: defaultState, + setState: (patch: Partial | ((previousState: JoyrideState) => Partial)) => {}, +}); + +export const JoyrideProvider: React.FC<{ children }> = (props) => { + const [state, setState] = useSetState(defaultState); + const value = React.useMemo(() => ({ state, setState }), [state, setState]); + return ( + + {props.children} + + ); +}; + +export const useJoyride = (): { + setState: (patch: Partial | ((previousState: JoyrideState) => Partial)) => void; + state: JoyrideState; +} => { + const context = React.useContext(JoyrideContext); + if (context === undefined) { + throw new Error('useCryostatJoyride must be used within a CryostatJoyrideProvider'); + } + return context; +}; diff --git a/src/app/QuickStarts/quickstarts/settings-quickstart.tsx b/src/app/QuickStarts/quickstarts/settings-quickstart.tsx index 13f02669c..8013d1e64 100644 --- a/src/app/QuickStarts/quickstarts/settings-quickstart.tsx +++ b/src/app/QuickStarts/quickstarts/settings-quickstart.tsx @@ -76,42 +76,47 @@ export const SettingsQuickStart: QuickStart = { { title: 'Navigate to the Settings page', description: ` - 1. Press the Settings cog icon.`, +1. Press the [Settings]{{highlight settings-link}} cog icon.`, }, { - title: 'Go to the Connectivity tab', + title: 'Navigate to the Connectivity settings tab', description: ` - 1. Here you can configure the WebSocket connection to the Cryostat backend. - 2. You can also configure Auto-Refresh period for content-views.`, +1. Go to the [Connectivity]{{highlight settings-connectivity-tab}} tab. +2. Here you can configure the WebSocket connection to the Cryostat backend. +3. You can also configure Auto-Refresh period for content-views.`, }, { - title: 'Go to the Languages & Region tab', + title: 'Navigate to the Languages & Region settings tab', description: ` - 1. Here you can configure the language and region settings for the Cryostat UI. - 2. You can also configure the date and time format.`, +1. Go to the [Languages & Region]{{highlight settings-language®ion-tab}} tab +2. Here you can configure the language and region settings for the Cryostat UI. +3. You can also configure the date and time format.`, }, { - title: 'Go to the Notification & Messages tab', + title: 'Go to the Notifications & Messages tab', description: ` - 1. Here you can configure the notification settings for the Cryostat UI. - 2. You can also configure the message settings.`, +1. Go to the [Notifications & Messages]{{highlight settings-notifications&messages-tab}} tab. +1. Here you can configure the notification settings for the Cryostat UI. +2. You can also configure the message settings.`, }, { title: 'Go to the Dashboard tab', description: ` - 1. Here you can configure the dashboard settings for the Cryostat UI. - 2. You can also configure the default dashboard.`, +1. Go to the [Dashboard]{{highlight settings-dashboard-tab}} tab. +1. Here you can configure the dashboard settings for the Cryostat UI. +2. You can also configure the default dashboard.`, }, { title: 'Go to the Advanced tab', description: ` - 1. Here you can configure the advanced settings for the Cryostat UI. - 2. You can also configure the default dashboard.`, +1. Go to the [Advanced]{{highlight settings-advanced-tab}} tab. +1. Here you can configure the advanced settings for the Cryostat UI. +2. You can also configure the default dashboard.`, }, ], - conclusion: `You finished **Getting Started with ${build.productName}**! + conclusion: `You finished **Using Settings**! -Learn more about [${build.productName}](https://cryostat.io) from our website. +Learn more about the **Settings** page from our guides at . `, type: { text: 'Featured', diff --git a/src/app/Settings/Settings.tsx b/src/app/Settings/Settings.tsx index fe113a26e..59fa2a46f 100644 --- a/src/app/Settings/Settings.tsx +++ b/src/app/Settings/Settings.tsx @@ -39,7 +39,7 @@ import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; import { FeatureFlag } from '@app/Shared/FeatureFlag/FeatureFlag'; import { FeatureLevel } from '@app/Shared/Services/Settings.service'; -import { hashCode } from '@app/utils/utils'; +import { cleanQSDataId, hashCode } from '@app/utils/utils'; import { Card, Form, @@ -279,9 +279,16 @@ interface SettingTabProps extends TabProps { // Workaround to the Tabs component requiring children to be React.FC const SettingTab: React.FC = ({ featureLevelConfig, eventKey, title, children }) => { + const { t } = useTranslation(); + return ( - + {children} diff --git a/src/app/Shared/InteractiveSpotlight.tsx b/src/app/Shared/InteractiveSpotlight.tsx new file mode 100644 index 000000000..b34c00249 --- /dev/null +++ b/src/app/Shared/InteractiveSpotlight.tsx @@ -0,0 +1,99 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { PopperOptions } from 'popper.js'; +import * as React from 'react'; +import './spotlight.css'; +import Popper from './popper/Popper'; + +type InteractiveSpotlightProps = { + element: Element; +}; + +const isInViewport = (elementToCheck: Element) => { + const rect = elementToCheck.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +}; + +const popperOptions: PopperOptions = { + modifiers: { + preventOverflow: { + enabled: false, + }, + flip: { + enabled: false, + }, + }, +}; + +const InteractiveSpotlight: React.FC = ({ element }) => { + const { height, width } = element.getBoundingClientRect(); + const style: React.CSSProperties = { + height, + width, + }; + const [clicked, setClicked] = React.useState(false); + + React.useEffect(() => { + if (!clicked) { + if (!isInViewport(element)) { + element.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); + } + const handleClick = () => setClicked(true); + document.addEventListener('click', handleClick); + return () => { + document.removeEventListener('click', handleClick); + }; + } + return () => {}; + }, [element, clicked]); + + if (clicked) return null; + + return ( + +
+ + ); +}; + +export default InteractiveSpotlight; diff --git a/src/app/Shared/Spotlight.tsx b/src/app/Shared/Spotlight.tsx new file mode 100644 index 000000000..9e40ba4e2 --- /dev/null +++ b/src/app/Shared/Spotlight.tsx @@ -0,0 +1,64 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import InteractiveSpotlight from './InteractiveSpotlight'; +import StaticSpotlight from './StaticSpotlight'; + +type SpotlightProps = { + selector: string; + interactive?: boolean; +}; + +const Spotlight: React.FC = ({ selector, interactive }) => { + // if target element is a hidden one return null + const element = React.useMemo(() => { + const highlightElement = document.querySelector(selector); + let hiddenElement = highlightElement; + while (hiddenElement && interactive) { + const ariaHidden = hiddenElement.getAttribute('aria-hidden'); + if (ariaHidden === 'true') return null; + hiddenElement = hiddenElement.parentElement; + } + return highlightElement; + }, [selector, interactive]); + + if (!element) return null; + return interactive ? : ; +}; + +export default Spotlight; diff --git a/src/app/Shared/SpotlightElement.tsx b/src/app/Shared/SpotlightElement.tsx new file mode 100644 index 000000000..5f3de3943 --- /dev/null +++ b/src/app/Shared/SpotlightElement.tsx @@ -0,0 +1,37 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ diff --git a/src/app/Shared/StaticSpotlight.tsx b/src/app/Shared/StaticSpotlight.tsx new file mode 100644 index 000000000..f3515fce3 --- /dev/null +++ b/src/app/Shared/StaticSpotlight.tsx @@ -0,0 +1,72 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { portalRoot } from '@app/utils/utils'; +import * as React from 'react'; +import ReactDOM from 'react-dom'; +import './spotlight.css'; +import { useBoundingClientRect } from './useBoundingClientRect'; + +type StaticSpotlightProps = { + element: Element | HTMLElement; +}; + +const StaticSpotlight: React.FC = ({ element }) => { + const clientRect = useBoundingClientRect(element as HTMLElement); + React.useEffect(() => { + console.log('clientRect', clientRect); + }, [clientRect]); + + const style: React.CSSProperties = clientRect + ? { + top: clientRect.top, + left: clientRect.left, + height: clientRect.height, + width: clientRect.width, + } + : {}; + return clientRect + ? ReactDOM.createPortal( +
+
+
, + portalRoot + ) + : null; +}; + +export default StaticSpotlight; diff --git a/src/app/Shared/highlight-consts.ts b/src/app/Shared/highlight-consts.ts new file mode 100644 index 000000000..53e0a12c1 --- /dev/null +++ b/src/app/Shared/highlight-consts.ts @@ -0,0 +1,46 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +export const LINK_LABEL = '[\\d\\w\\s-()$!&]+'; +export const HIGHLIGHT_ACTIONS = ['highlight']; +export const SELECTOR_ID = `[\\w-&]+`; + +// [linkLabel]{{action id}} +export const HIGHLIGHT_REGEXP = new RegExp( + `\\[(${LINK_LABEL})]{{(${HIGHLIGHT_ACTIONS.join('|')}) (${SELECTOR_ID})}}`, + 'g' +); diff --git a/src/app/Shared/popper/Popper.tsx b/src/app/Shared/popper/Popper.tsx new file mode 100644 index 000000000..88281c089 --- /dev/null +++ b/src/app/Shared/popper/Popper.tsx @@ -0,0 +1,276 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import PopperJS, { PopperOptions } from 'popper.js'; +import * as React from 'react'; +import Portal from './Portal'; + +export const useCombineRefs = (...refs: (React.Ref | undefined)[]) => + React.useCallback( + (element: RefType | null): void => + refs.forEach((ref) => { + if (ref) { + if (typeof ref === 'function') { + ref(element); + } else { + (ref as React.MutableRefObject).current = element; + } + } + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + refs + ); + +// alignment with PopperJS reference API +type PopperJSReference = { + getBoundingClientRect: PopperJS['reference']['getBoundingClientRect']; + clientWidth: number; + clientHeight: number; +}; + +type ClientRectProp = { x: number; y: number; width?: number; height?: number }; + +type Reference = Element | PopperJSReference | ClientRectProp; + +class VirtualReference implements PopperJSReference { + private rect: ClientRect; + + constructor({ height = 0, width = 0, x, y }: ClientRectProp) { + this.rect = { + bottom: y + height, + height, + left: x, + right: x + width, + top: y, + width, + }; + } + + getBoundingClientRect(): ClientRect { + return this.rect; + } + + get clientWidth(): number { + return this.rect.width || 0; + } + + get clientHeight(): number { + return this.rect.height || 0; + } +} + +const getReference = (reference: Reference): PopperJSReference => + 'getBoundingClientRect' in reference ? reference : new VirtualReference(reference); + +type PopperProps = { + children: React.ReactNode; + closeOnEsc?: boolean; + closeOnOutsideClick?: boolean; + container?: React.ComponentProps['container']; + className?: string; + open?: boolean; + onRequestClose?: (e?: MouseEvent) => void; + placement?: + | 'bottom-end' + | 'bottom-start' + | 'bottom' + | 'left-end' + | 'left-start' + | 'left' + | 'right-end' + | 'right-start' + | 'right' + | 'top-end' + | 'top-start' + | 'top'; + popperOptions?: PopperOptions; + popperRef?: React.Ref; + reference: Reference | (() => Reference); + zIndex?: number; + returnFocus?: boolean; +}; + +const DEFAULT_POPPER_OPTIONS: PopperOptions = {}; + +const Popper: React.FC = ({ + children, + container, + className, + open, + placement = 'bottom-start', + reference, + popperOptions = DEFAULT_POPPER_OPTIONS, + closeOnEsc, + closeOnOutsideClick, + onRequestClose, + popperRef: popperRefIn, + zIndex = 9999, + returnFocus, +}) => { + const controlled = typeof open === 'boolean'; + const openProp = controlled ? open || false : true; + const nodeRef = React.useRef(); + const popperRef = React.useRef(null); + const popperRefs = useCombineRefs(popperRef, popperRefIn); + const [isOpen, setOpenState] = React.useState(openProp); + const focusRef = React.useRef(); + const onRequestCloseRef = React.useRef(onRequestClose); + onRequestCloseRef.current = onRequestClose; + + const setOpen = React.useCallback( + (newOpen: boolean) => { + if (returnFocus && newOpen !== isOpen) { + if (newOpen) { + if (document.activeElement) { + focusRef.current = document.activeElement; + } + } else if (focusRef.current instanceof HTMLElement && focusRef.current.ownerDocument) { + focusRef.current.focus(); + } + } + setOpenState(newOpen); + }, + [returnFocus, isOpen] + ); + + React.useEffect(() => { + setOpen(openProp); + }, [openProp, setOpen]); + + const onKeyDown = React.useCallback( + (e: KeyboardEvent) => { + if (e.keyCode === 27) { + controlled ? onRequestCloseRef.current && onRequestCloseRef.current() : setOpen(false); + } + }, + [controlled, setOpen] + ); + + const onClickOutside = React.useCallback( + (e: MouseEvent) => { + if (!nodeRef.current || (e.target instanceof Node && !nodeRef.current.contains(e.target))) { + controlled ? onRequestCloseRef.current && onRequestCloseRef.current(e) : setOpen(false); + } + }, + [controlled, setOpen] + ); + + const destroy = React.useCallback(() => { + if (popperRef.current) { + popperRef.current.destroy(); + popperRefs(null); + document.removeEventListener('keydown', onKeyDown, true); + document.removeEventListener('mousedown', onClickOutside, true); + document.removeEventListener('touchstart', onClickOutside, true); + } + }, [onClickOutside, onKeyDown, popperRefs]); + + const initialize = React.useCallback(() => { + if (!nodeRef.current || !reference || !isOpen) { + return; + } + + destroy(); + + popperRefs( + new PopperJS(getReference(typeof reference === 'function' ? reference() : reference), nodeRef.current, { + placement, + ...popperOptions, + modifiers: { + preventOverflow: { + boundariesElement: 'window', + }, + ...popperOptions.modifiers, + }, + }) + ); + + // init document listenerrs + if (closeOnEsc) { + document.addEventListener('keydown', onKeyDown, true); + } + if (closeOnOutsideClick) { + document.addEventListener('mousedown', onClickOutside, true); + document.addEventListener('touchstart', onClickOutside, true); + } + }, [ + popperRefs, + reference, + isOpen, + destroy, + placement, + popperOptions, + closeOnEsc, + closeOnOutsideClick, + onKeyDown, + onClickOutside, + ]); + + const nodeRefCallback = React.useCallback( + (node) => { + nodeRef.current = node; + initialize(); + }, + [initialize] + ); + + React.useEffect(() => { + initialize(); + }, [initialize]); + + React.useEffect(() => { + return () => { + destroy(); + }; + }, [destroy]); + + React.useEffect(() => { + if (!isOpen) { + destroy(); + } + }, [destroy, isOpen]); + + return isOpen ? ( + +
+ {children} +
+
+ ) : null; +}; + +export default Popper; diff --git a/src/app/Shared/popper/Portal.tsx b/src/app/Shared/popper/Portal.tsx new file mode 100644 index 000000000..67be6ed9e --- /dev/null +++ b/src/app/Shared/popper/Portal.tsx @@ -0,0 +1,61 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +type GetContainer = Element | null | undefined | (() => Element); + +type PortalProps = { + children: React.ReactNode; + container?: GetContainer; +}; + +const getContainer = (container: GetContainer): Element | null | undefined => + typeof container === 'function' ? container() : container; + +const Portal: React.FC = ({ children, container }) => { + const [containerNode, setContainerNode] = React.useState(); + + React.useLayoutEffect(() => { + setContainerNode(getContainer(container) || document.body); + }, [container]); + + return containerNode ? ReactDOM.createPortal(children, containerNode) : null; +}; + +export default Portal; diff --git a/src/app/Shared/spotlight.css b/src/app/Shared/spotlight.css new file mode 100644 index 000000000..934964463 --- /dev/null +++ b/src/app/Shared/spotlight.css @@ -0,0 +1,97 @@ +/* +Copyright The Cryostat Authors + +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any +person obtaining a copy of this software, associated documentation and/or data +(collectively the "Software"), free of charge and under any and all copyright +rights in the Software, and any and all patent rights owned or freely +licensable by each licensor hereunder covering either (i) the unmodified +Software as contributed to or provided by such licensor, or (ii) the Larger +Works (as defined below), to deal in both + +(a) the Software, and +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +one is included with the Software (each a "Larger Work" to which the Software +is contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, +use, sell, offer for sale, import, export, have made, and have sold the +Software and the Larger Work(s), and to sublicense the foregoing rights on +either these or other terms. + +This license is subject to the following condition: +The above copyright notice and either this complete permission notice or at +a minimum a reference to the UPL must be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +@keyframes ocs-spotlight-expand { + 0% { + outline-offset: -4px; + outline-width: 4px; + opacity: 1; + } + 100% { + outline-offset: 21px; + outline-width: 12px; + opacity: 0; + } +} +@keyframes ocs-spotlight-fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes ocs-spotlight-fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +.ocs-spotlight { + pointer-events: none; + position: absolute; +} +.ocs-spotlight__with-backdrop { + mix-blend-mode: hard-light; +} +.ocs-spotlight__element-highlight-noanimate { + border: var(--pf-global--BorderWidth--xl) solid var(--pf-global--palette--blue-100); + background-color: var(--pf-global--palette--black-500); + z-index: 9999; +} +.ocs-spotlight__element-highlight-animate { + pointer-events: none; + position: absolute; + box-shadow: inset 0px 0px 0px 4px var(--pf-global--palette--blue-200); + opacity: 0; + animation: 0.4s ocs-spotlight-fade-in 0s ease-in-out, 5s ocs-spotlight-fade-out 12.8s ease-in-out; + animation-fill-mode: forwards; +} +.ocs-spotlight__element-highlight-animate::after { + content: ''; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + animation: 1.2s ocs-spotlight-expand 1.6s ease-out; + animation-fill-mode: forwards; + outline: 4px solid var(--pf-global--palette--blue-200); + outline-offset: -4px; +} diff --git a/src/app/Shared/useBoundingClientRect.ts b/src/app/Shared/useBoundingClientRect.ts new file mode 100644 index 000000000..bceebf91e --- /dev/null +++ b/src/app/Shared/useBoundingClientRect.ts @@ -0,0 +1,65 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; + +type BoundingClientRect = ClientRect | null; + +export const useResizeObserver = (callback: ResizeObserverCallback, targetElement?: HTMLElement | null): void => { + const element = React.useMemo(() => targetElement ?? document.querySelector('body'), [targetElement]); + React.useEffect(() => { + const observer = new ResizeObserver(callback); + observer.observe(element); + return () => { + observer.disconnect(); + }; + }, [callback, element]); +}; + +export const useBoundingClientRect = (targetElement: HTMLElement | null): BoundingClientRect => { + const [clientRect, setClientRect] = React.useState(() => + targetElement ? targetElement.getBoundingClientRect() : null + ); + + const observerCallback = React.useCallback(() => { + setClientRect(targetElement ? targetElement.getBoundingClientRect() : null); + }, [targetElement]); + + useResizeObserver(observerCallback); + + return clientRect; +}; diff --git a/src/app/index.tsx b/src/app/index.tsx index 282fef0c4..118caa229 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -49,15 +49,18 @@ import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; import * as React from 'react'; import { Provider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; +import { JoyrideProvider } from './Joyride/JoyrideProvider'; const App: React.FunctionComponent = () => ( - - - + + + + + diff --git a/src/app/utils/useSetState.ts b/src/app/utils/useSetState.ts new file mode 100644 index 000000000..676391419 --- /dev/null +++ b/src/app/utils/useSetState.ts @@ -0,0 +1,18 @@ +import { useCallback, useState } from "react"; + +// taken from streamich.github.io/react-use +const useSetState = ( + initialState: T = {} as T + ): [T, (patch: Partial | ((prevState: T) => Partial)) => void] => { + const [state, set] = useState(initialState); + const setState = useCallback((patch) => { + set((prevState) => + Object.assign({}, prevState, patch instanceof Function ? patch(prevState) : patch) + ); + }, []); + + return [state, setState]; +}; + +export default useSetState; + \ No newline at end of file diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index 5f86d91ea..77a095754 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -172,6 +172,10 @@ export const evaluateTargetWithExpr = (target: unknown, matchExpression: string) export const portalRoot = document.getElementById('portal-root') || document.body; +export const cleanQSDataId = (key: string): string => { + return key.toLocaleLowerCase().replace(/\s+/g, ''); +}; + export class StreamOf { private readonly _stream$: BehaviorSubject; From a5b276ad91ba722f05d5a29994f6d2c5153d7a27 Mon Sep 17 00:00:00 2001 From: Max Cao Date: Tue, 21 Mar 2023 13:49:24 -0400 Subject: [PATCH 05/21] add some quickstarts, cleanup Signed-off-by: Max Cao --- README.md | 23 +- src/app/AppLayout/AppLayout.tsx | 6 +- .../CreateRecording/CustomRecordingForm.tsx | 7 +- .../Quickstart/dashboard-quickstarts.ts | 3 +- src/app/QuickStarts/all-quickstarts.ts | 8 +- .../QuickStarts/quickstarts/my-quickstart.ts | 69 ----- .../quickstarts/settings-quickstart.tsx | 9 +- .../quickstarts/start-a-recording.tsx | 168 +++++++++++ src/app/Recordings/ActiveRecordingsTable.tsx | 8 +- src/app/Recordings/Recordings.tsx | 4 +- src/app/Settings/Settings.tsx | 2 +- src/app/Shared/InteractiveSpotlight.tsx | 99 ------- src/app/Shared/Spotlight.tsx | 64 ---- src/app/Shared/SpotlightElement.tsx | 37 --- src/app/Shared/StaticSpotlight.tsx | 72 ----- src/app/Shared/popper/Popper.tsx | 276 ------------------ src/app/Shared/popper/Portal.tsx | 61 ---- src/app/Shared/spotlight.css | 97 ------ src/app/Shared/useBoundingClientRect.ts | 65 ----- src/app/TargetView/TargetContextSelector.tsx | 2 +- .../SelectTemplateSelectorForm.tsx | 1 + 21 files changed, 219 insertions(+), 862 deletions(-) delete mode 100644 src/app/QuickStarts/quickstarts/my-quickstart.ts create mode 100644 src/app/QuickStarts/quickstarts/start-a-recording.tsx delete mode 100644 src/app/Shared/InteractiveSpotlight.tsx delete mode 100644 src/app/Shared/Spotlight.tsx delete mode 100644 src/app/Shared/SpotlightElement.tsx delete mode 100644 src/app/Shared/StaticSpotlight.tsx delete mode 100644 src/app/Shared/popper/Popper.tsx delete mode 100644 src/app/Shared/popper/Portal.tsx delete mode 100644 src/app/Shared/spotlight.css delete mode 100644 src/app/Shared/useBoundingClientRect.ts diff --git a/README.md b/README.md index 2eedef49d..a775e71be 100644 --- a/README.md +++ b/README.md @@ -95,4 +95,25 @@ To workaround this, specify static values in `i18n.ts` file under any top-level The color palette for Cryostat is defined in `src/app/app.css` in `:root`. The colors are defined as variables and can be used throughout the application. -![Palette](./src/app/assets/palette.svg) \ No newline at end of file +![Palette](./src/app/assets/palette.svg) + +## ADDING QUICKSTARTS + +To add a new quickstart, create a new tsx/ts file under `src/app/QuickStarts` with your Quick Start name, like `my-quickstart.tsx`. + +Cryostat's Quick Starts use a markdown extension which allows components to be highlighted using a button within the markdown in the Quick Start content itself. It was taken from OpenShift Console's GitHub repo and modified to fit Cryostat's needs. + +### Highlighting elements + +You can highlight an element on the page from within a quick start. The element that should be highlightable needs a data-quickstart-id attribute. + +Example: +``` + +``` + +In the quick start task description, you can add this type of markdown to target this element: + +``` +Highlight [special button]{{highlight special-btn}} +``` diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index 10a6c679c..a79192724 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -49,8 +49,7 @@ import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.s import { ServiceContext } from '@app/Shared/Services/Services'; import { FeatureLevel } from '@app/Shared/Services/Settings.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { openTabForUrl, portalRoot } from '@app/utils/utils'; -import { QuickStartCatalogPage, QuickStartDrawer } from '@patternfly/quickstarts'; +import { cleanQSDataId, openTabForUrl, portalRoot } from '@app/utils/utils'; import { Alert, AlertActionCloseButton, @@ -472,6 +471,7 @@ const AppLayout: React.FC = ({ children }) => { aria-label="Navigation" isNavOpen={isNavOpen} onNavToggle={isMobileView ? onNavToggleMobile : onNavToggle} + data-quickstart-id="nav-toggle-btn" > @@ -537,7 +537,7 @@ const AppLayout: React.FC = ({ children }) => { id={`${route.label}-${idx}`} isActive={isActiveRoute(route)} > - + {route.label} {route.featureLevel !== undefined && levelBadge(route.featureLevel)} diff --git a/src/app/CreateRecording/CustomRecordingForm.tsx b/src/app/CreateRecording/CustomRecordingForm.tsx index fc5fc1e39..81af37293 100644 --- a/src/app/CreateRecording/CustomRecordingForm.tsx +++ b/src/app/CreateRecording/CustomRecordingForm.tsx @@ -418,6 +418,7 @@ export const CustomRecordingForm: React.FC = ({ prefil aria-describedby="recording-name-helper" onChange={handleRecordingNameChange} validated={nameValid} + data-quickstart-id="crf-name" /> = ({ prefil : 'Time before the recording is automatically stopped' } helperTextInvalid="A recording may only have a positive integer duration" + data-quickstart-id="crf-duration" > @@ -491,7 +493,7 @@ export const CustomRecordingForm: React.FC = ({ prefil onSelect={handleTemplateChange} /> - + = ({ prefil /> - + A value of 0 for maximum size or age means unbounded. = ({ prefil onClick={handleSubmit} isDisabled={isFormInvalid || loading} {...createButtonLoadingProps} + data-quickstart-id="crf-create-btn" > {loading ? 'Creating' : 'Create'} diff --git a/src/app/Dashboard/Quickstart/dashboard-quickstarts.ts b/src/app/Dashboard/Quickstart/dashboard-quickstarts.ts index ffa2300c6..5ab89dd4e 100644 --- a/src/app/Dashboard/Quickstart/dashboard-quickstarts.ts +++ b/src/app/Dashboard/Quickstart/dashboard-quickstarts.ts @@ -35,8 +35,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { SampleQuickStart } from '@app/QuickStarts/quickstarts/my-quickstart'; import { QuickStart } from '@patternfly/quickstarts'; import { AddCardQuickStart } from './dashboard-quickstarts/add-card-quickstart'; -export const allQuickStarts: QuickStart[] = [SampleQuickStart, AddCardQuickStart]; +export const allQuickStarts: QuickStart[] = [AddCardQuickStart]; diff --git a/src/app/QuickStarts/all-quickstarts.ts b/src/app/QuickStarts/all-quickstarts.ts index 4017b1e6a..68f772a10 100644 --- a/src/app/QuickStarts/all-quickstarts.ts +++ b/src/app/QuickStarts/all-quickstarts.ts @@ -37,8 +37,8 @@ */ import { QuickStart } from '@patternfly/quickstarts'; // import { GenericQuickStart } from './quickstarts/generic-quickstart'; -import { SampleQuickStart } from './quickstarts/my-quickstart'; -import { SettingsQuickStart } from './quickstarts/settings-quickstart'; +import RecordingQuickStart from './quickstarts/start-a-recording'; +import SettingsQuickStart from './quickstarts/settings-quickstart'; -// Add your quick start here e.g. [GenericQuickStart, SampleQuickStart, AddCardQuickStart] -export const allQuickStarts: QuickStart[] = [SampleQuickStart, SettingsQuickStart]; +// Add your quick start here e.g. [GenericQuickStart, SampleQuickStart] +export const allQuickStarts: QuickStart[] = [RecordingQuickStart, SettingsQuickStart]; diff --git a/src/app/QuickStarts/quickstarts/my-quickstart.ts b/src/app/QuickStarts/quickstarts/my-quickstart.ts deleted file mode 100644 index f5380d2ad..000000000 --- a/src/app/QuickStarts/quickstarts/my-quickstart.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import cryostatLogo from '@app/assets/cryostat_icon_rgb_default.svg'; -import { QuickStart } from '@patternfly/quickstarts'; - -export const SampleQuickStart: QuickStart = { - apiVersion: 'v2.3.0', - metadata: { - name: 'sample-quickstart', - }, - spec: { - displayName: 'Sample QuickStart', - durationMinutes: 1, - icon: cryostatLogo, - description: 'This is a sample quickstart.', - introduction: '### This is a sample quickstart.', - tasks: [ - { - title: 'Task 1', - description: 'This is a sample task.', - review: { - instructions: '#### Verify that you have done the task.', - failedTaskHelp: 'This is how you can fix the failed task.', - }, - }, - ], - conclusion: '
You have completed the sample quickstart.
', - nextQuickStart: ['add-card-quickstart'], - type: { - text: 'Introduction', - color: 'green', - }, - }, -}; diff --git a/src/app/QuickStarts/quickstarts/settings-quickstart.tsx b/src/app/QuickStarts/quickstarts/settings-quickstart.tsx index 8013d1e64..575628d5c 100644 --- a/src/app/QuickStarts/quickstarts/settings-quickstart.tsx +++ b/src/app/QuickStarts/quickstarts/settings-quickstart.tsx @@ -40,8 +40,7 @@ import { QuickStart } from '@patternfly/quickstarts'; import { CogIcon } from '@patternfly/react-icons'; import React from 'react'; -// TODO: Add quickstarts based on the following example: -export const SettingsQuickStart: QuickStart = { +const SettingsQuickStart: QuickStart = { apiVersion: 'v2.3.0', metadata: { name: 'settings-quickstart', @@ -51,7 +50,7 @@ export const SettingsQuickStart: QuickStart = { displayName: 'Using Settings', durationMinutes: 5, icon: , - description: `Learn about the settings page in ${build.productName} and how to use it.`, + description: `Learn about the settings page in **${build.productName}** and how to use it.`, prerequisites: [''], introduction: `
@@ -114,7 +113,7 @@ export const SettingsQuickStart: QuickStart = { 2. You can also configure the default dashboard.`, }, ], - conclusion: `You finished **Using Settings**! + conclusion: `You completed the **Using Settings** quick start! Learn more about the **Settings** page from our guides at . `, @@ -124,3 +123,5 @@ Learn more about the **Settings** page from our guides at Note: If JMX Auth username and password is required, you will be prompted to enter them.`, + }, + { + title: 'Start a recording', + description: ` +There are two tabs within the Recordings page: \n +[Active Recordings]{{highlight active-recordings-tab}} and [Archived Recordings]{{highlight archived-recordings-tab}}.\n +Active recordings are recordings that are currently running, and Archived recordings are recordings that have been stopped. + +We will start a recording while on the Active tab. + +1. Click [Create]{{highlight recordings-create-btn}} to go to the Custom Flight Recording Form. +2. Enter a name for the recording in the [Name]{{highlight crf-name}} field. +3. Select the [Duration]{{highlight crf-duration}} for the recording. You can select CONTINUOUS to record until the recording is stopped. +4. Select the Events to record using the [Event Template]{{highlight template-selector}} selector. +5. Click [Create]{{highlight crf-create-btn}} to start the recording. + +After the creation of a recording, the recording will be displayed in the Active Recordings tab. You should be able to see the recording's name, start time, duration, state, and any attached labels. + +Note: You may also attach metadata labels to the recordings under the [Metadata]{{highlight crf-metadata-opt}} options or configure your custom recording further under the [Advanced]{{highlight crf-advanced-opt}} options.`, + review: { + instructions: '#### Verify that you see the recording within the table.', + failedTaskHelp: 'If you do not see the recording, try the steps again.', + }, +}, + { + title: 'Stop a recording', + description: ` +Stopping a recording will cut off the recording at the time that the recording is stopped. + +1. Click the [Stop]{{highlight recordings-stop-btn}} button to stop the recording.` + }, + { + title: 'Download a recording', + description: ` +Downloading a recording will save the recording to your local machine as a JFR file. You can then use JDK Mission Control (JMC) to analyze the recording. +1. Open the kebab menu next to the recording that you want to download. +2. Click the [Download]{{highlight recordings-download-btn}} button to download the recording to your local machine. +3. Choose what to do with the file. Your browser will present you to save the file to your local machine. + ` + }, + { + title: 'View an analysis report', + description: ` +1. Click the kebab menu next to the recording that you want to view an analysis report for. +2. Click [View Report]{{highlight recordings-view-analysis-btn}} to view an analysis report of the recording. +` + }, + { + title: 'Archive a recording', + description: ` +Archiving a recording will save the recording to Cryostat's archival storage. These recordings will show up in the target JVM's Archived Recordings tab, as well as in the [Archives]{{highlight nav-archives-tab}} view on the Cryostat console navigation bar. move the recording from the Active Recordings tab to the Archived Recordings tab. Archived recordings can be also be downloaded to your local machine. + +1. Click the [Archive]{{highlight recordings-archive-btn}} button to archive the recording. +2. Go to the Archived Recordings tab to see the archived recording. + +Note: You can also download and view an analysis report of the archived recording from the Archived Recordings tab. +` + }, + + ], + conclusion: ` +
+

You completed the Start a Recording quick start!

+ +
+ Cryostat Logo +
+

Learn more about Cryostat from our guides at cryostat.io.

+
`, + type: { + text: 'Featured', + color: 'blue', + }, + }, +}; + +export default RecordingQuickStart; diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index 27713ccc6..8cbfad952 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -663,7 +663,7 @@ const ActiveRecordingsToolbar: React.FunctionComponent + ), @@ -682,6 +682,7 @@ const ActiveRecordingsToolbar: React.FunctionComponent {props.actionLoadings['ARCHIVE'] ? 'Archiving' : 'Archive'} @@ -699,7 +700,8 @@ const ActiveRecordingsToolbar: React.FunctionComponent + ), @@ -716,6 +718,7 @@ const ActiveRecordingsToolbar: React.FunctionComponent {props.actionLoadings['STOP'] ? 'Stopping' : 'Stop'} @@ -734,6 +737,7 @@ const ActiveRecordingsToolbar: React.FunctionComponent {props.actionLoadings['DELETE'] ? 'Deleting' : 'Delete'} diff --git a/src/app/Recordings/Recordings.tsx b/src/app/Recordings/Recordings.tsx index 1290d8354..a32425175 100644 --- a/src/app/Recordings/Recordings.tsx +++ b/src/app/Recordings/Recordings.tsx @@ -74,10 +74,10 @@ export const Recordings: React.FC, Sta const cardBody = React.useMemo(() => { return archiveEnabled ? ( - Active Recordings}> + Active Recordings} data-quickstart-id="active-recordings-tab"> - Archived Recordings}> + Archived Recordings} data-quickstart-id="archived-recordings-tab"> diff --git a/src/app/Settings/Settings.tsx b/src/app/Settings/Settings.tsx index 59fa2a46f..13f3af167 100644 --- a/src/app/Settings/Settings.tsx +++ b/src/app/Settings/Settings.tsx @@ -284,7 +284,7 @@ const SettingTab: React.FC = ({ featureLevelConfig, eventKey, t return ( { - const rect = elementToCheck.getBoundingClientRect(); - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - rect.right <= (window.innerWidth || document.documentElement.clientWidth) - ); -}; - -const popperOptions: PopperOptions = { - modifiers: { - preventOverflow: { - enabled: false, - }, - flip: { - enabled: false, - }, - }, -}; - -const InteractiveSpotlight: React.FC = ({ element }) => { - const { height, width } = element.getBoundingClientRect(); - const style: React.CSSProperties = { - height, - width, - }; - const [clicked, setClicked] = React.useState(false); - - React.useEffect(() => { - if (!clicked) { - if (!isInViewport(element)) { - element.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); - } - const handleClick = () => setClicked(true); - document.addEventListener('click', handleClick); - return () => { - document.removeEventListener('click', handleClick); - }; - } - return () => {}; - }, [element, clicked]); - - if (clicked) return null; - - return ( - -
- - ); -}; - -export default InteractiveSpotlight; diff --git a/src/app/Shared/Spotlight.tsx b/src/app/Shared/Spotlight.tsx deleted file mode 100644 index 9e40ba4e2..000000000 --- a/src/app/Shared/Spotlight.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import * as React from 'react'; -import InteractiveSpotlight from './InteractiveSpotlight'; -import StaticSpotlight from './StaticSpotlight'; - -type SpotlightProps = { - selector: string; - interactive?: boolean; -}; - -const Spotlight: React.FC = ({ selector, interactive }) => { - // if target element is a hidden one return null - const element = React.useMemo(() => { - const highlightElement = document.querySelector(selector); - let hiddenElement = highlightElement; - while (hiddenElement && interactive) { - const ariaHidden = hiddenElement.getAttribute('aria-hidden'); - if (ariaHidden === 'true') return null; - hiddenElement = hiddenElement.parentElement; - } - return highlightElement; - }, [selector, interactive]); - - if (!element) return null; - return interactive ? : ; -}; - -export default Spotlight; diff --git a/src/app/Shared/SpotlightElement.tsx b/src/app/Shared/SpotlightElement.tsx deleted file mode 100644 index 5f3de3943..000000000 --- a/src/app/Shared/SpotlightElement.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ diff --git a/src/app/Shared/StaticSpotlight.tsx b/src/app/Shared/StaticSpotlight.tsx deleted file mode 100644 index f3515fce3..000000000 --- a/src/app/Shared/StaticSpotlight.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import { portalRoot } from '@app/utils/utils'; -import * as React from 'react'; -import ReactDOM from 'react-dom'; -import './spotlight.css'; -import { useBoundingClientRect } from './useBoundingClientRect'; - -type StaticSpotlightProps = { - element: Element | HTMLElement; -}; - -const StaticSpotlight: React.FC = ({ element }) => { - const clientRect = useBoundingClientRect(element as HTMLElement); - React.useEffect(() => { - console.log('clientRect', clientRect); - }, [clientRect]); - - const style: React.CSSProperties = clientRect - ? { - top: clientRect.top, - left: clientRect.left, - height: clientRect.height, - width: clientRect.width, - } - : {}; - return clientRect - ? ReactDOM.createPortal( -
-
-
, - portalRoot - ) - : null; -}; - -export default StaticSpotlight; diff --git a/src/app/Shared/popper/Popper.tsx b/src/app/Shared/popper/Popper.tsx deleted file mode 100644 index 88281c089..000000000 --- a/src/app/Shared/popper/Popper.tsx +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import PopperJS, { PopperOptions } from 'popper.js'; -import * as React from 'react'; -import Portal from './Portal'; - -export const useCombineRefs = (...refs: (React.Ref | undefined)[]) => - React.useCallback( - (element: RefType | null): void => - refs.forEach((ref) => { - if (ref) { - if (typeof ref === 'function') { - ref(element); - } else { - (ref as React.MutableRefObject).current = element; - } - } - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - refs - ); - -// alignment with PopperJS reference API -type PopperJSReference = { - getBoundingClientRect: PopperJS['reference']['getBoundingClientRect']; - clientWidth: number; - clientHeight: number; -}; - -type ClientRectProp = { x: number; y: number; width?: number; height?: number }; - -type Reference = Element | PopperJSReference | ClientRectProp; - -class VirtualReference implements PopperJSReference { - private rect: ClientRect; - - constructor({ height = 0, width = 0, x, y }: ClientRectProp) { - this.rect = { - bottom: y + height, - height, - left: x, - right: x + width, - top: y, - width, - }; - } - - getBoundingClientRect(): ClientRect { - return this.rect; - } - - get clientWidth(): number { - return this.rect.width || 0; - } - - get clientHeight(): number { - return this.rect.height || 0; - } -} - -const getReference = (reference: Reference): PopperJSReference => - 'getBoundingClientRect' in reference ? reference : new VirtualReference(reference); - -type PopperProps = { - children: React.ReactNode; - closeOnEsc?: boolean; - closeOnOutsideClick?: boolean; - container?: React.ComponentProps['container']; - className?: string; - open?: boolean; - onRequestClose?: (e?: MouseEvent) => void; - placement?: - | 'bottom-end' - | 'bottom-start' - | 'bottom' - | 'left-end' - | 'left-start' - | 'left' - | 'right-end' - | 'right-start' - | 'right' - | 'top-end' - | 'top-start' - | 'top'; - popperOptions?: PopperOptions; - popperRef?: React.Ref; - reference: Reference | (() => Reference); - zIndex?: number; - returnFocus?: boolean; -}; - -const DEFAULT_POPPER_OPTIONS: PopperOptions = {}; - -const Popper: React.FC = ({ - children, - container, - className, - open, - placement = 'bottom-start', - reference, - popperOptions = DEFAULT_POPPER_OPTIONS, - closeOnEsc, - closeOnOutsideClick, - onRequestClose, - popperRef: popperRefIn, - zIndex = 9999, - returnFocus, -}) => { - const controlled = typeof open === 'boolean'; - const openProp = controlled ? open || false : true; - const nodeRef = React.useRef(); - const popperRef = React.useRef(null); - const popperRefs = useCombineRefs(popperRef, popperRefIn); - const [isOpen, setOpenState] = React.useState(openProp); - const focusRef = React.useRef(); - const onRequestCloseRef = React.useRef(onRequestClose); - onRequestCloseRef.current = onRequestClose; - - const setOpen = React.useCallback( - (newOpen: boolean) => { - if (returnFocus && newOpen !== isOpen) { - if (newOpen) { - if (document.activeElement) { - focusRef.current = document.activeElement; - } - } else if (focusRef.current instanceof HTMLElement && focusRef.current.ownerDocument) { - focusRef.current.focus(); - } - } - setOpenState(newOpen); - }, - [returnFocus, isOpen] - ); - - React.useEffect(() => { - setOpen(openProp); - }, [openProp, setOpen]); - - const onKeyDown = React.useCallback( - (e: KeyboardEvent) => { - if (e.keyCode === 27) { - controlled ? onRequestCloseRef.current && onRequestCloseRef.current() : setOpen(false); - } - }, - [controlled, setOpen] - ); - - const onClickOutside = React.useCallback( - (e: MouseEvent) => { - if (!nodeRef.current || (e.target instanceof Node && !nodeRef.current.contains(e.target))) { - controlled ? onRequestCloseRef.current && onRequestCloseRef.current(e) : setOpen(false); - } - }, - [controlled, setOpen] - ); - - const destroy = React.useCallback(() => { - if (popperRef.current) { - popperRef.current.destroy(); - popperRefs(null); - document.removeEventListener('keydown', onKeyDown, true); - document.removeEventListener('mousedown', onClickOutside, true); - document.removeEventListener('touchstart', onClickOutside, true); - } - }, [onClickOutside, onKeyDown, popperRefs]); - - const initialize = React.useCallback(() => { - if (!nodeRef.current || !reference || !isOpen) { - return; - } - - destroy(); - - popperRefs( - new PopperJS(getReference(typeof reference === 'function' ? reference() : reference), nodeRef.current, { - placement, - ...popperOptions, - modifiers: { - preventOverflow: { - boundariesElement: 'window', - }, - ...popperOptions.modifiers, - }, - }) - ); - - // init document listenerrs - if (closeOnEsc) { - document.addEventListener('keydown', onKeyDown, true); - } - if (closeOnOutsideClick) { - document.addEventListener('mousedown', onClickOutside, true); - document.addEventListener('touchstart', onClickOutside, true); - } - }, [ - popperRefs, - reference, - isOpen, - destroy, - placement, - popperOptions, - closeOnEsc, - closeOnOutsideClick, - onKeyDown, - onClickOutside, - ]); - - const nodeRefCallback = React.useCallback( - (node) => { - nodeRef.current = node; - initialize(); - }, - [initialize] - ); - - React.useEffect(() => { - initialize(); - }, [initialize]); - - React.useEffect(() => { - return () => { - destroy(); - }; - }, [destroy]); - - React.useEffect(() => { - if (!isOpen) { - destroy(); - } - }, [destroy, isOpen]); - - return isOpen ? ( - -
- {children} -
-
- ) : null; -}; - -export default Popper; diff --git a/src/app/Shared/popper/Portal.tsx b/src/app/Shared/popper/Portal.tsx deleted file mode 100644 index 67be6ed9e..000000000 --- a/src/app/Shared/popper/Portal.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; - -type GetContainer = Element | null | undefined | (() => Element); - -type PortalProps = { - children: React.ReactNode; - container?: GetContainer; -}; - -const getContainer = (container: GetContainer): Element | null | undefined => - typeof container === 'function' ? container() : container; - -const Portal: React.FC = ({ children, container }) => { - const [containerNode, setContainerNode] = React.useState(); - - React.useLayoutEffect(() => { - setContainerNode(getContainer(container) || document.body); - }, [container]); - - return containerNode ? ReactDOM.createPortal(children, containerNode) : null; -}; - -export default Portal; diff --git a/src/app/Shared/spotlight.css b/src/app/Shared/spotlight.css deleted file mode 100644 index 934964463..000000000 --- a/src/app/Shared/spotlight.css +++ /dev/null @@ -1,97 +0,0 @@ -/* -Copyright The Cryostat Authors - -The Universal Permissive License (UPL), Version 1.0 - -Subject to the condition set forth below, permission is hereby granted to any -person obtaining a copy of this software, associated documentation and/or data -(collectively the "Software"), free of charge and under any and all copyright -rights in the Software, and any and all patent rights owned or freely -licensable by each licensor hereunder covering either (i) the unmodified -Software as contributed to or provided by such licensor, or (ii) the Larger -Works (as defined below), to deal in both - -(a) the Software, and -(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if -one is included with the Software (each a "Larger Work" to which the Software -is contributed by such licensors), - -without restriction, including without limitation the rights to copy, create -derivative works of, display, perform, and distribute the Software and make, -use, sell, offer for sale, import, export, have made, and have sold the -Software and the Larger Work(s), and to sublicense the foregoing rights on -either these or other terms. - -This license is subject to the following condition: -The above copyright notice and either this complete permission notice or at -a minimum a reference to the UPL must be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -@keyframes ocs-spotlight-expand { - 0% { - outline-offset: -4px; - outline-width: 4px; - opacity: 1; - } - 100% { - outline-offset: 21px; - outline-width: 12px; - opacity: 0; - } -} -@keyframes ocs-spotlight-fade-in { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} -@keyframes ocs-spotlight-fade-out { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} -.ocs-spotlight { - pointer-events: none; - position: absolute; -} -.ocs-spotlight__with-backdrop { - mix-blend-mode: hard-light; -} -.ocs-spotlight__element-highlight-noanimate { - border: var(--pf-global--BorderWidth--xl) solid var(--pf-global--palette--blue-100); - background-color: var(--pf-global--palette--black-500); - z-index: 9999; -} -.ocs-spotlight__element-highlight-animate { - pointer-events: none; - position: absolute; - box-shadow: inset 0px 0px 0px 4px var(--pf-global--palette--blue-200); - opacity: 0; - animation: 0.4s ocs-spotlight-fade-in 0s ease-in-out, 5s ocs-spotlight-fade-out 12.8s ease-in-out; - animation-fill-mode: forwards; -} -.ocs-spotlight__element-highlight-animate::after { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - animation: 1.2s ocs-spotlight-expand 1.6s ease-out; - animation-fill-mode: forwards; - outline: 4px solid var(--pf-global--palette--blue-200); - outline-offset: -4px; -} diff --git a/src/app/Shared/useBoundingClientRect.ts b/src/app/Shared/useBoundingClientRect.ts deleted file mode 100644 index bceebf91e..000000000 --- a/src/app/Shared/useBoundingClientRect.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import * as React from 'react'; - -type BoundingClientRect = ClientRect | null; - -export const useResizeObserver = (callback: ResizeObserverCallback, targetElement?: HTMLElement | null): void => { - const element = React.useMemo(() => targetElement ?? document.querySelector('body'), [targetElement]); - React.useEffect(() => { - const observer = new ResizeObserver(callback); - observer.observe(element); - return () => { - observer.disconnect(); - }; - }, [callback, element]); -}; - -export const useBoundingClientRect = (targetElement: HTMLElement | null): BoundingClientRect => { - const [clientRect, setClientRect] = React.useState(() => - targetElement ? targetElement.getBoundingClientRect() : null - ); - - const observerCallback = React.useCallback(() => { - setClientRect(targetElement ? targetElement.getBoundingClientRect() : null); - }, [targetElement]); - - useResizeObserver(observerCallback); - - return clientRect; -}; diff --git a/src/app/TargetView/TargetContextSelector.tsx b/src/app/TargetView/TargetContextSelector.tsx index 4bf02b749..03ccaf300 100644 --- a/src/app/TargetView/TargetContextSelector.tsx +++ b/src/app/TargetView/TargetContextSelector.tsx @@ -229,7 +229,7 @@ export const TargetContextSelector: React.FC<{ className?: string }> = ({ classN return ( <> -
+
{isLoading ? ( ) : ( diff --git a/src/app/TemplateSelector/SelectTemplateSelectorForm.tsx b/src/app/TemplateSelector/SelectTemplateSelectorForm.tsx index 17953a57f..2df695c65 100644 --- a/src/app/TemplateSelector/SelectTemplateSelectorForm.tsx +++ b/src/app/TemplateSelector/SelectTemplateSelectorForm.tsx @@ -112,6 +112,7 @@ export const SelectTemplateSelectorForm: React.FunctionComponent {groups.map((group, index) => ( From b6d073268d0088844b0460a0124b6f6f0d87cfd2 Mon Sep 17 00:00:00 2001 From: Max Cao Date: Tue, 21 Mar 2023 21:43:16 -0400 Subject: [PATCH 06/21] complete some quick starts Signed-off-by: Max Cao --- README.md | 35 ++-- src/app/AppLayout/AppLayout.tsx | 8 +- src/app/AppLayout/QuickStartDrawer.tsx | 47 +++++- .../CreateRecording/CustomRecordingForm.tsx | 12 +- .../QuickStarts/QuickStartsCatalogPage.tsx | 2 - src/app/QuickStarts/all-quickstarts.ts | 3 +- .../automated-rules-quickstart.tsx | 159 ++++++++++++++++++ .../quickstarts/cryostat-link-quickstart.tsx} | 37 +++- .../quickstarts/generic-quickstart.ts | 23 ++- .../quickstarts/settings-quickstart.tsx | 6 +- .../quickstarts/start-a-recording.tsx | 144 +++++++++------- src/app/Recordings/ActiveRecordingsTable.tsx | 6 +- src/app/Recordings/RecordingActions.tsx | 4 +- src/app/Recordings/Recordings.tsx | 14 +- src/app/Rules/CreateRule.tsx | 9 + src/app/Rules/Rules.tsx | 4 +- src/app/routes.tsx | 1 - src/app/utils/useSetState.ts | 58 +++++-- 18 files changed, 446 insertions(+), 126 deletions(-) create mode 100644 src/app/QuickStarts/quickstarts/automated-rules-quickstart.tsx rename src/app/{Shared/highlight-consts.ts => QuickStarts/quickstarts/cryostat-link-quickstart.tsx} (70%) diff --git a/README.md b/README.md index a775e71be..92e3dc1c2 100644 --- a/README.md +++ b/README.md @@ -91,29 +91,44 @@ The extraction tool is [`i18next-parser`](https://www.npmjs.com/package/i18next- To workaround this, specify static values in `i18n.ts` file under any top-level directory below `src/app`. For example, `src/app/Settings/i18n.ts`. -## COLOR PALETTE - -The color palette for Cryostat is defined in `src/app/app.css` in `:root`. The colors are defined as variables and can be used throughout the application. - -![Palette](./src/app/assets/palette.svg) - ## ADDING QUICKSTARTS To add a new quickstart, create a new tsx/ts file under `src/app/QuickStarts` with your Quick Start name, like `my-quickstart.tsx`. Cryostat's Quick Starts use a markdown extension which allows components to be highlighted using a button within the markdown in the Quick Start content itself. It was taken from OpenShift Console's GitHub repo and modified to fit Cryostat's needs. +The following are taken from patternfly/patternfly-quickstarts GitHub repo. ### Highlighting elements -You can highlight an element on the page from within a quick start. The element that should be highlightable needs a data-quickstart-id attribute. - -Example: +You can highlight an element on the page from within a quick start. The element that should be highlightable needs a data-quickstart-id attribute. Example: ``` ``` In the quick start task description, you can add this type of markdown to target this element: - ``` Highlight [special button]{{highlight special-btn}} ``` + +### Copyable text + +You can have inline or block copyable text. + +#### Inline copyable text example +``` +`echo "Donec id est ante"`{{copy}} +``` + +#### Multiline copyable text example +``` + ``` + First line of text. + Second line of text. + ```{{copy}} +``` + +## COLOR PALETTE + +The color palette for Cryostat is defined in `src/app/app.css` in `:root`. The colors are defined as variables and can be used throughout the application. + +![Palette](./src/app/assets/palette.svg) diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index a79192724..4c97c0c1a 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -336,7 +336,6 @@ const AppLayout: React.FC = ({ children }) => { }, []); const handleOpenGuidedTour = React.useCallback(() => { - console.log('handleOpenGuidedTour'); joyride.setState({ run: true }); }, [joyride]); @@ -537,7 +536,12 @@ const AppLayout: React.FC = ({ children }) => { id={`${route.label}-${idx}`} isActive={isActiveRoute(route)} > - + {route.label} {route.featureLevel !== undefined && levelBadge(route.featureLevel)} diff --git a/src/app/AppLayout/QuickStartDrawer.tsx b/src/app/AppLayout/QuickStartDrawer.tsx index 07bf869a0..4fc2f1807 100644 --- a/src/app/AppLayout/QuickStartDrawer.tsx +++ b/src/app/AppLayout/QuickStartDrawer.tsx @@ -35,8 +35,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +import { LoadingView } from '@app/LoadingView/LoadingView'; import { allQuickStarts } from '@app/QuickStarts/all-quickstarts'; -import { HIGHLIGHT_REGEXP } from '@app/Shared/highlight-consts'; +import { SessionState } from '@app/Shared/Services/Login.service'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { useSubscriptions } from '@app/utils/useSubscriptions'; import { QuickStartContext, QuickStartDrawer, @@ -45,14 +48,26 @@ import { } from '@patternfly/quickstarts'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; +import build from '@app/build.json'; export interface GlobalQuickStartDrawerProps { children: React.ReactNode; } +const LINK_LABEL = '[\\d\\w\\s-()$!&]+'; // has extra '&' in matcher +const HIGHLIGHT_ACTIONS = ['highlight']; // use native quickstarts highlight markdown extension +const SELECTOR_ID = `[\\w-&]+`; // has extra '&' + +// [linkLabel]{{action id}} +const HIGHLIGHT_REGEXP = new RegExp( + `\\[(${LINK_LABEL})]{{(${HIGHLIGHT_ACTIONS.join('|')}) (${SELECTOR_ID})}}`, + 'g', +); + export const GlobalQuickStartDrawer: React.FC = ({ children }) => { const { t, i18n } = useTranslation(); - + const context = React.useContext(ServiceContext); + const addSubscription = useSubscriptions(); const [activeQuickStartID, setActiveQuickStartID] = useLocalStorage('quickstartId', ''); const [allQuickStartStates, setAllQuickStartStates] = useLocalStorage('quickstarts', {}); const valuesForQuickStartContext = useValuesForQuickStartContext({ @@ -63,9 +78,9 @@ export const GlobalQuickStartDrawer: React.FC = ({ setAllQuickStartStates, language: i18n.language, markdown: { - // markdown extension for spotlighting elements from links extensions: [ { + // taken from patternfly/quickstarts but with extra '&' in regex matcher type: 'lang', regex: HIGHLIGHT_REGEXP, replace: (text: string, linkLabel: string, linkType: string, linkId: string): string => { @@ -73,13 +88,33 @@ export const GlobalQuickStartDrawer: React.FC = ({ return ``; }, }, + { + // replace [APP] with bolded productName like Cryostat + type: 'output', + regex: new RegExp(`\\[APP\\]`, 'g'), + replace: (_text: string): string => { + return `${build.productName}`; + }, + }, ], }, }); + React.useEffect(() => { + addSubscription( + context.login.getSessionState().subscribe((s) => { + if (s !== SessionState.USER_SESSION) { + setActiveQuickStartID(''); + } + }) + ); + }, [addSubscription, context.login, setActiveQuickStartID]); + return ( - - {children} - + }> + + {children} + + ); }; diff --git a/src/app/CreateRecording/CustomRecordingForm.tsx b/src/app/CreateRecording/CustomRecordingForm.tsx index 81af37293..c3ad07615 100644 --- a/src/app/CreateRecording/CustomRecordingForm.tsx +++ b/src/app/CreateRecording/CustomRecordingForm.tsx @@ -493,7 +493,11 @@ export const CustomRecordingForm: React.FC = ({ prefil onSelect={handleTemplateChange} /> - + = ({ prefil /> - + A value of 0 for maximum size or age means unbounded. }> - ); }; diff --git a/src/app/QuickStarts/all-quickstarts.ts b/src/app/QuickStarts/all-quickstarts.ts index 68f772a10..028ff3df6 100644 --- a/src/app/QuickStarts/all-quickstarts.ts +++ b/src/app/QuickStarts/all-quickstarts.ts @@ -39,6 +39,7 @@ import { QuickStart } from '@patternfly/quickstarts'; // import { GenericQuickStart } from './quickstarts/generic-quickstart'; import RecordingQuickStart from './quickstarts/start-a-recording'; import SettingsQuickStart from './quickstarts/settings-quickstart'; +import AutomatedRulesQuickStart from './quickstarts/automated-rules-quickstart'; // Add your quick start here e.g. [GenericQuickStart, SampleQuickStart] -export const allQuickStarts: QuickStart[] = [RecordingQuickStart, SettingsQuickStart]; +export const allQuickStarts: QuickStart[] = [AutomatedRulesQuickStart, RecordingQuickStart, SettingsQuickStart]; diff --git a/src/app/QuickStarts/quickstarts/automated-rules-quickstart.tsx b/src/app/QuickStarts/quickstarts/automated-rules-quickstart.tsx new file mode 100644 index 000000000..4fb7db811 --- /dev/null +++ b/src/app/QuickStarts/quickstarts/automated-rules-quickstart.tsx @@ -0,0 +1,159 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import cryostatLogo from '@app/assets/cryostat_icon_rgb_default.svg'; +import { QuickStart } from '@patternfly/quickstarts'; + +// TODO: Add quickstarts based on the following example: +export const AutomatedRulesQuickStart: QuickStart = { + apiVersion: 'v2.3.0', + metadata: { + name: 'automated-rule-quickstart', + }, + spec: { + version: 2.3, + displayName: 'Automated Rules', + durationMinutes: 10, + icon: cryostatLogo, + description: `Learn about automated rules in [APP] and how to create one.`, + prerequisites: [''], + introduction: ` +# Automated Rules +Automated Rules are configurations that instruct [APP] to create JDK Flight Recordings on matching target JVM applications. Each Automated Rule specifies parameters for which Event Template to use, how much data should be kept in the application recording buffer, and how frequently Cryostat should copy the application recording buffer into Cryostat’s own archived storage. + +Once you’ve created a rule, [APP] immediately matches it against all existing discovered targets and starts your flight recording. [APP] will also apply the rule to newly discovered targets that match its definition. You can create multiple rules to match different subsets of targets or to layer different recording options for your needs. +In this quick start, you will use [APP] to create an automated rule that will start a recording on an existing target JVM. + +### What you'll learn + +- How to create an automated rule in [APP] +- How to use match expressions to match one or more target JVMs + +### What you'll need + +- A running instance of [APP] which has discovered at least one target JVM +- JMX auth credentials for the target JVM (if required) + + `, + tasks: [ + { + title: 'Go to the Automated Rules page', + description: `1. Click the [Automated Rules]{{highlight nav-automatedrules-tab}} tab in the [APP] console navigation bar.`, + review: { + instructions: '#### Verify that you see the Automated Rules page.', + failedTaskHelp: + 'If you do not see the navigation bar, you can click the `☰` button in the [top left corner of the page]{{highlight nav-toggle-btn}}.', + }, + }, + { + title: 'Create a new Automated Rule', + description: ` +1. Click the [Create]{{highlight create-rule-btn}} button. +[Read the [About Automated Rules]{{highlight about-rules}} section of the for more information.]{{admonition tip}} +` , review: { + instructions: '#### Verify that you see the Automated Rules creation form.', + failedTaskHelp: + 'If you do not see the creation form, follow the previous steps again.', + }, + }, + { + title: 'Fill out the Automated Rule form', + description: ` +The Automated Rule creation form has several fields that you can fill out to create a new rule. Each field has helper text that explains what the field is for. + +**The most important field is the [Match Expression]{{highlight rule-matchexpr}} field.** This field is used to match one or more target JVMs. The match expression is a [Java regular expression]{{highlight rule-matchexpr}} that is matched against the target JVM’s. For example, if you wanted to match all discovered target JVMs, try using the match expression: \`true\`{{copy}}. + +Use the match expression tester to test your match expression against the target JVMs that are currently discovered by [APP]. + +1. Select a target JVM from the [Target]{{highlight rule-target}} dropdown menu. +2. Enter a [Match Expression]{{highlight rule-matchexpr}} for the rule. Try using the match hint to help you create a match expression. +3. Note the target JVM details code block in the tester. These details can be used in the match expression. + +**To create a new rule, you must fill out the following required fields:** +1. Enter a name for the rule in the [Name]{{highlight rule-name}} field. +2. Enter a [Match Expression]{{highlight rule-matchexpr}} for the rule. +3. Select the [Event Template]{{highlight rule-event-template}} you want to use for the rule. + +**The rest of the fields are optional and not required for this quick start: \`[Description, Maximum Size, Maximum Age, Maximum Age, Archival Period, Initial Delay, and Preserved Archives]\`.** + +[Learn more about these other Automated Rule attributes in the upstream [Cryostat documentation](https://cryostat.io/guides/#create-an-automated-rule).]{{admonition tip}} + +When you are finished, click the [Create]{{highlight create-rule-btn}} button. + +` , review: { + instructions: '#### Verify that you see the new rule in the list of rules.', + failedTaskHelp: + `If you do not see the new rule, follow the previous steps again. + If you cannot create the rule, check that you have entered valid values for each required field.`, + }, + }, + { + title: 'Find the recording that was created by the rule', + description: ` +The rule we just created should have created a new recording on the target JVM that we selected. Let's find the recording. +1. Click the [Recordings]{{highlight nav-recordings-tab}} tab in the [APP] console navigation bar. +2. Click the [Target]{{highlight recordings-target}} dropdown menu and select the target JVM that you used to create the rule, if not already selected. + +There should now be a new recording in the list of recordings started on the selected target JVM. + +This recording was created by the rule that we just created and should have a name that matches the name of the rule like \`auto_\`. + +[If you set any other attributes on the rule, you should see those attributes reflected in the recording.]{{admonition note}} +`, + review: { + instructions: '#### Verify that you see the new recording with the correct Automated Rule recording naming scheme in the list of recordings.', + failedTaskHelp: + 'If you do not see the new recording, try verifying that your rule match expression correctly matches the target JVM. Also make sure that the rule is enabled, and that the target JVM is still running, and selected in the Target dropdown menu.', + }, + } + ], + conclusion: ` +
+

You completed the Automated Rules quick start!

+
+ Cryostat Logo +
+

For more information about the Automated Rules feature, read our guide on the upstream Cryostat documentation.

+
`, + type: { + text: 'Featured', + color: 'blue', + }, + }, +}; + +export default AutomatedRulesQuickStart; diff --git a/src/app/Shared/highlight-consts.ts b/src/app/QuickStarts/quickstarts/cryostat-link-quickstart.tsx similarity index 70% rename from src/app/Shared/highlight-consts.ts rename to src/app/QuickStarts/quickstarts/cryostat-link-quickstart.tsx index 53e0a12c1..79941ecfb 100644 --- a/src/app/Shared/highlight-consts.ts +++ b/src/app/QuickStarts/quickstarts/cryostat-link-quickstart.tsx @@ -35,12 +35,33 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -export const LINK_LABEL = '[\\d\\w\\s-()$!&]+'; -export const HIGHLIGHT_ACTIONS = ['highlight']; -export const SELECTOR_ID = `[\\w-&]+`; +import cryostatLogo from '@app/assets/cryostat_icon_rgb_default.svg'; +import { QuickStart } from '@patternfly/quickstarts'; -// [linkLabel]{{action id}} -export const HIGHLIGHT_REGEXP = new RegExp( - `\\[(${LINK_LABEL})]{{(${HIGHLIGHT_ACTIONS.join('|')}) (${SELECTOR_ID})}}`, - 'g' -); +// TODO: Put link quickstarts in a separate QuickStartCatalogSection +const CryostatLinkQuickStart: QuickStart = { + apiVersion: 'v2.3.0', + metadata: { + name: 'cryostat-link-quickstart', + instructional: true + }, + spec: { + version: 2.3, + displayName: 'Cryostat Upstream Documentation', + durationMinutes: 1, + icon: cryostatLogo, + description: `Link to Cryostat's upstream documentation.`, + prerequisites: [''], + introduction: '### This is a generic quickstart.', + link: { + href: 'https://cryostat.io', + text: 'cryostat.io', + }, + type: { + text: 'External', + color: 'purple', + }, + }, +}; + +export default CryostatLinkQuickStart; diff --git a/src/app/QuickStarts/quickstarts/generic-quickstart.ts b/src/app/QuickStarts/quickstarts/generic-quickstart.ts index 1264a9dc6..4992bf8d0 100644 --- a/src/app/QuickStarts/quickstarts/generic-quickstart.ts +++ b/src/app/QuickStarts/quickstarts/generic-quickstart.ts @@ -40,7 +40,7 @@ import build from '@app/build.json'; import { QuickStart } from '@patternfly/quickstarts'; // TODO: Add quickstarts based on the following example: -export const GenericQuickStart: QuickStart = { +const GenericQuickStart: QuickStart = { apiVersion: 'v2.3.0', metadata: { name: 'generic-quickstart', @@ -50,7 +50,7 @@ export const GenericQuickStart: QuickStart = { displayName: 'Getting Started with', durationMinutes: 1, icon: cryostatLogo, - description: `Get started with ${build.productName}.`, + description: `Get started with [APP]`, prerequisites: [''], introduction: '### This is a generic quickstart.', tasks: [ @@ -60,13 +60,20 @@ export const GenericQuickStart: QuickStart = { 1. Press the bell icon.`, }, ], - conclusion: `You finished **Getting Started with ${build.productName}**! - -Learn more about [${build.productName}](https://cryostat.io) from our website. -`, + conclusion: ` +
+

You completed the Start a Recording quick start!

+ +
+ Cryostat Logo +
+

To learn more about [APP]'s extensive features and capabilities, read our upstream guides at cryostat.io.

+
`, type: { - text: 'Introduction', - color: 'green', + text: 'Featured', + color: 'blue', }, }, }; + +export default GenericQuickStart; diff --git a/src/app/QuickStarts/quickstarts/settings-quickstart.tsx b/src/app/QuickStarts/quickstarts/settings-quickstart.tsx index 575628d5c..6de633261 100644 --- a/src/app/QuickStarts/quickstarts/settings-quickstart.tsx +++ b/src/app/QuickStarts/quickstarts/settings-quickstart.tsx @@ -102,15 +102,13 @@ const SettingsQuickStart: QuickStart = { title: 'Go to the Dashboard tab', description: ` 1. Go to the [Dashboard]{{highlight settings-dashboard-tab}} tab. -1. Here you can configure the dashboard settings for the Cryostat UI. -2. You can also configure the default dashboard.`, +1. Here you can configure the dashboard settings for the Cryostat UI.`, }, { title: 'Go to the Advanced tab', description: ` 1. Go to the [Advanced]{{highlight settings-advanced-tab}} tab. -1. Here you can configure the advanced settings for the Cryostat UI. -2. You can also configure the default dashboard.`, +1. Here you can configure the advanced settings for the Cryostat UI.` }, ], conclusion: `You completed the **Using Settings** quick start! diff --git a/src/app/QuickStarts/quickstarts/start-a-recording.tsx b/src/app/QuickStarts/quickstarts/start-a-recording.tsx index d1b29f49e..b0cfa6a06 100644 --- a/src/app/QuickStarts/quickstarts/start-a-recording.tsx +++ b/src/app/QuickStarts/quickstarts/start-a-recording.tsx @@ -39,7 +39,6 @@ import cryostatLogoIcon from '@app/assets/cryostat_icon_rgb_default.svg'; import cryostatLogo from '@app/assets/cryostat_logo_vert_rgb_default.svg'; import { QuickStart } from '@patternfly/quickstarts'; -import build from '@app/build.json'; const RecordingQuickStart: QuickStart = { apiVersion: 'v2.3.0', @@ -51,48 +50,52 @@ const RecordingQuickStart: QuickStart = { displayName: 'Start a Recording', durationMinutes: 10, icon: cryostatLogoIcon, - description: `Learn how to start a recording with Java Flight Recorder (JFR) with **${build.productName}**.`, + description: `Learn how to start a recording with Java Flight Recorder (JFR) with [APP].`, prerequisites: [''], introduction: ` # Start a Recording **Java Flight Recorder (JFR)** is a profiling tool that is built into the JVM. It allows you to record events that happen in the JVM and then analyze the recording to find performance issues. -In this quick start, you will use **${build.productName}** to connect to a target JVM and start a recording of the target JVM's activity. You will then stop and download the recording to your local machine. Finally, you will view an automated analysis report of the recording with **Cryostat**'s capabilities. +In this quick start, you will use [APP] to connect to a target JVM and start a recording of the target JVM's activity. You will then stop and download the recording to your local machine. Finally, you will view an automated analysis report of the recording with [APP]'s capabilities. ### What you'll learn - How to start/stop a JFR recording on a target JVM -- How to download a recording from **${build.productName}** to your local machine -- How to view an automated analysis report of a recording with **Cryostat**'s capabilities +- How to download a recording from [APP] to your local machine +- How to view an automated analysis report of a recording with [APP]'s capabilities ### What you'll need -- A running instance of **${build.productName}** which has discovered at least one target JVM +- A running instance of [APP] which has discovered at least one target JVM - JMX auth credentials for the target JVM (if required) `, - tasks: [ - { - title: 'Go to the Recordings tab', - description: '1. Press the [Recordings]{{highlight nav-recordings-tab}} tab in the Cryostat console navigation bar.', - review: { - instructions: '#### Verify that you have done the task.', - failedTaskHelp: 'If you do not see the navigation bar, you can click the `☰` button in the [top left corner of the page]{{highlight nav-toggle-btn}}.', + tasks: [ + { + title: 'Go to the Recordings tab', + description: + '1. Click the [Recordings]{{highlight nav-recordings-tab}} tab in the [APP] console navigation bar.', + review: { + instructions: '#### Verify that you see the Recordings page.', + failedTaskHelp: + 'If you do not see the navigation bar, you can click the `☰` button in the [top left corner of the page]{{highlight nav-toggle-btn}}.', + }, }, - }, - { - title: 'Select a target JVM', - description: ` -Select a target JVM from the list of available targets that Cryostat has discovered. + { + title: 'Select a target JVM', + description: ` + +Select a target JVM from the list of available targets that [APP] has discovered. 1. Click the [Target Select]{{highlight target-select}} dropdown menu. 2. Select a target from the list of available targets. -Note: If JMX Auth username and password is required, you will be prompted to enter them.`, - }, - { - title: 'Start a recording', - description: ` + +[If JMX Auth username and password is required, you will be prompted to enter them.]{{admonition note}}`, + }, + { + title: 'Start a recording', + description: ` There are two tabs within the Recordings page: \n [Active Recordings]{{highlight active-recordings-tab}} and [Archived Recordings]{{highlight archived-recordings-tab}}.\n Active recordings are recordings that are currently running, and Archived recordings are recordings that have been stopped. @@ -107,56 +110,71 @@ We will start a recording while on the Active tab. After the creation of a recording, the recording will be displayed in the Active Recordings tab. You should be able to see the recording's name, start time, duration, state, and any attached labels. -Note: You may also attach metadata labels to the recordings under the [Metadata]{{highlight crf-metadata-opt}} options or configure your custom recording further under the [Advanced]{{highlight crf-advanced-opt}} options.`, - review: { - instructions: '#### Verify that you see the recording within the table.', - failedTaskHelp: 'If you do not see the recording, try the steps again.', - }, -}, - { - title: 'Stop a recording', - description: ` +[You may also attach metadata labels to the recordings under the [Metadata]{{highlight crf-metadata-opt}} options or configure your custom recording further under the [Advanced]{{highlight crf-advanced-opt}} options.]{{admonition note}}`, + review: { + instructions: '#### Verify that you see the recording within the table.', + failedTaskHelp: 'If you do not see the recording, try the steps again.', + }, + }, + { + title: 'Stop a recording', + description: ` Stopping a recording will cut off the recording at the time that the recording is stopped. -1. Click the [Stop]{{highlight recordings-stop-btn}} button to stop the recording.` - }, - { - title: 'Download a recording', - description: ` -Downloading a recording will save the recording to your local machine as a JFR file. You can then use JDK Mission Control (JMC) to analyze the recording. -1. Open the kebab menu next to the recording that you want to download. -2. Click the [Download]{{highlight recordings-download-btn}} button to download the recording to your local machine. -3. Choose what to do with the file. Your browser will present you to save the file to your local machine. - ` - }, - { - title: 'View an analysis report', - description: ` -1. Click the kebab menu next to the recording that you want to view an analysis report for. -2. Click [View Report]{{highlight recordings-view-analysis-btn}} to view an analysis report of the recording. -` - }, - { - title: 'Archive a recording', - description: ` -Archiving a recording will save the recording to Cryostat's archival storage. These recordings will show up in the target JVM's Archived Recordings tab, as well as in the [Archives]{{highlight nav-archives-tab}} view on the Cryostat console navigation bar. move the recording from the Active Recordings tab to the Archived Recordings tab. Archived recordings can be also be downloaded to your local machine. +1. Click the [Stop]{{highlight recordings-stop-btn}} button to stop the recording.`, + review: { + instructions: '#### Verify that the STATE field of the recording has changed to STOPPED.', + failedTaskHelp: + 'If you do not see the recording, try the Start a recording task again.', + }, + }, + { + title: 'Download a recording', + description: ` +Downloading a recording will save the recording to your local machine as a JFR file. You can then use **JDK Mission Control (JMC)** to analyze the recording. +1. Open the [kebab menu]{{highlight recording-kebab}} next to the recording that you want to download. +2. Click \`Download Recording\` to prompt your browser to open a dialog to save the recording to your local machine. +3. Choose what to do with the file. + `, + review: { + instructions: '#### Verify that you have downloaded the recording to your local machine.', + failedTaskHelp: + 'If you do not see the recording, try the Start a recording task again.', + }, + }, + { + title: 'View an analysis report', + description: ` +[APP] is able to generate an analysis report of a recording. This report is the same report that you would get if you were to view an automated analysis report in **JDK Mission Control**. The **JMC** rules engine analyzes your recording and looks for common problems and assigns a severity score from 0 (no problem) to 100 (potentially severe problem). +1. Click the [kebab menu]{{highlight recording-kebab}} next to the recording that you want to view an analysis report for. +2. Click \`View Report ...\` to view an analysis report of the recording in a new tab. +3. Right click on the page and select \`Save Page As...\` to download the report HTML file to your local machine. +`, + }, + { + title: 'Archive a recording', + description: ` +Archiving a recording will save the recording to [APP]'s archival storage, and will persist even after [APP] is restarted. These recordings will show up in the target JVM's Archived Recordings tab, as well as in the [Archives]{{highlight nav-archives-tab}} view on the [APP] console navigation bar. 1. Click the [Archive]{{highlight recordings-archive-btn}} button to archive the recording. -2. Go to the Archived Recordings tab to see the archived recording. - -Note: You can also download and view an analysis report of the archived recording from the Archived Recordings tab. -` - }, - - ], +2. Go to the [Archived Recordings]{{highlight archived-recordings-tab}} tab to see the archived recording in [APP]'s storage. + +[You can download archived recordings and view an analysis report of the archived recording from the [Archived Recordings]{{highlight archived-recordings-tab}} tab, similar to active recordings.]{{admonition tip}}`, + review: { + instructions: '#### Verify that the recording has been archived in the Archived Recordings tab.', + failedTaskHelp: + 'The recording name should have been saved like \`__.jfr\`. If you still do not see the recording, try the proceeding tasks again.', + }, + }, + ], conclusion: `

You completed the Start a Recording quick start!

- Cryostat Logo + Cryostat Logo
-

Learn more about Cryostat from our guides at cryostat.io.

+

To learn more about [APP]'s extensive features and capabilities, read our upstream guides at cryostat.io.

`, type: { text: 'Featured', diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index 8cbfad952..a7c22a441 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -700,7 +700,11 @@ const ActiveRecordingsToolbar: React.FunctionComponent Edit Labels diff --git a/src/app/Recordings/RecordingActions.tsx b/src/app/Recordings/RecordingActions.tsx index 9258cb2ad..8293e405a 100644 --- a/src/app/Recordings/RecordingActions.tsx +++ b/src/app/Recordings/RecordingActions.tsx @@ -140,11 +140,11 @@ export const RecordingActions: React.FunctionComponent = menuAppendTo={document.body} position="right" direction="down" - toggle={} + toggle={} isPlain isOpen={isOpen} dropdownItems={actionItems.map((action) => ( - onSelect(action)}> + onSelect(action)} data-quickstart-id={action.key}> {action.title} ))} diff --git a/src/app/Recordings/Recordings.tsx b/src/app/Recordings/Recordings.tsx index a32425175..42153ffdb 100644 --- a/src/app/Recordings/Recordings.tsx +++ b/src/app/Recordings/Recordings.tsx @@ -74,10 +74,20 @@ export const Recordings: React.FC, Sta const cardBody = React.useMemo(() => { return archiveEnabled ? ( - Active Recordings} data-quickstart-id="active-recordings-tab"> + Active Recordings} + data-quickstart-id="active-recordings-tab" + > - Archived Recordings} data-quickstart-id="archived-recordings-tab"> + Archived Recordings} + data-quickstart-id="archived-recordings-tab" + > diff --git a/src/app/Rules/CreateRule.tsx b/src/app/Rules/CreateRule.tsx index 7543c661b..5d9ef4c03 100644 --- a/src/app/Rules/CreateRule.tsx +++ b/src/app/Rules/CreateRule.tsx @@ -330,6 +330,7 @@ const CreateRuleForm: React.FC = ({ ...props }) => { helperText="Enter a rule name." helperTextInvalid="A rule name may only contain letters, numbers, and underscores." validated={nameValid} + data-quickstart-id="rule-name" > = ({ ...props }) => { label="Description" fieldId="rule-description" helperText="Enter a rule description. This is only used for display purposes to aid in identifying rules and their intentions." + data-quickstart-id="rule-description" >