diff --git a/dep-graph/client-e2e/src/integration/app.spec.ts b/dep-graph/client-e2e/src/integration/app.spec.ts index 8b3ab792c28d0..6dc020cf6c055 100644 --- a/dep-graph/client-e2e/src/integration/app.spec.ts +++ b/dep-graph/client-e2e/src/integration/app.spec.ts @@ -266,3 +266,47 @@ describe('loading dep-graph client with url params', () => { getCheckedProjectItems().should('have.length', 53); }); }); + +describe('theme preferences', () => { + let systemTheme: string; + beforeEach(() => { + cy.visit('/'); + systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; + }); + it('should initialize localstorage with default theme', () => { + expect(localStorage.getItem('nx-dep-graph-theme')).eq('system'); + }); + + it('has system default theme', () => { + cy.log('system theme is:', systemTheme); + cy.get('html').should('have.class', systemTheme); + }); + + describe('dark theme is set as prefered', () => { + before(() => { + cy.get('[data-cy="theme-open-modal-button"]').click(); + cy.get('[data-cy="dark-theme-button"]').click(); + }); + + it('should set dark theme', () => { + cy.log('Localstorage is: ', localStorage.getItem('nx-dep-graph-theme')); + expect(localStorage.getItem('nx-dep-graph-theme')).eq('dark'); + cy.get('html').should('have.class', 'dark'); + }); + }); + + describe('light theme is set as preferred', () => { + before(() => { + cy.get('[data-cy="theme-open-modal-button"]').click(); + cy.get('[data-cy="light-theme-button"]').click(); + }); + + it('should set light theme', () => { + cy.log('Localstorage is: ', localStorage.getItem('nx-dep-graph-theme')); + expect(localStorage.getItem('nx-dep-graph-theme')).eq('light'); + cy.get('html').should('have.class', 'light'); + }); + }); +}); diff --git a/dep-graph/client/jest.config.js b/dep-graph/client/jest.config.js index 313faa823dfa6..6c5973b3e0314 100644 --- a/dep-graph/client/jest.config.js +++ b/dep-graph/client/jest.config.js @@ -10,4 +10,9 @@ module.exports = { }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], coverageDirectory: '../../coverage/nx-dev/nx-dev', + // The mock for widnow.matchMedia has to be in a separete file and imported before the components to test + // for more info check : // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom + modulePathIgnorePatterns: [ + '/dep-graph/client/src/app/machines/match-media-mock.spec.ts', + ], }; diff --git a/dep-graph/client/project.json b/dep-graph/client/project.json index c9633da3b901d..bedd22828f0d8 100644 --- a/dep-graph/client/project.json +++ b/dep-graph/client/project.json @@ -15,20 +15,7 @@ "styles": ["dep-graph/client/src/styles.scss"], "scripts": [], "assets": [], - "optimization": true, - "outputHashing": "none", - "sourceMap": false, - "extractCss": true, - "namedChunks": false, - "extractLicenses": true, - "vendorChunk": false, - "budgets": [ - { - "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" - } - ], + "webpackConfig": "dep-graph/client/webpack.config.js" }, "configurations": { @@ -57,15 +44,35 @@ "maximumError": "5mb" } ] + }, + "release": { + "optimization": true, + "outputHashing": "none", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + } + ] } }, + "defaultConfiguration": "release", "outputs": ["{options.outputPath}"] }, "serve-base": { "executor": "@nrwl/web:dev-server", - "options": { - "buildTarget": "dep-graph-client:build-base:dev" - } + "configurations": { + "dev": { + "buildTarget": "dep-graph-client:build-base:dev" + } + }, + "defaultConfiguration": "dev" }, "lint": { "executor": "@nrwl/linter:eslint", diff --git a/dep-graph/client/src/app/app.tsx b/dep-graph/client/src/app/app.tsx index 6ed3388031724..1ae8d7ede3b12 100644 --- a/dep-graph/client/src/app/app.tsx +++ b/dep-graph/client/src/app/app.tsx @@ -1,5 +1,8 @@ import { Shell } from './shell'; import { GlobalStateProvider } from './state.provider'; +import { themeInit } from './theme-resolver'; + +themeInit(); export function App() { return ( diff --git a/dep-graph/client/src/app/dark-theme-styles.tsx b/dep-graph/client/src/app/dark-theme-styles.tsx new file mode 100644 index 0000000000000..903bd7f098728 --- /dev/null +++ b/dep-graph/client/src/app/dark-theme-styles.tsx @@ -0,0 +1,8 @@ +// Styling presets for dark mode, defined by agregating styling classes +export const DarkClasses = { + button: + 'dark:bg-sidebar-btn-dark dark:border-sidebar-border-dark hover:dark:bg-opacity-40 dark:text-sidebar-text-dark', + buttonAffected: 'dark:bg-sidebar-btn-dark hover:dark:bg-red-900/[.05]', + input: + 'dark:text-sidebar-text-dark dark:bg-sidebar-btn-dark/[0.5] dark:border-sidebar-border-dark', +}; diff --git a/dep-graph/client/src/app/debugger-panel.tsx b/dep-graph/client/src/app/debugger-panel.tsx index 383acb80f9279..0db3c1f351842 100644 --- a/dep-graph/client/src/app/debugger-panel.tsx +++ b/dep-graph/client/src/app/debugger-panel.tsx @@ -1,6 +1,7 @@ import { ProjectGraphList } from './interfaces'; import { GraphPerfReport } from './machines/interfaces'; import { memo } from 'react'; +import { DarkClasses } from './dark-theme-styles'; export interface DebuggerPanelProps { projectGraphs: ProjectGraphList[]; @@ -20,19 +21,26 @@ export const DebuggerPanel = memo(function ({ data-cy="debugger-panel" className=" flex-column + flex-column + dark:bg-sidebar-dark + dark:border-sidebar-border-dark flex flex - w-auto - items-center justify-items-center + w-auto items-center + items-center + justify-items-center + justify-items-center gap-4 gap-4 - border-b border-gray-200 + border-b + border-gray-200 bg-gray-50 - p-4 - text-gray-700 + p-4 transition-all " > -

