From f8171231e14f36cb815def22fdf442050dbd63ca Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Sat, 22 Apr 2023 08:24:03 -0700 Subject: [PATCH] feat(joyride): show guided tour upon cryostat upgrades (#971) * chore(comp): remove unused hint banner * feat(joyride): check asset version * chore(version): display cryostat version --- src/app/About/AboutCryostatModal.tsx | 2 +- src/app/AppLayout/AppLayout.tsx | 10 ++++- src/app/Joyride/CryostatJoyride.tsx | 2 +- src/app/Topology/Shared/HintBanner.tsx | 61 -------------------------- src/app/Topology/Topology.tsx | 17 +------ src/app/Topology/styles/base.css | 25 ----------- src/app/build.json | 3 +- src/app/utils/LocalStorage.ts | 2 +- src/app/utils/utils.ts | 43 ++++++++++++++++++ 9 files changed, 58 insertions(+), 107 deletions(-) delete mode 100644 src/app/Topology/Shared/HintBanner.tsx diff --git a/src/app/About/AboutCryostatModal.tsx b/src/app/About/AboutCryostatModal.tsx index 84d95d925..046f07cb9 100644 --- a/src/app/About/AboutCryostatModal.tsx +++ b/src/app/About/AboutCryostatModal.tsx @@ -49,7 +49,7 @@ export const AboutCryostatModal = ({ isOpen, onClose }) => { return ( = ({ children }) => { [handleCloseNotificationCenter] ); + React.useEffect(() => { + if (showUserIcon && isAssetNew(build.version)) { + handleOpenGuidedTour(); + saveToLocalStorage('ASSET_VERSION', build.version); + } + }, [handleOpenGuidedTour, showUserIcon]); + return ( diff --git a/src/app/Joyride/CryostatJoyride.tsx b/src/app/Joyride/CryostatJoyride.tsx index cfb33ab14..384fa2c67 100644 --- a/src/app/Joyride/CryostatJoyride.tsx +++ b/src/app/Joyride/CryostatJoyride.tsx @@ -85,7 +85,7 @@ const CryostatJoyride: React.FC = (props) => { width={300} />

- Welcome to Cryostat! + Welcome to Cryostat {build.version}!

), diff --git a/src/app/Topology/Shared/HintBanner.tsx b/src/app/Topology/Shared/HintBanner.tsx deleted file mode 100644 index 229f8c9bc..000000000 --- a/src/app/Topology/Shared/HintBanner.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 { portalRoot } from '@app/utils/utils'; -import { Tooltip } from '@patternfly/react-core'; -import { CloseIcon } from '@patternfly/react-icons'; -import { css } from '@patternfly/react-styles'; -import * as React from 'react'; - -export interface HintBannerProps { - className?: string; - style?: React.CSSProperties; - show?: boolean; - onClose?: () => void; - children?: React.ReactNode; -} - -export const HintBanner: React.FC = ({ className, style, children, onClose, show, ...props }) => { - return show ? ( -
- {children} - - - -
- ) : null; -}; diff --git a/src/app/Topology/Topology.tsx b/src/app/Topology/Topology.tsx index 2d9498b8e..2d2a9e518 100644 --- a/src/app/Topology/Topology.tsx +++ b/src/app/Topology/Topology.tsx @@ -38,22 +38,18 @@ import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; import { ErrorView } from '@app/ErrorView/ErrorView'; -import { FeatureFlag } from '@app/Shared/FeatureFlag/FeatureFlag'; import { LinearDotSpinner } from '@app/Shared/LinearDotSpinner'; import { ViewMode } from '@app/Shared/Redux/Configurations/TopologyConfigSlice'; import { RootState } from '@app/Shared/Redux/ReduxStore'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { ServiceContext } from '@app/Shared/Services/Services'; -import { FeatureLevel } from '@app/Shared/Services/Settings.service'; -import { getFromLocalStorage, saveToLocalStorage } from '@app/utils/LocalStorage'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Bullseye, Card, CardBody } from '@patternfly/react-core'; import * as React from 'react'; import { useSelector } from 'react-redux'; -import { Link, withRouter } from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; import { TopologyGraphView } from './GraphView/TopologyGraphView'; import { TopologyListView } from './ListView/TopologyListView'; -import { HintBanner } from './Shared/HintBanner'; import { DiscoveryTreeContext, SearchExprService, SearchExprServiceContext } from './Shared/utils'; import { DEFAULT_EMPTY_UNIVERSE } from './typings'; @@ -75,7 +71,6 @@ export const Topology: React.FC = ({ ..._props }) => { const [discoveryTree, setDiscoveryTree] = React.useState(DEFAULT_EMPTY_UNIVERSE); - const [shouldShowBanner, setShouldShowBanner] = React.useState(getFromLocalStorage('TOPOLOGY_SHOW_BANNER', true)); const isGraphView = useSelector((state: RootState) => { const _currentMode: ViewMode = state.topologyConfigs.viewMode; return _currentMode === 'graph'; @@ -83,11 +78,6 @@ export const Topology: React.FC = ({ ..._props }) => { const [error, setError] = React.useState(); - const closeBanner = React.useCallback(() => { - setShouldShowBanner(false); - saveToLocalStorage('TOPOLOGY_SHOW_BANNER', false); - }, [setShouldShowBanner]); - const _refreshDiscoveryTree = React.useCallback( (onSuccess?: () => void) => { addSubscription( @@ -178,11 +168,6 @@ export const Topology: React.FC = ({ ..._props }) => { return ( <> - - - For topology guides, see Quickstarts. - - diff --git a/src/app/Topology/styles/base.css b/src/app/Topology/styles/base.css index 274556f97..b1a211e6d 100644 --- a/src/app/Topology/styles/base.css +++ b/src/app/Topology/styles/base.css @@ -250,31 +250,6 @@ Below CSS rules only apply to Topology components font-weight: 700; } -.topology__hint-banner { - position: relative; - box-sizing: border-box; - text-align: center; - padding: 0.2em 0 0.2em 0; - background-color: var(--pf-global--palette--blue-400); - color: var(--pf-global--palette--white); - font-size: 0.9em; - margin-bottom: -1em; -} - -.topology__hint-banner a { - color: var(--pf-global--palette--white); - text-decoration: solid underline var(--pf-global--palette--white) 1px; -} - -.topology__hint-banner .close-icon { - position: relative; - float: right; - font-size: 1em; - margin-right: 1em; - transform: translateY(calc(50% - 0.25em)); - cursor: pointer; -} - #topology__visualization-container.topology__main-container { padding: 1em; } diff --git a/src/app/build.json b/src/app/build.json index 4750f2f4c..a4c194960 100644 --- a/src/app/build.json +++ b/src/app/build.json @@ -11,5 +11,6 @@ "fileIssueUrl": "https://github.com/cryostatio/cryostat/issues/new?labels=user+report,bug&body=Affects+__REPLACE_VERSION__", "mailingListName": "Google Groups", "mailingListUrl": "https://groups.google.com/g/cryostat-development", - "licenseUrl": "https://github.com/cryostatio/cryostat/blob/main/LICENSE" + "licenseUrl": "https://github.com/cryostatio/cryostat/blob/main/LICENSE", + "version": "2.4.0-dev" } diff --git a/src/app/utils/LocalStorage.ts b/src/app/utils/LocalStorage.ts index 88a3c2d3b..31ed9062b 100644 --- a/src/app/utils/LocalStorage.ts +++ b/src/app/utils/LocalStorage.ts @@ -37,6 +37,7 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ export enum LocalStorageKey { + ASSET_VERSION, FEATURE_LEVEL, DASHBOARD_CFG, AUTOMATED_ANALYSIS_FILTERS, @@ -44,7 +45,6 @@ export enum LocalStorageKey { CREDENTIAL_LOCATION, TARGET, TARGET_FAVORITES, - TOPOLOGY_SHOW_BANNER, TOPOLOGY_GRAPH_POSITONS, TOPOLOGY_NODE_POSITIONS, TOPOLOGY_CONFIG, diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index f67af912f..33376507a 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -40,6 +40,7 @@ import { ISortBy, SortByDirection } from '@patternfly/react-table'; import _ from 'lodash'; import { useHistory } from 'react-router-dom'; import { BehaviorSubject, Observable } from 'rxjs'; +import { getFromLocalStorage } from './LocalStorage'; const SECOND_MILLIS = 1000; const MINUTE_MILLIS = 60 * SECOND_MILLIS; @@ -266,3 +267,45 @@ export const getActiveTab = (search: string, key: string, supportedTabs: T[], }; export const clickOutside = () => document.body.click(); + +export interface SemVer { + major: number; + minor: number; + patch: number; +} + +// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +export const semverRegex = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; + +export const getSemVer = (str: string): SemVer | undefined => { + const matched = str.match(semverRegex); + if (matched) { + const [_, major, minor, patch] = matched; + return { + major: Number(major), + minor: Number(minor), + patch: Number(patch), + }; + } + return undefined; +}; + +const convert = (ver: SemVer) => ver.major * 100 + ver.minor * 10 + ver.patch; + +export const compareSemVer = (ver1: SemVer, ver2: SemVer): number => { + const _ver1 = convert(ver1); + const _ver2 = convert(ver2); + return _ver1 > _ver2 ? 1 : _ver1 < _ver2 ? -1 : 0; +}; + +export const isAssetNew = (currVerStr: string) => { + const oldVer = getSemVer(getFromLocalStorage('ASSET_VERSION', '0.0.0')); + const currVer = getSemVer(currVerStr); + + if (!currVer) { + throw new Error(`Invalid asset version: ${currVer}`); + } + // Invalid (old) version is ignored. + return !oldVer || compareSemVer(currVer, oldVer) > 0; +};