diff --git a/docs/development-guide/10-local-dev-tools.md b/docs/development-guide/10-local-dev-tools.md index 8dca8ebe66d..172269fb849 100644 --- a/docs/development-guide/10-local-dev-tools.md +++ b/docs/development-guide/10-local-dev-tools.md @@ -1,10 +1,24 @@ # Local Dev Tools -To facilitate development and debugging, Cloud Manager includes a "Dev Tools" mode. Currently this mode is used for feature flag toggling, data mocking, and environment switching. +To facilitate development and debugging, Cloud Manager includes a "Dev Tools" mode. Currently this mode is used for feature flag toggling, data mocking, and theme & environment switching. + +In order to access the dev tools, hover or click (mobile) on the 🛠 icon in the lower left corner of your browser window. The icon will be colored green if MSW is enabled. This mode is enabled by default while running the development server. To disable it, add `?dev-tools=false` to the URL, or write `dev-tools: false` to local storage. -This mode is disabled by default in production builds, but can be enabled by adding `?dev-tools=true` to the URL, or `dev-tools: true` to local storage. +This mode is disabled by default in production builds. + +## Feature Flags + +The display of the Flags in dev tools is defined in the `options` array in `FeatureFlagTool.tsx`. While it is convenient to add those switches to the dev tools, it is not always necessary as they can clutter the UI. Additionally, it is important to clean them up once the feature has been battle tested in production. + +The flags on/off values are stored in local storage for convenience and will be remembered on reload or app restart. + +By default, the boolean flags checkboxes represent their true values as returned by Launch Darkly (dev environment). Hitting the reset button will bring them back to those default values and clear local storage. + +## Theme Select + +The theme select in dev tools is a convenient way to store the theme choice while MSW is enabled (it won't affect your actual Application settings/preferences). The default is "system". ## Writing a new tool diff --git a/packages/manager/.changeset/pr-10220-changed-1708622627827.md b/packages/manager/.changeset/pr-10220-changed-1708622627827.md new file mode 100644 index 00000000000..5344aa3cf4e --- /dev/null +++ b/packages/manager/.changeset/pr-10220-changed-1708622627827.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Improve dev tools UI ([#10220](https://github.com/linode/manager/pull/10220)) diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index 7a5b7586832..26213e2d33d 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -1,4 +1,5 @@ import Grid from '@mui/material/Unstable_Grid2'; +import { useFlags as ldUseFlags } from 'launchdarkly-react-client-sdk'; import * as React from 'react'; import { useDispatch } from 'react-redux'; @@ -8,7 +9,6 @@ import { Dispatch } from 'src/hooks/types'; import { useFlags } from 'src/hooks/useFlags'; import { setMockFeatureFlags } from 'src/store/mockFeatureFlags'; import { getStorage, setStorage } from 'src/utilities/storage'; - const MOCK_FEATURE_FLAGS_STORAGE_KEY = 'devTools/mock-feature-flags'; const options: { flag: keyof Flags; label: string }[] = [ @@ -28,6 +28,7 @@ const options: { flag: keyof Flags; label: string }[] = [ export const FeatureFlagTool = withFeatureFlagProvider(() => { const dispatch: Dispatch = useDispatch(); const flags = useFlags(); + const ldFlags = ldUseFlags(); React.useEffect(() => { const storedFlags = getStorage(MOCK_FEATURE_FLAGS_STORAGE_KEY); @@ -48,6 +49,14 @@ export const FeatureFlagTool = withFeatureFlagProvider(() => { setStorage(MOCK_FEATURE_FLAGS_STORAGE_KEY, updatedFlags); }; + /** + * This will reset the flags values to the Launch Darkly defaults (as returned from the LD dev environment) + */ + const resetFlags = () => { + dispatch(setMockFeatureFlags(ldFlags)); + setStorage(MOCK_FEATURE_FLAGS_STORAGE_KEY, ''); + }; + return ( @@ -75,6 +84,9 @@ export const FeatureFlagTool = withFeatureFlagProvider(() => { ); })} + diff --git a/packages/manager/src/dev-tools/Preferences.tsx b/packages/manager/src/dev-tools/Preferences.tsx new file mode 100644 index 00000000000..de25a3a0216 --- /dev/null +++ b/packages/manager/src/dev-tools/Preferences.tsx @@ -0,0 +1,25 @@ +import LinkIcon from '@mui/icons-material/Link'; +import * as React from 'react'; + +export const Preferences = () => { + return ( + <> +

Preferences

