Dashboard UI should inform users when they are running a different UI version to the CLI. Closes #2728. (#2734)

This commit is contained in:
Mike Burgess
2022-11-10 19:00:27 +00:00
parent cbd358a7aa
commit c31707751b
16 changed files with 211 additions and 103 deletions

View File

@@ -156,6 +156,7 @@ jobs:
yarn build
env:
REACT_APP_HEAP_ID: ${{ secrets.HEAP_ANALYTICS_PRODUCTION_ID }}
REACT_APP_VERSION: ${{ env.VERSION }}
- name: Move Build Assets
run: |-

View File

@@ -3,6 +3,14 @@ _What's new?_
* Add support for visualisations of your data with graphs, with easily composable data structures using nodes and edges. ([tbd])
* Improved dashboard UI panel controls for quicker access to common tasks such as downloading panel data. ([#2510](https://github.com/turbot/steampipe/issues/2510), [#2663](https://github.com/turbot/steampipe/issues/2663))
## v0.17.1 [tbd]
_Bug fixes_
* Fix query command `--export` flag raising an error that it cannot be used in interactive mode, even when not in interactive mode. ([#2707](https://github.com/turbot/steampipe/issues/2707))
* Fix RefreshConnections sometimes storing an unset plugin ModTime property in the connection state file. This leads to failure to refresh connections when plugin has been rebuilt or updated. ([#2721](https://github.com/turbot/steampipe/issues/2721))
* Fix dashboard text inputs being editable in snapshot mode. ([#2717](https://github.com/turbot/steampipe/issues/2717))
* Fix dashboard JSONB columns in CSV data downloads not serialising correctly. ([#2733](https://github.com/turbot/steampipe/issues/2733))
* Add dashboard error modal when users are running a different UI and CLI version ([#2728](https://github.com/turbot/steampipe/issues/2728))
## v0.17.0 [2022-11-08]
_What's new?_

View File

@@ -10,6 +10,7 @@ import (
"github.com/turbot/steampipe/pkg/dashboard/dashboardexecute"
"github.com/turbot/steampipe/pkg/steampipeconfig"
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
"github.com/turbot/steampipe/pkg/version"
)
func buildDashboardMetadataPayload(workspaceResources *modconfig.ResourceMaps, cloudMetadata *steampipeconfig.CloudMetadata) ([]byte, error) {
@@ -29,6 +30,9 @@ func buildDashboardMetadataPayload(workspaceResources *modconfig.ResourceMaps, c
payload := DashboardMetadataPayload{
Action: "dashboard_metadata",
Metadata: DashboardMetadata{
CLI: DashboardCLIMetadata{
Version: version.VersionString,
},
InstalledMods: installedMods,
Telemetry: viper.GetString(constants.ArgTelemetry),
},

View File

@@ -147,9 +147,14 @@ type ModDashboardMetadata struct {
ShortName string `json:"short_name"`
}
type DashboardCLIMetadata struct {
Version string `json:"version,omitempty"`
}
type DashboardMetadata struct {
Mod *ModDashboardMetadata `json:"mod,omitempty"`
InstalledMods map[string]ModDashboardMetadata `json:"installed_mods,omitempty"`
CLI DashboardCLIMetadata `json:"cli"`
Cloud *steampipeconfig.CloudMetadata `json:"cloud,omitempty"`
Telemetry string `json:"telemetry"`
}

View File

@@ -45,6 +45,7 @@
"react-use-websocket": "4.2.0",
"reactflow": "11.2.0",
"remark-gfm": "3.0.1",
"semver": "7.3.8",
"use-deep-compare-effect": "1.8.1",
"uuid": "9.0.0",
"web-vitals": "3.0.4"

View File

@@ -1,8 +1,10 @@
import { isValidElement } from "react";
const getErrorMessage = (error: any, fallbackMessage: string) => {
if (!error) {
return fallbackMessage;
}
if (typeof error === "string") {
if (isValidElement(error)) {
return error;
}
if (error.message) {

View File

@@ -1,19 +0,0 @@
import ErrorMessage from "../ErrorMessage";
import Modal from "./index";
import { ErrorIcon } from "../../constants/icons";
const ErrorModal = ({ error, title }) => {
return (
<Modal
icon={<ErrorIcon className="h-8 w-8 text-red-600" aria-hidden="true" />}
message={
<div className="break-all">
<ErrorMessage error={error} />
</div>
}
title={title}
/>
);
};
export default ErrorModal;

View File

@@ -0,0 +1,17 @@
import ErrorMessage from "../ErrorMessage";
import Modal from "./index";
import { ErrorIcon } from "../../constants/icons";
const ErrorModal = ({ error, title }) => (
<Modal
icon={<ErrorIcon className="h-8 w-8 text-red-600" aria-hidden="true" />}
message={
<div className="break-all">
<ErrorMessage error={error} />
</div>
}
title={title}
/>
);
export default ErrorModal;

View File

@@ -1,83 +0,0 @@
import { CloseIcon } from "../../constants/icons";
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";
const Modal = ({ icon, message, title }) => {
const [open, setOpen] = useState(true);
return (
<Transition.Root show={open} as={Fragment}>
<Dialog
as="div"
static
className="fixed z-10 inset-0 overflow-y-auto"
open={open}
onClose={setOpen}
>
<div className="min-h-screen pt-4 px-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="inline-block align-middle h-screen"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-0 scale-95"
enterTo="opacity-100 translate-y-0 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 scale-100"
leaveTo="opacity-0 translate-y-0 scale-95"
>
<div className="inline-block h-full sm:h-auto align-middle bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all my-8 w-full sm:max-w-xl sm:p-6 lg:max-w-3xl">
<div className="absolute top-0 right-0 pt-4 pr-4">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={() => setOpen(false)}
>
<span className="sr-only">Close</span>
<CloseIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="flex items-start">
<div className="flex-shrink-0 flex items-start justify-center h-12 w-12 rounded-full h-12 w-12">
{icon}
</div>
<div className="mt-1 ml-4 text-left">
<Dialog.Title
as="h2"
className="text-xl leading-6 font-medium text-gray-900"
>
{title}
</Dialog.Title>
<div className="mt-2 mb-2">
<p className="w-full sm:w-11/12 text-sm text-foreground-light break-words whitespace-pre-wrap">
{message}
</p>
</div>
</div>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};
export default Modal;

View File

@@ -0,0 +1,86 @@
import { CloseIcon } from "../../constants/icons";
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";
import { ModalThemeWrapper } from "../../hooks/useTheme";
const Modal = ({ icon, message, title }) => {
const [open, setOpen] = useState(true);
return (
<Transition.Root show={open} as={Fragment}>
<Dialog
as="div"
static
className="fixed z-10 inset-0 overflow-y-auto"
open={open}
onClose={setOpen}
>
<ModalThemeWrapper>
<div className="min-h-screen pt-4 px-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="inline-block align-middle h-screen"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-0 scale-95"
enterTo="opacity-100 translate-y-0 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 scale-100"
leaveTo="opacity-0 translate-y-0 scale-95"
>
<div className="inline-block h-full sm:h-auto align-middle bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all my-8 w-full sm:max-w-xl sm:p-6 lg:max-w-3xl">
<div className="absolute top-0 right-0 pt-4 pr-4">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={() => setOpen(false)}
>
<span className="sr-only">Close</span>
<CloseIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="flex items-start">
<div className="flex-shrink-0 flex items-start justify-center h-12 w-12 rounded-full h-12 w-12">
{icon}
</div>
<div className="grow mt-1 ml-4 text-left">
<Dialog.Title
as="h2"
className="text-xl leading-6 font-medium text-gray-900"
>
{title}
</Dialog.Title>
<div className="mt-2 mb-2">
<p className="w-full sm:w-11/12 text-sm text-foreground-light break-words whitespace-pre-wrap">
{message}
</p>
</div>
</div>
</div>
</div>
</Transition.Child>
</div>
</ModalThemeWrapper>
</Dialog>
</Transition.Root>
);
};
export default Modal;

View File

@@ -0,0 +1,35 @@
import semver from "semver";
const VersionErrorMismatch = ({ cliVersion, uiVersion }) => {
const uiOlder = semver.lt(uiVersion, cliVersion);
return (
<div className="space-y-2">
<p>
{!uiOlder && (
<>Steampipe Dashboard UI version is newer than the CLI version.</>
)}
{uiOlder && (
<>Steampipe Dashboard UI version is older than the CLI version.</>
)}
</p>
<div>
<span className="block text-foreground-light">UI:</span>
<span className="font-semibold">{uiVersion}</span>
</div>
<div>
<span className="block text-foreground-light">CLI:</span>
<span className="font-semibold">{cliVersion}</span>
</div>
<p>
{!uiOlder && (
<>Please stop and restart your Steampipe dashboard process.</>
)}
{uiOlder && (
<>Please hard refresh this page, or close and re-open your browser.</>
)}
</p>
</div>
);
};
export default VersionErrorMismatch;

View File

@@ -14,6 +14,7 @@ import sortBy from "lodash/sortBy";
import useDashboardWebSocket, { SocketActions } from "./useDashboardWebSocket";
import useDashboardWebSocketEventHandler from "./useDashboardWebSocketEventHandler";
import usePrevious from "./usePrevious";
import VersionErrorMismatch from "../components/VersionErrorMismatch";
import {
AvailableDashboard,
AvailableDashboardsDictionary,
@@ -190,14 +191,36 @@ const wrapDefinitionInArtificialDashboard = (
};
function reducer(state, action) {
if (state.ignore_events) {
return state;
}
switch (action.type) {
case DashboardActions.DASHBOARD_METADATA:
const cliVersionRaw = get(action.metadata, "cli.version");
const uiVersionRaw = process.env.REACT_APP_VERSION;
const hasVersionsSet = !!cliVersionRaw && !!uiVersionRaw;
const cliVersion = !!cliVersionRaw
? cliVersionRaw.startsWith("v")
? cliVersionRaw.substring(1)
: cliVersionRaw
: null;
const uiVersion = !!uiVersionRaw
? uiVersionRaw.startsWith("v")
? uiVersionRaw.substring(1)
: uiVersionRaw
: null;
const mismatchedVersions = hasVersionsSet && cliVersion !== uiVersion;
return {
...state,
metadata: {
mod: {},
...action.metadata,
},
error: mismatchedVersions ? (
<VersionErrorMismatch cliVersion={cliVersion} uiVersion={uiVersion} />
) : null,
ignore_events: mismatchedVersions,
};
case DashboardActions.AVAILABLE_DASHBOARDS:
const { dashboards, dashboardsMap } = buildDashboards(
@@ -483,6 +506,7 @@ const buildSelectedDashboardInputsFromSearchParams = (searchParams) => {
const getInitialState = (searchParams, defaults: any = {}) => {
return {
ignore_events: false,
availableDashboardsLoaded: false,
metadata: null,
dashboards: [],

View File

@@ -108,6 +108,18 @@ const ThemeWrapper = ({ children }) => {
);
};
const ModalThemeWrapper = ({ children }) => {
const { setWrapperRef, theme } = useTheme();
return (
<div
ref={setWrapperRef}
className={`theme-${theme.name} print:bg-white print:theme-steampipe-default text-foreground print:text-black`}
>
{children}
</div>
);
};
const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
@@ -118,6 +130,7 @@ const useTheme = () => {
export {
FullHeightThemeWrapper,
ModalThemeWrapper,
Themes,
ThemeNames,
ThemeProvider,

View File

@@ -3,6 +3,7 @@ import { Ref } from "react";
import { Theme } from "../hooks/useTheme";
export interface IDashboardContext {
ignore_events: boolean;
metadata: DashboardMetadata | null;
availableDashboardsLoaded: boolean;
@@ -200,6 +201,10 @@ interface InstalledModsDashboardMetadata {
[key: string]: ModDashboardMetadata;
}
interface CliDashboardMetadata {
version: string;
}
export interface CloudDashboardActorMetadata {
id: string;
handle: string;
@@ -225,6 +230,7 @@ interface CloudDashboardMetadata {
export interface DashboardMetadata {
mod: ModDashboardMetadata;
installed_mods?: InstalledModsDashboardMetadata;
cli?: CliDashboardMetadata;
cloud?: CloudDashboardMetadata;
telemetry: "info" | "none";
}

View File

@@ -49,6 +49,7 @@ export const PanelStoryDecorator = ({
return (
<DashboardContext.Provider
value={{
ignore_events: false,
metadata: {
mod: {
title: "Storybook",

View File

@@ -16212,6 +16212,13 @@ semver@7.0.0:
resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz"
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
semver@7.3.8:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
dependencies:
lru-cache "^6.0.0"
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz"