diff --git a/docs/customizing.md b/docs/customizing.md
index 2171212e3c..c167a77516 100644
--- a/docs/customizing.md
+++ b/docs/customizing.md
@@ -50,7 +50,7 @@ In this file you can set any or all of the following variables:
|---|---|
|$stratos-theme|The main theme to use for Stratos|
|$stratos-nav-theme|Theme to use for the side navigation panel|
-$stratos-status-theme|Theme to use for displaying status in Stratos|
+|$stratos-status-theme|Theme to use for displaying status in Stratos|
Note that you do not have to specify all of these - defaults will be used if they are not set.
@@ -72,6 +72,24 @@ $suse-app-theme: mat-light-theme($suse-app-primary, $suse-app-primary, $suse-the
$stratos-theme: $suse-app-theme;
```
+#### Creating or disabling the Dark theme
+
+You can also change the Dark theme, if you wish, by defining the following variables:
+
+|Variable|Purpose|
+|---|---|
+|$stratos-dark-theme|The dark theme to use for Stratos|
+|$stratos-dark-nav-theme|Dark theme to use for the side navigation panel|
+|$stratos-dark-status-theme|Dark theme to use for displaying status in Stratos|
+
+Note that minimally you must supply `stratos-dark-theme` to create a dark theme.
+
+By default a dark theme is assumed to be available and the default will be used if not overridden. You can disable dark theme support in the UI by setting the following variable in your `custom.scss`:
+
+```
+$stratos-dark-theme-supported: false;
+```
+
### Changing Styles
We don't generally recommend modifying styles, since from version to version of Stratos, we may change the styles used slightly which can mean any modifications you made will need updating. Should you wish to do so, you can modify these in the same `custom.scss` file that is used for theming.
diff --git a/examples/custom-src/frontend/sass/custom.scss b/examples/custom-src/frontend/sass/custom.scss
index 308b1ef362..e3eb84c38a 100644
--- a/examples/custom-src/frontend/sass/custom.scss
+++ b/examples/custom-src/frontend/sass/custom.scss
@@ -2,11 +2,17 @@
// ACME Primary Material Design pallette
// From http://mcg.mbitson.com/#!?mcgpalette0=%233f51b5
-$acme-primary: (50: #e0f7f0, 100: #b3ecd9, 200: #80e0c0, 300: #4dd3a7, 400: #26c994, 500: #00c081, 600: #00ba79, 700: #00b26e, 800: #00aa64, 900: #009c51, A100: #c7ffe0, A200: #94ffc4, A400: #61ffa8, A700: #47ff9a, contrast: (50: #000, 100: #000, 200: #000, 300: #000, 400: #000, 500: #fff, 600: #fff, 700: #fff, 800: #fff, 900: #fff, A100: #000, A200: #000, A400: #000, A700: #000));
+$acme-primary: (50: #e8eaf6, 100: #c5cbe9, 200: #9fa8da, 300: #7985cb, 400: #5c6bc0, 500: #3f51b5, 600: #394aae, 700: #3140a5, 800: #29379d, 900: #1b278d, A100: #c6cbff, A200: #939dff, A400: #606eff, A700: #4757ff, contrast: ( 50: #000000, 100: #000000, 200: #000000, 300: #000000, 400: #ffffff, 500: #ffffff, 600: #ffffff, 700: #ffffff, 800: #ffffff, 900: #ffffff, A100: #000000, A200: #000000, A400: #ffffff, A700: #ffffff, ));
$mat-red: ( 50: #ffebee, 100: #ffcdd2, 200: #ef9a9a, 300: #e57373, 400: #ef5350, 500: #f44336, 600: #e53935, 700: #d32f2f, 800: #c62828, 900: #b71c1c, A100: #ff8a80, A200: #ff5252, A400: #ff1744, A700: #d50000, contrast: ( 50: $black-87-opacity, 100: $black-87-opacity, 200: $black-87-opacity, 300: $black-87-opacity, 400: $black-87-opacity, 500: white, 600: white, 700: white, 800: $white-87-opacity, 900: $white-87-opacity, A100: $black-87-opacity, A200: white, A400: white, A700: white, ));
+// Common
$acme-theme-primary: mat-palette($acme-primary);
$acme-theme-warn: mat-palette($mat-red);
+
+// Dark Theme
+$stratos-dark-theme: mat-dark-theme($acme-theme-primary, $acme-theme-primary, $acme-theme-warn);
+
+// Default Theme
$stratos-theme: mat-light-theme($acme-theme-primary, $acme-theme-primary, $acme-theme-warn);
@import 'custom/acme';
diff --git a/examples/custom-src/frontend/sass/custom/acme.scss b/examples/custom-src/frontend/sass/custom/acme.scss
index d153c4bc36..75794ebb6e 100644
--- a/examples/custom-src/frontend/sass/custom/acme.scss
+++ b/examples/custom-src/frontend/sass/custom/acme.scss
@@ -11,7 +11,7 @@ $acme-blue: #073155;
$acme-side-nav: $acme-secondary;
$acme-side-nav-active: #003358;
-.stratos {
+body.stratos {
app-page-subheader {
.page-subheader {
background-color: #fff;
diff --git a/package.json b/package.json
index b53a22ada8..6f65152326 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"ng": "ng",
"start": "npm run customize && ng serve",
"start-high-mem": "npm run customize && node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng serve",
+ "start-dev-high-mem": "npm run customize && node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng serve --aot=false",
"start-prod-high-mem": "npm run customize && node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng serve --prod",
"start-dev": "ng serve --aot=false",
"test": "run-s test-frontend:* --continue-on-error",
diff --git a/src/frontend/packages/cf-autoscaler/src/shared/list-types/app-autoscaler-metric-chart/app-autoscaler-metric-chart-card/app-autoscaler-metric-chart-card.component.scss b/src/frontend/packages/cf-autoscaler/src/shared/list-types/app-autoscaler-metric-chart/app-autoscaler-metric-chart-card/app-autoscaler-metric-chart-card.component.scss
index 002bddcdbd..de27bc5c5e 100644
--- a/src/frontend/packages/cf-autoscaler/src/shared/list-types/app-autoscaler-metric-chart/app-autoscaler-metric-chart-card/app-autoscaler-metric-chart-card.component.scss
+++ b/src/frontend/packages/cf-autoscaler/src/shared/list-types/app-autoscaler-metric-chart/app-autoscaler-metric-chart-card/app-autoscaler-metric-chart-card.component.scss
@@ -1,6 +1,5 @@
mat-card-header {
.autoscaler-metric-subtitle {
- color: rgba(0, 0, 0, .54);
font-size: .9em;
margin-top: .3em;
}
diff --git a/src/frontend/packages/cloud-foundry/src/actions/deploy-applications.actions.ts b/src/frontend/packages/cloud-foundry/src/actions/deploy-applications.actions.ts
index 7264dee6a7..36baef9f80 100644
--- a/src/frontend/packages/cloud-foundry/src/actions/deploy-applications.actions.ts
+++ b/src/frontend/packages/cloud-foundry/src/actions/deploy-applications.actions.ts
@@ -9,7 +9,7 @@ import { GitAppDetails, OverrideAppDetails, SourceType } from '../store/types/de
import { GitBranch, GitCommit } from '../store/types/git.types';
export const SET_APP_SOURCE_DETAILS = '[Deploy App] Application Source';
-export const CHECK_PROJECT_EXISTS = '[Deploy App] Check Projet exists';
+export const CHECK_PROJECT_EXISTS = '[Deploy App] Check Project exists';
export const PROJECT_DOESNT_EXIST = '[Deploy App] Project Doesn\'t exist';
export const PROJECT_FETCH_FAILED = '[Deploy App] Project Fetch Failed';
export const PROJECT_EXISTS = '[Deploy App] Project exists';
diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.theme.scss b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.theme.scss
index 0ce40249ca..d5f3293582 100644
--- a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.theme.scss
+++ b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application.component.theme.scss
@@ -1,7 +1,17 @@
@import '~@angular/material/theming';
@mixin app-deploy-app-theme($theme, $app-theme) {
+ $is-dark: map-get($theme, is-dark);
+
$ansi-colors: map-get($app-theme, ansi-colors);
+ $background-color: map-get(map-get($ansi-colors, 'white'), intense);
+
+ @if $is-dark == true {
+ $background-colors: map-get($theme, background);
+ $foreground-colors: map-get($theme, foreground);
+ $background-color: lighten(mat-color($background-colors, background), 5%);
+ }
+
.deploy-app {
- background-color: map-get(map-get($ansi-colors, 'white'), intense);
+ background-color: $background-color;
}
}
diff --git a/src/frontend/packages/core/misc/custom/custom.scss b/src/frontend/packages/core/misc/custom/custom.scss
index eb5d596b7e..f6d83b743c 100644
--- a/src/frontend/packages/core/misc/custom/custom.scss
+++ b/src/frontend/packages/core/misc/custom/custom.scss
@@ -3,4 +3,3 @@
// This file is in the .gitignore - changes will not be flagged
// The customization build step will replace this file with the custom one if provided
-
diff --git a/src/frontend/packages/core/sass/_all-theme.scss b/src/frontend/packages/core/sass/_all-theme.scss
index 184dc594e9..924e13afcf 100644
--- a/src/frontend/packages/core/sass/_all-theme.scss
+++ b/src/frontend/packages/core/sass/_all-theme.scss
@@ -32,6 +32,7 @@
@import '../src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.theme';
@import '../src/shared/components/upload-progress-indicator/upload-progress-indicator.component.theme';
@import '../src/shared/components/list/list-cards/meta-card/meta-card-base/meta-card.component.theme';
+@import '../src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.theme';
@import '../src/shared/components/start-end-date/start-end-date.component.theme';
@import '../src/shared/components/metrics-chart/metrics-chart.component.theme';
@import '../src/shared/components/metrics-range-selector/metrics-range-selector.component.theme';
@@ -41,7 +42,8 @@
@import '../src/features/user-profile/profile-info/profile-info.component.theme';
@import '../src/core/stateful-icon/stateful-icon.component.theme';
@import '../src/shared/components/markdown-preview/markdown-preview.component.theme';
-@import './components/mat-tabs.theme';
+@import './components/mat-snack-bar.theme';
+@import './components/ngx-charts-gauge.theme';
@import './components/text-status.theme';
@import './components/hyperlinks.theme';
@import './mat-themes';
@@ -82,12 +84,10 @@ $side-nav-light-active: #484848;
$warn: map-get($theme, warn);
$subdued: mat-contrast($primary, 50);
- @if $is-dark==true {
+ @if $is-dark == true {
$app-background-color: lighten(mat-color($background-colors, background), 10%);
- $subdued: darken($subdued, 50);
- }
-
- @else {
+ $subdued: lighten($subdued, 90);
+ } @else {
$app-background-color: darken(mat-color($background-colors, background), 2%);
$subdued: lighten($subdued, 50);
}
@@ -106,7 +106,8 @@ $side-nav-light-active: #484848;
@include steppers-theme($theme, $app-theme);
@include list-theme($theme, $app-theme);
@include app-base-page-theme($theme, $app-theme);
- @include app-mat-tabs-theme($theme, $app-theme);
+ @include app-mat-snack-bar-theme($theme, $app-theme);
+ @include ngx-charts-gauge($theme, $app-theme);
@include app-text-status-theme($theme, $app-theme);
@include app-card-status-theme($theme, $app-theme);
@include app-usage-gauge-theme($theme, $app-theme);
@@ -149,15 +150,14 @@ $side-nav-light-active: #484848;
@include page-side-nav-theme($theme, $app-theme);
@include cf-admin-add-user-warning($theme, $app-theme);
@include entity-summary-title-theme($theme, $app-theme);
+ @include app-meta-card-item-theme($theme, $app-theme);
@include error-page-theme($theme, $app-theme);
}
@function app-generate-nav-theme($theme, $nav-theme: null) {
@if ($nav-theme) {
@return $nav-theme;
- }
-
- @else {
+ } @else {
// Use default palette for side navigation
@return (background: $side-nav-light-bg, background-top: $side-nav-light-bg, text: darken($side-nav-light-text, 10%), active: $side-nav-light-active, active-text: $side-nav-light-text, hover: $side-nav-light-hover, hover-text: $side-nav-light-text);
}
@@ -166,9 +166,7 @@ $side-nav-light-active: #484848;
@function app-generate-status-theme($theme, $status-theme: null) {
@if ($status-theme) {
@return $status-theme;
- }
-
- @else {
+ } @else {
$warn: map-get($theme, warn);
$primary: map-get($theme, primary);
$white: #fff; // Use default palette for status
diff --git a/src/frontend/packages/core/sass/components/mat-snack-bar.theme.scss b/src/frontend/packages/core/sass/components/mat-snack-bar.theme.scss
new file mode 100644
index 0000000000..536d6434d7
--- /dev/null
+++ b/src/frontend/packages/core/sass/components/mat-snack-bar.theme.scss
@@ -0,0 +1,20 @@
+@mixin app-mat-snack-bar-theme($theme, $app-theme) {
+ $is-dark: map-get($theme, is-dark);
+ $background-colors: map-get($theme, background);
+ $foreground-colors: map-get($theme, foreground);
+
+ $background-color: mat-color($foreground-colors, text);
+ $color: mat-color($background-colors, text);
+
+ @if $is-dark == true {
+ $background-color: lighten(mat-color($background-colors, background), 5%);
+ $color: mat-color($foreground-colors, text);
+ }
+
+ .mat-snack-bar-container {
+ background-color: $background-color;
+ .mat-simple-snackbar {
+ color: $color;
+ }
+ }
+}
diff --git a/src/frontend/packages/core/sass/components/mat-tabs.theme.scss b/src/frontend/packages/core/sass/components/mat-tabs.theme.scss
deleted file mode 100644
index 29f7023533..0000000000
--- a/src/frontend/packages/core/sass/components/mat-tabs.theme.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-// Fix the color of the underline for the active tab
-// when on a primary backgrdound
-// Need to revist this and check if this is an issue with the Angular Material library
-@mixin app-mat-tabs-theme($theme, $app-theme) {
- $is-dark: map-get($theme, is-dark);
- $primary: map-get($theme, primary);
- $tabs-ink-color: mat-color($primary);
- @if $is-dark == true {
- $tabs-ink-color: lighten($tabs-ink-color, 20%);
- } @else {
- $tabs-ink-color: darken($tabs-ink-color, 20%);
- }
-
- .mat-tab-nav-bar.mat-primary.mat-background-primary {
- .mat-ink-bar {
- //background-color: $tabs-ink-color;
- }
-
- }
-}
diff --git a/src/frontend/packages/core/sass/components/ngx-charts-gauge.theme.scss b/src/frontend/packages/core/sass/components/ngx-charts-gauge.theme.scss
new file mode 100644
index 0000000000..406a49f950
--- /dev/null
+++ b/src/frontend/packages/core/sass/components/ngx-charts-gauge.theme.scss
@@ -0,0 +1,6 @@
+@mixin ngx-charts-gauge($theme, $app-theme) {
+ $foreground-colors: map-get($theme, foreground);
+ ngx-charts-chart text {
+ fill: mat-color($foreground-colors, text);
+ }
+}
diff --git a/src/frontend/packages/core/sass/theme.scss b/src/frontend/packages/core/sass/theme.scss
index eb3e41787c..ed6428d931 100644
--- a/src/frontend/packages/core/sass/theme.scss
+++ b/src/frontend/packages/core/sass/theme.scss
@@ -6,18 +6,38 @@
// Custom theme support
@import './custom';
-// Themes palettes and colors
-$oss-theme-primary: mat-palette($mat-blue);
-$oss-theme-accent: mat-palette($mat-blue);
-$oss-theme-warn: mat-palette($mat-red);
-$oss-theme: mat-light-theme($oss-theme-primary, $oss-theme-accent, $oss-theme-warn);
+.dark-theme {
+ // Dark Theme defaults
+ $dark-primary: mat-palette($mat-blue);
+ $dark-accent: mat-palette($mat-amber, A400, A100, A700);
+ $dark-warn: mat-palette($mat-red);
+ $dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn);
-// Default to using the open source theme
-$stratos-theme: $oss-theme !default;
-$stratos-nav-theme: null !default;
-$stratos-status-theme: null !default;
+ $stratos-dark-theme: $dark-theme !default;
+ $stratos-dark-nav-theme: null !default;
+ $stratos-dark-status-theme: null !default;
+
+ @include angular-material-theme($stratos-dark-theme);
+ @include app-theme($stratos-dark-theme, $stratos-dark-nav-theme, $stratos-dark-status-theme);
+}
+
+.default {
+ // Themes palettes and colors
+ $oss-theme-primary: mat-palette($mat-blue);
+ $oss-theme-accent: mat-palette($mat-blue);
+ $oss-theme-warn: mat-palette($mat-red);
+ $oss-theme: mat-light-theme($oss-theme-primary, $oss-theme-accent, $oss-theme-warn);
+
+ // Default to using the open source theme
+ $stratos-theme: $oss-theme !default;
+ $stratos-nav-theme: null !default;
+ $stratos-status-theme: null !default;
+
+ @include angular-material-theme($stratos-theme);
+ @include app-theme($stratos-theme, $stratos-nav-theme, $stratos-status-theme);
+}
+
+$stratos-dark-theme-supported: true !default;
// Create the theme
-@include mat-core();
-@include angular-material-theme($stratos-theme);
-@include app-theme($stratos-theme, $stratos-nav-theme, $stratos-status-theme);
+@include mat-core;
diff --git a/src/frontend/packages/core/src/app.component.html b/src/frontend/packages/core/src/app.component.html
index 192fe40149..16017c9762 100644
--- a/src/frontend/packages/core/src/app.component.html
+++ b/src/frontend/packages/core/src/app.component.html
@@ -1,2 +1,5 @@
-
-
{{userId}}
\ No newline at end of file
+
+
+
+ {{userId}}
+
\ No newline at end of file
diff --git a/src/frontend/packages/core/src/app.component.ts b/src/frontend/packages/core/src/app.component.ts
index 62df36b02f..5dce6095bc 100644
--- a/src/frontend/packages/core/src/app.component.ts
+++ b/src/frontend/packages/core/src/app.component.ts
@@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
import { create } from 'rxjs-spy';
import { AuthOnlyAppState } from '../../store/src/app-state';
+import { ThemeService } from './core/theme.service';
import { environment } from './environments/environment';
import { LoggedInService } from './logged-in.service';
@@ -20,7 +21,8 @@ export class AppComponent implements OnInit, OnDestroy, AfterContentInit {
constructor(
private loggedInService: LoggedInService,
- store: Store
+ store: Store,
+ public themeService: ThemeService
) {
// We use the username to key the session storage. We could replace this with the users id?
this.userId$ = store.select(state => state.auth.sessionData && state.auth.sessionData.user ? state.auth.sessionData.user.name : null);
diff --git a/src/frontend/packages/core/src/core/style.service.ts b/src/frontend/packages/core/src/core/style.service.ts
new file mode 100644
index 0000000000..f832f06c0d
--- /dev/null
+++ b/src/frontend/packages/core/src/core/style.service.ts
@@ -0,0 +1,38 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class StyleService {
+
+ private rules: string[] = [];
+ constructor() {
+ this.rules = this.getAllSelectors();
+ }
+
+ hasSelector = (selector) => {
+ return !!this.rules.find(ruleSelector => ruleSelector === selector);
+ }
+
+ private getAllSelectors = (): string[] => {
+ const ret = [];
+ // tslint:disable-next-line:prefer-for-of
+ for (let i = 0; i < document.styleSheets.length; i++) {
+ const styleSheet = document.styleSheets[i];
+ if (!(styleSheet instanceof CSSStyleSheet)) {
+ continue;
+ }
+ const rules = styleSheet.rules || styleSheet.cssRules;
+ // tslint:disable-next-line:prefer-for-of
+ for (let y = 0; y < rules.length; y++) {
+ const rule = rules[y];
+ if (!(rule instanceof CSSStyleRule)) {
+ continue;
+ }
+ if (typeof rule.selectorText === 'string') { ret.push(rule.selectorText); }
+ }
+ }
+ return ret;
+ }
+
+}
diff --git a/src/frontend/packages/core/src/core/theme.service.ts b/src/frontend/packages/core/src/core/theme.service.ts
new file mode 100644
index 0000000000..f5ee16ba18
--- /dev/null
+++ b/src/frontend/packages/core/src/core/theme.service.ts
@@ -0,0 +1,151 @@
+import { OverlayContainer } from '@angular/cdk/overlay';
+import { Injectable } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { Observable } from 'rxjs';
+import { first, map } from 'rxjs/operators';
+
+import { SetThemeAction } from '../../../store/src/actions/dashboard-actions';
+import { DashboardOnlyAppState } from '../../../store/src/app-state';
+import { selectDashboardState } from '../../../store/src/selectors/dashboard.selectors';
+import { StyleService } from './style.service';
+
+export interface StratosTheme {
+ key: string;
+ label: string;
+ styleName: string;
+}
+
+const lightTheme: StratosTheme = {
+ key: 'default',
+ label: 'Light',
+ styleName: 'default'
+};
+const darkTheme: StratosTheme = {
+ key: 'dark',
+ label: 'Dark',
+ styleName: 'dark-theme'
+};
+const osTheme: StratosTheme = {
+ key: 'os',
+ label: 'OS',
+ styleName: ''
+};
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ThemeService {
+
+ private osThemeInfo = {
+ supports: false,
+ isDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
+ isLightMode: window.matchMedia('(prefers-color-scheme: light)').matches,
+ isNotSpecified: window.matchMedia('(prefers-color-scheme: no-preference)').matches
+ };
+ private themes: StratosTheme[] = [lightTheme];
+
+ constructor(
+ private store: Store,
+ private overlayContainer: OverlayContainer,
+ private styleService: StyleService) {
+ this.initialiseStratosThemeInfo();
+ }
+
+ getThemes(): StratosTheme[] {
+ return this.themes;
+ }
+
+ getTheme(): Observable {
+ return this.store.select(selectDashboardState).pipe(
+ map(dashboardState => this.findTheme(dashboardState.themeKey)),
+ );
+ }
+
+ setTheme(themeKey: string) {
+ const findTheme = this.findTheme(themeKey);
+ this.setOverlay(findTheme);
+ this.store.dispatch(new SetThemeAction(findTheme));
+ }
+
+ /**
+ * Initialize the service with a theme that may already exists in the store
+ */
+ initialize() {
+ this.getTheme().pipe(first()).subscribe(theme => this.setOverlay(theme));
+ }
+
+ private initialiseStratosThemeInfo() {
+ const hasDarkTheme = this.styleService.hasSelector('.dark-theme-supported');
+
+ if (hasDarkTheme) {
+ this.themes.push(darkTheme);
+
+ this.initialiseOsThemeInfo();
+ }
+
+ }
+
+ private initialiseOsThemeInfo() {
+ this.osThemeInfo.supports = this.osThemeInfo.isDarkMode || this.osThemeInfo.isLightMode || this.osThemeInfo.isNotSpecified;
+
+ if (this.osThemeInfo.supports) {
+ this.themes.push(osTheme);
+
+ // Watch for changes at run time
+ window.matchMedia('(prefers-color-scheme: dark)').addListener(e => e.matches && this.updateFollowingOsThemeChange());
+ window.matchMedia('(prefers-color-scheme: light)').addListener(e => e.matches && this.updateFollowingOsThemeChange());
+ window.matchMedia('(prefers-color-scheme: no-preference)').addListener(e => e.matches && this.updateFollowingOsThemeChange());
+ }
+ }
+
+ /**
+ * Find a theme in a safe way with fall backs
+ */
+ private findTheme(themeKey: string): StratosTheme {
+ if (themeKey === osTheme.key && this.getThemes().find(theme => theme.key === osTheme.key)) {
+ return this.getOsTheme() || lightTheme;
+ }
+ return this.getThemes().find(theme => theme.key === themeKey) || lightTheme;
+ }
+
+ /**
+ * Create an `OS` theme that contains the relevant style
+ */
+ private getOsTheme(): StratosTheme {
+ if (this.osThemeInfo.supports) {
+ return this.osThemeInfo.isDarkMode ? {
+ ...osTheme,
+ styleName: darkTheme.styleName
+ } : this.osThemeInfo.isLightMode || this.osThemeInfo.isNotSpecified ? {
+ ...osTheme,
+ styleName: lightTheme.styleName
+ } : null;
+ }
+ }
+
+ /**
+ * Overlays require the theme specifically set, see https://material.angular.io/guide/theming#multiple-themes
+ * `Multiple themes and overlay-based components`
+ */
+ private setOverlay(newTheme: StratosTheme) {
+ // Remove pre-existing styles
+ this.getThemes()
+ .filter(theme => theme.styleName)
+ .forEach(theme => this.overlayContainer.getContainerElement().classList.remove(theme.styleName));
+ // Add new style (not from getThemes list, handles OS case)
+ this.overlayContainer.getContainerElement().classList.add(newTheme.styleName);
+ }
+
+ /**
+ * Update theme given changes in OS theme settings
+ */
+ private updateFollowingOsThemeChange() {
+ this.osThemeInfo.isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ this.osThemeInfo.isLightMode = window.matchMedia('(prefers-color-scheme: light)').matches;
+ this.osThemeInfo.isNotSpecified = window.matchMedia('(prefers-color-scheme: no-preference)').matches;
+
+ this.store.select(selectDashboardState).pipe(
+ first()
+ ).subscribe(dashboardState => dashboardState.themeKey === osTheme.key && this.setTheme(osTheme.key));
+ }
+}
diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss
index 0884263a2b..8c10e82191 100644
--- a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss
+++ b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss
@@ -3,7 +3,6 @@
$app-sub-header-height: 48px;
.dashboard {
- background-color: transparent;
display: flex;
flex-direction: column;
height: 100%;
diff --git a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss
index a05435ff5a..d7c1ebb669 100644
--- a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss
+++ b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.theme.scss
@@ -23,7 +23,7 @@
}
}
&__item {
- color: rgba(0, 0, 0, .6);
+ color: map-get($app-theme, subdued-color);
&--active {
background-color: transparentize($primary-color, .9);
color: $primary-color;
diff --git a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html
index 93feabd5f5..7e807d5687 100644
--- a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html
+++ b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.html
@@ -92,6 +92,18 @@ User Profile
polling may result in some pages showing out-of-date information.
+
+
+
+
+ {{ theme.label }}
+
+
+
+
diff --git a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts
index fda5d8da05..b504c6832f 100644
--- a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts
+++ b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.ts
@@ -7,10 +7,11 @@ import { SetPollingEnabledAction, SetSessionTimeoutAction } from '../../../../..
import { DashboardOnlyAppState } from '../../../../../store/src/app-state';
import { selectDashboardState } from '../../../../../store/src/selectors/dashboard.selectors';
import { UserProfileInfo } from '../../../../../store/src/types/user-profile.types';
+import { ThemeService } from '../../../core/theme.service';
+import { UserService } from '../../../core/user.service';
import { ConfirmationDialogConfig } from '../../../shared/components/confirmation-dialog.config';
import { ConfirmationDialogService } from '../../../shared/components/confirmation-dialog.service';
import { UserProfileService } from '../user-profile.service';
-import { UserService } from '../../../core/user.service';
@Component({
selector: 'app-profile-info',
@@ -31,6 +32,7 @@ export class ProfileInfoComponent implements OnInit {
userProfile$: Observable;
primaryEmailAddress$: Observable;
+ hasMultipleThemes: boolean;
private sessionDialogConfig = new ConfirmationDialogConfig(
'Disable session timeout',
@@ -60,6 +62,7 @@ export class ProfileInfoComponent implements OnInit {
private store: Store,
private confirmDialog: ConfirmationDialogService,
public userService: UserService,
+ public themeService: ThemeService
) {
this.isError$ = userProfileService.isError$;
this.userProfile$ = userProfileService.userProfile$;
@@ -67,6 +70,8 @@ export class ProfileInfoComponent implements OnInit {
this.primaryEmailAddress$ = this.userProfile$.pipe(
map((profile: UserProfileInfo) => userProfileService.getPrimaryEmailAddress(profile))
);
+
+ this.hasMultipleThemes = themeService.getThemes().length > 1;
}
ngOnInit() {
diff --git a/src/frontend/packages/core/src/shared/components/code-block/code-block.component.theme.scss b/src/frontend/packages/core/src/shared/components/code-block/code-block.component.theme.scss
index 1010773073..bfac5a8b6e 100644
--- a/src/frontend/packages/core/src/shared/components/code-block/code-block.component.theme.scss
+++ b/src/frontend/packages/core/src/shared/components/code-block/code-block.component.theme.scss
@@ -1,16 +1,22 @@
@import '~@angular/material/theming';
@mixin display-value-theme($theme, $app-theme) {
+ $is-dark: map-get($theme, is-dark);
$primary: map-get($theme, primary);
$background-colors: map-get($theme, background);
$foreground-colors: map-get($theme, foreground);
.app-code-block {
- background-color: mat-color($foreground-colors, text);
- color: darken(mat-color($background-colors, background), 2%);
- &___copied-icon {
- color: mat-color($primary);
+ $background-color: mat-color($foreground-colors, text);
+ $color: darken(mat-color($background-colors, background), 2%);
+ @if $is-dark == true {
+ // See the app variable and cf cli pages
+ $background-color: lighten(mat-color($background-colors, background), 5%);
+ $color: mat-color($foreground-colors, text);
}
- &__copied-div {
- background-color: mat-color($foreground-colors, text);
+
+ background-color: $background-color;
+ color: $color;
+ &__copied-icon {
+ color: mat-color($primary);
}
}
}
diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.scss b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.scss
index ab1ec9abec..7ad3af708e 100644
--- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.scss
+++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.scss
@@ -1,6 +1,5 @@
.meta-card-item-row,
.meta-card-item-row-top {
- $text-color: rgba(0, 0, 0, .6);
align-items: center;
display: flex;
justify-content: space-between;
@@ -20,8 +19,6 @@
}
.meta-card-item__key {
- $text-color: rgba(0, 0, 0, .6);
- color: $text-color;
flex: none;
font-weight: 300;
padding-right: 10px;
@@ -51,7 +48,6 @@
line-height: 18px;
position: relative;
&::after {
- background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 75%);
bottom: 0;
content: '';
height: 1.2em;
diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.theme.scss b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.theme.scss
new file mode 100644
index 0000000000..890951c936
--- /dev/null
+++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.theme.scss
@@ -0,0 +1,12 @@
+@mixin app-meta-card-item-theme($theme, $app-theme) {
+ $backgrounds: map-get($theme, background);
+ $background: mat-color($backgrounds, card);
+
+ .meta-card-item-long-text-fixed {
+ .meta-card-item__value {
+ &::after {
+ background: linear-gradient(to right, rgba(255, 255, 255, 0), $background 50%);
+ }
+ }
+ }
+}
diff --git a/src/frontend/packages/core/src/shared/components/list/list.component.theme.scss b/src/frontend/packages/core/src/shared/components/list/list.component.theme.scss
index 6c51012b83..c7e86ff821 100644
--- a/src/frontend/packages/core/src/shared/components/list/list.component.theme.scss
+++ b/src/frontend/packages/core/src/shared/components/list/list.component.theme.scss
@@ -4,10 +4,19 @@
$primary: map-get($theme, primary);
$foreground-colors: map-get($theme, foreground);
$background-colors: map-get($theme, background);
+ $is-dark: map-get($theme, is-dark);
+
+ $header-selected-color: #fff;
+ @if $is-dark == true {
+ $header-selected-color: lighten(mat-color($background-colors, background), 20%);
+ } @else {
+ $header-selected-color: mat-color($primary, 50);
+ }
+
.list-component {
&__header {
&--selected {
- background-color: mat-color($primary, 50);
+ background-color: $header-selected-color;
}
&__right,
&__left--multi-filters {
diff --git a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss
index 736c50de6b..65ed55245f 100644
--- a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss
+++ b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.theme.scss
@@ -7,6 +7,7 @@
$error: map-get($status-colors, danger);
$warning: map-get($status-colors, warning);
$info: lighten($primmary-color, 20%);
+ $subdued: map-get($app-theme, subdued-color);
.page-header {
&__warning-count {
background-color: mat-color($primary);
@@ -18,6 +19,12 @@
&__menu-separator {
background-color: mat-color($foreground, divider);
}
+ &__menu-inner {
+ color: mat-contrast($primary, 500);
+ }
+ &__history {
+ color: $subdued;
+ }
&__underflow {
background-color: mat-color($primary);
}
diff --git a/src/frontend/packages/core/src/shared/shared.module.ts b/src/frontend/packages/core/src/shared/shared.module.ts
index 12aae65f83..4447caf735 100644
--- a/src/frontend/packages/core/src/shared/shared.module.ts
+++ b/src/frontend/packages/core/src/shared/shared.module.ts
@@ -58,8 +58,12 @@ import { TableCellStatusDirective } from './components/list/list-table/table-cel
import { TableComponent } from './components/list/list-table/table.component';
import { listTableComponents } from './components/list/list-table/table.types';
import { EndpointCardComponent } from './components/list/list-types/endpoint/endpoint-card/endpoint-card.component';
+import { EndpointListHelper } from './components/list/list-types/endpoint/endpoint-list.helpers';
+import { EndpointsListConfigService } from './components/list/list-types/endpoint/endpoints-list-config.service';
import { ListComponent } from './components/list/list.component';
import { ListConfig } from './components/list/list.component.types';
+import { ListHostDirective } from './components/list/simple-list/list-host.directive';
+import { SimpleListComponent } from './components/list/simple-list/simple-list.component';
import { LoadingPageComponent } from './components/loading-page/loading-page.component';
import { LogViewerComponent } from './components/log-viewer/log-viewer.component';
import { MarkdownContentObserverDirective } from './components/markdown-preview/markdown-content-observer.directive';
@@ -112,10 +116,6 @@ import { ValuesPipe } from './pipes/values.pipe';
import { CloudFoundryUserProvidedServicesService } from './services/cloud-foundry-user-provided-services.service';
import { MetricsRangeSelectorService } from './services/metrics-range-selector.service';
import { UserPermissionDirective } from './user-permission.directive';
-import { SimpleListComponent } from './components/list/simple-list/simple-list.component';
-import { ListHostDirective } from './components/list/simple-list/list-host.directive';
-import { EndpointListHelper } from './components/list/list-types/endpoint/endpoint-list.helpers';
-import { EndpointsListConfigService } from './components/list/list-types/endpoint/endpoints-list-config.service';
/* tslint:disable:max-line-length */
diff --git a/src/frontend/packages/core/src/styles.scss b/src/frontend/packages/core/src/styles.scss
index 1fe35bd298..f6e062111e 100644
--- a/src/frontend/packages/core/src/styles.scss
+++ b/src/frontend/packages/core/src/styles.scss
@@ -65,3 +65,11 @@ button.mat-simple-snackbar-action {
// flex: 1;
// flex-direction: column;
// }
+
+
+// Add selector so that the UI can detect if a dark theme is available
+@if $stratos-dark-theme-supported {
+ .dark-theme-supported {
+ margin: 0;
+ }
+}
diff --git a/src/frontend/packages/core/test-framework/store-test-helper.ts b/src/frontend/packages/core/test-framework/store-test-helper.ts
index c2383e2937..3a6277b913 100644
--- a/src/frontend/packages/core/test-framework/store-test-helper.ts
+++ b/src/frontend/packages/core/test-framework/store-test-helper.ts
@@ -164,7 +164,8 @@ function getDefaultInitialTestStratosStoreState() {
isMobile: false,
isMobileNavOpen: false,
sideNavPinned: false,
- pollingEnabled: true
+ pollingEnabled: true,
+ themeKey: null
},
actionHistory: [],
lists: {},
diff --git a/src/frontend/packages/store/src/actions/dashboard-actions.ts b/src/frontend/packages/store/src/actions/dashboard-actions.ts
index 88c8f8019c..3918585f85 100644
--- a/src/frontend/packages/store/src/actions/dashboard-actions.ts
+++ b/src/frontend/packages/store/src/actions/dashboard-actions.ts
@@ -1,5 +1,6 @@
import { Action } from '@ngrx/store';
+import { StratosTheme } from '../../../core/src/core/theme.service';
import { DashboardState } from '../reducers/dashboard-reducer';
export const OPEN_SIDE_NAV = '[Dashboard] Open side nav';
@@ -16,6 +17,7 @@ export const CLOSE_SIDE_HELP = '[Dashboard] Close side help';
export const TIMEOUT_SESSION = '[Dashboard] Timeout Session';
export const ENABLE_POLLING = '[Dashboard] Enable Polling';
+export const SET_STRATOS_THEME = '[Dashboard] Set Theme';
export const HYDRATE_DASHBOARD_STATE = '[Dashboard] Hydrate dashboard state';
@@ -73,3 +75,7 @@ export class HydrateDashboardStateAction implements Action {
type = HYDRATE_DASHBOARD_STATE;
}
+export class SetThemeAction implements Action {
+ constructor(public theme: StratosTheme) { }
+ type = SET_STRATOS_THEME;
+}
diff --git a/src/frontend/packages/store/src/effects/dashboard.effects.ts b/src/frontend/packages/store/src/effects/dashboard.effects.ts
new file mode 100644
index 0000000000..0e796db044
--- /dev/null
+++ b/src/frontend/packages/store/src/effects/dashboard.effects.ts
@@ -0,0 +1,24 @@
+import { Injectable } from '@angular/core';
+import { Actions, Effect, ofType } from '@ngrx/effects';
+import { map } from 'rxjs/operators';
+
+import { ThemeService } from '../../../core/src/core/theme.service';
+import { HYDRATE_DASHBOARD_STATE, HydrateDashboardStateAction } from '../actions/dashboard-actions';
+
+
+@Injectable()
+export class DashboardEffect {
+
+ constructor(
+ private actions$: Actions,
+ private themeService: ThemeService
+ ) { }
+
+ @Effect({ dispatch: false }) hydrate$ = this.actions$.pipe(
+ ofType(HYDRATE_DASHBOARD_STATE),
+ map(() => {
+ // Ensure the previous theme is applied
+ this.themeService.initialize();
+ })
+ );
+}
diff --git a/src/frontend/packages/store/src/reducers/dashboard-reducer.ts b/src/frontend/packages/store/src/reducers/dashboard-reducer.ts
index 5963b912b9..b5f606d521 100644
--- a/src/frontend/packages/store/src/reducers/dashboard-reducer.ts
+++ b/src/frontend/packages/store/src/reducers/dashboard-reducer.ts
@@ -7,8 +7,10 @@ import {
HYDRATE_DASHBOARD_STATE,
HydrateDashboardStateAction,
OPEN_SIDE_NAV,
+ SET_STRATOS_THEME,
SetPollingEnabledAction,
SetSessionTimeoutAction,
+ SetThemeAction,
SHOW_SIDE_HELP,
TIMEOUT_SESSION,
TOGGLE_SIDE_NAV,
@@ -23,6 +25,7 @@ export interface DashboardState {
sideNavPinned: boolean;
sideHelpOpen: boolean;
sideHelpDocument: string;
+ themeKey: string;
}
export const defaultDashboardState: DashboardState = {
@@ -34,6 +37,7 @@ export const defaultDashboardState: DashboardState = {
sideNavPinned: true,
sideHelpOpen: false,
sideHelpDocument: null,
+ themeKey: null
};
export function dashboardReducer(state: DashboardState = defaultDashboardState, action): DashboardState {
@@ -79,6 +83,12 @@ export function dashboardReducer(state: DashboardState = defaultDashboardState,
...state,
...hydrateDashboardStateAction.dashboardState
};
+ case SET_STRATOS_THEME:
+ const setThemeAction = action as SetThemeAction;
+ return {
+ ...state,
+ themeKey: setThemeAction.theme ? setThemeAction.theme.key : null
+ };
default:
return state;
}
diff --git a/src/frontend/packages/store/src/store.module.ts b/src/frontend/packages/store/src/store.module.ts
index 386db19890..14c8160af6 100644
--- a/src/frontend/packages/store/src/store.module.ts
+++ b/src/frontend/packages/store/src/store.module.ts
@@ -6,6 +6,7 @@ import { ActionHistoryEffect } from './effects/action-history.effects';
import { APIEffect } from './effects/api.effects';
import { AppEffects } from './effects/app.effects';
import { AuthEffect } from './effects/auth.effects';
+import { DashboardEffect } from './effects/dashboard.effects';
import { EndpointApiError } from './effects/endpoint-api-errors.effects';
import { EndpointsEffect } from './effects/endpoint.effects';
import { MetricsEffect } from './effects/metrics.effects';
@@ -21,8 +22,8 @@ import { UpdateAppEffects } from './effects/update-app-effects';
import { UserFavoritesEffect } from './effects/user-favorites-effect';
import { UserProfileEffect } from './effects/user-profile.effects';
import { UsersRolesEffects } from './effects/users-roles.effects';
-import { AppReducersModule } from './reducers.module';
import { PipelineHttpClient } from './entity-request-pipeline/pipline-http-client.service';
+import { AppReducersModule } from './reducers.module';
@NgModule({
@@ -33,6 +34,7 @@ import { PipelineHttpClient } from './entity-request-pipeline/pipline-http-clien
AppReducersModule,
HttpClientModule,
EffectsModule.forRoot([
+ DashboardEffect,
APIEffect,
EndpointApiError,
AuthEffect,