Просмотр исходного кода

Merge remote-tracking branch 'grafana/master' into alpha-react-virtualized-table

* grafana/master:
  Minor refactoring of PR #15770
  Alternative fix to detecting when to stop a playlist, fixes #15701 and #15702
  fix discord notifier so it doesn't crash when there are no image generated
  Add a keybinding that toggles all legends in a dashboard
  removed color in color variables names
  changed some more color variables to use variables
ryan 6 лет назад
Родитель
Сommit
6a8a60bcdf

+ 23 - 23
packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts

@@ -54,34 +54,34 @@ $orange: ${theme.colors.orange};
 $purple: ${theme.colors.purple};
 $purple: ${theme.colors.purple};
 $variable: ${theme.colors.variable};
 $variable: ${theme.colors.variable};
 
 
-$brand-primary: $orange;
-$brand-success: $green-base;
-$brand-warning: $brand-primary;
-$brand-danger: $red-base;
+$brand-primary: ${theme.colors.brandPrimary};
+$brand-success: ${theme.colors.brandSuccess};
+$brand-warning: ${theme.colors.brandWarning};
+$brand-danger: ${theme.colors.brandDanger};
 
 
-$query-red: $red-base;
-$query-green: #74e680;
-$query-purple: #fe85fc;
-$query-keyword: #66d9ef;
-$query-orange: $orange;
+$query-red: ${theme.colors.queryRed};
+$query-green: ${theme.colors.queryGreen};
+$query-purple: ${theme.colors.queryPurple};
+$query-orange: ${theme.colors.orange};
+$query-keyword: ${theme.colors.queryKeyword};
 
 
 // Status colors
 // Status colors
 // -------------------------
 // -------------------------
-$online: $green-base;
-$warn: #f79520;
-$critical: $red-base;
+$online: ${theme.colors.online};
+$warn: ${theme.colors.warn};
+$critical: ${theme.colors.critical};
 
 
 // Scaffolding
 // Scaffolding
 // -------------------------
 // -------------------------
 $body-bg: ${theme.colors.bodyBg};
 $body-bg: ${theme.colors.bodyBg};
 $page-bg: ${theme.colors.pageBg};
 $page-bg: ${theme.colors.pageBg};
 
 
-$body-color: $gray-4;
-$text-color: $gray-4;
-$text-color-strong: $white;
-$text-color-weak: $gray-2;
-$text-color-faint: $dark-10;
-$text-color-emphasis: $gray-5;
+$body-color: ${theme.colors.body};
+$text-color: ${theme.colors.text};
+$text-color-strong: ${theme.colors.textStrong};
+$text-color-weak: ${theme.colors.textWeak};
+$text-color-faint: ${theme.colors.textFaint};
+$text-color-emphasis: ${theme.colors.textEmphasis};
 
 
 $text-shadow-faint: 1px 1px 4px rgb(45, 45, 45);
 $text-shadow-faint: 1px 1px 4px rgb(45, 45, 45);
 $textShadow: none;
 $textShadow: none;
@@ -99,14 +99,14 @@ $edit-gradient: linear-gradient(180deg, $dark-2 50%, $input-black);
 
 
 // Links
 // Links
 // -------------------------
 // -------------------------
-$link-color: darken($white, 11%);
-$link-color-disabled: darken($link-color, 30%);
-$link-hover-color: $white;
-$external-link-color: $blue-light;
+$link-color: ${theme.colors.link};
+$link-color-disabled: ${theme.colors.linkDisabled};
+$link-hover-color: ${theme.colors.linkHover};
+$external-link-color: ${theme.colors.linkExternal};
 
 
 // Typography
 // Typography
 // -------------------------
 // -------------------------
-$headings-color: darken($white, 11%);
+$headings-color: ${theme.colors.headingColor};
 $abbr-border-color: $gray-2 !default;
 $abbr-border-color: $gray-2 !default;
 $text-muted: $text-color-weak;
 $text-muted: $text-color-weak;
 
 

