Commit 178bb1d3 by Dominik Prokop Committed by Torkel Ödegaard

Echo: mechanism for collecting custom events lazily (#20365)

* Introduce Echo for collecting frontend metrics

* Update public/app/core/services/echo/Echo.ts

Co-Authored-By: Peter Holmberg <peterholmberg@users.noreply.github.com>

* Custom meta when adding event

* Rename consumer to backend

* Remove buffer from Echo

* Minor tweaks

* Update package.json

* Update public/app/app.ts

* Update public/app/app.ts

* Collect paint metrics when collecting tti. Remove echoBackendFactory

* Update yarn.lock

* Move Echo interfaces to runtime

* progress on meta and echo

* Collect meta analytics events

* Move MetaanalyticsBackend to enterprise repo

* Fixed unit tests

* Removed unused type from test

* Fixed issues with chunk loading (reverted index-template changes)

* Restored changes

* Fixed webpack prod
parent 4b8a50e7
...@@ -256,6 +256,7 @@ ...@@ -256,6 +256,7 @@
"tether": "1.4.5", "tether": "1.4.5",
"tether-drop": "https://github.com/torkelo/drop/tarball/master", "tether-drop": "https://github.com/torkelo/drop/tarball/master",
"tinycolor2": "1.4.1", "tinycolor2": "1.4.1",
"tti-polyfill": "0.2.2",
"xss": "1.0.3" "xss": "1.0.3"
}, },
"resolutions": { "resolutions": {
......
export * from './services'; export * from './services';
export * from './config'; export * from './config';
export * from './types';
export { loadPluginCss, SystemJS } from './utils/plugin'; export { loadPluginCss, SystemJS } from './utils/plugin';
export { reportMetaAnalytics } from './utils/analytics';
interface SizeMeta {
width: number;
height: number;
}
export interface EchoMeta {
screenSize: SizeMeta;
windowSize: SizeMeta;
userAgent: string;
url?: string;
/**
* A unique browser session
*/
sessionId: string;
userLogin: string;
userId: number;
userSignedIn: boolean;
ts: number;
}
export interface EchoBackend<T extends EchoEvent = any, O = any> {
options: O;
supportedEvents: EchoEventType[];
flush: () => void;
addEvent: (event: T) => void;
}
export interface EchoEvent<T extends EchoEventType = any, P = any> {
type: EchoEventType;
payload: P;
meta: EchoMeta;
}
export enum EchoEventType {
Performance = 'performance',
MetaAnalytics = 'meta-analytics',
}
export interface EchoSrv {
flush(): void;
addBackend(backend: EchoBackend): void;
addEvent<T extends EchoEvent>(event: Omit<T, 'meta'>, meta?: {}): void;
}
let singletonInstance: EchoSrv;
export function setEchoSrv(instance: EchoSrv) {
singletonInstance = instance;
}
export function getEchoSrv(): EchoSrv {
return singletonInstance;
}
export const registerEchoBackend = (backend: EchoBackend) => {
getEchoSrv().addBackend(backend);
};
...@@ -2,3 +2,4 @@ export * from './backendSrv'; ...@@ -2,3 +2,4 @@ export * from './backendSrv';
export * from './AngularLoader'; export * from './AngularLoader';
export * from './dataSourceSrv'; export * from './dataSourceSrv';
export * from './LocationSrv'; export * from './LocationSrv';
export * from './EchoSrv';
import { EchoEvent, EchoEventType } from '../services/EchoSrv';
export interface MetaAnalyticsEventPayload {
eventName: string;
dashboardId?: number;
dashboardUid?: string;
dashboardName?: string;
folderName?: string;
panelId?: number;
panelName?: string;
datasourceName: string;
datasourceId?: number;
error?: string;
duration: number;
dataSize?: number;
}
export interface MetaAnalyticsEvent extends EchoEvent<EchoEventType.MetaAnalytics, MetaAnalyticsEventPayload> {}
import { getEchoSrv, EchoEventType } from '../services/EchoSrv';
import { MetaAnalyticsEvent, MetaAnalyticsEventPayload } from '../types/analytics';
export const reportMetaAnalytics = (payload: MetaAnalyticsEventPayload) => {
getEchoSrv().addEvent<MetaAnalyticsEvent>({
type: EchoEventType.MetaAnalytics,
payload,
});
};
...@@ -187,7 +187,7 @@ $btn-drag-image: '../img/grab_dark.svg'; ...@@ -187,7 +187,7 @@ $btn-drag-image: '../img/grab_dark.svg';
$navbar-btn-gicon-brightness: brightness(0.5); $navbar-btn-gicon-brightness: brightness(0.5);
$btn-active-box-shadow: 0px 0px 4px rgba(255,120,10,0.5); $btn-active-box-shadow: 0px 0px 4px rgba(255, 120, 10, 0.5);
// Forms // Forms
// ------------------------- // -------------------------
......
...@@ -16,6 +16,8 @@ import 'vendor/angular-other/angular-strap'; ...@@ -16,6 +16,8 @@ import 'vendor/angular-other/angular-strap';
import $ from 'jquery'; import $ from 'jquery';
import angular from 'angular'; import angular from 'angular';
import config from 'app/core/config'; import config from 'app/core/config';
// @ts-ignore
import ttiPolyfill from 'tti-polyfill';
// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move // @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
import _ from 'lodash'; import _ from 'lodash';
import { AppEvents, setMarkdownOptions, setLocale } from '@grafana/data'; import { AppEvents, setMarkdownOptions, setLocale } from '@grafana/data';
...@@ -34,6 +36,10 @@ _.move = (array: [], fromIndex: number, toIndex: number) => { ...@@ -34,6 +36,10 @@ _.move = (array: [], fromIndex: number, toIndex: number) => {
import { coreModule, angularModules } from 'app/core/core_module'; import { coreModule, angularModules } from 'app/core/core_module';
import { registerAngularDirectives } from 'app/core/core'; import { registerAngularDirectives } from 'app/core/core';
import { setupAngularRoutes } from 'app/routes/routes'; import { setupAngularRoutes } from 'app/routes/routes';
import { setEchoSrv, registerEchoBackend } from '@grafana/runtime';
import { Echo } from './core/services/echo/Echo';
import { reportPerformance } from './core/services/echo/EchoSrv';
import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend';
import 'app/routes/GrafanaCtrl'; import 'app/routes/GrafanaCtrl';
import 'app/features/all'; import 'app/features/all';
...@@ -163,6 +169,26 @@ export class GrafanaApp { ...@@ -163,6 +169,26 @@ export class GrafanaApp {
importPluginModule(modulePath); importPluginModule(modulePath);
} }
} }
initEchoSrv() {
setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' }));
ttiPolyfill.getFirstConsistentlyInteractive().then((tti: any) => {
// Collecting paint metrics first
const paintMetrics = performance.getEntriesByType('paint');
for (const metric of paintMetrics) {
reportPerformance(metric.name, Math.round(metric.startTime + metric.duration));
}
reportPerformance('tti', tti);
});
registerEchoBackend(new PerformanceBackend({}));
window.addEventListener('DOMContentLoaded', () => {
reportPerformance('dcl', Math.round(performance.now()));
});
}
} }
export default new GrafanaApp(); export default new GrafanaApp();
...@@ -21,6 +21,7 @@ const setup = (propOverrides?: object) => { ...@@ -21,6 +21,7 @@ const setup = (propOverrides?: object) => {
orgCount: 2, orgCount: 2,
orgRole: '', orgRole: '',
orgId: 1, orgId: 1,
login: 'hello',
orgName: 'Grafana', orgName: 'Grafana',
timezone: 'UTC', timezone: 'UTC',
helpFlags1: 1, helpFlags1: 1,
......
...@@ -9,6 +9,7 @@ export class User { ...@@ -9,6 +9,7 @@ export class User {
orgRole: any; orgRole: any;
orgId: number; orgId: number;
orgName: string; orgName: string;
login: string;
orgCount: number; orgCount: number;
timezone: string; timezone: string;
helpFlags1: number; helpFlags1: number;
......
import { EchoBackend, EchoMeta, EchoEvent, EchoSrv } from '@grafana/runtime';
import { contextSrv } from '../context_srv';
interface EchoConfig {
// How often should metrics be reported
flushInterval: number;
// Enables debug mode
debug: boolean;
}
/**
* Echo is a service for collecting events from Grafana client-app
* It collects events, distributes them across registered backend and flushes once per configured interval
* It's up to the registered backend to decide what to do with a given type of metric
*/
export class Echo implements EchoSrv {
private config: EchoConfig = {
flushInterval: 10000, // By default Echo flushes every 10s
debug: false,
};
private backends: EchoBackend[] = [];
// meta data added to every event collected
constructor(config?: Partial<EchoConfig>) {
this.config = {
...this.config,
...config,
};
setInterval(this.flush, this.config.flushInterval);
}
logDebug = (...msg: any) => {
if (this.config.debug) {
// tslint:disable-next-line
// console.debug('ECHO:', ...msg);
}
};
flush = () => {
for (const backend of this.backends) {
backend.flush();
}
};
addBackend = (backend: EchoBackend) => {
this.logDebug('Adding backend', backend);
this.backends.push(backend);
};
addEvent = <T extends EchoEvent>(event: Omit<T, 'meta'>, _meta?: {}) => {
const meta = this.getMeta();
const _event = {
...event,
meta: {
...meta,
..._meta,
},
};
for (const backend of this.backends) {
if (backend.supportedEvents.length === 0 || backend.supportedEvents.indexOf(_event.type) > -1) {
backend.addEvent(_event);
}
}
this.logDebug('Adding event', _event);
};
getMeta = (): EchoMeta => {
return {
sessionId: '',
userId: contextSrv.user.id,
userLogin: contextSrv.user.login,
userSignedIn: contextSrv.user.isSignedIn,
screenSize: {
width: window.innerWidth,
height: window.innerHeight,
},
windowSize: {
width: window.screen.width,
height: window.screen.height,
},
userAgent: window.navigator.userAgent,
ts: performance.now(),
url: window.location.href,
};
};
}
import { getEchoSrv, EchoEventType } from '@grafana/runtime';
import { PerformanceEvent } from './backends/PerformanceBackend';
export const reportPerformance = (metric: string, value: number) => {
getEchoSrv().addEvent<PerformanceEvent>({
type: EchoEventType.Performance,
payload: {
metricName: metric,
duration: value,
},
});
};
import { EchoBackend, EchoEvent, EchoEventType } from '@grafana/runtime';
export interface PerformanceEventPayload {
metricName: string;
duration: number;
}
export interface PerformanceEvent extends EchoEvent<EchoEventType.Performance, PerformanceEventPayload> {}
export interface PerformanceBackendOptions {
url?: string;
}
/**
* Echo's performance metrics consumer
* Reports performance metrics to given url (TODO)
*/
export class PerformanceBackend implements EchoBackend<PerformanceEvent, PerformanceBackendOptions> {
private buffer: PerformanceEvent[] = [];
supportedEvents = [EchoEventType.Performance];
constructor(public options: PerformanceBackendOptions) {}
addEvent = (e: EchoEvent) => {
this.buffer.push(e);
};
flush = () => {
if (this.buffer.length === 0) {
return;
}
const result = {
metrics: this.buffer,
};
// Currently we don have API for sending the metrics hence loging to console in dev environment
if (process.env.NODE_ENV === 'development') {
console.log('PerformanceBackend flushing:', result);
}
this.buffer = [];
// TODO: Enable backend request when we have metrics API
// if (this.options.url) {
// getBackendSrv().post(this.options.url, result);
// }
};
}
import { PanelQueryRunner } from './PanelQueryRunner'; import { PanelQueryRunner } from './PanelQueryRunner';
import { PanelData, DataQueryRequest, dateTime, ScopedVars } from '@grafana/data'; import { PanelData, DataQueryRequest, dateTime, ScopedVars } from '@grafana/data';
import { PanelModel } from './PanelModel'; import { DashboardModel } from './index';
import { setEchoSrv } from '@grafana/runtime';
import { Echo } from '../../../core/services/echo/Echo';
jest.mock('app/core/services/backend_srv'); jest.mock('app/core/services/backend_srv');
// Defined within setup functions const dashboardModel = new DashboardModel({
const panelsForCurrentDashboardMock: { [key: number]: PanelModel } = {}; panels: [{ id: 1, type: 'graph' }],
});
jest.mock('app/features/dashboard/services/DashboardSrv', () => ({ jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
getDashboardSrv: () => { getDashboardSrv: () => {
return { return {
getCurrent: () => { getCurrent: () => dashboardModel,
return {
getPanelById: (id: number) => {
return panelsForCurrentDashboardMock[id];
},
};
},
}; };
}, },
})); }));
...@@ -68,6 +65,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn ...@@ -68,6 +65,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
}; };
beforeEach(async () => { beforeEach(async () => {
setEchoSrv(new Echo());
setupFn(); setupFn();
const datasource: any = { const datasource: any = {
...@@ -103,13 +101,6 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn ...@@ -103,13 +101,6 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
}, },
}); });
panelsForCurrentDashboardMock[1] = {
id: 1,
getQueryRunner: () => {
return ctx.runner;
},
} as PanelModel;
ctx.events = []; ctx.events = [];
ctx.runner.run(args); ctx.runner.run(args);
}); });
......
import { getDashboardSrv } from '../services/DashboardSrv';
import { PanelData, LoadingState, DataSourceApi } from '@grafana/data';
import { reportMetaAnalytics, MetaAnalyticsEventPayload } from '@grafana/runtime';
export function getAnalyticsProcessor(datasource: DataSourceApi) {
let done = false;
return (data: PanelData) => {
if (!data.request || done) {
return;
}
if (data.state !== LoadingState.Done && data.state !== LoadingState.Error) {
return;
}
const eventData: MetaAnalyticsEventPayload = {
datasourceName: datasource.name,
datasourceId: datasource.id,
panelId: data.request.panelId,
dashboardId: data.request.dashboardId,
// app: 'dashboard',
// count: 1,
dataSize: 0,
duration: data.request.endTime - data.request.startTime,
eventName: 'data-request',
// sessionId: '',
};
// enrich with dashboard info
const dashboard = getDashboardSrv().getCurrent();
if (dashboard) {
eventData.dashboardId = dashboard.id;
eventData.dashboardName = dashboard.title;
eventData.dashboardUid = dashboard.uid;
eventData.folderName = dashboard.meta.folderTitle;
}
if (data.series.length > 0) {
// estimate size
eventData.dataSize = data.series.length * data.series[0].length;
}
if (data.error) {
eventData.error = data.error.message;
}
reportMetaAnalytics(eventData);
// this done check is to make sure we do not double emit events in case
// there are multiple responses with done state
done = true;
};
}
...@@ -10,9 +10,24 @@ import { ...@@ -10,9 +10,24 @@ import {
import { Subscriber, Observable, Subscription } from 'rxjs'; import { Subscriber, Observable, Subscription } from 'rxjs';
import { runRequest } from './runRequest'; import { runRequest } from './runRequest';
import { deepFreeze } from '../../../../test/core/redux/reducerTester'; import { deepFreeze } from '../../../../test/core/redux/reducerTester';
import { DashboardModel } from './DashboardModel';
import { setEchoSrv } from '@grafana/runtime';
import { Echo } from '../../../core/services/echo/Echo';
jest.mock('app/core/services/backend_srv'); jest.mock('app/core/services/backend_srv');
const dashboardModel = new DashboardModel({
panels: [{ id: 1, type: 'graph' }],
});
jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
getDashboardSrv: () => {
return {
getCurrent: () => dashboardModel,
};
},
}));
class ScenarioCtx { class ScenarioCtx {
ds: DataSourceApi; ds: DataSourceApi;
request: DataQueryRequest; request: DataQueryRequest;
...@@ -84,6 +99,7 @@ function runRequestScenario(desc: string, fn: (ctx: ScenarioCtx) => void) { ...@@ -84,6 +99,7 @@ function runRequestScenario(desc: string, fn: (ctx: ScenarioCtx) => void) {
const ctx = new ScenarioCtx(); const ctx = new ScenarioCtx();
beforeEach(() => { beforeEach(() => {
setEchoSrv(new Echo());
ctx.reset(); ctx.reset();
return ctx.setupFn(); return ctx.setupFn();
}); });
......
// Libraries // Libraries
import { Observable, of, timer, merge, from } from 'rxjs'; import { Observable, of, timer, merge, from } from 'rxjs';
import { flatten, map as lodashMap, isArray, isString } from 'lodash'; import { flatten, map as lodashMap, isArray, isString } from 'lodash';
import { map, catchError, takeUntil, mapTo, share, finalize } from 'rxjs/operators'; import { map, catchError, takeUntil, mapTo, share, finalize, tap } from 'rxjs/operators';
// Utils & Services // Utils & Services
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
// Types // Types
...@@ -18,6 +18,7 @@ import { ...@@ -18,6 +18,7 @@ import {
DataFrame, DataFrame,
guessFieldTypes, guessFieldTypes,
} from '@grafana/data'; } from '@grafana/data';
import { getAnalyticsProcessor } from './analyticsProcessor';
import { ExpressionDatasourceID, expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { ExpressionDatasourceID, expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
type MapOfResponsePackets = { [str: string]: DataQueryResponse }; type MapOfResponsePackets = { [str: string]: DataQueryResponse };
...@@ -119,6 +120,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest) ...@@ -119,6 +120,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest)
error: processQueryError(err), error: processQueryError(err),
}) })
), ),
tap(getAnalyticsProcessor(datasource)),
// finalize is triggered when subscriber unsubscribes // finalize is triggered when subscriber unsubscribes
// This makes sure any still running network requests are cancelled // This makes sure any still running network requests are cancelled
finalize(cancelNetworkRequestsOnUnsubscribe(request)), finalize(cancelNetworkRequestsOnUnsubscribe(request)),
......
import app from './app'; import app from './app';
app.initEchoSrv();
app.init(); app.init();
...@@ -141,7 +141,6 @@ export function grafanaAppDirective( ...@@ -141,7 +141,6 @@ export function grafanaAppDirective(
controller: GrafanaCtrl, controller: GrafanaCtrl,
link: (scope: IRootScopeService & AppEventEmitter, elem: JQuery) => { link: (scope: IRootScopeService & AppEventEmitter, elem: JQuery) => {
const body = $('body'); const body = $('body');
// see https://github.com/zenorocha/clipboard.js/issues/155 // see https://github.com/zenorocha/clipboard.js/issues/155
$.fn.modal.Constructor.prototype.enforceFocus = () => {}; $.fn.modal.Constructor.prototype.enforceFocus = () => {};
......
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<script>
// https://github.com/GoogleChromeLabs/tti-polyfill
!(function() {
if ('PerformanceLongTaskTiming' in window) {
var g = (window.__tti = { e: [] });
g.o = new PerformanceObserver(function(l) {
g.e = g.e.concat(l.getEntries());
});
g.o.observe({ entryTypes: ['longtask'] });
}
})();
<head> </script>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#000"> <meta name="theme-color" content="#000" />
<title>Grafana</title> <title>Grafana</title>
<base href="[[.AppSubUrl]]/" /> <base href="[[.AppSubUrl]]/" />
<link rel="preload" href="public/fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2" as="font" crossorigin /> <link
<link rel="icon" type="image/png" href="public/img/fav32.png"> rel="preload"
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28"> href="public/fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2"
<link rel="apple-touch-icon" sizes="180x180" href="public/img/apple-touch-icon.png"> as="font"
crossorigin
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css"> />
<link rel="icon" type="image/png" href="public/img/fav32.png" />
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28" />
<link rel="apple-touch-icon" sizes="180x180" href="public/img/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-capable" content="yes"/> <link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="msapplication-config" content="public/img/browserconfig.xml">
</head>
<body class="theme-[[ .Theme ]] [[.AppNameBodyClass]]"> <script>
performance.mark('css done blocking');
</script>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="msapplication-TileColor" content="#2b5797" />
<meta name="msapplication-config" content="public/img/browserconfig.xml" />
</head>
<body class="theme-[[ .Theme ]] [[.AppNameBodyClass]]">
<style> <style>
.preloader { .preloader {
height: 100%; height: 100%;
...@@ -39,7 +58,7 @@ ...@@ -39,7 +58,7 @@
opacity: 0; opacity: 0;
animation-name: preloader-fade-in; animation-name: preloader-fade-in;
animation-iteration-count: 1; animation-iteration-count: 1;
animation-duration: .9s; animation-duration: 0.9s;
animation-delay: 1.35s; animation-delay: 1.35s;
animation-fill-mode: forwards; animation-fill-mode: forwards;
} }
...@@ -47,14 +66,14 @@ ...@@ -47,14 +66,14 @@
.preloader__bounce { .preloader__bounce {
text-align: center; text-align: center;
animation-name: preloader-bounce; animation-name: preloader-bounce;
animation-duration: .9s; animation-duration: 0.9s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
} }
.preloader__logo { .preloader__logo {
display: inline-block; display: inline-block;
animation-name: preloader-squash; animation-name: preloader-squash;
animation-duration: .9s; animation-duration: 0.9s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
width: 60px; width: 60px;
height: 60px; height: 60px;
...@@ -70,7 +89,7 @@ ...@@ -70,7 +89,7 @@
font-family: Sans-serif; font-family: Sans-serif;
opacity: 0; opacity: 0;
animation-name: preloader-fade-in; animation-name: preloader-fade-in;
animation-duration: .9s; animation-duration: 0.9s;
animation-delay: 1.8s; animation-delay: 1.8s;
animation-fill-mode: forwards; animation-fill-mode: forwards;
} }
...@@ -87,7 +106,7 @@ ...@@ -87,7 +106,7 @@
0% { 0% {
opacity: 0; opacity: 0;
/*animation-timing-function: linear;*/ /*animation-timing-function: linear;*/
animation-timing-function: cubic-bezier(0, 0, 0.5, 1) animation-timing-function: cubic-bezier(0, 0, 0.5, 1);
} }
100% { 100% {
opacity: 1; opacity: 1;
...@@ -98,37 +117,37 @@ ...@@ -98,37 +117,37 @@
from, from,
to { to {
transform: translateY(0px); transform: translateY(0px);
animation-timing-function: cubic-bezier(0.3, 0.0, 0.1, 1) animation-timing-function: cubic-bezier(0.3, 0, 0.1, 1);
} }
50% { 50% {
transform: translateY(-50px); transform: translateY(-50px);
animation-timing-function: cubic-bezier(.9, 0, .7, 1) animation-timing-function: cubic-bezier(0.9, 0, 0.7, 1);
} }
} }
@keyframes preloader-squash { @keyframes preloader-squash {
0% { 0% {
transform: scaleX(1.3) scaleY(.8); transform: scaleX(1.3) scaleY(0.8);
animation-timing-function: cubic-bezier(.3, 0, .1, 1); animation-timing-function: cubic-bezier(0.3, 0, 0.1, 1);
transform-origin: bottom center; transform-origin: bottom center;
} }
15% { 15% {
transform: scaleX(.75) scaleY(1.25); transform: scaleX(0.75) scaleY(1.25);
animation-timing-function: cubic-bezier(0, 0, .7, .75); animation-timing-function: cubic-bezier(0, 0, 0.7, 0.75);
transform-origin: bottom center; transform-origin: bottom center;
} }
55% { 55% {
transform: scaleX(1.05) scaleY(.95); transform: scaleX(1.05) scaleY(0.95);
animation-timing-function: cubic-bezier(.9, 0, 1, 1); animation-timing-function: cubic-bezier(0.9, 0, 1, 1);
transform-origin: top center; transform-origin: top center;
} }
95% { 95% {
transform: scaleX(.75) scaleY(1.25); transform: scaleX(0.75) scaleY(1.25);
animation-timing-function: cubic-bezier(0, 0, 0, 1); animation-timing-function: cubic-bezier(0, 0, 0, 1);
transform-origin: bottom center; transform-origin: bottom center;
} }
100% { 100% {
transform: scaleX(1.3) scaleY(.8); transform: scaleX(1.3) scaleY(0.8);
transform-origin: bottom center; transform-origin: bottom center;
animation-timing-function: cubic-bezier(0, 0, 0.7, 1); animation-timing-function: cubic-bezier(0, 0, 0.7, 1);
} }
...@@ -179,8 +198,11 @@ ...@@ -179,8 +198,11 @@
</p> </p>
<p> <p>
1. This could be caused by your reverse proxy settings.<br /><br /> 1. This could be caused by your reverse proxy settings.<br /><br />
2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath<br /> <br /> 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath<br />
3. If you have a local dev build make sure you build frontend using: yarn start, yarn start:hot, or yarn build<br /> <br /> <br />
3. If you have a local dev build make sure you build frontend using: yarn start, yarn start:hot, or yarn
build<br />
<br />
4. Sometimes restarting grafana-server can help<br /> 4. Sometimes restarting grafana-server can help<br />
</p> </p>
</div> </div>
...@@ -253,25 +275,54 @@ ...@@ -253,25 +275,54 @@
[[if .GoogleTagManagerId]] [[if .GoogleTagManagerId]]
<!-- Google Tag Manager --> <!-- Google Tag Manager -->
<script> <script>
dataLayer = [{ dataLayer = [
'IsSignedIn': '[[.User.IsSignedIn]]', {
'Email': '[[.User.Email]]', IsSignedIn: '[[.User.IsSignedIn]]',
'Name': '[[.User.Name]]', Email: '[[.User.Email]]',
'UserId': '[[.User.Id]]', Name: '[[.User.Name]]',
'OrgId': '[[.User.OrgId]]', UserId: '[[.User.Id]]',
'OrgName': '[[.User.OrgName]]', OrgId: '[[.User.OrgId]]',
}]; OrgName: '[[.User.OrgName]]',
},
];
</script> </script>
<noscript> <noscript>
<iframe src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]" height="0" width="0" style="display:none;visibility:hidden"></iframe> <iframe
src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]"
height="0"
width="0"
style="display:none;visibility:hidden"
></iframe>
</noscript> </noscript>
<script>(function (w, d, s, l, i) { <script>
w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); var f = d.getElementsByTagName(s)[0], (function(w, d, s, l, i) {
j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f); w[l] = w[l] || [];
})(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]');</script> w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s),
dl = l != 'dataLayer' ? '&l=' + l : '';
j.async = true;
j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]');
</script>
<!-- End Google Tag Manager --> <!-- End Google Tag Manager -->
[[end]] [[end]]
</body> <%
for (key in htmlWebpackPlugin.files.chunks) { %><%
if (htmlWebpackPlugin.files.jsIntegrity) { %>
<script
src="<%= htmlWebpackPlugin.files.chunks[key].entry %>"
type="text/javascript"
integrity="<%= htmlWebpackPlugin.files.jsIntegrity[htmlWebpackPlugin.files.js.indexOf(htmlWebpackPlugin.files.chunks[key].entry)] %>"
crossorigin="<%= webpackConfig.output.crossOriginLoading %>"></script><%
} else { %>
<script src="<%= htmlWebpackPlugin.files.chunks[key].entry %>" type="text/javascript"></script><%
} %><%
} %>
<script>
performance.mark('js done blocking');
</script>
</body>
</html> </html>
...@@ -81,7 +81,7 @@ module.exports = (env = {}) => ...@@ -81,7 +81,7 @@ module.exports = (env = {}) =>
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'), filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index-template.html'), template: path.resolve(__dirname, '../../public/views/index-template.html'),
inject: 'body', inject: false,
chunksSortMode: 'none', chunksSortMode: 'none',
excludeChunks: ['dark', 'light'] excludeChunks: ['dark', 'light']
}), }),
......
...@@ -77,7 +77,7 @@ module.exports = merge(common, { ...@@ -77,7 +77,7 @@ module.exports = merge(common, {
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'), filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index-template.html'), template: path.resolve(__dirname, '../../public/views/index-template.html'),
inject: 'body', inject: false,
excludeChunks: ['manifest', 'dark', 'light'], excludeChunks: ['manifest', 'dark', 'light'],
chunksSortMode: 'none' chunksSortMode: 'none'
}), }),
......
...@@ -20371,6 +20371,11 @@ tsutils@^3.9.1: ...@@ -20371,6 +20371,11 @@ tsutils@^3.9.1:
dependencies: dependencies:
tslib "^1.8.1" tslib "^1.8.1"
tti-polyfill@0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/tti-polyfill/-/tti-polyfill-0.2.2.tgz#f7bbf71b13afa9edf60c8bb0d0c05f134e1513b9"
integrity sha512-URIoJxvsHThbQEJij29hIBUDHx9UNoBBCQVjy7L8PnzkqY8N6lsAI6h8JrT1Wt2lA0avus/DkuiJxd9qpfCpqw==
tty-browserify@0.0.0: tty-browserify@0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment