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
This commit is contained in:
ryan 2019-03-06 16:03:48 -08:00
commit 6a8a60bcdf
15 changed files with 301 additions and 169 deletions

View File

@ -54,34 +54,34 @@ $orange: ${theme.colors.orange};
$purple: ${theme.colors.purple};
$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
// -------------------------
$online: $green-base;
$warn: #f79520;
$critical: $red-base;
$online: ${theme.colors.online};
$warn: ${theme.colors.warn};
$critical: ${theme.colors.critical};
// Scaffolding
// -------------------------
$body-bg: ${theme.colors.bodyBg};
$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);
$textShadow: none;
@ -99,14 +99,14 @@ $edit-gradient: linear-gradient(180deg, $dark-2 50%, $input-black);
// 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
// -------------------------
$headings-color: darken($white, 11%);
$headings-color: ${theme.colors.headingColor};
$abbr-border-color: $gray-2 !default;
$text-muted: $text-color-weak;

View File

@ -46,34 +46,34 @@ $orange: ${theme.colors.orange};
$purple: ${theme.colors.purple};
$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
// -------------------------
$online: $green-shade;
$warn: #f79520;
$critical: $red-shade;
$online: ${theme.colors.online};
$warn: ${theme.colors.warn};
$critical: ${theme.colors.critical};
// Scaffolding
// -------------------------
$body-bg: ${theme.colors.bodyBg};
$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;
@ -85,14 +85,14 @@ $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
// 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
// -------------------------
$headings-color: $text-color;
$headings-color: ${theme.colors.headingColor};
$abbr-border-color: $gray-2 !default;
$text-muted: $text-color-weak;

View File

@ -46,6 +46,10 @@ const darkTheme: GrafanaTheme = {
colors: {
...basicColors,
inputBlack: '#09090b',
brandPrimary: basicColors.orange,
brandSuccess: basicColors.greenBase,
brandWarning: basicColors.orange,
brandDanger: basicColors.redBase,
queryRed: basicColors.redBase,
queryGreen: '#74e680',
queryPurple: '#fe85fc',
@ -56,16 +60,16 @@ const darkTheme: GrafanaTheme = {
critical: basicColors.redBase,
bodyBg: 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(),
},
background: {

View File

@ -47,26 +47,30 @@ const lightTheme: GrafanaTheme = {
...basicColors,
variable: basicColors.blue,
inputBlack: '#09090b',
queryRed: basicColors.red,
brandPrimary: basicColors.orange,
brandSuccess: basicColors.greenBase,
brandWarning: basicColors.orange,
brandDanger: basicColors.redBase,
queryRed: basicColors.redBase,
queryGreen: basicColors.greenBase,
queryPurple: basicColors.purple,
queryKeyword: basicColors.blue,
queryKeyword: basicColors.blueBase,
queryOrange: basicColors.orange,
online: basicColors.greenShade,
warn: '#f79520',
critical: basicColors.redShade,
bodyBg: 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,
},
background: {

View File

@ -113,25 +113,33 @@ export interface GrafanaTheme extends GrafanaThemeCommons {
queryPurple: string;
queryKeyword: string;
queryOrange: string;
brandPrimary: string;
brandSuccess: string;
brandWarning: string;
brandDanger: string;
// Status colors
online: string;
warn: 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
bodyBg: 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;
};
}

View File

@ -111,57 +111,20 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
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)
if err != nil {
this.log.Error("Can't open graph file", err)
return err
}
defer f.Close()
fw, err := w.CreateFormField("payload_json")
if err != nil {
return err
}
if _, err = fw.Write([]byte(string(json))); err != nil {
return err
}
fw, err = w.CreateFormFile("file", "graph.png")
if err != nil {
return err
}
if _, err = io.Copy(fw, f); err != nil {
return err
}
w.Close()
body = b.Bytes()
content_type = w.FormDataContentType()
} else {
body = json
}
cmd := &m.SendWebhookSync{
Url: this.WebhookURL,
Body: string(body),
HttpMethod: "POST",
ContentType: content_type,
ContentType: "application/json",
}
if !embeddedImage {
cmd.Body = string(json)
} else {
err := this.embedImage(cmd, evalContext.ImageOnDiskPath, json)
if err != nil {
this.log.Error("failed to embed image", "error", err)
return err
}
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
@ -171,3 +134,45 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
return nil
}
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 !os.IsNotExist(err) {
return err
}
}
var b bytes.Buffer
w := multipart.NewWriter(&b)
fw, err := w.CreateFormField("payload_json")
if err != nil {
return err
}
if _, err = fw.Write([]byte(string(existingJSONBody))); err != nil {
return err
}
fw, err = w.CreateFormFile("file", "graph.png")
if err != nil {
return err
}
if _, err = io.Copy(fw, f); err != nil {
return err
}
w.Close()
cmd.Body = string(b.Bytes())
cmd.ContentType = w.FormDataContentType()
return nil
}

View File

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

View File

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

View File

@ -635,4 +635,32 @@ describe('DashboardModel', () => {
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);
});
});
});

View File

@ -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();
}
}
}

View File

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

View File

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

View File

@ -1,4 +1,14 @@
import configureMockStore from 'redux-mock-store';
import { PlaylistSrv } from '../playlist_srv';
import { setStore } from 'app/store/store';
const mockStore = configureMockStore();
setStore(
mockStore({
location: {},
})
);
const dashboards = [{ url: 'dash1' }, { url: 'dash2' }];
@ -19,6 +29,7 @@ const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance<any, any> }
const mockLocation = {
url: jest.fn(),
search: () => ({}),
path: () => '/playlists/1',
};
const mockTimeout = jest.fn();
@ -96,4 +107,32 @@ describe('PlaylistSrv', () => {
expect(hrefMock).toHaveBeenCalledTimes(3);
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);
});
});

View File

@ -57,34 +57,34 @@ $orange: #eb7b18;
$purple: #9933cc;
$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-purple: #fe85fc;
$query-orange: #eb7b18;
$query-keyword: #66d9ef;
$query-orange: $orange;
// Status colors
// -------------------------
$online: $green-base;
$online: #299c46;
$warn: #f79520;
$critical: $red-base;
$critical: #e02f44;
// Scaffolding
// -------------------------
$body-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);
$textShadow: none;
@ -102,14 +102,14 @@ $edit-gradient: linear-gradient(180deg, $dark-2 50%, $input-black);
// 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
// -------------------------
$headings-color: darken($white, 11%);
$headings-color: #e3e3e3;
$abbr-border-color: $gray-2 !default;
$text-muted: $text-color-weak;

View File

@ -49,34 +49,34 @@ $orange: #ff7941;
$purple: #9954bb;
$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
// -------------------------
$online: $green-shade;
$online: #369b4f;
$warn: #f79520;
$critical: $red-shade;
$critical: #c4162a;
// Scaffolding
// -------------------------
$body-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;
@ -88,14 +88,14 @@ $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
// 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
// -------------------------
$headings-color: $text-color;
$headings-color: #52545c;
$abbr-border-color: $gray-2 !default;
$text-muted: $text-color-weak;