+ 23 - 23
packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts

@@ -46,34 +46,34 @@ $orange: ${theme.colors.orange};
 $purple: ${theme.colors.purple};
 $purple: ${theme.colors.purple};
 $variable: ${theme.colors.variable};
 $variable: ${theme.colors.variable};
 
 
-$brand-primary: $orange;
-$brand-success: $green-base;
-$brand-warning: $orange;
-$brand-danger: $red-base;
+$brand-primary: ${theme.colors.brandPrimary};
+$brand-success: ${theme.colors.brandSuccess};
+$brand-warning: ${theme.colors.brandWarning};
+$brand-danger: ${theme.colors.brandDanger};
 
 
-$query-red: $red-base;
-$query-green: $green-base;
-$query-purple: $purple;
-$query-orange: $orange;
-$query-keyword: $blue-base;
+$query-red: ${theme.colors.queryRed};
+$query-green: ${theme.colors.queryGreen};
+$query-purple: ${theme.colors.queryPurple};
+$query-orange: ${theme.colors.orange};
+$query-keyword: ${theme.colors.queryKeyword};
 
 
 // Status colors
 // Status colors
 // -------------------------
 // -------------------------
-$online: $green-shade;
-$warn: #f79520;
-$critical: $red-shade;
+$online: ${theme.colors.online};
+$warn: ${theme.colors.warn};
+$critical: ${theme.colors.critical};
 
 
 // Scaffolding
 // Scaffolding
 // -------------------------
 // -------------------------
 $body-bg: ${theme.colors.bodyBg};
 $body-bg: ${theme.colors.bodyBg};
 $page-bg: ${theme.colors.pageBg};
 $page-bg: ${theme.colors.pageBg};
 
 
-$body-color: $gray-1;
-$text-color: $gray-1;
-$text-color-strong: $dark-1;
-$text-color-weak: $gray-2;
-$text-color-faint: $gray-4;
-$text-color-emphasis: $dark-2;
+$body-color: ${theme.colors.body};
+$text-color: ${theme.colors.text};
+$text-color-strong: ${theme.colors.textStrong};
+$text-color-weak: ${theme.colors.textWeak};
+$text-color-faint: ${theme.colors.textFaint};
+$text-color-emphasis: ${theme.colors.textEmphasis};
 
 
 $text-shadow-faint: none;
 $text-shadow-faint: none;
 
 
@@ -85,14 +85,14 @@ $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
 
 
 // Links
 // Links
 // -------------------------
 // -------------------------
-$link-color: $gray-1;
-$link-color-disabled: lighten($link-color, 30%);
-$link-hover-color: darken($link-color, 20%);
-$external-link-color: $blue-shade;
+$link-color: ${theme.colors.link};
+$link-color-disabled: ${theme.colors.linkDisabled};
+$link-hover-color: ${theme.colors.linkHover};
+$external-link-color: ${theme.colors.linkExternal};
 
 
 // Typography
 // Typography
 // -------------------------
 // -------------------------
-$headings-color: $text-color;
+$headings-color: ${theme.colors.headingColor};
 $abbr-border-color: $gray-2 !default;
 $abbr-border-color: $gray-2 !default;
 $text-muted: $text-color-weak;
 $text-muted: $text-color-weak;
 
 

+ 14 - 10
packages/grafana-ui/src/themes/dark.ts