Debugger

+

+ Debugger +

-

+

Last render took {lastPerfReport.renderTime}ms:{' '} {lastPerfReport.numNodes} nodes{' '} |{' '} diff --git a/dep-graph/client/src/app/experimental-feature.tsx b/dep-graph/client/src/app/experimental-feature.tsx index 59406c31df04f..bdea528aeb2be 100644 --- a/dep-graph/client/src/app/experimental-feature.tsx +++ b/dep-graph/client/src/app/experimental-feature.tsx @@ -5,14 +5,7 @@ function ExperimentalFeature(props) { const showExperimentalFeatures = environment.appConfig.showExperimentalFeatures; - return showExperimentalFeatures ? ( -

-

- Experimental Features -

- {props.children} -
- ) : null; + return showExperimentalFeatures ? props.children : null; } export default ExperimentalFeature; diff --git a/dep-graph/client/src/app/hooks/use-environment-config.ts b/dep-graph/client/src/app/hooks/use-environment-config.ts index 1d43862147bf3..d1dd91b43169c 100644 --- a/dep-graph/client/src/app/hooks/use-environment-config.ts +++ b/dep-graph/client/src/app/hooks/use-environment-config.ts @@ -12,7 +12,13 @@ export function useEnvironmentConfig(): { appConfig: AppConfig; useXstateInspect: boolean; } { - const environmentConfig = useRef({ + const environmentConfig = useRef(getEnvironmentConfig()); + + return environmentConfig.current; +} + +export function getEnvironmentConfig() { + return { exclude: window.exclude, watch: window.watch, localMode: window.localMode, @@ -20,7 +26,5 @@ export function useEnvironmentConfig(): { environment: window.environment, appConfig: window.appConfig, useXstateInspect: window.useXstateInspect, - }); - - return environmentConfig.current; + }; } diff --git a/dep-graph/client/src/app/machines/dep-graph.spec.ts b/dep-graph/client/src/app/machines/dep-graph.spec.ts index d6993473fd987..86eec937fc3fc 100644 --- a/dep-graph/client/src/app/machines/dep-graph.spec.ts +++ b/dep-graph/client/src/app/machines/dep-graph.spec.ts @@ -1,12 +1,10 @@ // nx-ignore-next-line -import type { - ProjectGraphDependency, - ProjectGraphProjectNode, -} from '@nrwl/devkit'; +import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; +import './match-media-mock.spec'; import { depGraphMachine } from './dep-graph.machine'; import { interpret } from 'xstate'; -export const mockProjects: ProjectGraphProjectNode[] = [ +export const mockProjects: ProjectGraphNode[] = [ { name: 'app1', type: 'app', diff --git a/dep-graph/client/src/app/machines/graph.ts b/dep-graph/client/src/app/machines/graph.ts index 7d62f04a72ccc..a25fd73881115 100644 --- a/dep-graph/client/src/app/machines/graph.ts +++ b/dep-graph/client/src/app/machines/graph.ts @@ -18,6 +18,7 @@ import { ProjectNode, } from '../util-cytoscape'; import { GraphRenderEvents, GraphPerfReport } from './interfaces'; +import { selectValueByThemeStatic } from '../theme-resolver'; export class GraphService { private traversalGraph: cy.Core; @@ -557,6 +558,16 @@ export class GraphService { } getImage() { - return this.renderGraph.png({ bg: '#fff', full: true }); + const bg = selectValueByThemeStatic('#262626', '#fff'); + return this.renderGraph.png({ bg, full: true }); + } + + evaluateStyles() { + if (this.renderGraph) { + const container = this.renderGraph.container(); + this.renderGraph.unmount(); + + this.renderGraph.mount(container); + } } } diff --git a/dep-graph/client/src/app/machines/match-media-mock.spec.ts b/dep-graph/client/src/app/machines/match-media-mock.spec.ts new file mode 100644 index 0000000000000..71db5812591a8 --- /dev/null +++ b/dep-graph/client/src/app/machines/match-media-mock.spec.ts @@ -0,0 +1,14 @@ +// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); diff --git a/dep-graph/client/src/app/shell.tsx b/dep-graph/client/src/app/shell.tsx index e171af675a627..93d1057d7165f 100644 --- a/dep-graph/client/src/app/shell.tsx +++ b/dep-graph/client/src/app/shell.tsx @@ -14,6 +14,7 @@ import { projectIsSelectedSelector, } from './machines/selectors'; import Sidebar from './sidebar/sidebar'; +import { selectValueByThemeStatic } from './theme-resolver'; export function Shell() { const depGraphService = useDepGraphService(); @@ -97,7 +98,10 @@ export function Shell() { return ( <> -
+
{environment.appConfig.showDebugger ? ( +
- + + + + + +
+ ); +} diff --git a/dep-graph/client/src/app/sidebar/project-list.tsx b/dep-graph/client/src/app/sidebar/project-list.tsx index 1196015d5d95e..5d9528326d2e1 100644 --- a/dep-graph/client/src/app/sidebar/project-list.tsx +++ b/dep-graph/client/src/app/sidebar/project-list.tsx @@ -62,7 +62,11 @@ function ProjectListItem({ focusProject: (projectId: string) => void; }) { return ( -
  • +