+ + Open preference Modal + + + + ); +}; diff --git a/packages/manager/src/dev-tools/ServiceWorkerTool.tsx b/packages/manager/src/dev-tools/ServiceWorkerTool.tsx index 38190d8ccf0..6d7c6d70986 100644 --- a/packages/manager/src/dev-tools/ServiceWorkerTool.tsx +++ b/packages/manager/src/dev-tools/ServiceWorkerTool.tsx @@ -15,7 +15,9 @@ export const ServiceWorkerTool = () => { <> Mock Service Worker: - {isMSWEnabled ? 'Enabled' : 'Disabled'} + + {isMSWEnabled ? 'Enabled' : 'Disabled'} + { + const [mockTheme, setMockTheme] = React.useState('system'); + + React.useEffect(() => { + const storedTheme = getStorage(MOCK_THEME_STORAGE_KEY); + if (storedTheme) { + setMockTheme(storedTheme); + } + }, []); + + const handleSetTheme = (e: React.ChangeEvent) => { + const selectedValue = e.target.value as ThemeChoice; + + setMockTheme(selectedValue); + setStorage(MOCK_THEME_STORAGE_KEY, selectedValue); + window.location.reload(); + }; + + return ( + + +

MSW Theme Selector

+

+ (only when MSW is enabled) +

+
+ + + +
+ ); +}; diff --git a/packages/manager/src/dev-tools/dev-tools.css b/packages/manager/src/dev-tools/dev-tools.css index 535828c6756..23f3b0a5a28 100644 --- a/packages/manager/src/dev-tools/dev-tools.css +++ b/packages/manager/src/dev-tools/dev-tools.css @@ -10,13 +10,21 @@ width: 60px; transition: all 0.3s; z-index: 1; - overflow: auto; +} + +#dev-tools.mswEnabled svg { + color: #aaff00; } #dev-tools:hover { - height: 325px; + height: 375px; width: 100%; opacity: 0.9; + overflow: auto; +} + +#dev-tools:hover svg { + color: white; } #dev-tools .tools { diff --git a/packages/manager/src/dev-tools/dev-tools.tsx b/packages/manager/src/dev-tools/dev-tools.tsx index 99ba883b528..531961e1fe2 100644 --- a/packages/manager/src/dev-tools/dev-tools.tsx +++ b/packages/manager/src/dev-tools/dev-tools.tsx @@ -1,3 +1,4 @@ +import Handyman from '@mui/icons-material/Handyman'; import Grid from '@mui/material/Unstable_Grid2'; import React from 'react'; import { createRoot } from 'react-dom/client'; @@ -10,12 +11,17 @@ import './dev-tools.css'; import { EnvironmentToggleTool } from './EnvironmentToggleTool'; import { FeatureFlagTool } from './FeatureFlagTool'; import { MockDataTool } from './MockDataTool'; +import { Preferences } from './Preferences'; +import { isMSWEnabled } from './ServiceWorkerTool'; +import { ThemeSelector } from './ThemeSelector'; function install(store: ApplicationStore) { function DevTools() { return ( -
-
🛠
+
+
+ +
@@ -28,6 +34,8 @@ function install(store: ApplicationStore) { {!isProductionBuild || ENABLE_DEV_TOOLS ? ( + + ) : null} diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 8bae0035f00..e2375fb63ef 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -11,9 +11,10 @@ import { DateTime } from 'luxon'; import { rest } from 'msw'; import { regions } from 'src/__data__/regionsData'; +import { MOCK_THEME_STORAGE_KEY } from 'src/dev-tools/ThemeSelector'; import { VLANFactory, - abuseTicketNotificationFactory, + // abuseTicketNotificationFactory, accountAvailabilityFactory, accountBetaFactory, accountFactory, @@ -103,6 +104,7 @@ import { accountLoginFactory } from 'src/factories/accountLogin'; import { accountUserFactory } from 'src/factories/accountUsers'; import { grantFactory, grantsFactory } from 'src/factories/grants'; import { pickRandom } from 'src/utilities/random'; +import { getStorage } from 'src/utilities/storage'; export const makeResourcePage = ( e: T[], @@ -1146,7 +1148,11 @@ export const handlers = [ return res(ctx.json(makeResourcePage(vlans))); }), rest.get('*/profile/preferences', (req, res, ctx) => { - return res(ctx.json({})); + return res( + ctx.json({ + theme: getStorage(MOCK_THEME_STORAGE_KEY) ?? 'system', + }) + ); }), rest.get('*/profile/devices', (req, res, ctx) => { return res(ctx.json(makeResourcePage([]))); @@ -1819,18 +1825,18 @@ export const handlers = [ when: null, }; - const emailBounce = notificationFactory.build({ - body: null, - entity: null, - label: 'We are unable to send emails to your billing email address!', - message: 'We are unable to send emails to your billing email address!', - severity: 'major', - type: 'billing_email_bounce', - until: null, - when: null, - }); + // const emailBounce = notificationFactory.build({ + // body: null, + // entity: null, + // label: 'We are unable to send emails to your billing email address!', + // message: 'We are unable to send emails to your billing email address!', + // severity: 'major', + // type: 'billing_email_bounce', + // until: null, + // when: null, + // }); - const abuseTicket = abuseTicketNotificationFactory.build(); + // const abuseTicket = abuseTicketNotificationFactory.build(); const migrationNotification = notificationFactory.build({ entity: { id: 0, label: 'linode-0', type: 'linode' }, @@ -1926,8 +1932,8 @@ export const handlers = [ outageNotification, minorSeverityNotification, criticalSeverityNotification, - abuseTicket, - emailBounce, + // abuseTicket, + // emailBounce, migrationNotification, balanceNotification, blockStorageMigrationScheduledNotification,