@@ -46,6 +46,10 @@ const darkTheme: GrafanaTheme = {
   colors: {
   colors: {
     ...basicColors,
     ...basicColors,
     inputBlack: '#09090b',
     inputBlack: '#09090b',
+    brandPrimary: basicColors.orange,
+    brandSuccess: basicColors.greenBase,
+    brandWarning: basicColors.orange,
+    brandDanger: basicColors.redBase,
     queryRed: basicColors.redBase,
     queryRed: basicColors.redBase,
     queryGreen: '#74e680',
     queryGreen: '#74e680',
     queryPurple: '#fe85fc',
     queryPurple: '#fe85fc',
@@ -56,16 +60,16 @@ const darkTheme: GrafanaTheme = {
     critical: basicColors.redBase,
     critical: basicColors.redBase,
     bodyBg: basicColors.dark2,
     bodyBg: basicColors.dark2,
     pageBg: basicColors.dark2,
     pageBg: basicColors.dark2,
-    bodyColor: basicColors.gray4,
-    textColor: basicColors.gray4,
-    textColorStrong: basicColors.white,
-    textColorWeak: basicColors.gray2,
-    textColorEmphasis: basicColors.gray5,
-    textColorFaint: basicColors.dark5,
-    linkColor: new tinycolor(basicColors.white).darken(11).toString(),
-    linkColorDisabled: new tinycolor(basicColors.white).darken(11).toString(),
-    linkColorHover: basicColors.white,
-    linkColorExternal: basicColors.blue,
+    body: basicColors.gray4,
+    text: basicColors.gray4,
+    textStrong: basicColors.white,
+    textWeak: basicColors.gray2,
+    textEmphasis: basicColors.gray5,
+    textFaint: basicColors.dark5,
+    link: new tinycolor(basicColors.white).darken(11).toString(),
+    linkDisabled: new tinycolor(basicColors.white).darken(11).toString(),
+    linkHover: basicColors.white,
+    linkExternal: basicColors.blue,
     headingColor: new tinycolor(basicColors.white).darken(11).toString(),
     headingColor: new tinycolor(basicColors.white).darken(11).toString(),
   },
   },
   background: {
   background: {

+ 16 - 12
packages/grafana-ui/src/themes/light.ts

@@ -47,26 +47,30 @@ const lightTheme: GrafanaTheme = {
     ...basicColors,
     ...basicColors,
     variable: basicColors.blue,
     variable: basicColors.blue,
     inputBlack: '#09090b',
     inputBlack: '#09090b',
-    queryRed: basicColors.red,
+    brandPrimary: basicColors.orange,
+    brandSuccess: basicColors.greenBase,
+    brandWarning: basicColors.orange,
+    brandDanger: basicColors.redBase,
+    queryRed: basicColors.redBase,
     queryGreen: basicColors.greenBase,
     queryGreen: basicColors.greenBase,
     queryPurple: basicColors.purple,
     queryPurple: basicColors.purple,
-    queryKeyword: basicColors.blue,
+    queryKeyword: basicColors.blueBase,
     queryOrange: basicColors.orange,
     queryOrange: basicColors.orange,
     online: basicColors.greenShade,
     online: basicColors.greenShade,
     warn: '#f79520',
     warn: '#f79520',
     critical: basicColors.redShade,
     critical: basicColors.redShade,
     bodyBg: basicColors.gray7,
     bodyBg: basicColors.gray7,
     pageBg: basicColors.gray7,
     pageBg: basicColors.gray7,
-    bodyColor: basicColors.gray1,
-    textColor: basicColors.gray1,
-    textColorStrong: basicColors.dark2,
-    textColorWeak: basicColors.gray2,
-    textColorEmphasis: basicColors.gray5,
-    textColorFaint: basicColors.dark4,
-    linkColor: basicColors.gray1,
-    linkColorDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(),
-    linkColorHover: new tinycolor(basicColors.gray1).darken(20).toString(),
-    linkColorExternal: basicColors.blueLight,
+    body: basicColors.gray1,
+    text: basicColors.gray1,
+    textStrong: basicColors.dark2,
+    textWeak: basicColors.gray2,
+    textEmphasis: basicColors.gray5,
+    textFaint: basicColors.dark4,
+    link: basicColors.gray1,
+    linkDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(),
+    linkHover: new tinycolor(basicColors.gray1).darken(20).toString(),
+    linkExternal: basicColors.blueLight,
     headingColor: basicColors.gray1,
     headingColor: basicColors.gray1,
   },
   },
   background: {
   background: {

+ 18 - 10
packages/grafana-ui/src/types/theme.ts

@@ -113,25 +113,33 @@ export interface GrafanaTheme extends GrafanaThemeCommons {
     queryPurple: string;
     queryPurple: string;
     queryKeyword: string;
     queryKeyword: string;
     queryOrange: string;
     queryOrange: string;
+    brandPrimary: string;
+    brandSuccess: string;
+    brandWarning: string;
+    brandDanger: string;
 
 
     // Status colors
     // Status colors
     online: string;
     online: string;
     warn: string;
     warn: string;
     critical: string;
     critical: string;
 
 
+    // Link colors
+    link: string;
+    linkDisabled: string;
+    linkHover: string;
+    linkExternal: string;
+
+    // Text colors
+    body: string;
+    text: string;
+    textStrong: string;
+    textWeak: string;
+    textFaint: string;
+    textEmphasis: string;
+
     // TODO: move to background section
     // TODO: move to background section
     bodyBg: string;
     bodyBg: string;
     pageBg: string;
     pageBg: string;
-    bodyColor: string;
-    textColor: string;
-    textColorStrong: string;
-    textColorWeak: string;
-    textColorFaint: string;
-    textColorEmphasis: string;
-    linkColor: string;
-    linkColorDisabled: string;
-    linkColorHover: string;
-    linkColorExternal: string;
     headingColor: string;
     headingColor: string;
   };
   };
 }
 }

+ 43 - 38
pkg/services/alerting/notifiers/discord.go

@@ -111,63 +111,68 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
 
 
 	json, _ := bodyJSON.MarshalJSON()
 	json, _ := bodyJSON.MarshalJSON()
 
 
-	content_type := "application/json"
-
-	var body []byte
-
-	if embeddedImage {
-
-		var b bytes.Buffer
-
-		w := multipart.NewWriter(&b)
-
-		f, err := os.Open(evalContext.ImageOnDiskPath)
+	cmd := &m.SendWebhookSync{
+		Url:         this.WebhookURL,
+		HttpMethod:  "POST",
+		ContentType: "application/json",
+	}
 
 
+	if !embeddedImage {
+		cmd.Body = string(json)
+	} else {
+		err := this.embedImage(cmd, evalContext.ImageOnDiskPath, json)
 		if err != nil {
 		if err != nil {
-			this.log.Error("Can't open graph file", err)
+			this.log.Error("failed to embed image", "error", err)
 			return err
 			return err
 		}
 		}
+	}
 
 
-		defer f.Close()
-
-		fw, err := w.CreateFormField("payload_json")
-		if err != nil {
-			return err
-		}
+	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+		this.log.Error("Failed to send notification to Discord", "error", err)
+		return err
+	}
 
 
-		if _, err = fw.Write([]byte(string(json))); err != nil {
-			return err
-		}
+	return nil
+}
 
 
-		fw, err = w.CreateFormFile("file", "graph.png")
-		if err != nil {
-			return err
+func (this *DiscordNotifier) embedImage(cmd *m.SendWebhookSync, imagePath string, existingJSONBody []byte) error {
+	f, err := os.Open(imagePath)
+	defer f.Close()
+	if err != nil {
+		if os.IsNotExist(err) {
+			cmd.Body = string(existingJSONBody)
+			return nil
 		}
 		}
-
-		if _, err = io.Copy(fw, f); err != nil {
+		if !os.IsNotExist(err) {
 			return err
 			return err
 		}
 		}
+	}
 
 
-		w.Close()
+	var b bytes.Buffer
+	w := multipart.NewWriter(&b)
 
 
-		body = b.Bytes()
-		content_type = w.FormDataContentType()
+	fw, err := w.CreateFormField("payload_json")
+	if err != nil {
+		return err
+	}
 
 
-	} else {
-		body = json
+	if _, err = fw.Write([]byte(string(existingJSONBody))); err != nil {
+		return err
 	}
 	}
 
 
-	cmd := &m.SendWebhookSync{
-		Url:         this.WebhookURL,
-		Body:        string(body),
-		HttpMethod:  "POST",
-		ContentType: content_type,
+	fw, err = w.CreateFormFile("file", "graph.png")
+	if err != nil {
+		return err
 	}
 	}
 
 
-	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
-		this.log.Error("Failed to send notification to Discord", "error", err)
+	if _, err = io.Copy(fw, f); err != nil {
 		return err
 		return err
 	}
 	}
 
 
