mirror of
https://github.com/turbot/steampipe.git
synced 2026-02-19 07:00:17 -05:00
Dashboard UI should inform users when they are running a different UI version to the CLI. Closes #2728. (#2734)
This commit is contained in:
1
.github/workflows/release_cli_and_assets.yml
vendored
1
.github/workflows/release_cli_and_assets.yml
vendored
@@ -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: |-
|
||||
|
||||
@@ -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?_
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
17
ui/dashboard/src/components/Modal/ErrorModal.tsx
Normal file
17
ui/dashboard/src/components/Modal/ErrorModal.tsx
Normal 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;
|
||||
@@ -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"
|
||||
>
|
||||
​
|
||||
</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;
|
||||
86
ui/dashboard/src/components/Modal/index.tsx
Normal file
86
ui/dashboard/src/components/Modal/index.tsx
Normal 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"
|
||||
>
|
||||
​
|
||||
</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;
|
||||
35
ui/dashboard/src/components/VersionErrorMismatch/index.tsx
Normal file
35
ui/dashboard/src/components/VersionErrorMismatch/index.tsx
Normal 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;
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export const PanelStoryDecorator = ({
|
||||
return (
|
||||
<DashboardContext.Provider
|
||||
value={{
|
||||
ignore_events: false,
|
||||
metadata: {
|
||||
mod: {
|
||||
title: "Storybook",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user