From dd40d8a59698ceb63cf3092bc2ccc8fd66a30af5 Mon Sep 17 00:00:00 2001 From: Mike Burgess Date: Wed, 28 Sep 2022 11:37:07 +0100 Subject: [PATCH] Add hidden DOM when dashboard UI is complete to indicate completion to headless/WebDriver clients. Closes #2467. (#2469) --- ui/dashboard/package.json | 4 +- .../layout/Dashboard/DashboardProgress.tsx | 14 ++- .../dashboards/layout/Dashboard/index.tsx | 34 ++---- .../SnapshotRenderComplete/index.test.jsx | 33 ++++++ .../snapshot/SnapshotRenderComplete/index.tsx | 15 +++ ui/dashboard/src/hooks/useDashboard.tsx | 56 ++++++--- ui/dashboard/src/styles/index.css | 21 ++-- ui/dashboard/src/utils/storybook.tsx | 1 + ui/dashboard/yarn.lock | 112 ++++++++++++++++-- 9 files changed, 229 insertions(+), 61 deletions(-) create mode 100644 ui/dashboard/src/components/snapshot/SnapshotRenderComplete/index.test.jsx create mode 100644 ui/dashboard/src/components/snapshot/SnapshotRenderComplete/index.tsx diff --git a/ui/dashboard/package.json b/ui/dashboard/package.json index df672e9d9..ed441c08c 100644 --- a/ui/dashboard/package.json +++ b/ui/dashboard/package.json @@ -64,11 +64,13 @@ "@tailwindcss/forms": "0.5.3", "@tailwindcss/line-clamp": "0.4.2", "@tailwindcss/typography": "0.5.7", + "@testing-library/jest-dom": "5.16.5", + "@testing-library/react": "13.4.0", "@tsconfig/create-react-app": "1.0.2", "@types/echarts": "4.9.16", "@types/jest": "29.0.3", "@types/lodash": "4.14.185", - "@types/node": "18.7.22", + "@types/node": "18.7.23", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", "autoprefixer": "10.4.12", diff --git a/ui/dashboard/src/components/dashboards/layout/Dashboard/DashboardProgress.tsx b/ui/dashboard/src/components/dashboards/layout/Dashboard/DashboardProgress.tsx index 482615824..345ba5e75 100644 --- a/ui/dashboard/src/components/dashboards/layout/Dashboard/DashboardProgress.tsx +++ b/ui/dashboard/src/components/dashboards/layout/Dashboard/DashboardProgress.tsx @@ -1,11 +1,13 @@ -import { DashboardRunState } from "../../../../hooks/useDashboard"; +import { useDashboard } from "../../../../hooks/useDashboard"; -interface DashboardProgressProps { - state?: DashboardRunState; - progress?: number; -} +const DashboardProgress = () => { + const { dataMode, progress, state } = useDashboard(); + + // We only show a progress indicator in live mode + if (dataMode === "snapshot") { + return null; + } -const DashboardProgress = ({ state, progress }: DashboardProgressProps) => { return (
{state === "ready" ? ( diff --git a/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx b/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx index b74b75e91..1e7c012a7 100644 --- a/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx +++ b/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx @@ -2,9 +2,9 @@ import Children from "../common/Children"; import DashboardProgress from "./DashboardProgress"; import LayoutPanel from "../common/LayoutPanel"; import PanelDetail from "../PanelDetail"; +import SnapshotRenderComplete from "../../../snapshot/SnapshotRenderComplete"; import { DashboardDefinition, - DashboardRunState, useDashboard, } from "../../../../hooks/useDashboard"; import { registerComponent } from "../../index"; @@ -13,8 +13,6 @@ interface DashboardProps { allowPanelExpand?: boolean; definition: DashboardDefinition; isRoot?: boolean; - progress?: number; - state?: DashboardRunState; withPadding?: boolean; } @@ -26,13 +24,11 @@ interface DashboardWrapperProps { const Dashboard = ({ allowPanelExpand = true, definition, - progress = 0, isRoot = true, - state = "ready", withPadding = false, }: DashboardProps) => ( <> - {isRoot ? : <>} + {isRoot ? : <>} { - const { - dashboard, - dataMode, - progress, - search, - selectedDashboard, - selectedPanel, - state, - } = useDashboard(); + const { dashboard, dataMode, search, selectedDashboard, selectedPanel } = + useDashboard(); if ( search.value || @@ -73,13 +62,14 @@ const DashboardWrapper = ({ } return ( - + <> + + + ); }; diff --git a/ui/dashboard/src/components/snapshot/SnapshotRenderComplete/index.test.jsx b/ui/dashboard/src/components/snapshot/SnapshotRenderComplete/index.test.jsx new file mode 100644 index 000000000..1fb71e8ed --- /dev/null +++ b/ui/dashboard/src/components/snapshot/SnapshotRenderComplete/index.test.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import SnapshotRenderComplete from "./index.tsx"; +import { DashboardContext } from "../../../hooks/useDashboard"; +import { render } from "@testing-library/react"; +import "@testing-library/jest-dom"; + +test("return null when should not render snapshot complete div", async () => { + // ARRANGE + const { container } = render( + + + + ); + + // ASSERT + expect(container).toBeEmptyDOMElement(); +}); + +test("return null when should not render snapshot complete div", async () => { + // ARRANGE + render( + + + + ); + + // ASSERT + expect(document.querySelector("#snapshot-complete")).toBeTruthy(); +}); diff --git a/ui/dashboard/src/components/snapshot/SnapshotRenderComplete/index.tsx b/ui/dashboard/src/components/snapshot/SnapshotRenderComplete/index.tsx new file mode 100644 index 000000000..0678ac76e --- /dev/null +++ b/ui/dashboard/src/components/snapshot/SnapshotRenderComplete/index.tsx @@ -0,0 +1,15 @@ +import { useDashboard } from "../../../hooks/useDashboard"; + +const SnapshotRenderComplete = () => { + const { + render: { snapshotCompleteDiv }, + } = useDashboard(); + + if (!snapshotCompleteDiv) { + return null; + } + + return
; +}; + +export default SnapshotRenderComplete; diff --git a/ui/dashboard/src/hooks/useDashboard.tsx b/ui/dashboard/src/hooks/useDashboard.tsx index 83b06477d..5b6c9811b 100644 --- a/ui/dashboard/src/hooks/useDashboard.tsx +++ b/ui/dashboard/src/hooks/useDashboard.tsx @@ -2,15 +2,7 @@ import get from "lodash/get"; import has from "lodash/has"; import isEqual from "lodash/isEqual"; import paths from "deepdash/paths"; -import set from "lodash/set"; -import sortBy from "lodash/sortBy"; -import useDashboardWebSocket, { - SocketActions, - SocketURLFactory, -} from "./useDashboardWebSocket"; -import usePrevious from "./usePrevious"; -import { buildComponentsMap } from "../components"; -import { +import React, { createContext, Ref, useCallback, @@ -19,6 +11,14 @@ import { useReducer, useState, } from "react"; +import set from "lodash/set"; +import sortBy from "lodash/sortBy"; +import useDashboardWebSocket, { + SocketActions, + SocketURLFactory, +} from "./useDashboardWebSocket"; +import usePrevious from "./usePrevious"; +import { buildComponentsMap } from "../components"; import { GlobalHotKeys } from "react-hotkeys"; import { LeafNodeData, Width } from "../components/dashboards/common"; import { noop } from "../utils/func"; @@ -95,6 +95,10 @@ interface IDashboardContext { progress: number; state: DashboardRunState; + render: { + headless: boolean; + snapshotCompleteDiv: boolean; + }; } export interface IActions { @@ -314,6 +318,10 @@ export interface DashboardDataOptions { snapshotId?: string; } +export interface DashboardRenderOptions { + headless?: boolean; +} + interface DashboardProviderProps { analyticsContext: any; breakpointContext: any; @@ -322,6 +330,7 @@ interface DashboardProviderProps { dataOptions?: DashboardDataOptions; eventHooks?: {}; featureFlags?: string[]; + renderOptions?: DashboardRenderOptions; socketUrlFactory?: SocketURLFactory; stateDefaults?: {}; themeContext: any; @@ -842,6 +851,9 @@ const DashboardProvider = ({ }, eventHooks, featureFlags = [], + renderOptions = { + headless: false, + }, socketUrlFactory, stateDefaults = {}, themeContext, @@ -851,7 +863,11 @@ const DashboardProvider = ({ const [searchParams, setSearchParams] = useSearchParams(); const [state, dispatchInner] = useReducer( reducer, - getInitialState(searchParams, { ...stateDefaults, ...dataOptions }) + getInitialState(searchParams, { + ...stateDefaults, + ...dataOptions, + ...renderOptions, + }) ); const dispatch = useCallback((action) => { // console.log(action.type, action); @@ -923,11 +939,6 @@ const DashboardProvider = ({ searchParams.get("tag") || get(stateDefaults, "search.groupBy.tag", "service"); const inputs = buildSelectedDashboardInputsFromSearchParams(searchParams); - // console.log({ - // // @ts-ignore - // previous: previousSelectedDashboardStates?.selectedDashboardInputs, - // current: inputs, - // }); dispatch({ type: DashboardActions.SET_DASHBOARD_SEARCH_VALUE, value: goneFromDashboardToDashboard ? "" : search, @@ -943,7 +954,6 @@ const DashboardProvider = ({ previousSelectedDashboardStates?.selectedDashboardInputs ) !== JSON.stringify(inputs) ) { - // console.log("dispatching inputs", inputs); dispatch({ type: DashboardActions.SET_DASHBOARD_INPUTS, value: inputs, @@ -1282,6 +1292,16 @@ const DashboardProvider = ({ }); }, [closePanelDetail]); + const [renderSnapshotCompleteDiv, setRenderSnapshotCompleteDiv] = + useState(false); + + useEffect(() => { + if (dataOptions?.dataMode !== "snapshot" || state.state !== "complete") { + return; + } + setRenderSnapshotCompleteDiv(true); + }, [dataOptions?.dataMode, state.state]); + return ( diff --git a/ui/dashboard/yarn.lock b/ui/dashboard/yarn.lock index 45e97023e..1aea6aeb1 100644 --- a/ui/dashboard/yarn.lock +++ b/ui/dashboard/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.0.1.tgz#b38b444ad3aa5fedbb15f2f746dcd934226a12dd" + integrity sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g== + "@apideck/better-ajv-errors@^0.3.1": version "0.3.1" resolved "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.1.tgz" @@ -2547,6 +2552,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.9.2": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" + integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.12.13", "@babel/template@^7.12.7", "@babel/template@^7.3.3": version "7.12.13" resolved "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz" @@ -4338,6 +4350,44 @@ lodash.merge "^4.6.2" postcss-selector-parser "6.0.10" +"@testing-library/dom@^8.5.0": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.18.1.tgz#80f91be02bc171fe5a3a7003f88207be31ac2cf3" + integrity sha512-oEvsm2B/WtcHKE+IcEeeCqNU/ltFGaVyGbpcm4g/2ytuT49jrlH9x5qRKL/H3A6yfM4YAbSbC0ceT5+9CEXnLg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@5.16.5": + version "5.16.5" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz#3912846af19a29b2dbf32a6ae9c31ef52580074e" + integrity sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA== + dependencies: + "@adobe/css-tools" "^4.0.1" + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.5.6" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react@13.4.0": + version "13.4.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.4.0.tgz#6a31e3bf5951615593ad984e96b9e5e2d9380966" + integrity sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.5.0" + "@types/react-dom" "^18.0.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" @@ -4378,6 +4428,11 @@ resolved "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz" integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== +"@types/aria-query@^4.2.0": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" + integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.12" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz" @@ -4773,7 +4828,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@29.0.3": +"@types/jest@*", "@types/jest@29.0.3": version "29.0.3" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.0.3.tgz#b61a5ed100850686b8d3c5e28e3a1926b2001b59" integrity sha512-F6ukyCTwbfsEX5F2YmVYmM5TcTHy1q9P5rWlRbrk56KyMh3v9xRGUO3aa8+SkvMi0SHXtASJv1283enXimC0Og== @@ -4848,10 +4903,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448" integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== -"@types/node@18.7.22": - version "18.7.22" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.22.tgz#76f7401362ad63d9d7eefa7dcdfa5fcd9baddff3" - integrity sha512-TsmoXYd4zrkkKjJB0URF/mTIKPl+kVcbqClB2F/ykU7vil1BfWZVndOnpEIozPv4fURD28gyPFeIkW2G+KXOvw== +"@types/node@18.7.23": + version "18.7.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f" + integrity sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg== "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0": version "16.11.15" @@ -4910,7 +4965,7 @@ resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz" integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA== -"@types/react-dom@18.0.6": +"@types/react-dom@18.0.6", "@types/react-dom@^18.0.0": version "18.0.6" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1" integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA== @@ -4984,6 +5039,13 @@ resolved "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.7.tgz" integrity sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ== +"@types/testing-library__jest-dom@^5.9.1": + version "5.14.5" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz#d113709c90b3c75fdb127ec338dad7d5f86c974f" + integrity sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ== + dependencies: + "@types/jest" "*" + "@types/trusted-types@^2.0.2": version "2.0.2" resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz" @@ -5755,6 +5817,11 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" +aria-query@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.2.tgz#0b8a744295271861e1d933f8feca13f9b70cfdc1" + integrity sha512-eigU3vhqSO+Z8BKDnVLN/ompjhf3pYzecKXz8+whRy+9gZu8n1TCGfwzQUUPnqdHl9ax1Hr9031orZ+UOEYr7Q== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz" @@ -6756,6 +6823,14 @@ chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^4.0.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz" @@ -7625,6 +7700,11 @@ css-what@^5.1.0: resolved "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz" integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssdb@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/cssdb/-/cssdb-5.0.0.tgz" @@ -8146,6 +8226,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: + version "0.5.14" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56" + integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg== + dom-converter@^0.2, dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz" @@ -12057,6 +12142,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== + magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.7" resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz" @@ -14608,7 +14698,7 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" -pretty-format@^27.4.2, pretty-format@^27.5.1: +pretty-format@^27.0.2, pretty-format@^27.4.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== @@ -15317,6 +15407,14 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reduce-css-calc@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"