+	w.Close()
+
+	cmd.Body = string(b.Bytes())
+	cmd.ContentType = w.FormDataContentType()
+
 	return nil
 	return nil
 }
 }

+ 1 - 0
public/app/core/components/help/help.ts

@@ -27,6 +27,7 @@ export class HelpCtrl {
         { keys: ['d', 'C'], description: 'Collapse all rows' },
         { keys: ['d', 'C'], description: 'Collapse all rows' },
         { keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' },
         { keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' },
         { keys: ['mod+o'], description: 'Toggle shared graph crosshair' },
         { keys: ['mod+o'], description: 'Toggle shared graph crosshair' },
+        { keys: ['d', 'l'], description: 'Toggle all panel legends' },
       ],
       ],
       'Focused Panel': [
       'Focused Panel': [
         { keys: ['e'], description: 'Toggle panel edit view' },
         { keys: ['e'], description: 'Toggle panel edit view' },

+ 5 - 0
public/app/core/services/keybindingSrv.ts

@@ -256,6 +256,11 @@ export class KeybindingSrv {
       }
       }
     });
     });
 
 
+    // toggle all panel legends
+    this.bind('d l', () => {
+      dashboard.toggleLegendsForAll();
+    });
+
     // collapse all rows
     // collapse all rows
     this.bind('d shift+c', () => {
     this.bind('d shift+c', () => {
       dashboard.collapseRows();
       dashboard.collapseRows();

+ 28 - 0
public/app/features/dashboard/state/DashboardModel.test.ts

@@ -635,4 +635,32 @@ describe('DashboardModel', () => {
       expect(saveModel.templating.list[0].filters[0].value).toBe('server 1');
       expect(saveModel.templating.list[0].filters[0].value).toBe('server 1');
     });
     });
   });
   });
+
+  describe('Given a dashboard with one panel legend on and two off', () => {
+    let model;
+
+    beforeEach(() => {
+      const data = {
+        panels: [
+          { id: 1, type: 'graph', gridPos: { x: 0, y: 0, w: 24, h: 2 }, legend: { show: true } },
+          { id: 3, type: 'graph', gridPos: { x: 0, y: 4, w: 12, h: 2 }, legend: { show: false } },
+          { id: 4, type: 'graph', gridPos: { x: 12, y: 4, w: 12, h: 2 }, legend: { show: false } },
+        ],
+      };
+      model = new DashboardModel(data);
+    });
+
+    it('toggleLegendsForAll should toggle all legends on on first execution', () => {
+      model.toggleLegendsForAll();
+      const legendsOn = model.panels.filter(panel => panel.legend.show === true);
+      expect(legendsOn.length).toBe(3);
+    });
+
+    it('toggleLegendsForAll should toggle all legends off on second execution', () => {
+      model.toggleLegendsForAll();
+      model.toggleLegendsForAll();
+      const legendsOn = model.panels.filter(panel => panel.legend.show === true);
+      expect(legendsOn.length).toBe(0);
+    });
+  });
 });
 });

+ 16 - 0
public/app/features/dashboard/state/DashboardModel.ts

@@ -917,4 +917,20 @@ export class DashboardModel {
       }
       }
     }
     }
   }
   }
+
+  toggleLegendsForAll() {
+    const panelsWithLegends = this.panels.filter(panel => {
+      return panel.legend !== undefined && panel.legend !== null;
+    });
+
+    // determine if more panels are displaying legends or not
+    const onCount = panelsWithLegends.filter(panel => panel.legend.show).length;
+    const offCount = panelsWithLegends.length - onCount;
+    const panelLegendsOn = onCount >= offCount;
+
+    for (const panel of panelsWithLegends) {
+      panel.legend.show = !panelLegendsOn;
+      panel.render();
+    }
+  }
 }
 }

+ 1 - 0
public/app/features/dashboard/state/PanelModel.ts

@@ -106,6 +106,7 @@ export class PanelModel {
   events: Emitter;
   events: Emitter;
   cacheTimeout?: any;
   cacheTimeout?: any;
   cachedPluginOptions?: any;
   cachedPluginOptions?: any;
+  legend?: { show: boolean };
 
 
   constructor(model) {
   constructor(model) {
     this.events = new Emitter();
     this.events = new Emitter();

+ 23 - 2
public/app/features/playlist/playlist_srv.ts

@@ -7,6 +7,7 @@ import coreModule from '../../core/core_module';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
 import locationUtil from 'app/core/utils/location_util';
 import locationUtil from 'app/core/utils/location_util';
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
+import { store } from 'app/store/store';
 
 
 export class PlaylistSrv {
 export class PlaylistSrv {
   private cancelPromise: any;
   private cancelPromise: any;
@@ -15,6 +16,8 @@ export class PlaylistSrv {
   private interval: number;
   private interval: number;
   private startUrl: string;
   private startUrl: string;
   private numberOfLoops = 0;
   private numberOfLoops = 0;
+  private storeUnsub: () => void;
+  private validPlaylistUrl: string;
   isPlaying: boolean;
   isPlaying: boolean;
 
 
   /** @ngInject */
   /** @ngInject */
@@ -39,15 +42,16 @@ export class PlaylistSrv {
     const dash = this.dashboards[this.index];
     const dash = this.dashboards[this.index];
     const queryParams = this.$location.search();
     const queryParams = this.$location.search();
     const filteredParams = _.pickBy(queryParams, value => value !== null);
     const filteredParams = _.pickBy(queryParams, value => value !== null);
+    const nextDashboardUrl = locationUtil.stripBaseFromUrl(dash.url);
 
 
     // this is done inside timeout to make sure digest happens after
     // this is done inside timeout to make sure digest happens after
     // as this can be called from react
     // as this can be called from react
     this.$timeout(() => {
     this.$timeout(() => {
-      const stripedUrl = locationUtil.stripBaseFromUrl(dash.url);
-      this.$location.url(stripedUrl + '?' + toUrlParams(filteredParams));
+      this.$location.url(nextDashboardUrl + '?' + toUrlParams(filteredParams));
     });
     });
 
 
     this.index++;
     this.index++;
+    this.validPlaylistUrl = nextDashboardUrl;
     this.cancelPromise = this.$timeout(() => this.next(), this.interval);
     this.cancelPromise = this.$timeout(() => this.next(), this.interval);
   }
   }
 
 
@@ -56,6 +60,15 @@ export class PlaylistSrv {
     this.next();
     this.next();
   }
   }
 
 
+  // Detect url changes not caused by playlist srv and stop playlist
+  storeUpdated() {
+    const state = store.getState();
+
+    if (state.location.path !== this.validPlaylistUrl) {
+      this.stop();
+    }
+  }
+
   start(playlistId) {
   start(playlistId) {
     this.stop();
     this.stop();
 
 
@@ -63,6 +76,10 @@ export class PlaylistSrv {
     this.index = 0;
     this.index = 0;
     this.isPlaying = true;
     this.isPlaying = true;
 
 
+    // setup location tracking
+    this.storeUnsub = store.subscribe(() => this.storeUpdated());
+    this.validPlaylistUrl = this.$location.path();
+
     appEvents.emit('playlist-started');
     appEvents.emit('playlist-started');
 
 
     return this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
     return this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
@@ -85,6 +102,10 @@ export class PlaylistSrv {
     this.index = 0;
     this.index = 0;
     this.isPlaying = false;
     this.isPlaying = false;
 
 
+    if (this.storeUnsub) {
+      this.storeUnsub();
+    }
+
     if (this.cancelPromise) {
     if (this.cancelPromise) {
       this.$timeout.cancel(this.cancelPromise);
       this.$timeout.cancel(this.cancelPromise);
     }
     }

+ 39 - 0
public/app/features/playlist/specs/playlist_srv.test.ts

@@ -1,4 +1,14 @@
+import configureMockStore from 'redux-mock-store';
 import { PlaylistSrv } from '../playlist_srv';
 import { PlaylistSrv } from '../playlist_srv';
+import { setStore } from 'app/store/store';
+
+const mockStore = configureMockStore();
+
+setStore(
+  mockStore({
+    location: {},
+  })
+);
 
 
 const dashboards = [{ url: 'dash1' }, { url: 'dash2' }];
 const dashboards = [{ url: 'dash1' }, { url: 'dash2' }];
 
 
@@ -19,6 +29,7 @@ const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance<any, any> }
   const mockLocation = {
   const mockLocation = {
     url: jest.fn(),
     url: jest.fn(),
     search: () => ({}),
     search: () => ({}),
+    path: () => '/playlists/1',
   };
   };
 
 
   const mockTimeout = jest.fn();
   const mockTimeout = jest.fn();
@@ -96,4 +107,32 @@ describe('PlaylistSrv', () => {
     expect(hrefMock).toHaveBeenCalledTimes(3);
     expect(hrefMock).toHaveBeenCalledTimes(3);
     expect(hrefMock).toHaveBeenLastCalledWith(initialUrl);
     expect(hrefMock).toHaveBeenLastCalledWith(initialUrl);
   });
   });
+
+  it('storeUpdated should stop playlist when navigating away', async () => {
+    await srv.start(1);
+
+    srv.storeUpdated();
+
+    expect(srv.isPlaying).toBe(false);
+  });
+
+  it('storeUpdated should not stop playlist when navigating to next dashboard', async () => {
+    await srv.start(1);
+
+    srv.next();
+
+    setStore(
+      mockStore({
+        location: {
+          path: 'dash2',
+        },
+      })
+    );
+
+    expect((srv as any).validPlaylistUrl).toBe('dash2');
+
+    srv.storeUpdated();
+
+    expect(srv.isPlaying).toBe(true);
+  });
 });
 });

+ 19 - 19
public/sass/_variables.dark.generated.scss

@@ -57,34 +57,34 @@ $orange: #eb7b18;
 $purple: #9933cc;
 $purple: #9933cc;
 $variable: #32d1df;
 $variable: #32d1df;
 
 
-$brand-primary: $orange;
-$brand-success: $green-base;
-$brand-warning: $brand-primary;
-$brand-danger: $red-base;
+$brand-primary: #eb7b18;
+$brand-success: #299c46;
+$brand-warning: #eb7b18;
+$brand-danger: #e02f44;
 
 
-$query-red: $red-base;
+$query-red: #e02f44;
 $query-green: #74e680;
 $query-green: #74e680;
 $query-purple: #fe85fc;
 $query-purple: #fe85fc;
+$query-orange: #eb7b18;
 $query-keyword: #66d9ef;
 $query-keyword: #66d9ef;
-$query-orange: $orange;
 
 
 // Status colors
 // Status colors
 // -------------------------
 // -------------------------
-$online: $green-base;
+$online: #299c46;
 $warn: #f79520;
 $warn: #f79520;
-$critical: $red-base;
+$critical: #e02f44;
 
 
 // Scaffolding
 // Scaffolding
 // -------------------------
 // -------------------------
 $body-bg: #161719;
 $body-bg: #161719;
 $page-bg: #161719;
 $page-bg: #161719;
 
 
-$body-color: $gray-4;
-$text-color: $gray-4;
-$text-color-strong: $white;
-$text-color-weak: $gray-2;
-$text-color-faint: $dark-10;
-$text-color-emphasis: $gray-5;
+$body-color: #d8d9da;
+$text-color: #d8d9da;
+$text-color-strong: #ffffff;
+$text-color-weak: #8e8e8e;
+$text-color-faint: #222426;
+$text-color-emphasis: #ececec;
 
 
 $text-shadow-faint: 1px 1px 4px rgb(45, 45, 45);
 $text-shadow-faint: 1px 1px 4px rgb(45, 45, 45);
 $textShadow: none;
 $textShadow: none;
@@ -102,14 +102,14 @@ $edit-gradient: linear-gradient(180deg, $dark-2 50%, $input-black);
 
 
 // Links
 // Links
 // -------------------------
 // -------------------------
-$link-color: darken($white, 11%);
-$link-color-disabled: darken($link-color, 30%);
-$link-hover-color: $white;
-$external-link-color: $blue-light;
+$link-color: #e3e3e3;
+$link-color-disabled: #e3e3e3;
+$link-hover-color: #ffffff;
+$external-link-color: #33b5e5;
 
 
 // Typography
 // Typography
 // -------------------------
 // -------------------------
-$headings-color: darken($white, 11%);
+$headings-color: #e3e3e3;
 $abbr-border-color: $gray-2 !default;
 $abbr-border-color: $gray-2 !default;
 $text-muted: $text-color-weak;
 $text-muted: $text-color-weak;
 
 

+ 22 - 22
public/sass/_variables.light.generated.scss

@@ -49,34 +49,34 @@ $orange: #ff7941;
 $purple: #9954bb;
 $purple: #9954bb;
 $variable: #0083b3;
 $variable: #0083b3;
 
 
-$brand-primary: $orange;
-$brand-success: $green-base;
-$brand-warning: $orange;
-$brand-danger: $red-base;
+$brand-primary: #ff7941;
+$brand-success: #3eb15b;
+$brand-warning: #ff7941;
+$brand-danger: #e02f44;
 
 
-$query-red: $red-base;
-$query-green: $green-base;
-$query-purple: $purple;
-$query-orange: $orange;
-$query-keyword: $blue-base;
+$query-red: #e02f44;
+$query-green: #3eb15b;
+$query-purple: #9954bb;
+$query-orange: #ff7941;
+$query-keyword: #3274d9;
 
 
 // Status colors
 // Status colors
 // -------------------------
 // -------------------------
-$online: $green-shade;
+$online: #369b4f;
 $warn: #f79520;
 $warn: #f79520;
-$critical: $red-shade;
+$critical: #c4162a;
 
 
 // Scaffolding
 // Scaffolding
 // -------------------------
 // -------------------------
 $body-bg: #f7f8fa;
 $body-bg: #f7f8fa;
 $page-bg: #f7f8fa;
 $page-bg: #f7f8fa;
 
 
-$body-color: $gray-1;
-$text-color: $gray-1;
-$text-color-strong: $dark-1;
-$text-color-weak: $gray-2;
-$text-color-faint: $gray-4;
-$text-color-emphasis: $dark-2;
+$body-color: #52545c;
+$text-color: #52545c;
+$text-color-strong: #41444b;
+$text-color-weak: #767980;
+$text-color-faint: #35373f;
+$text-color-emphasis: #dde4ed;
 
 
 $text-shadow-faint: none;
 $text-shadow-faint: none;
 
 
@@ -88,14 +88,14 @@ $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
 
 
 // Links
 // Links
 // -------------------------
 // -------------------------
-$link-color: $gray-1;
-$link-color-disabled: lighten($link-color, 30%);
-$link-hover-color: darken($link-color, 20%);
-$external-link-color: $blue-shade;
+$link-color: #52545c;
+$link-color-disabled: #9ea0a9;
+$link-hover-color: #222326;
+$external-link-color: #5794f2;
 
 
 // Typography
 // Typography
 // -------------------------
 // -------------------------
-$headings-color: $text-color;
+$headings-color: #52545c;
 $abbr-border-color: $gray-2 !default;
 $abbr-border-color: $gray-2 !default;
 $text-muted: $text-color-weak;
 $text-muted: $text-color-weak;