From abf015ace227a113fcda67542f2aa77c7f348e66 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 12:13:38 -0800 Subject: [PATCH 001/194] use TableData for timeseries in react --- packages/grafana-ui/src/types/data.ts | 10 ++-- packages/grafana-ui/src/types/panel.ts | 9 +--- .../grafana-ui/src/utils/processTimeSeries.ts | 20 ++++---- public/app/core/table_model.ts | 12 ++--- .../features/dashboard/dashgrid/DataPanel.tsx | 33 +++++-------- .../dashboard/dashgrid/PanelChrome.tsx | 10 ++-- public/app/features/dashboard/utils/panel.ts | 46 +++++++++++++------ public/app/plugins/panel/gauge/GaugePanel.tsx | 22 ++++++--- .../app/plugins/panel/graph2/GraphPanel.tsx | 11 +++-- 9 files changed, 94 insertions(+), 79 deletions(-) diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts index 1e4ccba3948..1ea89bcd28e 100644 --- a/packages/grafana-ui/src/types/data.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -53,12 +53,9 @@ export interface TimeSeriesVMs { length: number; } -interface Column { - text: string; - title?: string; - type?: string; - sort?: boolean; - desc?: boolean; +export interface Column { + text: string; // name + type?: 'time' | 'number' | 'string' | 'object'; filterable?: boolean; unit?: string; } @@ -67,5 +64,4 @@ export interface TableData { columns: Column[]; rows: any[]; type: string; - columnMap: any; } diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 260ff78df76..4d9dc820b96 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,12 +1,12 @@ import { ComponentClass } from 'react'; -import { TimeSeries, LoadingState, TableData } from './data'; +import { LoadingState, TableData } from './data'; import { TimeRange } from './time'; import { ScopedVars } from './datasource'; export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string; export interface PanelProps { - panelData: PanelData; + data?: TableData[]; timeRange: TimeRange; loading: LoadingState; options: T; @@ -16,11 +16,6 @@ export interface PanelProps { replaceVariables: InterpolateFunction; } -export interface PanelData { - timeSeries?: TimeSeries[]; - tableData?: TableData; -} - export interface PanelEditorProps { options: T; onOptionsChange: (options: T) => void; diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index f5e9f96efba..d8827e567e3 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -4,17 +4,19 @@ import isNumber from 'lodash/isNumber'; import { colors } from './colors'; // Types -import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types'; +import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types'; interface Options { - timeSeries: TimeSeries[]; + data: TableData[]; + xColumn: number; // Time + yColumn: number; // Value nullValueMode: NullValueMode; } -export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { - const vmSeries = timeSeries.map((item, index) => { +export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs { + const vmSeries = data.map((item, index) => { const colorIndex = index % colors.length; - const label = item.target; + const label = item.columns[yColumn].text; const result = []; // stat defaults @@ -42,9 +44,9 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS let previousValue = 0; let previousDeltaUp = true; - for (let i = 0; i < item.datapoints.length; i++) { - currentValue = item.datapoints[i][0]; - currentTime = item.datapoints[i][1]; + for (let i = 0; i < item.rows.length; i++) { + currentValue = item.rows[i][yColumn]; + currentTime = item.rows[i][xColumn]; if (typeof currentTime !== 'number') { continue; @@ -95,7 +97,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS if (previousValue > currentValue) { // counter reset previousDeltaUp = false; - if (i === item.datapoints.length - 1) { + if (i === item.rows.length - 1) { // reset on last delta += currentValue; } diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index fa7170bed13..988c3b1992e 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -1,17 +1,15 @@ import _ from 'lodash'; +import { Column, TableData } from '@grafana/ui'; -interface Column { - text: string; +// This class mutates and uses the extra column fields +interface ColumnEX extends Column { title?: string; - type?: string; sort?: boolean; desc?: boolean; - filterable?: boolean; - unit?: string; } -export default class TableModel { - columns: Column[]; +export default class TableModel implements TableData { + columns: ColumnEX[]; rows: any[]; type: string; columnMap: any; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 09864d85960..c12462e2b49 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -11,16 +11,16 @@ import { DataQueryResponse, DataQueryError, LoadingState, - PanelData, TableData, TimeRange, - TimeSeries, ScopedVars, } from '@grafana/ui'; +import { toTableData } from '../utils/panel'; + interface RenderProps { loading: LoadingState; - panelData: PanelData; + data: TableData[]; } export interface Props { @@ -44,7 +44,7 @@ export interface State { isFirstLoad: boolean; loading: LoadingState; response: DataQueryResponse; - panelData: PanelData; + data?: TableData[]; } export class DataPanel extends Component { @@ -64,7 +64,6 @@ export class DataPanel extends Component { response: { data: [], }, - panelData: {}, isFirstLoad: true, }; } @@ -146,10 +145,12 @@ export class DataPanel extends Component { onDataResponse(resp); } + const data = toTableData(resp.data); + console.log('Converted:', data); this.setState({ loading: LoadingState.Done, response: resp, - panelData: this.getPanelData(resp), + data, isFirstLoad: false, }); } catch (err) { @@ -172,23 +173,9 @@ export class DataPanel extends Component { } }; - getPanelData(response: DataQueryResponse) { - if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') { - return { - tableData: response.data[0] as TableData, - timeSeries: null, - }; - } - - return { - timeSeries: response.data as TimeSeries[], - tableData: null, - }; - } - render() { const { queries } = this.props; - const { loading, isFirstLoad, panelData } = this.state; + const { loading, isFirstLoad, data } = this.state; // do not render component until we have first data if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { @@ -203,10 +190,12 @@ export class DataPanel extends Component { ); } + console.log('RENDER', data); + return ( <> {loading === LoadingState.Loading && this.renderLoadingState()} - {this.props.children({ loading, panelData })} + {this.props.children({ loading, data })} ); } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 0a9d1d44ceb..0dd82b7dad9 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -18,7 +18,7 @@ import { profiler } from 'app/core/profiler'; // Types import { DashboardModel, PanelModel } from '../state'; import { PanelPlugin } from 'app/types'; -import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui'; +import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui'; import variables from 'sass/_variables.generated.scss'; @@ -142,7 +142,7 @@ export class PanelChrome extends PureComponent { return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null; } - renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element { + renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element { const { panel, plugin } = this.props; const { timeRange, renderCounter } = this.state; const PanelComponent = plugin.exports.reactPanel.panel; @@ -157,7 +157,7 @@ export class PanelChrome extends PureComponent {
{ onDataResponse={this.onDataResponse} onError={this.onDataError} > - {({ loading, panelData }) => { - return this.renderPanelPlugin(loading, panelData, width, height); + {({ loading, data }) => { + return this.renderPanelPlugin(loading, data, width, height); }} ) : ( diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index d14432cb2eb..3058e2b9db6 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -4,8 +4,7 @@ import store from 'app/core/store'; // Models import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { PanelData, TimeRange, TimeSeries } from '@grafana/ui'; -import { TableData } from '@grafana/ui/src'; +import { TableData, TimeRange, TimeSeries } from '@grafana/ui'; // Utils import { isString as _isString } from 'lodash'; @@ -173,16 +172,37 @@ export function getResolution(panel: PanelModel): number { const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints'); const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); -export const snapshotDataToPanelData = (panel: PanelModel): PanelData => { - const snapshotData = panel.snapshotData; - if (isTimeSeries(snapshotData[0])) { - return { - timeSeries: snapshotData, - } as PanelData; - } else if (isTableData(snapshotData[0])) { - return { - tableData: snapshotData[0], - } as PanelData; +export const snapshotDataToPanelData = (panel: PanelModel): TableData[] => { + return toTableData(panel.snapshotData); +}; + +export const toTableData = (results: any[]): TableData[] => { + if (!results) { + return []; } - throw new Error('snapshotData is invalid:' + snapshotData.toString()); + return results.map(data => { + if (isTableData(data)) { + return data as TableData; + } + if (isTimeSeries(data)) { + const ts = data as TimeSeries; + return { + type: 'timeseries', + columns: [ + { + text: ts.target, + unit: ts.unit, + type: 'number', // Is this really true? + }, + { + text: 'time', + type: 'time', + }, + ], + rows: ts.datapoints, + } as TableData; + } + console.warn('Can not convert', data); + throw new Error('Unsupported data format'); + }); }; diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index b75d4a1c7f3..7913aa33eb0 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -22,29 +22,39 @@ export class GaugePanel extends Component { this.state = { value: this.findValue(props), }; + console.log('CONSTRUCTOR!', this.props.data); } componentDidUpdate(prevProps: Props) { - if (this.props.panelData !== prevProps.panelData) { + console.log('UPDATE', this.props.data); + + if (this.props.data !== prevProps.data) { this.setState({ value: this.findValue(this.props) }); } } findValue(props: Props): number | null { - const { panelData, options } = props; + const { data, options } = props; const { valueOptions } = options; - if (panelData.timeSeries) { + console.log('FIND VALUE', data); + + if (data) { + // For now, assume timeseries defaults + const xColumn = 1; // time + const yColumn = 0; // value const vmSeries = processTimeSeries({ - timeSeries: panelData.timeSeries, + data, + xColumn, + yColumn, nullValueMode: NullValueMode.Null, }); + console.log('GOT', vmSeries); + if (vmSeries[0]) { return vmSeries[0].stats[valueOptions.stat]; } - } else if (panelData.tableData) { - return panelData.tableData.rows[0].find(prop => prop > 0); } return null; } diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index f1fc2b43d51..c859b5f9cf3 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -16,13 +16,18 @@ interface Props extends PanelProps {} export class GraphPanel extends PureComponent { render() { - const { panelData, timeRange, width, height } = this.props; + const { data, timeRange, width, height } = this.props; const { showLines, showBars, showPoints } = this.props.options; let vmSeries: TimeSeriesVMs; - if (panelData.timeSeries) { + if (data) { + // For now, assume timeseries defaults + const xColumn = 1; // time + const yColumn = 0; // value vmSeries = processTimeSeries({ - timeSeries: panelData.timeSeries, + data, + xColumn, + yColumn, nullValueMode: NullValueMode.Ignore, }); } From 8bf57359ab745a0ed67bc876c26e753c24f492e0 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 12:47:21 -0800 Subject: [PATCH 002/194] don't require x & y columns for timeSeries --- .../grafana-ui/src/utils/processTimeSeries.ts | 19 +++++++++++++++++-- .../features/dashboard/dashgrid/DataPanel.tsx | 6 +----- public/app/plugins/panel/gauge/GaugePanel.tsx | 12 ------------ .../app/plugins/panel/graph2/GraphPanel.tsx | 5 ----- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index d8827e567e3..4b64ae3dd99 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -8,13 +8,28 @@ import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../typ interface Options { data: TableData[]; - xColumn: number; // Time - yColumn: number; // Value + xColumn?: number; // Time + yColumn?: number; // Value nullValueMode: NullValueMode; } export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs { const vmSeries = data.map((item, index) => { + if (!isNumber(xColumn)) { + xColumn = 1; // Default timeseries colum. TODO, find first time field! + } + if (!isNumber(yColumn)) { + yColumn = 0; // TODO, find first non-time field + } + + // TODO? either % or throw error? + if (xColumn >= item.columns.length) { + throw new Error('invalid colum: ' + xColumn); + } + if (yColumn >= item.columns.length) { + throw new Error('invalid colum: ' + yColumn); + } + const colorIndex = index % colors.length; const label = item.columns[yColumn].text; const result = []; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index c12462e2b49..1dd62c58e5c 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -145,12 +145,10 @@ export class DataPanel extends Component { onDataResponse(resp); } - const data = toTableData(resp.data); - console.log('Converted:', data); this.setState({ loading: LoadingState.Done, response: resp, - data, + data: toTableData(resp.data), isFirstLoad: false, }); } catch (err) { @@ -190,8 +188,6 @@ export class DataPanel extends Component { ); } - console.log('RENDER', data); - return ( <> {loading === LoadingState.Loading && this.renderLoadingState()} diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 7913aa33eb0..c4df1bb48ca 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -22,12 +22,9 @@ export class GaugePanel extends Component { this.state = { value: this.findValue(props), }; - console.log('CONSTRUCTOR!', this.props.data); } componentDidUpdate(prevProps: Props) { - console.log('UPDATE', this.props.data); - if (this.props.data !== prevProps.data) { this.setState({ value: this.findValue(this.props) }); } @@ -37,21 +34,12 @@ export class GaugePanel extends Component { const { data, options } = props; const { valueOptions } = options; - console.log('FIND VALUE', data); - if (data) { - // For now, assume timeseries defaults - const xColumn = 1; // time - const yColumn = 0; // value const vmSeries = processTimeSeries({ data, - xColumn, - yColumn, nullValueMode: NullValueMode.Null, }); - console.log('GOT', vmSeries); - if (vmSeries[0]) { return vmSeries[0].stats[valueOptions.stat]; } diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index c859b5f9cf3..f04e73e56fb 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -21,13 +21,8 @@ export class GraphPanel extends PureComponent { let vmSeries: TimeSeriesVMs; if (data) { - // For now, assume timeseries defaults - const xColumn = 1; // time - const yColumn = 0; // value vmSeries = processTimeSeries({ data, - xColumn, - yColumn, nullValueMode: NullValueMode.Ignore, }); } From 439b04420454f6cbf4a2bb769189002c919bf713 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 13:09:53 -0800 Subject: [PATCH 003/194] move toTableData to grafana/ui --- .../grafana-ui/src/utils/processTimeSeries.ts | 41 +++++++++++++++++-- .../features/dashboard/dashgrid/DataPanel.tsx | 3 +- .../dashboard/dashgrid/PanelChrome.tsx | 6 +-- public/app/features/dashboard/utils/panel.ts | 39 +----------------- 4 files changed, 43 insertions(+), 46 deletions(-) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 4b64ae3dd99..3b2d1bd05aa 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -4,12 +4,12 @@ import isNumber from 'lodash/isNumber'; import { colors } from './colors'; // Types -import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types'; +import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData, TimeSeries } from '../types'; interface Options { data: TableData[]; - xColumn?: number; // Time - yColumn?: number; // Value + xColumn?: number; // Time (or null to guess) + yColumn?: number; // Value (or null to guess) nullValueMode: NullValueMode; } @@ -190,3 +190,38 @@ export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Opt return vmSeries; } + +export const toTableData = (results: any[]): TableData[] => { + const tables: TableData[] = []; + if (results) { + for (let i = 0; i < results.length; i++) { + const data = results[i]; + if (data) { + if (data.hasOwnProperty('columns')) { + tables.push(data as TableData); + } else if (data.hasOwnProperty('datapoints')) { + const ts = data as TimeSeries; + tables.push({ + type: 'timeseries', + columns: [ + { + text: ts.target, + unit: ts.unit, + type: 'number', // Is this really true? + }, + { + text: 'time', + type: 'time', + }, + ], + rows: ts.datapoints, + } as TableData); + } else { + console.warn('Can not convert', data); + throw new Error('Unsupported data format'); + } + } + } + } + return tables; +}; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 1dd62c58e5c..872e2823553 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -14,10 +14,9 @@ import { TableData, TimeRange, ScopedVars, + toTableData, } from '@grafana/ui'; -import { toTableData } from '../utils/panel'; - interface RenderProps { loading: LoadingState; data: TableData[]; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 0dd82b7dad9..af58202dfa9 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -11,14 +11,14 @@ import { DataPanel } from './DataPanel'; import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary'; // Utils -import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel'; +import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { PANEL_HEADER_HEIGHT } from 'app/core/constants'; import { profiler } from 'app/core/profiler'; // Types import { DashboardModel, PanelModel } from '../state'; import { PanelPlugin } from 'app/types'; -import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError } from '@grafana/ui'; +import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError, toTableData } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui'; import variables from 'sass/_variables.generated.scss'; @@ -139,7 +139,7 @@ export class PanelChrome extends PureComponent { } get getDataForPanel() { - return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null; + return this.hasPanelSnapshot ? toTableData(this.props.panel.snapshotData) : null; } renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element { diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 3058e2b9db6..57f4b81a0e0 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -4,7 +4,7 @@ import store from 'app/core/store'; // Models import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { TableData, TimeRange, TimeSeries } from '@grafana/ui'; +import { TimeRange } from '@grafana/ui'; // Utils import { isString as _isString } from 'lodash'; @@ -169,40 +169,3 @@ export function getResolution(panel: PanelModel): number { return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24)); } - -const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints'); -const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); -export const snapshotDataToPanelData = (panel: PanelModel): TableData[] => { - return toTableData(panel.snapshotData); -}; - -export const toTableData = (results: any[]): TableData[] => { - if (!results) { - return []; - } - return results.map(data => { - if (isTableData(data)) { - return data as TableData; - } - if (isTimeSeries(data)) { - const ts = data as TimeSeries; - return { - type: 'timeseries', - columns: [ - { - text: ts.target, - unit: ts.unit, - type: 'number', // Is this really true? - }, - { - text: 'time', - type: 'time', - }, - ], - rows: ts.datapoints, - } as TableData; - } - console.warn('Can not convert', data); - throw new Error('Unsupported data format'); - }); -}; From d4f578966056ea1820186327ca35138df5032fa2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 14 Feb 2019 23:13:46 +0100 Subject: [PATCH 004/194] cache: initial version of db cache --- pkg/infra/distcache/database_storage.go | 82 +++++++++++++++++ pkg/infra/distcache/distcache.go | 68 +++++++++++++++ pkg/infra/distcache/distcache_test.go | 87 +++++++++++++++++++ .../sqlstore/migrations/cache_data_mig.go | 17 ++++ .../sqlstore/migrations/migrations.go | 1 + 5 files changed, 255 insertions(+) create mode 100644 pkg/infra/distcache/database_storage.go create mode 100644 pkg/infra/distcache/distcache.go create mode 100644 pkg/infra/distcache/distcache_test.go create mode 100644 pkg/services/sqlstore/migrations/cache_data_mig.go diff --git a/pkg/infra/distcache/database_storage.go b/pkg/infra/distcache/database_storage.go new file mode 100644 index 00000000000..8286f65fea6 --- /dev/null +++ b/pkg/infra/distcache/database_storage.go @@ -0,0 +1,82 @@ +package distcache + +import ( + "time" + + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +type databaseCache struct { + SQLStore *sqlstore.SqlStore +} + +var getTime = time.Now + +func (dc *databaseCache) Get(key string) (interface{}, error) { + //now := getTime().Unix() + + cacheHits := []CacheData{} + err := dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) + if err != nil { + return nil, err + } + + var cacheHit CacheData + if len(cacheHits) == 0 { + return nil, ErrCacheItemNotFound + } + + cacheHit = cacheHits[0] + if cacheHit.Expires > 0 { + if getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires { + dc.Delete(key) + return nil, ErrCacheItemNotFound + } + } + + item := &Item{} + if err = DecodeGob(cacheHit.Data, item); err != nil { + return nil, err + } + + return item.Val, nil +} + +type CacheData struct { + Key string + Data []byte + Expires int64 + CreatedAt int64 +} + +func (dc *databaseCache) Put(key string, value interface{}, expire int64) error { + item := &Item{Val: value} + data, err := EncodeGob(item) + if err != nil { + return err + } + + now := getTime().Unix() + + cacheHits := []CacheData{} + err = dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) + if err != nil { + return err + } + + if len(cacheHits) > 0 { + _, err = dc.SQLStore.NewSession().Exec("UPDATE cached_data SET data=?, created=?, expire=? WHERE key=?", data, now, expire, key) + } else { + _, err = dc.SQLStore.NewSession().Exec("INSERT INTO cache_data(key,data,created_at,expires) VALUES(?,?,?,?)", key, data, now, expire) + } + + return err +} + +func (dc *databaseCache) Delete(key string) error { + sql := `DELETE FROM cache_data WHERE key = ?` + + _, err := dc.SQLStore.NewSession().Exec(sql, key) + + return err +} diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go new file mode 100644 index 00000000000..11efd435de3 --- /dev/null +++ b/pkg/infra/distcache/distcache.go @@ -0,0 +1,68 @@ +package distcache + +import ( + "bytes" + "encoding/gob" + "errors" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" + + "github.com/grafana/grafana/pkg/registry" +) + +var ( + ErrCacheItemNotFound = errors.New("cache item not found") +) + +func init() { + registry.RegisterService(&DistributedCache{}) +} + +// Init initializes the service +func (ds *DistributedCache) Init() error { + ds.log = log.New("distributed.cache") + + // memory + // redis + // memcache + // database. using SQLSTORE + ds.Client = &databaseCache{SQLStore: ds.SQLStore} + + return nil +} + +// DistributedCache allows Grafana to cache data outside its own process +type DistributedCache struct { + log log.Logger + Client cacheStorage + SQLStore *sqlstore.SqlStore `inject:""` +} + +type Item struct { + Val interface{} + Created int64 + Expire int64 +} + +func EncodeGob(item *Item) ([]byte, error) { + buf := bytes.NewBuffer(nil) + err := gob.NewEncoder(buf).Encode(item) + return buf.Bytes(), err +} + +func DecodeGob(data []byte, out *Item) error { + buf := bytes.NewBuffer(data) + return gob.NewDecoder(buf).Decode(&out) +} + +type cacheStorage interface { + // Get reads object from Cache + Get(key string) (interface{}, error) + + // Puts an object into the cache + Put(key string, value interface{}, expire int64) error + + // Delete object from cache + Delete(key string) error +} diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go new file mode 100644 index 00000000000..88066daec7e --- /dev/null +++ b/pkg/infra/distcache/distcache_test.go @@ -0,0 +1,87 @@ +package distcache + +import ( + "encoding/gob" + "testing" + "time" + + "github.com/bmizerany/assert" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +type CacheableStruct struct { + String string + Int64 int64 +} + +func init() { + gob.Register(CacheableStruct{}) +} + +func createClient(t *testing.T) cacheStorage { + t.Helper() + + sqlstore := sqlstore.InitTestDB(t) + dc := DistributedCache{log: log.New("test.logger"), SQLStore: sqlstore} + dc.Init() + return dc.Client +} + +func TestCanPutIntoDatabaseStorage(t *testing.T) { + client := createClient(t) + cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + + err := client.Put("key", cacheableStruct, 1000) + assert.Equal(t, err, nil) + + data, err := client.Get("key") + s, ok := data.(CacheableStruct) + + assert.Equal(t, ok, true) + assert.Equal(t, s.String, "hej") + assert.Equal(t, s.Int64, int64(2000)) + + err = client.Delete("key") + assert.Equal(t, err, nil) + + _, err = client.Get("key") + assert.Equal(t, err, ErrCacheItemNotFound) +} + +func TestCanNotFetchExpiredItems(t *testing.T) { + client := createClient(t) + + cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + + // insert cache item one day back + getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } + err := client.Put("key", cacheableStruct, 10000) + assert.Equal(t, err, nil) + + // should not be able to read that value since its expired + getTime = time.Now + _, err = client.Get("key") + assert.Equal(t, err, ErrCacheItemNotFound) +} + +func TestCanSetInfiniteCacheExpiration(t *testing.T) { + client := createClient(t) + + cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + + // insert cache item one day back + getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } + err := client.Put("key", cacheableStruct, 0) + assert.Equal(t, err, nil) + + // should not be able to read that value since its expired + getTime = time.Now + data, err := client.Get("key") + s, ok := data.(CacheableStruct) + + assert.Equal(t, ok, true) + assert.Equal(t, s.String, "hej") + assert.Equal(t, s.Int64, int64(2000)) +} diff --git a/pkg/services/sqlstore/migrations/cache_data_mig.go b/pkg/services/sqlstore/migrations/cache_data_mig.go new file mode 100644 index 00000000000..1201b38e337 --- /dev/null +++ b/pkg/services/sqlstore/migrations/cache_data_mig.go @@ -0,0 +1,17 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addCacheMigration(mg *Migrator) { + var cacheDataV1 = Table{ + Name: "cache_data", + Columns: []*Column{ + {Name: "key", Type: DB_Char, IsPrimaryKey: true, Length: 16}, + {Name: "data", Type: DB_Blob}, + {Name: "expires", Type: DB_Integer, Length: 255, Nullable: false}, + {Name: "created_at", Type: DB_Integer, Length: 255, Nullable: false}, + }, + } + + mg.AddMigration("create cache_data table", NewAddTableMigration(cacheDataV1)) +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 931259ec3ed..3e40c749f37 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -33,6 +33,7 @@ func AddMigrations(mg *Migrator) { addUserAuthMigrations(mg) addServerlockMigrations(mg) addUserAuthTokenMigrations(mg) + addCacheMigration(mg) } func addMigrationLogMigrations(mg *Migrator) { From 996d5059b119a9927059812a5384edda7bf2a9d8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Feb 2019 09:48:32 +0100 Subject: [PATCH 005/194] test at interface level instead impl --- pkg/infra/distcache/distcache.go | 26 +++++++++++++++++----- pkg/infra/distcache/distcache_test.go | 32 +++++++++++++++------------ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index 11efd435de3..3a2d553953a 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -23,15 +23,31 @@ func init() { func (ds *DistributedCache) Init() error { ds.log = log.New("distributed.cache") - // memory - // redis - // memcache - // database. using SQLSTORE - ds.Client = &databaseCache{SQLStore: ds.SQLStore} + ds.Client = createClient(CacheOpts{}, ds.SQLStore) return nil } +type CacheOpts struct { + name string +} + +func createClient(opts CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { + if opts.name == "redis" { + return nil + } + + if opts.name == "memcache" { + return nil + } + + if opts.name == "memory" { + return nil + } + + return &databaseCache{SQLStore: sqlstore} +} + // DistributedCache allows Grafana to cache data outside its own process type DistributedCache struct { log log.Logger diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index 88066daec7e..d3009753a14 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -7,7 +7,6 @@ import ( "github.com/bmizerany/assert" - "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/sqlstore" ) @@ -20,20 +19,29 @@ func init() { gob.Register(CacheableStruct{}) } -func createClient(t *testing.T) cacheStorage { +func createTestClient(t *testing.T, name string) cacheStorage { t.Helper() sqlstore := sqlstore.InitTestDB(t) - dc := DistributedCache{log: log.New("test.logger"), SQLStore: sqlstore} - dc.Init() - return dc.Client + return createClient(CacheOpts{name: name}, sqlstore) } -func TestCanPutIntoDatabaseStorage(t *testing.T) { - client := createClient(t) +func TestAllCacheClients(t *testing.T) { + clients := []string{"database"} // add redis, memcache, memory + + for _, v := range clients { + client := createTestClient(t, v) + + CanPutGetAndDeleteCachedObjects(t, client) + CanNotFetchExpiredItems(t, client) + CanSetInfiniteCacheExpiration(t, client) + } +} + +func CanPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} - err := client.Put("key", cacheableStruct, 1000) + err := client.Put("key", cacheableStruct, 0) assert.Equal(t, err, nil) data, err := client.Get("key") @@ -50,9 +58,7 @@ func TestCanPutIntoDatabaseStorage(t *testing.T) { assert.Equal(t, err, ErrCacheItemNotFound) } -func TestCanNotFetchExpiredItems(t *testing.T) { - client := createClient(t) - +func CanNotFetchExpiredItems(t *testing.T, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} // insert cache item one day back @@ -66,9 +72,7 @@ func TestCanNotFetchExpiredItems(t *testing.T) { assert.Equal(t, err, ErrCacheItemNotFound) } -func TestCanSetInfiniteCacheExpiration(t *testing.T) { - client := createClient(t) - +func CanSetInfiniteCacheExpiration(t *testing.T, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} // insert cache item one day back From d99af239462cf015db935d2e34a6fd885f350dc0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Feb 2019 14:31:52 +0100 Subject: [PATCH 006/194] add garbage collector for database cache --- pkg/infra/distcache/database_storage.go | 36 +++++++++++-- pkg/infra/distcache/database_storage_test.go | 50 +++++++++++++++++++ .../sqlstore/migrations/cache_data_mig.go | 23 +++++---- 3 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 pkg/infra/distcache/database_storage_test.go diff --git a/pkg/infra/distcache/database_storage.go b/pkg/infra/distcache/database_storage.go index 8286f65fea6..ed55208e18d 100644 --- a/pkg/infra/distcache/database_storage.go +++ b/pkg/infra/distcache/database_storage.go @@ -3,18 +3,48 @@ package distcache import ( "time" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/sqlstore" ) type databaseCache struct { SQLStore *sqlstore.SqlStore + log log.Logger +} + +func newDatabaseCache(sqlstore *sqlstore.SqlStore) *databaseCache { + dc := &databaseCache{ + SQLStore: sqlstore, + log: log.New("distcache.database"), + } + + go dc.StartGC() + return dc } var getTime = time.Now -func (dc *databaseCache) Get(key string) (interface{}, error) { - //now := getTime().Unix() +func (dc *databaseCache) internalRunGC() { + now := getTime().Unix() + sql := `DELETE FROM cache_data WHERE (? - created) >= expire` + //EXTRACT(EPOCH FROM NOW()) - created >= expire + //UNIX_TIMESTAMP(NOW()) - created >= expire + _, err := dc.SQLStore.NewSession().Exec(sql, now) + if err != nil { + dc.log.Error("failed to run garbage collect", "error", err) + } +} + +func (dc *databaseCache) StartGC() { + dc.internalRunGC() + + time.AfterFunc(time.Second*10, func() { + dc.StartGC() + }) +} + +func (dc *databaseCache) Get(key string) (interface{}, error) { cacheHits := []CacheData{} err := dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) if err != nil { @@ -65,7 +95,7 @@ func (dc *databaseCache) Put(key string, value interface{}, expire int64) error } if len(cacheHits) > 0 { - _, err = dc.SQLStore.NewSession().Exec("UPDATE cached_data SET data=?, created=?, expire=? WHERE key=?", data, now, expire, key) + _, err = dc.SQLStore.NewSession().Exec("UPDATE cache_data SET data=?, created=?, expire=? WHERE key=?", data, now, expire, key) } else { _, err = dc.SQLStore.NewSession().Exec("INSERT INTO cache_data(key,data,created_at,expires) VALUES(?,?,?,?)", key, data, now, expire) } diff --git a/pkg/infra/distcache/database_storage_test.go b/pkg/infra/distcache/database_storage_test.go new file mode 100644 index 00000000000..2e6339c7c32 --- /dev/null +++ b/pkg/infra/distcache/database_storage_test.go @@ -0,0 +1,50 @@ +package distcache + +import ( + "testing" + "time" + + "github.com/bmizerany/assert" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func TestDatabaseStorageGarbageCollection(t *testing.T) { + sqlstore := sqlstore.InitTestDB(t) + + db := &databaseCache{ + SQLStore: sqlstore, + log: log.New("distcache.database"), + } + + obj := &CacheableStruct{String: "foolbar"} + + //set time.now to 2 weeks ago + getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } + db.Put("key1", obj, 1000) + db.Put("key2", obj, 1000) + db.Put("key3", obj, 1000) + + // insert object that should never expire + db.Put("key4", obj, 0) + + getTime = time.Now + db.Put("key5", obj, 1000) + + //run GC + db.internalRunGC() + + //try to read values + _, err := db.Get("key1") + assert.Equal(t, err, ErrCacheItemNotFound) + _, err = db.Get("key2") + assert.Equal(t, err, ErrCacheItemNotFound) + _, err = db.Get("key3") + assert.Equal(t, err, ErrCacheItemNotFound) + + _, err = db.Get("key4") + assert.Equal(t, err, nil) + _, err = db.Get("key5") + assert.Equal(t, err, nil) +} diff --git a/pkg/services/sqlstore/migrations/cache_data_mig.go b/pkg/services/sqlstore/migrations/cache_data_mig.go index 1201b38e337..f12f7f797c8 100644 --- a/pkg/services/sqlstore/migrations/cache_data_mig.go +++ b/pkg/services/sqlstore/migrations/cache_data_mig.go @@ -1,17 +1,22 @@ package migrations -import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" -func addCacheMigration(mg *Migrator) { - var cacheDataV1 = Table{ +func addCacheMigration(mg *migrator.Migrator) { + var cacheDataV1 = migrator.Table{ Name: "cache_data", - Columns: []*Column{ - {Name: "key", Type: DB_Char, IsPrimaryKey: true, Length: 16}, - {Name: "data", Type: DB_Blob}, - {Name: "expires", Type: DB_Integer, Length: 255, Nullable: false}, - {Name: "created_at", Type: DB_Integer, Length: 255, Nullable: false}, + Columns: []*migrator.Column{ + {Name: "key", Type: migrator.DB_NVarchar, IsPrimaryKey: true, Length: 168}, + {Name: "data", Type: migrator.DB_Blob}, + {Name: "expires", Type: migrator.DB_Integer, Length: 255, Nullable: false}, + {Name: "created_at", Type: migrator.DB_Integer, Length: 255, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"key"}, Type: migrator.UniqueIndex}, }, } - mg.AddMigration("create cache_data table", NewAddTableMigration(cacheDataV1)) + mg.AddMigration("create cache_data table", migrator.NewAddTableMigration(cacheDataV1)) + + mg.AddMigration("add unique index cache_data.key", migrator.NewAddIndexMigration(cacheDataV1, cacheDataV1.Indices[0])) } From 5ced863f7527a1eb366ff8d63df7ff78e6e4b51f Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 23 Feb 2019 16:12:37 +0100 Subject: [PATCH 007/194] add support for redis storage --- package.json | 5 -- pkg/infra/distcache/database_storage.go | 13 +++- pkg/infra/distcache/database_storage_test.go | 8 +- pkg/infra/distcache/distcache.go | 7 +- pkg/infra/distcache/distcache_test.go | 20 +++-- pkg/infra/distcache/redis_storage.go | 80 ++++++++++++++++++++ pkg/infra/distcache/redis_storage_test.go | 1 + 7 files changed, 110 insertions(+), 24 deletions(-) create mode 100644 pkg/infra/distcache/redis_storage.go create mode 100644 pkg/infra/distcache/redis_storage_test.go diff --git a/package.json b/package.json index a937ba6f717..af270d47ad0 100644 --- a/package.json +++ b/package.json @@ -142,11 +142,6 @@ "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p", "cli": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts" }, - "husky": { - "hooks": { - "pre-commit": "lint-staged && grunt precommit" - } - }, "lint-staged": { "*.{ts,tsx,json,scss}": [ "prettier --write", diff --git a/pkg/infra/distcache/database_storage.go b/pkg/infra/distcache/database_storage.go index ed55208e18d..cff5e0fc499 100644 --- a/pkg/infra/distcache/database_storage.go +++ b/pkg/infra/distcache/database_storage.go @@ -18,7 +18,7 @@ func newDatabaseCache(sqlstore *sqlstore.SqlStore) *databaseCache { log: log.New("distcache.database"), } - go dc.StartGC() + //go dc.StartGC() //TODO: start the GC somehow return dc } @@ -79,7 +79,7 @@ type CacheData struct { CreatedAt int64 } -func (dc *databaseCache) Put(key string, value interface{}, expire int64) error { +func (dc *databaseCache) Put(key string, value interface{}, expire time.Duration) error { item := &Item{Val: value} data, err := EncodeGob(item) if err != nil { @@ -94,10 +94,15 @@ func (dc *databaseCache) Put(key string, value interface{}, expire int64) error return err } + var expiresInEpoch int64 + if expire != 0 { + expiresInEpoch = int64(expire) / int64(time.Second) + } + if len(cacheHits) > 0 { - _, err = dc.SQLStore.NewSession().Exec("UPDATE cache_data SET data=?, created=?, expire=? WHERE key=?", data, now, expire, key) + _, err = dc.SQLStore.NewSession().Exec("UPDATE cache_data SET data=?, created=?, expire=? WHERE key=?", data, now, expiresInEpoch, key) } else { - _, err = dc.SQLStore.NewSession().Exec("INSERT INTO cache_data(key,data,created_at,expires) VALUES(?,?,?,?)", key, data, now, expire) + _, err = dc.SQLStore.NewSession().Exec("INSERT INTO cache_data(key,data,created_at,expires) VALUES(?,?,?,?)", key, data, now, expiresInEpoch) } return err diff --git a/pkg/infra/distcache/database_storage_test.go b/pkg/infra/distcache/database_storage_test.go index 2e6339c7c32..931fbc81c7f 100644 --- a/pkg/infra/distcache/database_storage_test.go +++ b/pkg/infra/distcache/database_storage_test.go @@ -22,15 +22,15 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) { //set time.now to 2 weeks ago getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - db.Put("key1", obj, 1000) - db.Put("key2", obj, 1000) - db.Put("key3", obj, 1000) + db.Put("key1", obj, 1000*time.Second) + db.Put("key2", obj, 1000*time.Second) + db.Put("key3", obj, 1000*time.Second) // insert object that should never expire db.Put("key4", obj, 0) getTime = time.Now - db.Put("key5", obj, 1000) + db.Put("key5", obj, 1000*time.Second) //run GC db.internalRunGC() diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index 3a2d553953a..a60b3d309c2 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/gob" "errors" + "time" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -34,7 +35,7 @@ type CacheOpts struct { func createClient(opts CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { if opts.name == "redis" { - return nil + return newRedisStorage(nil) } if opts.name == "memcache" { @@ -45,7 +46,7 @@ func createClient(opts CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { return nil } - return &databaseCache{SQLStore: sqlstore} + return newDatabaseCache(sqlstore) //&databaseCache{SQLStore: sqlstore} } // DistributedCache allows Grafana to cache data outside its own process @@ -77,7 +78,7 @@ type cacheStorage interface { Get(key string) (interface{}, error) // Puts an object into the cache - Put(key string, value interface{}, expire int64) error + Put(key string, value interface{}, expire time.Duration) error // Delete object from cache Delete(key string) error diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index d3009753a14..dd35744506c 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -27,18 +27,18 @@ func createTestClient(t *testing.T, name string) cacheStorage { } func TestAllCacheClients(t *testing.T) { - clients := []string{"database"} // add redis, memcache, memory + clients := []string{"database", "redis"} // add redis, memcache, memory for _, v := range clients { client := createTestClient(t, v) - CanPutGetAndDeleteCachedObjects(t, client) - CanNotFetchExpiredItems(t, client) - CanSetInfiniteCacheExpiration(t, client) + CanPutGetAndDeleteCachedObjects(t, v, client) + CanNotFetchExpiredItems(t, v, client) + CanSetInfiniteCacheExpiration(t, v, client) } } -func CanPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { +func CanPutGetAndDeleteCachedObjects(t *testing.T, name string, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} err := client.Put("key", cacheableStruct, 0) @@ -58,12 +58,16 @@ func CanPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { assert.Equal(t, err, ErrCacheItemNotFound) } -func CanNotFetchExpiredItems(t *testing.T, client cacheStorage) { +func CanNotFetchExpiredItems(t *testing.T, name string, client cacheStorage) { + if name == "redis" { + t.Skip() //this test does not work with redis since it uses its own getTime fn + } + cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} // insert cache item one day back getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - err := client.Put("key", cacheableStruct, 10000) + err := client.Put("key", cacheableStruct, 10000*time.Second) assert.Equal(t, err, nil) // should not be able to read that value since its expired @@ -72,7 +76,7 @@ func CanNotFetchExpiredItems(t *testing.T, client cacheStorage) { assert.Equal(t, err, ErrCacheItemNotFound) } -func CanSetInfiniteCacheExpiration(t *testing.T, client cacheStorage) { +func CanSetInfiniteCacheExpiration(t *testing.T, name string, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} // insert cache item one day back diff --git a/pkg/infra/distcache/redis_storage.go b/pkg/infra/distcache/redis_storage.go new file mode 100644 index 00000000000..06fc6931758 --- /dev/null +++ b/pkg/infra/distcache/redis_storage.go @@ -0,0 +1,80 @@ +package distcache + +import ( + "time" + + redis "gopkg.in/redis.v2" +) + +type redisStorage struct { + c *redis.Client +} + +func newRedisStorage(c *redis.Client) *redisStorage { + opt := &redis.Options{ + Network: "tcp", + Addr: "localhost:6379", + } + return &redisStorage{ + c: redis.NewClient(opt), + } +} + +// Set sets value to given key in session. +func (s *redisStorage) Put(key string, val interface{}, expires time.Duration) error { + item := &Item{Created: getTime().Unix(), Val: val} + value, err := EncodeGob(item) + if err != nil { + return err + } + + var status *redis.StatusCmd + if expires == 0 { + status = s.c.Set(key, string(value)) + } else { + status = s.c.SetEx(key, expires, string(value)) + } + + return status.Err() +} + +// Get gets value by given key in session. +func (s *redisStorage) Get(key string) (interface{}, error) { + v := s.c.Get(key) + + item := &Item{} + err := DecodeGob([]byte(v.Val()), item) + + if err == nil { + return item.Val, nil + } + + if err.Error() == "EOF" { + return nil, ErrCacheItemNotFound + } + + if err != nil { + return nil, err + } + + return item.Val, nil +} + +// Delete delete a key from session. +func (s *redisStorage) Delete(key string) error { + cmd := s.c.Del(key) + return cmd.Err() +} + +// RedisProvider represents a redis session provider implementation. +type RedisProvider struct { + c *redis.Client + duration time.Duration + prefix string +} + +// Exist returns true if session with given ID exists. +func (p *RedisProvider) Exist(sid string) bool { + has, err := p.c.Exists(p.prefix + sid).Result() + return err == nil && has +} diff --git a/pkg/infra/distcache/redis_storage_test.go b/pkg/infra/distcache/redis_storage_test.go new file mode 100644 index 00000000000..e793fbec4c4 --- /dev/null +++ b/pkg/infra/distcache/redis_storage_test.go @@ -0,0 +1 @@ +package distcache From 11d671c637fea63b74d9082a907b0a97f424e6e0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 23 Feb 2019 18:28:33 +0100 Subject: [PATCH 008/194] add support for memcached --- pkg/infra/distcache/distcache.go | 8 +-- pkg/infra/distcache/distcache_test.go | 2 +- pkg/infra/distcache/memcached_storage.go | 62 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 pkg/infra/distcache/memcached_storage.go diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index a60b3d309c2..8a6f7daf90c 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -39,12 +39,12 @@ func createClient(opts CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { } if opts.name == "memcache" { - return nil + return newMemcacheStorage("localhost:9090") } - if opts.name == "memory" { - return nil - } + // if opts.name == "memory" { + // return nil + // } return newDatabaseCache(sqlstore) //&databaseCache{SQLStore: sqlstore} } diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index dd35744506c..a04b5d0228f 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -27,7 +27,7 @@ func createTestClient(t *testing.T, name string) cacheStorage { } func TestAllCacheClients(t *testing.T) { - clients := []string{"database", "redis"} // add redis, memcache, memory + clients := []string{"database", "redis", "memcached"} // add redis, memcache, memory for _, v := range clients { client := createTestClient(t, v) diff --git a/pkg/infra/distcache/memcached_storage.go b/pkg/infra/distcache/memcached_storage.go new file mode 100644 index 00000000000..44fbbcc33c6 --- /dev/null +++ b/pkg/infra/distcache/memcached_storage.go @@ -0,0 +1,62 @@ +package distcache + +import ( + "time" + + "github.com/bradfitz/gomemcache/memcache" +) + +type memcacheStorage struct { + c *memcache.Client +} + +func newMemcacheStorage(connStr string) *memcacheStorage { + return &memcacheStorage{ + c: memcache.New(connStr), + } +} + +func NewItem(sid string, data []byte, expire int32) *memcache.Item { + return &memcache.Item{ + Key: sid, + Value: data, + Expiration: expire, + } +} + +// Set sets value to given key in the cache. +func (s *memcacheStorage) Put(key string, val interface{}, expires time.Duration) error { + item := &Item{Val: val} + + bytes, err := EncodeGob(item) + if err != nil { + return err + } + + memcacheItem := NewItem(key, bytes, int32(expires)) + + s.c.Add(memcacheItem) + return nil +} + +// Get gets value by given key in the cache. +func (s *memcacheStorage) Get(key string) (interface{}, error) { + i, err := s.c.Get(key) + if err != nil { + return nil, err + } + + item := &Item{} + + err = DecodeGob(i.Value, item) + if err != nil { + return nil, err + } + + return item.Val, nil +} + +// Delete delete a key from the cache +func (s *memcacheStorage) Delete(key string) error { + return s.c.Delete(key) +} From 3890bd14ebebe9518c33d99727b7267f33e3765b Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 23 Feb 2019 22:59:12 +0100 Subject: [PATCH 009/194] fixes typo in redis devenv --- devenv/docker/blocks/redis/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devenv/docker/blocks/redis/docker-compose.yaml b/devenv/docker/blocks/redis/docker-compose.yaml index 65071d4966b..fb56afaac1c 100644 --- a/devenv/docker/blocks/redis/docker-compose.yaml +++ b/devenv/docker/blocks/redis/docker-compose.yaml @@ -1,4 +1,4 @@ - memcached: + redis: image: redis:latest ports: - "6379:6379" From 188dc862465e3e4251e6b59efb1eed504ff778a5 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 8 Mar 2019 00:41:35 -0800 Subject: [PATCH 010/194] remove type field and add helper functions to check if data isTableData --- packages/grafana-ui/src/types/data.ts | 1 - packages/grafana-ui/src/utils/processTimeSeries.ts | 6 +++--- public/app/plugins/panel/singlestat/module.ts | 4 ++-- public/app/plugins/panel/table/module.ts | 3 ++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts index 1ea89bcd28e..21b863049fc 100644 --- a/packages/grafana-ui/src/types/data.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -63,5 +63,4 @@ export interface Column { export interface TableData { columns: Column[]; rows: any[]; - type: string; } diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 3b2d1bd05aa..e7582c9f13c 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -191,6 +191,8 @@ export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Opt return vmSeries; } +export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); + export const toTableData = (results: any[]): TableData[] => { const tables: TableData[] = []; if (results) { @@ -202,15 +204,13 @@ export const toTableData = (results: any[]): TableData[] => { } else if (data.hasOwnProperty('datapoints')) { const ts = data as TimeSeries; tables.push({ - type: 'timeseries', columns: [ { text: ts.target, unit: ts.unit, - type: 'number', // Is this really true? }, { - text: 'time', + text: 'Time', type: 'time', }, ], diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index bcf09297cf7..5b75de44949 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import { MetricsPanelCtrl } from 'app/plugins/sdk'; -import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui'; +import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName, isTableData } from '@grafana/ui'; class SingleStatCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -112,7 +112,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { scopedVars: _.extend({}, this.panel.scopedVars), }; - if (dataList.length > 0 && dataList[0].type === 'table') { + if (dataList.length > 0 && isTableData(dataList[0])) { this.dataType = 'table'; const tableData = dataList.map(this.tableHandler.bind(this)); this.setTableValues(tableData, data); diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 268f5aa7ac4..b7b3c0312a3 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -6,6 +6,7 @@ import { transformDataToTable } from './transformers'; import { tablePanelEditor } from './editor'; import { columnOptionsTab } from './column_options'; import { TableRenderer } from './renderer'; +import { isTableData } from '@grafana/ui'; class TablePanelCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -104,7 +105,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { // automatically correct transform mode based on data if (this.dataRaw && this.dataRaw.length) { - if (this.dataRaw[0].type === 'table') { + if (isTableData(this.dataRaw[0])) { this.panel.transform = 'table'; } else { if (this.dataRaw[0].type === 'docs') { From 229dff757c310438bb3706c96d0116974ca6515c Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 8 Mar 2019 02:14:35 -0800 Subject: [PATCH 011/194] less nesting and add test --- .../src/utils/processTimeSeries.test.ts | 31 ++++++++++ .../grafana-ui/src/utils/processTimeSeries.ts | 60 +++++++++---------- 2 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 packages/grafana-ui/src/utils/processTimeSeries.test.ts diff --git a/packages/grafana-ui/src/utils/processTimeSeries.test.ts b/packages/grafana-ui/src/utils/processTimeSeries.test.ts new file mode 100644 index 00000000000..fe291898c55 --- /dev/null +++ b/packages/grafana-ui/src/utils/processTimeSeries.test.ts @@ -0,0 +1,31 @@ +import { toTableData } from './processTimeSeries'; + +describe('toTableData', () => { + it('converts timeseries to table skipping nulls', () => { + const input = { + target: 'Field Name', + datapoints: [[100, 1], [200, 2]], + }; + const data = toTableData([null, input, null, null]); + expect(data.length).toBe(1); + expect(data[0].columns[0].text).toBe(input.target); + expect(data[0].rows).toBe(input.datapoints); + }); + + it('keeps tableData unchanged', () => { + const input = { + columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }], + rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]], + }; + const data = toTableData([null, input, null, null]); + expect(data.length).toBe(1); + expect(data[0]).toBe(input); + }); + + it('supports null values OK', () => { + expect(toTableData([null, null, null, null])).toEqual([]); + expect(toTableData(undefined)).toEqual([]); + expect(toTableData((null as unknown) as any[])).toEqual([]); + expect(toTableData([])).toEqual([]); + }); +}); diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index e7582c9f13c..112bd67f481 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -193,35 +193,35 @@ export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Opt export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); -export const toTableData = (results: any[]): TableData[] => { - const tables: TableData[] = []; - if (results) { - for (let i = 0; i < results.length; i++) { - const data = results[i]; - if (data) { - if (data.hasOwnProperty('columns')) { - tables.push(data as TableData); - } else if (data.hasOwnProperty('datapoints')) { - const ts = data as TimeSeries; - tables.push({ - columns: [ - { - text: ts.target, - unit: ts.unit, - }, - { - text: 'Time', - type: 'time', - }, - ], - rows: ts.datapoints, - } as TableData); - } else { - console.warn('Can not convert', data); - throw new Error('Unsupported data format'); - } - } - } +export const toTableData = (results?: any[]): TableData[] => { + if (!results) { + return []; } - return tables; + + return results + .filter(d => !!d) + .map(data => { + if (data.hasOwnProperty('columns')) { + return data as TableData; + } + if (data.hasOwnProperty('datapoints')) { + const ts = data as TimeSeries; + return { + columns: [ + { + text: ts.target || 'Timeseries', + unit: ts.unit, + }, + { + text: 'Time', + type: 'time', + }, + ], + rows: ts.datapoints, + } as TableData; + } + // TODO, try to convert JSON to table? + console.warn('Can not convert', data); + throw new Error('Unsupported data format'); + }); }; From 8029e48588215b5ec4a54c60866ba994bf036cf2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 8 Mar 2019 15:15:17 +0100 Subject: [PATCH 012/194] support get user tokens/revoke all user tokens in UserTokenService --- pkg/middleware/middleware_test.go | 67 +++-------------------- pkg/middleware/org_redirect_test.go | 4 +- pkg/middleware/quota_test.go | 5 +- pkg/middleware/recovery_test.go | 3 +- pkg/models/user_token.go | 3 ++ pkg/services/auth/auth_token.go | 51 ++++++++++++++++++ pkg/services/auth/auth_token_test.go | 41 ++++++++++++++ pkg/services/auth/testing.go | 81 ++++++++++++++++++++++++++++ 8 files changed, 190 insertions(+), 65 deletions(-) create mode 100644 pkg/services/auth/testing.go diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 1fbd303bebd..2fc8e0c456f 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -11,6 +11,7 @@ import ( msession "github.com/go-macaron/session" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -155,7 +156,7 @@ func TestMiddlewareContext(t *testing.T) { return nil }) - sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 12, UnhashedToken: unhashedToken, @@ -184,14 +185,14 @@ func TestMiddlewareContext(t *testing.T) { return nil }) - sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 12, UnhashedToken: "", }, nil } - sc.userAuthTokenService.tryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) { + sc.userAuthTokenService.TryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) { userToken.UnhashedToken = "rotated" return true, nil } @@ -226,7 +227,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario("Invalid/expired auth token in cookie", func(sc *scenarioContext) { sc.withTokenSessionCookie("token") - sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { return nil, m.ErrUserTokenNotFound } @@ -562,7 +563,7 @@ func middlewareScenario(desc string, fn scenarioFunc) { })) session.Init(&msession.Options{}, 0) - sc.userAuthTokenService = newFakeUserAuthTokenService() + sc.userAuthTokenService = auth.NewFakeUserAuthTokenService() sc.m.Use(GetContextHandler(sc.userAuthTokenService)) // mock out gc goroutine session.StartSessionGC = func() {} @@ -595,7 +596,7 @@ type scenarioContext struct { handlerFunc handlerFunc defaultHandler macaron.Handler url string - userAuthTokenService *fakeUserAuthTokenService + userAuthTokenService *auth.FakeUserAuthTokenService req *http.Request } @@ -676,57 +677,3 @@ func (sc *scenarioContext) exec() { type scenarioFunc func(c *scenarioContext) type handlerFunc func(c *m.ReqContext) - -type fakeUserAuthTokenService struct { - createTokenProvider func(userId int64, clientIP, userAgent string) (*m.UserToken, error) - tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error) - lookupTokenProvider func(unhashedToken string) (*m.UserToken, error) - revokeTokenProvider func(token *m.UserToken) error - activeAuthTokenCount func() (int64, error) -} - -func newFakeUserAuthTokenService() *fakeUserAuthTokenService { - return &fakeUserAuthTokenService{ - createTokenProvider: func(userId int64, clientIP, userAgent string) (*m.UserToken, error) { - return &m.UserToken{ - UserId: 0, - UnhashedToken: "", - }, nil - }, - tryRotateTokenProvider: func(token *m.UserToken, clientIP, userAgent string) (bool, error) { - return false, nil - }, - lookupTokenProvider: func(unhashedToken string) (*m.UserToken, error) { - return &m.UserToken{ - UserId: 0, - UnhashedToken: "", - }, nil - }, - revokeTokenProvider: func(token *m.UserToken) error { - return nil - }, - activeAuthTokenCount: func() (int64, error) { - return 10, nil - }, - } -} - -func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*m.UserToken, error) { - return s.createTokenProvider(userId, clientIP, userAgent) -} - -func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (*m.UserToken, error) { - return s.lookupTokenProvider(unhashedToken) -} - -func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP, userAgent string) (bool, error) { - return s.tryRotateTokenProvider(token, clientIP, userAgent) -} - -func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error { - return s.revokeTokenProvider(token) -} - -func (s *fakeUserAuthTokenService) ActiveTokenCount() (int64, error) { - return s.activeAuthTokenCount() -} diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index e01d1a68d21..fe5b2736035 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -24,7 +24,7 @@ func TestOrgRedirectMiddleware(t *testing.T) { return nil }) - sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 0, UnhashedToken: "", @@ -50,7 +50,7 @@ func TestOrgRedirectMiddleware(t *testing.T) { return nil }) - sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 12, UnhashedToken: "", diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index 52b696cf037..0ba42e708bc 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -3,6 +3,7 @@ package middleware import ( "testing" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/bus" @@ -36,7 +37,7 @@ func TestMiddlewareQuota(t *testing.T) { }, } - fakeAuthTokenService := newFakeUserAuthTokenService() + fakeAuthTokenService := auth.NewFakeUserAuthTokenService() qs := "a.QuotaService{ AuthTokenService: fakeAuthTokenService, } @@ -87,7 +88,7 @@ func TestMiddlewareQuota(t *testing.T) { return nil }) - sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 12, UnhashedToken: "", diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 6736d699a39..00f3b7a3032 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" macaron "gopkg.in/macaron.v1" @@ -62,7 +63,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) { Delims: macaron.Delims{Left: "[[", Right: "]]"}, })) - sc.userAuthTokenService = newFakeUserAuthTokenService() + sc.userAuthTokenService = auth.NewFakeUserAuthTokenService() sc.m.Use(GetContextHandler(sc.userAuthTokenService)) // mock out gc goroutine sc.m.Use(OrgRedirect()) diff --git a/pkg/models/user_token.go b/pkg/models/user_token.go index 388bc2dd4a2..22f92cb21d2 100644 --- a/pkg/models/user_token.go +++ b/pkg/models/user_token.go @@ -29,5 +29,8 @@ type UserTokenService interface { LookupToken(unhashedToken string) (*UserToken, error) TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error) RevokeToken(token *UserToken) error + RevokeAllUserTokens(userId int64) error ActiveTokenCount() (int64, error) + GetUserToken(userId, userTokenId int64) (*UserToken, error) + GetUserTokens(userId int64) ([]*UserToken, error) } diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 648575d54cd..255866a9ba0 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -221,6 +221,57 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error { return nil } +func (s *UserAuthTokenService) RevokeAllUserTokens(userId int64) error { + sql := `DELETE from user_auth_token WHERE user_id = ?` + res, err := s.SQLStore.NewSession().Exec(sql, userId) + if err != nil { + return err + } + + affected, err := res.RowsAffected() + if err != nil { + return err + } + + s.log.Debug("all user tokens for user revoked", "userId", userId, "count", affected) + + return nil +} + +func (s *UserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) { + var token userAuthToken + exists, err := s.SQLStore.NewSession().Where("id = ? AND user_id = ?", userTokenId, userId).Get(&token) + if err != nil { + return nil, err + } + + if !exists { + return nil, models.ErrUserTokenNotFound + } + + var result models.UserToken + token.toUserToken(&result) + + return &result, nil +} + +func (s *UserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) { + var tokens []*userAuthToken + err := s.SQLStore.NewSession().Where("user_id = ? AND created_at > ? AND rotated_at > ?", userId, s.createdAfterParam(), s.rotatedAfterParam()).Find(&tokens) + if err != nil { + return nil, err + } + + result := []*models.UserToken{} + for _, token := range tokens { + var userToken models.UserToken + token.toUserToken(&userToken) + result = append(result, &userToken) + } + + return result, nil +} + func (s *UserAuthTokenService) createdAfterParam() int64 { tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour return getTime().Add(-tokenMaxLifetime).Unix() diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 49e7acc3a5b..33eb309ad18 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -75,6 +75,47 @@ func TestUserAuthToken(t *testing.T) { err = userAuthTokenService.RevokeToken(userToken) So(err, ShouldEqual, models.ErrUserTokenNotFound) }) + + Convey("When creating an additional token", func() { + userToken2, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + So(userToken2, ShouldNotBeNil) + + Convey("Can get first user token", func() { + token, err := userAuthTokenService.GetUserToken(userID, userToken.Id) + So(err, ShouldBeNil) + So(token, ShouldNotBeNil) + So(token.Id, ShouldEqual, userToken.Id) + }) + + Convey("Can get second user token", func() { + token, err := userAuthTokenService.GetUserToken(userID, userToken2.Id) + So(err, ShouldBeNil) + So(token, ShouldNotBeNil) + So(token.Id, ShouldEqual, userToken2.Id) + }) + + Convey("Can get user tokens", func() { + tokens, err := userAuthTokenService.GetUserTokens(userID) + So(err, ShouldBeNil) + So(tokens, ShouldHaveLength, 2) + So(tokens[0].Id, ShouldEqual, userToken.Id) + So(tokens[1].Id, ShouldEqual, userToken2.Id) + }) + + Convey("Can revoke all user tokens", func() { + err := userAuthTokenService.RevokeAllUserTokens(userID) + So(err, ShouldBeNil) + + model, err := ctx.getAuthTokenByID(userToken.Id) + So(err, ShouldBeNil) + So(model, ShouldBeNil) + + model2, err := ctx.getAuthTokenByID(userToken2.Id) + So(err, ShouldBeNil) + So(model2, ShouldBeNil) + }) + }) }) Convey("expires correctly", func() { diff --git a/pkg/services/auth/testing.go b/pkg/services/auth/testing.go new file mode 100644 index 00000000000..68e65466c3d --- /dev/null +++ b/pkg/services/auth/testing.go @@ -0,0 +1,81 @@ +package auth + +import "github.com/grafana/grafana/pkg/models" + +type FakeUserAuthTokenService struct { + CreateTokenProvider func(userId int64, clientIP, userAgent string) (*models.UserToken, error) + TryRotateTokenProvider func(token *models.UserToken, clientIP, userAgent string) (bool, error) + LookupTokenProvider func(unhashedToken string) (*models.UserToken, error) + RevokeTokenProvider func(token *models.UserToken) error + RevokeAllUserTokensProvider func(userId int64) error + ActiveAuthTokenCount func() (int64, error) + GetUserTokenProvider func(userId, userTokenId int64) (*models.UserToken, error) + GetUserTokensProvider func(userId int64) ([]*models.UserToken, error) +} + +func NewFakeUserAuthTokenService() *FakeUserAuthTokenService { + return &FakeUserAuthTokenService{ + CreateTokenProvider: func(userId int64, clientIP, userAgent string) (*models.UserToken, error) { + return &models.UserToken{ + UserId: 0, + UnhashedToken: "", + }, nil + }, + TryRotateTokenProvider: func(token *models.UserToken, clientIP, userAgent string) (bool, error) { + return false, nil + }, + LookupTokenProvider: func(unhashedToken string) (*models.UserToken, error) { + return &models.UserToken{ + UserId: 0, + UnhashedToken: "", + }, nil + }, + RevokeTokenProvider: func(token *models.UserToken) error { + return nil + }, + RevokeAllUserTokensProvider: func(userId int64) error { + return nil + }, + ActiveAuthTokenCount: func() (int64, error) { + return 10, nil + }, + GetUserTokenProvider: func(userId, userTokenId int64) (*models.UserToken, error) { + return nil, nil + }, + GetUserTokensProvider: func(userId int64) ([]*models.UserToken, error) { + return nil, nil + }, + } +} + +func (s *FakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) { + return s.CreateTokenProvider(userId, clientIP, userAgent) +} + +func (s *FakeUserAuthTokenService) LookupToken(unhashedToken string) (*models.UserToken, error) { + return s.LookupTokenProvider(unhashedToken) +} + +func (s *FakeUserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, userAgent string) (bool, error) { + return s.TryRotateTokenProvider(token, clientIP, userAgent) +} + +func (s *FakeUserAuthTokenService) RevokeToken(token *models.UserToken) error { + return s.RevokeTokenProvider(token) +} + +func (s *FakeUserAuthTokenService) RevokeAllUserTokens(userId int64) error { + return s.RevokeAllUserTokensProvider(userId) +} + +func (s *FakeUserAuthTokenService) ActiveTokenCount() (int64, error) { + return s.ActiveAuthTokenCount() +} + +func (s *FakeUserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) { + return s.GetUserTokenProvider(userId, userTokenId) +} + +func (s *FakeUserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) { + return s.GetUserTokensProvider(userId) +} From 0cd5a6772d188fa5bf1ada0c1cb0e7597f3579ac Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 8 Mar 2019 15:15:38 +0100 Subject: [PATCH 013/194] feat(api): support list/revoke auth token in admin/current user api --- pkg/api/admin_users.go | 23 +++ pkg/api/admin_users_test.go | 138 +++++++++++++++++ pkg/api/api.go | 7 + pkg/api/common_test.go | 16 +- pkg/api/dtos/user_token.go | 12 ++ pkg/api/user_token.go | 110 ++++++++++++++ pkg/api/user_token_test.go | 294 ++++++++++++++++++++++++++++++++++++ pkg/models/user_token.go | 8 +- 8 files changed, 600 insertions(+), 8 deletions(-) create mode 100644 pkg/api/dtos/user_token.go create mode 100644 pkg/api/user_token.go create mode 100644 pkg/api/user_token_test.go diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index c16c2f126f8..4ad8a2b84ab 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -110,3 +110,26 @@ func AdminDeleteUser(c *m.ReqContext) { c.JsonOK("User deleted") } + +// POST /api/admin/users/:id/logout +func (server *HTTPServer) AdminLogoutUser(c *m.ReqContext) Response { + userID := c.ParamsInt64(":id") + + if c.UserId == userID { + return Error(400, "You cannot logout yourself", nil) + } + + return server.logoutUserFromAllDevicesInternal(userID) +} + +// GET /api/admin/users/:id/auth-tokens +func (server *HTTPServer) AdminGetUserAuthTokens(c *m.ReqContext) Response { + userID := c.ParamsInt64(":id") + return server.getUserAuthTokensInternal(c, userID) +} + +// POST /api/admin/users/:id/revoke-auth-token +func (server *HTTPServer) AdminRevokeUserAuthToken(c *m.ReqContext, cmd m.RevokeAuthTokenCmd) Response { + userID := c.ParamsInt64(":id") + return server.revokeUserAuthTokenInternal(c, userID, cmd) +} diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index 0b94a64b3fb..b94f09b0b75 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" . "github.com/smartystreets/goconvey/convey" ) @@ -27,6 +28,62 @@ func TestAdminApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 400) }) }) + + Convey("When a server admin attempts to logout himself from all devices", t, func() { + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + cmd.Result = &m.User{Id: TestUserID} + return nil + }) + + adminLogoutUserScenario("Should not be allowed when calling POST on", "/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 400) + }) + }) + + Convey("When a server admin attempts to logout a non-existing user from all devices", t, func() { + userId := int64(0) + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + userId = cmd.Id + return m.ErrUserNotFound + }) + + adminLogoutUserScenario("Should return not found when calling POST on", "/api/admin/users/200/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 404) + So(userId, ShouldEqual, 200) + }) + }) + + Convey("When a server admin attempts to revoke an auth token for a non-existing user", t, func() { + userId := int64(0) + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + userId = cmd.Id + return m.ErrUserNotFound + }) + + cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2} + + adminRevokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/admin/users/200/revoke-auth-token", "/api/admin/users/:id/revoke-auth-token", cmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 404) + So(userId, ShouldEqual, 200) + }) + }) + + Convey("When a server admin gets auth tokens for a non-existing user", t, func() { + userId := int64(0) + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + userId = cmd.Id + return m.ErrUserNotFound + }) + + adminGetUserAuthTokensScenario("Should return not found when calling GET on", "/api/admin/users/200/auth-tokens", "/api/admin/users/:id/auth-tokens", func(sc *scenarioContext) { + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 404) + So(userId, ShouldEqual, 200) + }) + }) } func putAdminScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) { @@ -48,3 +105,84 @@ func putAdminScenario(desc string, url string, routePattern string, role m.RoleT fn(sc) }) } + +func adminLogoutUserScenario(desc string, url string, routePattern string, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + hs := HTTPServer{ + Bus: bus.GetBus(), + AuthTokenService: auth.NewFakeUserAuthTokenService(), + } + + sc := setupScenarioContext(url) + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = m.ROLE_ADMIN + + return hs.AdminLogoutUser(c) + }) + + sc.m.Post(routePattern, sc.defaultHandler) + + fn(sc) + }) +} + +func adminRevokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd m.RevokeAuthTokenCmd, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + + hs := HTTPServer{ + Bus: bus.GetBus(), + AuthTokenService: fakeAuthTokenService, + } + + sc := setupScenarioContext(url) + sc.userAuthTokenService = fakeAuthTokenService + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = m.ROLE_ADMIN + + return hs.AdminRevokeUserAuthToken(c, cmd) + }) + + sc.m.Post(routePattern, sc.defaultHandler) + + fn(sc) + }) +} + +func adminGetUserAuthTokensScenario(desc string, url string, routePattern string, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + + hs := HTTPServer{ + Bus: bus.GetBus(), + AuthTokenService: fakeAuthTokenService, + } + + sc := setupScenarioContext(url) + sc.userAuthTokenService = fakeAuthTokenService + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = m.ROLE_ADMIN + + return hs.AdminGetUserAuthTokens(c) + }) + + sc.m.Get(routePattern, sc.defaultHandler) + + fn(sc) + }) +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 81ea83eae61..f3dc35b6b06 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -133,6 +133,9 @@ func (hs *HTTPServer) registerRoutes() { userRoute.Get("/preferences", Wrap(GetUserPreferences)) userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences)) + + userRoute.Get("/auth-tokens", Wrap(hs.GetUserAuthTokens)) + userRoute.Post("/revoke-auth-token", bind(m.RevokeAuthTokenCmd{}), Wrap(hs.RevokeUserAuthToken)) }) // users (admin permission required) @@ -375,6 +378,10 @@ func (hs *HTTPServer) registerRoutes() { adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota)) adminRoute.Get("/stats", AdminGetStats) adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts)) + + adminRoute.Post("/users/:id/logout", Wrap(hs.AdminLogoutUser)) + adminRoute.Get("/users/:id/auth-tokens", Wrap(hs.AdminGetUserAuthTokens)) + adminRoute.Post("/users/:id/revoke-auth-token", bind(m.RevokeAuthTokenCmd{}), Wrap(hs.AdminRevokeUserAuthToken)) }, reqGrafanaAdmin) // rendering diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 3f3a50aae69..4e0b0dcd998 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "gopkg.in/macaron.v1" . "github.com/smartystreets/goconvey/convey" @@ -94,13 +95,14 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map } type scenarioContext struct { - m *macaron.Macaron - context *m.ReqContext - resp *httptest.ResponseRecorder - handlerFunc handlerFunc - defaultHandler macaron.Handler - req *http.Request - url string + m *macaron.Macaron + context *m.ReqContext + resp *httptest.ResponseRecorder + handlerFunc handlerFunc + defaultHandler macaron.Handler + req *http.Request + url string + userAuthTokenService *auth.FakeUserAuthTokenService } func (sc *scenarioContext) exec() { diff --git a/pkg/api/dtos/user_token.go b/pkg/api/dtos/user_token.go new file mode 100644 index 00000000000..1542421e2f6 --- /dev/null +++ b/pkg/api/dtos/user_token.go @@ -0,0 +1,12 @@ +package dtos + +import "time" + +type UserToken struct { + Id int64 `json:"id"` + IsActive bool `json:"isActive"` + ClientIp string `json:"clientIp"` + UserAgent string `json:"userAgent"` + CreatedAt time.Time `json:"createdAt"` + SeenAt time.Time `json:"seenAt"` +} diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go new file mode 100644 index 00000000000..2f74eedea5d --- /dev/null +++ b/pkg/api/user_token.go @@ -0,0 +1,110 @@ +package api + +import ( + "time" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" +) + +// GET /api/user/auth-tokens +func (server *HTTPServer) GetUserAuthTokens(c *models.ReqContext) Response { + return server.getUserAuthTokensInternal(c, c.UserId) +} + +// POST /api/user/revoke-auth-token +func (server *HTTPServer) RevokeUserAuthToken(c *models.ReqContext, cmd models.RevokeAuthTokenCmd) Response { + return server.revokeUserAuthTokenInternal(c, c.UserId, cmd) +} + +func (server *HTTPServer) logoutUserFromAllDevicesInternal(userID int64) Response { + userQuery := models.GetUserByIdQuery{Id: userID} + + if err := bus.Dispatch(&userQuery); err != nil { + if err == models.ErrUserNotFound { + return Error(404, "User not found", err) + } + return Error(500, "Could not read user from database", err) + } + + err := server.AuthTokenService.RevokeAllUserTokens(userID) + if err != nil { + return Error(500, "Failed to logout user", err) + } + + return JSON(200, util.DynMap{ + "message": "User logged out", + }) +} + +func (server *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int64) Response { + userQuery := models.GetUserByIdQuery{Id: userID} + + if err := bus.Dispatch(&userQuery); err != nil { + if err == models.ErrUserNotFound { + return Error(404, "User not found", err) + } + return Error(500, "Failed to get user", err) + } + + tokens, err := server.AuthTokenService.GetUserTokens(userID) + if err != nil { + return Error(500, "Failed to get user auth tokens", err) + } + + result := []*dtos.UserToken{} + for _, token := range tokens { + isActive := false + if c.UserToken != nil && c.UserToken.Id == token.Id { + isActive = true + } + + result = append(result, &dtos.UserToken{ + Id: token.Id, + IsActive: isActive, + ClientIp: token.ClientIp, + UserAgent: token.UserAgent, + CreatedAt: time.Unix(token.CreatedAt, 0), + SeenAt: time.Unix(token.SeenAt, 0), + }) + } + + return JSON(200, result) +} + +func (server *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd models.RevokeAuthTokenCmd) Response { + userQuery := models.GetUserByIdQuery{Id: userID} + + if err := bus.Dispatch(&userQuery); err != nil { + if err == models.ErrUserNotFound { + return Error(404, "User not found", err) + } + return Error(500, "Failed to get user", err) + } + + token, err := server.AuthTokenService.GetUserToken(userID, cmd.AuthTokenId) + if err != nil { + if err == models.ErrUserTokenNotFound { + return Error(404, "User auth token not found", err) + } + return Error(500, "Failed to get user auth token", err) + } + + if c.UserToken != nil && c.UserToken.Id == token.Id { + return Error(400, "Cannot revoke active user auth token", nil) + } + + err = server.AuthTokenService.RevokeToken(token) + if err != nil { + if err == models.ErrUserTokenNotFound { + return Error(404, "User auth token not found", err) + } + return Error(500, "Failed to revoke user auth token", err) + } + + return JSON(200, util.DynMap{ + "message": "User auth token revoked", + }) +} diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go new file mode 100644 index 00000000000..111070dca92 --- /dev/null +++ b/pkg/api/user_token_test.go @@ -0,0 +1,294 @@ +package api + +import ( + "testing" + "time" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestUserTokenApiEndpoint(t *testing.T) { + Convey("When current user attempts to revoke an auth token for a non-existing user", t, func() { + userId := int64(0) + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + userId = cmd.Id + return m.ErrUserNotFound + }) + + cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2} + + revokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/user/revoke-auth-token", "/api/user/revoke-auth-token", cmd, 200, func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 404) + So(userId, ShouldEqual, 200) + }) + }) + + Convey("When current user gets auth tokens for a non-existing user", t, func() { + userId := int64(0) + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + userId = cmd.Id + return m.ErrUserNotFound + }) + + getUserAuthTokensScenario("Should return not found when calling GET on", "/api/user/auth-tokens", "/api/user/auth-tokens", 200, func(sc *scenarioContext) { + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 404) + So(userId, ShouldEqual, 200) + }) + }) + + Convey("When logout an existing user from all devices", t, func() { + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + cmd.Result = &m.User{Id: 200} + return nil + }) + + logoutUserFromAllDevicesInternalScenario("Should be successful", 1, func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + + Convey("When logout a non-existing user from all devices", t, func() { + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + return m.ErrUserNotFound + }) + + logoutUserFromAllDevicesInternalScenario("Should return not found", TestUserID, func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 404) + }) + }) + + Convey("When revoke an auth token for a user", t, func() { + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + cmd.Result = &m.User{Id: 200} + return nil + }) + + cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2} + token := &m.UserToken{Id: 1} + + revokeUserAuthTokenInternalScenario("Should be successful", cmd, 200, token, func(sc *scenarioContext) { + sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) { + return &m.UserToken{Id: 2}, nil + } + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + + Convey("When revoke the active auth token used by himself", t, func() { + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + cmd.Result = &m.User{Id: TestUserID} + return nil + }) + + cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2} + token := &m.UserToken{Id: 2} + + revokeUserAuthTokenInternalScenario("Should not be successful", cmd, TestUserID, token, func(sc *scenarioContext) { + sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) { + return token, nil + } + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 400) + }) + }) + + Convey("When gets auth tokens for a user", t, func() { + bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error { + cmd.Result = &m.User{Id: TestUserID} + return nil + }) + + currentToken := &m.UserToken{Id: 1} + + getUserAuthTokensInternalScenario("Should be successful", currentToken, func(sc *scenarioContext) { + tokens := []*m.UserToken{ + { + Id: 1, + ClientIp: "127.0.0.1", + UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36", + CreatedAt: time.Now().Unix(), + SeenAt: time.Now().Unix(), + }, + { + Id: 2, + ClientIp: "127.0.0.2", + UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", + CreatedAt: time.Now().Unix(), + SeenAt: time.Now().Unix(), + }, + } + sc.userAuthTokenService.GetUserTokensProvider = func(userId int64) ([]*m.UserToken, error) { + return tokens, nil + } + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + result := sc.ToJSON() + So(result.MustArray(), ShouldHaveLength, 2) + + resultOne := result.GetIndex(0) + So(resultOne.Get("id").MustInt64(), ShouldEqual, tokens[0].Id) + So(resultOne.Get("isActive").MustBool(), ShouldBeTrue) + So(resultOne.Get("clientIp").MustString(), ShouldEqual, "127.0.0.1") + So(resultOne.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36") + So(resultOne.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[0].CreatedAt, 0).Format(time.RFC3339)) + So(resultOne.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[0].SeenAt, 0).Format(time.RFC3339)) + + resultTwo := result.GetIndex(1) + So(resultTwo.Get("id").MustInt64(), ShouldEqual, tokens[1].Id) + So(resultTwo.Get("isActive").MustBool(), ShouldBeFalse) + So(resultTwo.Get("clientIp").MustString(), ShouldEqual, "127.0.0.2") + So(resultTwo.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1") + So(resultTwo.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[1].CreatedAt, 0).Format(time.RFC3339)) + So(resultTwo.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[1].SeenAt, 0).Format(time.RFC3339)) + }) + }) +} + +func revokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd m.RevokeAuthTokenCmd, userId int64, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + + hs := HTTPServer{ + Bus: bus.GetBus(), + AuthTokenService: fakeAuthTokenService, + } + + sc := setupScenarioContext(url) + sc.userAuthTokenService = fakeAuthTokenService + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = userId + sc.context.OrgId = TestOrgID + sc.context.OrgRole = m.ROLE_ADMIN + + return hs.RevokeUserAuthToken(c, cmd) + }) + + sc.m.Post(routePattern, sc.defaultHandler) + + fn(sc) + }) +} + +func getUserAuthTokensScenario(desc string, url string, routePattern string, userId int64, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + + hs := HTTPServer{ + Bus: bus.GetBus(), + AuthTokenService: fakeAuthTokenService, + } + + sc := setupScenarioContext(url) + sc.userAuthTokenService = fakeAuthTokenService + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = userId + sc.context.OrgId = TestOrgID + sc.context.OrgRole = m.ROLE_ADMIN + + return hs.GetUserAuthTokens(c) + }) + + sc.m.Get(routePattern, sc.defaultHandler) + + fn(sc) + }) +} + +func logoutUserFromAllDevicesInternalScenario(desc string, userId int64, fn scenarioFunc) { + Convey(desc, func() { + defer bus.ClearBusHandlers() + + hs := HTTPServer{ + Bus: bus.GetBus(), + AuthTokenService: auth.NewFakeUserAuthTokenService(), + } + + sc := setupScenarioContext("/") + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = m.ROLE_ADMIN + + return hs.logoutUserFromAllDevicesInternal(userId) + }) + + sc.m.Post("/", sc.defaultHandler) + + fn(sc) + }) +} + +func revokeUserAuthTokenInternalScenario(desc string, cmd m.RevokeAuthTokenCmd, userId int64, token *m.UserToken, fn scenarioFunc) { + Convey(desc, func() { + defer bus.ClearBusHandlers() + + fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + + hs := HTTPServer{ + Bus: bus.GetBus(), + AuthTokenService: fakeAuthTokenService, + } + + sc := setupScenarioContext("/") + sc.userAuthTokenService = fakeAuthTokenService + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = m.ROLE_ADMIN + sc.context.UserToken = token + + return hs.revokeUserAuthTokenInternal(c, userId, cmd) + }) + + sc.m.Post("/", sc.defaultHandler) + + fn(sc) + }) +} + +func getUserAuthTokensInternalScenario(desc string, token *m.UserToken, fn scenarioFunc) { + Convey(desc, func() { + defer bus.ClearBusHandlers() + + fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + + hs := HTTPServer{ + Bus: bus.GetBus(), + AuthTokenService: fakeAuthTokenService, + } + + sc := setupScenarioContext("/") + sc.userAuthTokenService = fakeAuthTokenService + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = m.ROLE_ADMIN + sc.context.UserToken = token + + return hs.getUserAuthTokensInternal(c, TestUserID) + }) + + sc.m.Get("/", sc.defaultHandler) + + fn(sc) + }) +} diff --git a/pkg/models/user_token.go b/pkg/models/user_token.go index 22f92cb21d2..8c3e7985995 100644 --- a/pkg/models/user_token.go +++ b/pkg/models/user_token.go @@ -1,6 +1,8 @@ package models -import "errors" +import ( + "errors" +) // Typed errors var ( @@ -23,6 +25,10 @@ type UserToken struct { UnhashedToken string } +type RevokeAuthTokenCmd struct { + AuthTokenId int64 `json:"authTokenId"` +} + // UserTokenService are used for generating and validating user tokens type UserTokenService interface { CreateToken(userId int64, clientIP, userAgent string) (*UserToken, error) From 80ce11a4a433a755a66b9b7782892d2b5e1436cd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 8 Mar 2019 15:15:57 +0100 Subject: [PATCH 014/194] docs: update admin and user http api documentation --- docs/sources/http_api/admin.md | 102 +++++++++++++++++++++++++++++++++ docs/sources/http_api/user.md | 72 +++++++++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index a27fd2aac14..c2d540c452b 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -341,3 +341,105 @@ Content-Type: application/json {"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100} ``` + +## Auth tokens for User + +`GET /api/admin/users/:id/auth-tokens` + +Return a list of all auth tokens (devices) that the user currently have logged in from. + +Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. + +**Example Request**: + +```http +GET /api/admin/users/1/auth-tokens HTTP/1.1 +Accept: application/json +Content-Type: application/json +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id": 361, + "isActive": false, + "clientIp": "127.0.0.1", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36", + "createdAt": "2019-03-05T21:22:54+01:00", + "seenAt": "2019-03-06T19:41:06+01:00" + }, + { + "id": 364, + "isActive": false, + "clientIp": "127.0.0.1", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", + "createdAt": "2019-03-06T19:41:19+01:00", + "seenAt": "2019-03-06T19:41:21+01:00" + } +] +``` + +## Revoke auth token for User + +`POST /api/admin/users/:id/revoke-auth-token` + +Revokes the given auth token (device) for the user. User of issued auth token (device) will no longer be logged in +and will be required to authenticate again upon next activity. + +Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. + +**Example Request**: + +```http +POST /api/admin/users/1/revoke-auth-token HTTP/1.1 +Accept: application/json +Content-Type: application/json + +{ + "authTokenId": 364 +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "message": "User auth token revoked" +} +``` + +## Logout User + +`POST /api/admin/users/:id/logout` + +Logout user revokes all auth tokens (devices) for the user. User of issued auth tokens (devices) will no longer be logged in +and will be required to authenticate again upon next activity. + +Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. + +**Example Request**: + +```http +POST /api/admin/users/1/logout HTTP/1.1 +Accept: application/json +Content-Type: application/json +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "message": "User auth token revoked" +} +``` diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index 669e8003247..a81f608c2f5 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -478,3 +478,75 @@ Content-Type: application/json {"message":"Dashboard unstarred"} ``` + +## Auth tokens of the actual User + +`GET /api/user/auth-tokens` + +Return a list of all auth tokens (devices) that the actual user currently have logged in from. + +**Example Request**: + +```http +GET /api/user/auth-tokens HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id": 361, + "isActive": true, + "clientIp": "127.0.0.1", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36", + "createdAt": "2019-03-05T21:22:54+01:00", + "seenAt": "2019-03-06T19:41:06+01:00" + }, + { + "id": 364, + "isActive": false, + "clientIp": "127.0.0.1", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", + "createdAt": "2019-03-06T19:41:19+01:00", + "seenAt": "2019-03-06T19:41:21+01:00" + } +] +``` + +## Revoke an auth token of the actual User + +`POST /api/user/revoke-auth-token` + +Revokes the given auth token (device) for the actual user. User of issued auth token (device) will no longer be logged in +and will be required to authenticate again upon next activity. + +**Example Request**: + +```http +POST /api/user/revoke-auth-token HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "authTokenId": 364 +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "message": "User auth token revoked" +} +``` From 7ce7da1251d89eaef687a8c617d0b742a3e75b71 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 8 Mar 2019 08:27:48 -0800 Subject: [PATCH 015/194] merge master --- .../src/utils/processTimeSeries.test.ts | 18 +++++++++++++----- .../grafana-ui/src/utils/processTimeSeries.ts | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.test.ts b/packages/grafana-ui/src/utils/processTimeSeries.test.ts index fe291898c55..bf88a43e7fb 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.test.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.test.ts @@ -2,14 +2,22 @@ import { toTableData } from './processTimeSeries'; describe('toTableData', () => { it('converts timeseries to table skipping nulls', () => { - const input = { + const input1 = { target: 'Field Name', datapoints: [[100, 1], [200, 2]], }; - const data = toTableData([null, input, null, null]); - expect(data.length).toBe(1); - expect(data[0].columns[0].text).toBe(input.target); - expect(data[0].rows).toBe(input.datapoints); + const input2 = { + // without target + target: '', + datapoints: [[100, 1], [200, 2]], + }; + const data = toTableData([null, input1, input2, null, null]); + expect(data.length).toBe(2); + expect(data[0].columns[0].text).toBe(input1.target); + expect(data[0].rows).toBe(input1.datapoints); + + // Default name + expect(data[1].columns[0].text).toEqual('Value'); }); it('keeps tableData unchanged', () => { diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 112bd67f481..85457a718ab 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -209,7 +209,7 @@ export const toTableData = (results?: any[]): TableData[] => { return { columns: [ { - text: ts.target || 'Timeseries', + text: ts.target || 'Value', unit: ts.unit, }, { From 84fa9be29f33b89fa7ddd7d72a4c76f73b67b999 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 10 Mar 2019 15:51:19 -0700 Subject: [PATCH 016/194] add comment --- packages/grafana-ui/src/utils/processTimeSeries.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 85457a718ab..a56fe004b05 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -13,6 +13,8 @@ interface Options { nullValueMode: NullValueMode; } +// NOTE -- this should be refactored into a TableData utility file. +// I left it as is so the merge changes are more clear. export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs { const vmSeries = data.map((item, index) => { if (!isNumber(xColumn)) { From c8ff698d9094dc43192f825874ccb5ea8f27bd83 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 23 Feb 2019 22:59:34 +0100 Subject: [PATCH 017/194] avoid exposing internal structs and functions --- pkg/infra/distcache/database_storage.go | 16 ++++++++-------- pkg/infra/distcache/distcache.go | 12 +++++------- pkg/infra/distcache/distcache_test.go | 14 +++++--------- pkg/infra/distcache/memcached_storage.go | 12 ++++++------ pkg/infra/distcache/redis_storage.go | 21 ++++----------------- 5 files changed, 28 insertions(+), 47 deletions(-) diff --git a/pkg/infra/distcache/database_storage.go b/pkg/infra/distcache/database_storage.go index cff5e0fc499..f4365383b82 100644 --- a/pkg/infra/distcache/database_storage.go +++ b/pkg/infra/distcache/database_storage.go @@ -45,13 +45,13 @@ func (dc *databaseCache) StartGC() { } func (dc *databaseCache) Get(key string) (interface{}, error) { - cacheHits := []CacheData{} + cacheHits := []cacheData{} err := dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) if err != nil { return nil, err } - var cacheHit CacheData + var cacheHit cacheData if len(cacheHits) == 0 { return nil, ErrCacheItemNotFound } @@ -64,15 +64,15 @@ func (dc *databaseCache) Get(key string) (interface{}, error) { } } - item := &Item{} - if err = DecodeGob(cacheHit.Data, item); err != nil { + item := &cachedItem{} + if err = decodeGob(cacheHit.Data, item); err != nil { return nil, err } return item.Val, nil } -type CacheData struct { +type cacheData struct { Key string Data []byte Expires int64 @@ -80,15 +80,15 @@ type CacheData struct { } func (dc *databaseCache) Put(key string, value interface{}, expire time.Duration) error { - item := &Item{Val: value} - data, err := EncodeGob(item) + item := &cachedItem{Val: value} + data, err := encodeGob(item) if err != nil { return err } now := getTime().Unix() - cacheHits := []CacheData{} + cacheHits := []cacheData{} err = dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) if err != nil { return err diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index 8a6f7daf90c..8ba1a306a3f 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -46,7 +46,7 @@ func createClient(opts CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { // return nil // } - return newDatabaseCache(sqlstore) //&databaseCache{SQLStore: sqlstore} + return newDatabaseCache(sqlstore) } // DistributedCache allows Grafana to cache data outside its own process @@ -56,19 +56,17 @@ type DistributedCache struct { SQLStore *sqlstore.SqlStore `inject:""` } -type Item struct { - Val interface{} - Created int64 - Expire int64 +type cachedItem struct { + Val interface{} } -func EncodeGob(item *Item) ([]byte, error) { +func encodeGob(item *cachedItem) ([]byte, error) { buf := bytes.NewBuffer(nil) err := gob.NewEncoder(buf).Encode(item) return buf.Bytes(), err } -func DecodeGob(data []byte, out *Item) error { +func decodeGob(data []byte, out *cachedItem) error { buf := bytes.NewBuffer(data) return gob.NewDecoder(buf).Decode(&out) } diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index a04b5d0228f..6f59c40f0e9 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -27,7 +27,7 @@ func createTestClient(t *testing.T, name string) cacheStorage { } func TestAllCacheClients(t *testing.T) { - clients := []string{"database", "redis", "memcached"} // add redis, memcache, memory + clients := []string{"database", "redis"} // add redis, memcache, memory for _, v := range clients { client := createTestClient(t, v) @@ -59,19 +59,15 @@ func CanPutGetAndDeleteCachedObjects(t *testing.T, name string, client cacheStor } func CanNotFetchExpiredItems(t *testing.T, name string, client cacheStorage) { - if name == "redis" { - t.Skip() //this test does not work with redis since it uses its own getTime fn - } - cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} - // insert cache item one day back - getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - err := client.Put("key", cacheableStruct, 10000*time.Second) + err := client.Put("key", cacheableStruct, time.Second) assert.Equal(t, err, nil) + //not sure how this can be avoided when testing redis/memcached :/ + <-time.After(time.Second + time.Millisecond) + // should not be able to read that value since its expired - getTime = time.Now _, err = client.Get("key") assert.Equal(t, err, ErrCacheItemNotFound) } diff --git a/pkg/infra/distcache/memcached_storage.go b/pkg/infra/distcache/memcached_storage.go index 44fbbcc33c6..71e037cf196 100644 --- a/pkg/infra/distcache/memcached_storage.go +++ b/pkg/infra/distcache/memcached_storage.go @@ -16,7 +16,7 @@ func newMemcacheStorage(connStr string) *memcacheStorage { } } -func NewItem(sid string, data []byte, expire int32) *memcache.Item { +func newItem(sid string, data []byte, expire int32) *memcache.Item { return &memcache.Item{ Key: sid, Value: data, @@ -26,14 +26,14 @@ func NewItem(sid string, data []byte, expire int32) *memcache.Item { // Set sets value to given key in the cache. func (s *memcacheStorage) Put(key string, val interface{}, expires time.Duration) error { - item := &Item{Val: val} + item := &cachedItem{Val: val} - bytes, err := EncodeGob(item) + bytes, err := encodeGob(item) if err != nil { return err } - memcacheItem := NewItem(key, bytes, int32(expires)) + memcacheItem := newItem(key, bytes, int32(expires)) s.c.Add(memcacheItem) return nil @@ -46,9 +46,9 @@ func (s *memcacheStorage) Get(key string) (interface{}, error) { return nil, err } - item := &Item{} + item := &cachedItem{} - err = DecodeGob(i.Value, item) + err = decodeGob(i.Value, item) if err != nil { return nil, err } diff --git a/pkg/infra/distcache/redis_storage.go b/pkg/infra/distcache/redis_storage.go index 06fc6931758..49055fd8356 100644 --- a/pkg/infra/distcache/redis_storage.go +++ b/pkg/infra/distcache/redis_storage.go @@ -22,8 +22,8 @@ func newRedisStorage(c *redis.Client) *redisStorage { // Set sets value to given key in session. func (s *redisStorage) Put(key string, val interface{}, expires time.Duration) error { - item := &Item{Created: getTime().Unix(), Val: val} - value, err := EncodeGob(item) + item := &cachedItem{Val: val} + value, err := encodeGob(item) if err != nil { return err } @@ -42,8 +42,8 @@ func (s *redisStorage) Put(key string, val interface{}, expires time.Duration) e func (s *redisStorage) Get(key string) (interface{}, error) { v := s.c.Get(key) - item := &Item{} - err := DecodeGob([]byte(v.Val()), item) + item := &cachedItem{} + err := decodeGob([]byte(v.Val()), item) if err == nil { return item.Val, nil @@ -65,16 +65,3 @@ func (s *redisStorage) Delete(key string) error { cmd := s.c.Del(key) return cmd.Err() } - -// RedisProvider represents a redis session provider implementation. -type RedisProvider struct { - c *redis.Client - duration time.Duration - prefix string -} - -// Exist returns true if session with given ID exists. -func (p *RedisProvider) Exist(sid string) bool { - has, err := p.c.Exists(p.prefix + sid).Result() - return err == nil && has -} From a60bb83a70376639ac3460ba5b0d51b2e3fdc6dd Mon Sep 17 00:00:00 2001 From: bergquist Date: Sun, 3 Mar 2019 04:42:11 +0100 Subject: [PATCH 018/194] extract tests into seperate files --- pkg/infra/distcache/distcache.go | 10 ++++++++-- pkg/infra/distcache/distcache_test.go | 15 ++++++++------- pkg/infra/distcache/memcached_storage.go | 10 +++++++--- pkg/infra/distcache/redis_storage.go | 8 +------- pkg/infra/distcache/redis_storage_test.go | 11 +++++++++++ 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index 8ba1a306a3f..87a6da45029 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/sqlstore" + redis "gopkg.in/redis.v2" "github.com/grafana/grafana/pkg/registry" ) @@ -35,11 +36,16 @@ type CacheOpts struct { func createClient(opts CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { if opts.name == "redis" { - return newRedisStorage(nil) + opt := &redis.Options{ + Network: "tcp", + Addr: "localhost:6379", + } + + return newRedisStorage(redis.NewClient(opt)) } if opts.name == "memcache" { - return newMemcacheStorage("localhost:9090") + return newMemcacheStorage("localhost:11211") } // if opts.name == "memory" { diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index 6f59c40f0e9..af6f426e1c0 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -27,18 +27,19 @@ func createTestClient(t *testing.T, name string) cacheStorage { } func TestAllCacheClients(t *testing.T) { - clients := []string{"database", "redis"} // add redis, memcache, memory + //clients := []string{"database", "redis", "memcache"} // add redis, memcache, memory + clients := []string{} // add redis, memcache, memory for _, v := range clients { client := createTestClient(t, v) - CanPutGetAndDeleteCachedObjects(t, v, client) - CanNotFetchExpiredItems(t, v, client) - CanSetInfiniteCacheExpiration(t, v, client) + CanPutGetAndDeleteCachedObjects(t, client) + CanNotFetchExpiredItems(t, client) + CanSetInfiniteCacheExpiration(t, client) } } -func CanPutGetAndDeleteCachedObjects(t *testing.T, name string, client cacheStorage) { +func CanPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} err := client.Put("key", cacheableStruct, 0) @@ -58,7 +59,7 @@ func CanPutGetAndDeleteCachedObjects(t *testing.T, name string, client cacheStor assert.Equal(t, err, ErrCacheItemNotFound) } -func CanNotFetchExpiredItems(t *testing.T, name string, client cacheStorage) { +func CanNotFetchExpiredItems(t *testing.T, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} err := client.Put("key", cacheableStruct, time.Second) @@ -72,7 +73,7 @@ func CanNotFetchExpiredItems(t *testing.T, name string, client cacheStorage) { assert.Equal(t, err, ErrCacheItemNotFound) } -func CanSetInfiniteCacheExpiration(t *testing.T, name string, client cacheStorage) { +func CanSetInfiniteCacheExpiration(t *testing.T, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} // insert cache item one day back diff --git a/pkg/infra/distcache/memcached_storage.go b/pkg/infra/distcache/memcached_storage.go index 71e037cf196..1186bef626b 100644 --- a/pkg/infra/distcache/memcached_storage.go +++ b/pkg/infra/distcache/memcached_storage.go @@ -24,7 +24,7 @@ func newItem(sid string, data []byte, expire int32) *memcache.Item { } } -// Set sets value to given key in the cache. +// Put sets value to given key in the cache. func (s *memcacheStorage) Put(key string, val interface{}, expires time.Duration) error { item := &cachedItem{Val: val} @@ -35,13 +35,17 @@ func (s *memcacheStorage) Put(key string, val interface{}, expires time.Duration memcacheItem := newItem(key, bytes, int32(expires)) - s.c.Add(memcacheItem) - return nil + return s.c.Add(memcacheItem) } // Get gets value by given key in the cache. func (s *memcacheStorage) Get(key string) (interface{}, error) { i, err := s.c.Get(key) + + if err != nil && err.Error() == "memcache: cache miss" { + return nil, ErrCacheItemNotFound + } + if err != nil { return nil, err } diff --git a/pkg/infra/distcache/redis_storage.go b/pkg/infra/distcache/redis_storage.go index 49055fd8356..bb21b26473e 100644 --- a/pkg/infra/distcache/redis_storage.go +++ b/pkg/infra/distcache/redis_storage.go @@ -11,13 +11,7 @@ type redisStorage struct { } func newRedisStorage(c *redis.Client) *redisStorage { - opt := &redis.Options{ - Network: "tcp", - Addr: "localhost:6379", - } - return &redisStorage{ - c: redis.NewClient(opt), - } + return &redisStorage{c: c} } // Set sets value to given key in session. diff --git a/pkg/infra/distcache/redis_storage_test.go b/pkg/infra/distcache/redis_storage_test.go index e793fbec4c4..39d39d41b12 100644 --- a/pkg/infra/distcache/redis_storage_test.go +++ b/pkg/infra/distcache/redis_storage_test.go @@ -1 +1,12 @@ package distcache + +import "testing" + +func TestRedisCacheStorage(t *testing.T) { + + client := createTestClient(t, "redis") + + CanPutGetAndDeleteCachedObjects(t, client) + CanNotFetchExpiredItems(t, client) + CanSetInfiniteCacheExpiration(t, client) +} From 8db2864feef388f9ee1894c84783e5d05ff61a60 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sun, 3 Mar 2019 05:25:17 +0100 Subject: [PATCH 019/194] adds memory as dist storage alt --- .../database_storage_integration_test.go | 12 +++++++ pkg/infra/distcache/distcache.go | 6 ++-- pkg/infra/distcache/distcache_test.go | 3 +- pkg/infra/distcache/memcached_storage_test.go | 12 +++++++ pkg/infra/distcache/memory_storage.go | 35 +++++++++++++++++++ 5 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 pkg/infra/distcache/database_storage_integration_test.go create mode 100644 pkg/infra/distcache/memcached_storage_test.go create mode 100644 pkg/infra/distcache/memory_storage.go diff --git a/pkg/infra/distcache/database_storage_integration_test.go b/pkg/infra/distcache/database_storage_integration_test.go new file mode 100644 index 00000000000..e305759983d --- /dev/null +++ b/pkg/infra/distcache/database_storage_integration_test.go @@ -0,0 +1,12 @@ +package distcache + +import "testing" + +func TestIntegrationDatabaseCacheStorage(t *testing.T) { + + client := createTestClient(t, "database") + + CanPutGetAndDeleteCachedObjects(t, client) + CanNotFetchExpiredItems(t, client) + CanSetInfiniteCacheExpiration(t, client) +} diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index 87a6da45029..d21ada1e6a3 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -48,9 +48,9 @@ func createClient(opts CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { return newMemcacheStorage("localhost:11211") } - // if opts.name == "memory" { - // return nil - // } + if opts.name == "memory" { + return newMemoryStorage() + } return newDatabaseCache(sqlstore) } diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index af6f426e1c0..ec778b0c335 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -27,8 +27,7 @@ func createTestClient(t *testing.T, name string) cacheStorage { } func TestAllCacheClients(t *testing.T) { - //clients := []string{"database", "redis", "memcache"} // add redis, memcache, memory - clients := []string{} // add redis, memcache, memory + clients := []string{"memory"} // add redis, memcache, memory for _, v := range clients { client := createTestClient(t, v) diff --git a/pkg/infra/distcache/memcached_storage_test.go b/pkg/infra/distcache/memcached_storage_test.go new file mode 100644 index 00000000000..b02f67f062f --- /dev/null +++ b/pkg/infra/distcache/memcached_storage_test.go @@ -0,0 +1,12 @@ +package distcache + +import "testing" + +func TestMemcachedCacheStorage(t *testing.T) { + + client := createTestClient(t, "memcache") + + CanPutGetAndDeleteCachedObjects(t, client) + CanNotFetchExpiredItems(t, client) + CanSetInfiniteCacheExpiration(t, client) +} diff --git a/pkg/infra/distcache/memory_storage.go b/pkg/infra/distcache/memory_storage.go new file mode 100644 index 00000000000..a1203cabe75 --- /dev/null +++ b/pkg/infra/distcache/memory_storage.go @@ -0,0 +1,35 @@ +package distcache + +import ( + "time" + + gocache "github.com/patrickmn/go-cache" +) + +type memoryStorage struct { + c *gocache.Cache +} + +func newMemoryStorage() *memoryStorage { + return &memoryStorage{ + c: gocache.New(time.Minute*30, time.Minute*30), + } +} + +func (s *memoryStorage) Put(key string, val interface{}, expires time.Duration) error { + return s.c.Add(key, val, expires) +} + +func (s *memoryStorage) Get(key string) (interface{}, error) { + val, exist := s.c.Get(key) + if !exist { + return nil, ErrCacheItemNotFound + } + + return val, nil +} + +func (s *memoryStorage) Delete(key string) error { + s.c.Delete(key) + return nil +} From 33935b09f0e543d0fc9583fabd2810685ca2c0cb Mon Sep 17 00:00:00 2001 From: bergquist Date: Sun, 3 Mar 2019 12:34:41 +0100 Subject: [PATCH 020/194] uses set instead of add for memcache set always sets the value regardless. --- .../database_storage_integration_test.go | 6 +----- pkg/infra/distcache/distcache_test.go | 16 ++++++++-------- pkg/infra/distcache/memcached_storage.go | 2 +- pkg/infra/distcache/memcached_storage_test.go | 7 +------ pkg/infra/distcache/redis_storage_test.go | 7 +------ 5 files changed, 12 insertions(+), 26 deletions(-) diff --git a/pkg/infra/distcache/database_storage_integration_test.go b/pkg/infra/distcache/database_storage_integration_test.go index e305759983d..b8f564f9710 100644 --- a/pkg/infra/distcache/database_storage_integration_test.go +++ b/pkg/infra/distcache/database_storage_integration_test.go @@ -4,9 +4,5 @@ import "testing" func TestIntegrationDatabaseCacheStorage(t *testing.T) { - client := createTestClient(t, "database") - - CanPutGetAndDeleteCachedObjects(t, client) - CanNotFetchExpiredItems(t, client) - CanSetInfiniteCacheExpiration(t, client) + RunTestsForClient(t, createTestClient(t, "database")) } diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index ec778b0c335..a40066b788f 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -26,16 +26,16 @@ func createTestClient(t *testing.T, name string) cacheStorage { return createClient(CacheOpts{name: name}, sqlstore) } -func TestAllCacheClients(t *testing.T) { - clients := []string{"memory"} // add redis, memcache, memory +func TestMemoryStorageClient(t *testing.T) { - for _, v := range clients { - client := createTestClient(t, v) + client := createTestClient(t, "memory") + RunTestsForClient(t, client) +} - CanPutGetAndDeleteCachedObjects(t, client) - CanNotFetchExpiredItems(t, client) - CanSetInfiniteCacheExpiration(t, client) - } +func RunTestsForClient(t *testing.T, client cacheStorage) { + CanPutGetAndDeleteCachedObjects(t, client) + CanNotFetchExpiredItems(t, client) + CanSetInfiniteCacheExpiration(t, client) } func CanPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { diff --git a/pkg/infra/distcache/memcached_storage.go b/pkg/infra/distcache/memcached_storage.go index 1186bef626b..7f97a043628 100644 --- a/pkg/infra/distcache/memcached_storage.go +++ b/pkg/infra/distcache/memcached_storage.go @@ -35,7 +35,7 @@ func (s *memcacheStorage) Put(key string, val interface{}, expires time.Duration memcacheItem := newItem(key, bytes, int32(expires)) - return s.c.Add(memcacheItem) + return s.c.Set(memcacheItem) } // Get gets value by given key in the cache. diff --git a/pkg/infra/distcache/memcached_storage_test.go b/pkg/infra/distcache/memcached_storage_test.go index b02f67f062f..524a4fcea10 100644 --- a/pkg/infra/distcache/memcached_storage_test.go +++ b/pkg/infra/distcache/memcached_storage_test.go @@ -3,10 +3,5 @@ package distcache import "testing" func TestMemcachedCacheStorage(t *testing.T) { - - client := createTestClient(t, "memcache") - - CanPutGetAndDeleteCachedObjects(t, client) - CanNotFetchExpiredItems(t, client) - CanSetInfiniteCacheExpiration(t, client) + RunTestsForClient(t, createTestClient(t, "memcache")) } diff --git a/pkg/infra/distcache/redis_storage_test.go b/pkg/infra/distcache/redis_storage_test.go index 39d39d41b12..6ba093a205c 100644 --- a/pkg/infra/distcache/redis_storage_test.go +++ b/pkg/infra/distcache/redis_storage_test.go @@ -3,10 +3,5 @@ package distcache import "testing" func TestRedisCacheStorage(t *testing.T) { - - client := createTestClient(t, "redis") - - CanPutGetAndDeleteCachedObjects(t, client) - CanNotFetchExpiredItems(t, client) - CanSetInfiniteCacheExpiration(t, client) + RunTestsForClient(t, createTestClient(t, "redis")) } From f9f2d9fcf3074123d96750ff1b428d2cf3c09911 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sun, 3 Mar 2019 12:41:38 +0100 Subject: [PATCH 021/194] avoid exporting test helpers --- .../database_storage_integration_test.go | 3 +-- pkg/infra/distcache/distcache_test.go | 20 +++++++------------ pkg/infra/distcache/memcached_storage_test.go | 2 +- pkg/infra/distcache/memory_storage_test.go | 7 +++++++ pkg/infra/distcache/redis_storage_test.go | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) create mode 100644 pkg/infra/distcache/memory_storage_test.go diff --git a/pkg/infra/distcache/database_storage_integration_test.go b/pkg/infra/distcache/database_storage_integration_test.go index b8f564f9710..fac430e7e8d 100644 --- a/pkg/infra/distcache/database_storage_integration_test.go +++ b/pkg/infra/distcache/database_storage_integration_test.go @@ -3,6 +3,5 @@ package distcache import "testing" func TestIntegrationDatabaseCacheStorage(t *testing.T) { - - RunTestsForClient(t, createTestClient(t, "database")) + runTestsForClient(t, createTestClient(t, "database")) } diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index a40066b788f..33a6d2c9c7b 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -26,19 +26,13 @@ func createTestClient(t *testing.T, name string) cacheStorage { return createClient(CacheOpts{name: name}, sqlstore) } -func TestMemoryStorageClient(t *testing.T) { - - client := createTestClient(t, "memory") - RunTestsForClient(t, client) +func runTestsForClient(t *testing.T, client cacheStorage) { + canPutGetAndDeleteCachedObjects(t, client) + canNotFetchExpiredItems(t, client) + canSetInfiniteCacheExpiration(t, client) } -func RunTestsForClient(t *testing.T, client cacheStorage) { - CanPutGetAndDeleteCachedObjects(t, client) - CanNotFetchExpiredItems(t, client) - CanSetInfiniteCacheExpiration(t, client) -} - -func CanPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { +func canPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} err := client.Put("key", cacheableStruct, 0) @@ -58,7 +52,7 @@ func CanPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { assert.Equal(t, err, ErrCacheItemNotFound) } -func CanNotFetchExpiredItems(t *testing.T, client cacheStorage) { +func canNotFetchExpiredItems(t *testing.T, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} err := client.Put("key", cacheableStruct, time.Second) @@ -72,7 +66,7 @@ func CanNotFetchExpiredItems(t *testing.T, client cacheStorage) { assert.Equal(t, err, ErrCacheItemNotFound) } -func CanSetInfiniteCacheExpiration(t *testing.T, client cacheStorage) { +func canSetInfiniteCacheExpiration(t *testing.T, client cacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} // insert cache item one day back diff --git a/pkg/infra/distcache/memcached_storage_test.go b/pkg/infra/distcache/memcached_storage_test.go index 524a4fcea10..de784730e4a 100644 --- a/pkg/infra/distcache/memcached_storage_test.go +++ b/pkg/infra/distcache/memcached_storage_test.go @@ -3,5 +3,5 @@ package distcache import "testing" func TestMemcachedCacheStorage(t *testing.T) { - RunTestsForClient(t, createTestClient(t, "memcache")) + runTestsForClient(t, createTestClient(t, "memcache")) } diff --git a/pkg/infra/distcache/memory_storage_test.go b/pkg/infra/distcache/memory_storage_test.go new file mode 100644 index 00000000000..cbf4c3790af --- /dev/null +++ b/pkg/infra/distcache/memory_storage_test.go @@ -0,0 +1,7 @@ +package distcache + +import "testing" + +func TestMemoryCacheStorage(t *testing.T) { + runTestsForClient(t, createTestClient(t, "memory")) +} diff --git a/pkg/infra/distcache/redis_storage_test.go b/pkg/infra/distcache/redis_storage_test.go index 6ba093a205c..b33d2b22e53 100644 --- a/pkg/infra/distcache/redis_storage_test.go +++ b/pkg/infra/distcache/redis_storage_test.go @@ -3,5 +3,5 @@ package distcache import "testing" func TestRedisCacheStorage(t *testing.T) { - RunTestsForClient(t, createTestClient(t, "redis")) + runTestsForClient(t, createTestClient(t, "redis")) } From 196cdf97106f1ed8c3d20d11eca17e2286a6a70a Mon Sep 17 00:00:00 2001 From: bergquist Date: Sun, 3 Mar 2019 21:48:00 +0100 Subject: [PATCH 022/194] adds config to default settings --- conf/defaults.ini | 12 ++++++++ .../database_storage_integration_test.go | 7 ----- pkg/infra/distcache/distcache.go | 27 +++++++---------- pkg/infra/distcache/distcache_test.go | 30 +++++++++++++++++-- pkg/infra/distcache/memcached_storage.go | 5 ++-- pkg/infra/distcache/memcached_storage_test.go | 9 ++++-- pkg/infra/distcache/memory_storage_test.go | 9 ++++-- pkg/infra/distcache/redis_storage.go | 9 ++++-- pkg/infra/distcache/redis_storage_test.go | 10 +++++-- pkg/setting/setting.go | 16 ++++++++++ 10 files changed, 97 insertions(+), 37 deletions(-) delete mode 100644 pkg/infra/distcache/database_storage_integration_test.go diff --git a/conf/defaults.ini b/conf/defaults.ini index df02e01235b..d77f980f806 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -106,6 +106,18 @@ path = grafana.db # For "sqlite3" only. cache mode setting used for connecting to the database cache_mode = private +#################################### Cache server ############################# +[cache_server] +# Either "memory", "redis", "memcache" or "database" default is "database" +type = database + +# cache connectionstring options +# memory: no config required. Should only be used on single install grafana. +# database: will use Grafana primary database. +# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` +# memcache: 127.0.0.1:11211 +connstr = + #################################### Session ############################# [session] # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file" diff --git a/pkg/infra/distcache/database_storage_integration_test.go b/pkg/infra/distcache/database_storage_integration_test.go deleted file mode 100644 index fac430e7e8d..00000000000 --- a/pkg/infra/distcache/database_storage_integration_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package distcache - -import "testing" - -func TestIntegrationDatabaseCacheStorage(t *testing.T) { - runTestsForClient(t, createTestClient(t, "database")) -} diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index d21ada1e6a3..ee824ae4c52 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -6,9 +6,10 @@ import ( "errors" "time" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/sqlstore" - redis "gopkg.in/redis.v2" "github.com/grafana/grafana/pkg/registry" ) @@ -25,30 +26,21 @@ func init() { func (ds *DistributedCache) Init() error { ds.log = log.New("distributed.cache") - ds.Client = createClient(CacheOpts{}, ds.SQLStore) + ds.Client = createClient(ds.Cfg.CacheOptions, ds.SQLStore) return nil } -type CacheOpts struct { - name string -} - -func createClient(opts CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { - if opts.name == "redis" { - opt := &redis.Options{ - Network: "tcp", - Addr: "localhost:6379", - } - - return newRedisStorage(redis.NewClient(opt)) +func createClient(opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { + if opts.Name == "redis" { + return newRedisStorage(opts) } - if opts.name == "memcache" { - return newMemcacheStorage("localhost:11211") + if opts.Name == "memcache" { + return newMemcacheStorage(opts) } - if opts.name == "memory" { + if opts.Name == "memory" { return newMemoryStorage() } @@ -60,6 +52,7 @@ type DistributedCache struct { log log.Logger Client cacheStorage SQLStore *sqlstore.SqlStore `inject:""` + Cfg *setting.Cfg `inject:""` } type cachedItem struct { diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index 33a6d2c9c7b..f6ed13d4f06 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -8,6 +8,7 @@ import ( "github.com/bmizerany/assert" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" ) type CacheableStruct struct { @@ -19,11 +20,34 @@ func init() { gob.Register(CacheableStruct{}) } -func createTestClient(t *testing.T, name string) cacheStorage { +func createTestClient(t *testing.T, opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { t.Helper() - sqlstore := sqlstore.InitTestDB(t) - return createClient(CacheOpts{name: name}, sqlstore) + dc := &DistributedCache{ + SQLStore: sqlstore, + Cfg: &setting.Cfg{ + CacheOptions: opts, + }, + } + + err := dc.Init() + if err != nil { + t.Fatalf("failed to init client for test. error: %v", err) + } + + return dc.Client +} + +func TestCachedBasedOnConfig(t *testing.T) { + + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ + HomePath: "../../../", + }) + + client := createTestClient(t, cfg.CacheOptions, sqlstore.InitTestDB(t)) + + runTestsForClient(t, client) } func runTestsForClient(t *testing.T, client cacheStorage) { diff --git a/pkg/infra/distcache/memcached_storage.go b/pkg/infra/distcache/memcached_storage.go index 7f97a043628..df1346bf350 100644 --- a/pkg/infra/distcache/memcached_storage.go +++ b/pkg/infra/distcache/memcached_storage.go @@ -4,15 +4,16 @@ import ( "time" "github.com/bradfitz/gomemcache/memcache" + "github.com/grafana/grafana/pkg/setting" ) type memcacheStorage struct { c *memcache.Client } -func newMemcacheStorage(connStr string) *memcacheStorage { +func newMemcacheStorage(opts *setting.CacheOpts) *memcacheStorage { return &memcacheStorage{ - c: memcache.New(connStr), + c: memcache.New(opts.ConnStr), } } diff --git a/pkg/infra/distcache/memcached_storage_test.go b/pkg/infra/distcache/memcached_storage_test.go index de784730e4a..3f885700cb4 100644 --- a/pkg/infra/distcache/memcached_storage_test.go +++ b/pkg/infra/distcache/memcached_storage_test.go @@ -1,7 +1,12 @@ package distcache -import "testing" +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" +) func TestMemcachedCacheStorage(t *testing.T) { - runTestsForClient(t, createTestClient(t, "memcache")) + opts := &setting.CacheOpts{Name: "memcache", ConnStr: "localhost:11211"} + runTestsForClient(t, createTestClient(t, opts, nil)) } diff --git a/pkg/infra/distcache/memory_storage_test.go b/pkg/infra/distcache/memory_storage_test.go index cbf4c3790af..5318b7c19b8 100644 --- a/pkg/infra/distcache/memory_storage_test.go +++ b/pkg/infra/distcache/memory_storage_test.go @@ -1,7 +1,12 @@ package distcache -import "testing" +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" +) func TestMemoryCacheStorage(t *testing.T) { - runTestsForClient(t, createTestClient(t, "memory")) + opts := &setting.CacheOpts{Name: "memory"} + runTestsForClient(t, createTestClient(t, opts, nil)) } diff --git a/pkg/infra/distcache/redis_storage.go b/pkg/infra/distcache/redis_storage.go index bb21b26473e..4e6a8b6d325 100644 --- a/pkg/infra/distcache/redis_storage.go +++ b/pkg/infra/distcache/redis_storage.go @@ -3,6 +3,7 @@ package distcache import ( "time" + "github.com/grafana/grafana/pkg/setting" redis "gopkg.in/redis.v2" ) @@ -10,8 +11,12 @@ type redisStorage struct { c *redis.Client } -func newRedisStorage(c *redis.Client) *redisStorage { - return &redisStorage{c: c} +func newRedisStorage(opts *setting.CacheOpts) *redisStorage { + opt := &redis.Options{ + Network: "tcp", + Addr: opts.ConnStr, + } + return &redisStorage{c: redis.NewClient(opt)} } // Set sets value to given key in session. diff --git a/pkg/infra/distcache/redis_storage_test.go b/pkg/infra/distcache/redis_storage_test.go index b33d2b22e53..7c63ce46b38 100644 --- a/pkg/infra/distcache/redis_storage_test.go +++ b/pkg/infra/distcache/redis_storage_test.go @@ -1,7 +1,13 @@ package distcache -import "testing" +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" +) func TestRedisCacheStorage(t *testing.T) { - runTestsForClient(t, createTestClient(t, "redis")) + + opts := &setting.CacheOpts{Name: "redis", ConnStr: "localhost:6379"} + runTestsForClient(t, createTestClient(t, opts, nil)) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 5d44a3585dc..f25f2211b40 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -240,6 +240,9 @@ type Cfg struct { // User EditorsCanOwn bool + + // DistributedCache + CacheOptions *CacheOpts } type CommandLineArgs struct { @@ -779,9 +782,22 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { enterprise := iniFile.Section("enterprise") cfg.EnterpriseLicensePath = enterprise.Key("license_path").MustString(filepath.Join(cfg.DataPath, "license.jwt")) + cacheServer := iniFile.Section("cache_server") + //cfg.DistCacheType = cacheServer.Key("type").MustString("database") + //cfg.DistCacheConnStr = cacheServer.Key("connstr").MustString("") + cfg.CacheOptions = &CacheOpts{ + Name: cacheServer.Key("type").MustString("database"), + ConnStr: cacheServer.Key("connstr").MustString(""), + } + return nil } +type CacheOpts struct { + Name string + ConnStr string +} + func (cfg *Cfg) readSessionConfig() { sec := cfg.Raw.Section("session") SessionOptions = session.Options{} From b933b4efc8a9dcd9f73e00d063b806d3d429a640 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sun, 3 Mar 2019 22:04:11 +0100 Subject: [PATCH 023/194] test redis and memcached during integration tests --- ...ed_storage_test.go => memcached_storage_integration_test.go} | 2 ++ ...{redis_storage_test.go => redis_storage_integration_test.go} | 2 ++ 2 files changed, 4 insertions(+) rename pkg/infra/distcache/{memcached_storage_test.go => memcached_storage_integration_test.go} (92%) rename pkg/infra/distcache/{redis_storage_test.go => redis_storage_integration_test.go} (93%) diff --git a/pkg/infra/distcache/memcached_storage_test.go b/pkg/infra/distcache/memcached_storage_integration_test.go similarity index 92% rename from pkg/infra/distcache/memcached_storage_test.go rename to pkg/infra/distcache/memcached_storage_integration_test.go index 3f885700cb4..128abb6923f 100644 --- a/pkg/infra/distcache/memcached_storage_test.go +++ b/pkg/infra/distcache/memcached_storage_integration_test.go @@ -1,3 +1,5 @@ +// +build memcached + package distcache import ( diff --git a/pkg/infra/distcache/redis_storage_test.go b/pkg/infra/distcache/redis_storage_integration_test.go similarity index 93% rename from pkg/infra/distcache/redis_storage_test.go rename to pkg/infra/distcache/redis_storage_integration_test.go index 7c63ce46b38..289a3ff4e2d 100644 --- a/pkg/infra/distcache/redis_storage_test.go +++ b/pkg/infra/distcache/redis_storage_integration_test.go @@ -1,3 +1,5 @@ +// +build redis + package distcache import ( From 995647be2c99224ffa60cb5f572e649b11ad0530 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Mar 2019 14:22:22 +0100 Subject: [PATCH 024/194] removes memory as distcache option if database caching is to expensive if should not use distcache in the first place. --- pkg/infra/distcache/distcache.go | 4 --- pkg/infra/distcache/memory_storage.go | 35 ---------------------- pkg/infra/distcache/memory_storage_test.go | 12 -------- 3 files changed, 51 deletions(-) delete mode 100644 pkg/infra/distcache/memory_storage.go delete mode 100644 pkg/infra/distcache/memory_storage_test.go diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index ee824ae4c52..44ab2e08583 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -40,10 +40,6 @@ func createClient(opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) cacheSto return newMemcacheStorage(opts) } - if opts.Name == "memory" { - return newMemoryStorage() - } - return newDatabaseCache(sqlstore) } diff --git a/pkg/infra/distcache/memory_storage.go b/pkg/infra/distcache/memory_storage.go deleted file mode 100644 index a1203cabe75..00000000000 --- a/pkg/infra/distcache/memory_storage.go +++ /dev/null @@ -1,35 +0,0 @@ -package distcache - -import ( - "time" - - gocache "github.com/patrickmn/go-cache" -) - -type memoryStorage struct { - c *gocache.Cache -} - -func newMemoryStorage() *memoryStorage { - return &memoryStorage{ - c: gocache.New(time.Minute*30, time.Minute*30), - } -} - -func (s *memoryStorage) Put(key string, val interface{}, expires time.Duration) error { - return s.c.Add(key, val, expires) -} - -func (s *memoryStorage) Get(key string) (interface{}, error) { - val, exist := s.c.Get(key) - if !exist { - return nil, ErrCacheItemNotFound - } - - return val, nil -} - -func (s *memoryStorage) Delete(key string) error { - s.c.Delete(key) - return nil -} diff --git a/pkg/infra/distcache/memory_storage_test.go b/pkg/infra/distcache/memory_storage_test.go deleted file mode 100644 index 5318b7c19b8..00000000000 --- a/pkg/infra/distcache/memory_storage_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package distcache - -import ( - "testing" - - "github.com/grafana/grafana/pkg/setting" -) - -func TestMemoryCacheStorage(t *testing.T) { - opts := &setting.CacheOpts{Name: "memory"} - runTestsForClient(t, createTestClient(t, opts, nil)) -} From 98f54326595f861867aca27f44c7af997f653b72 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Mar 2019 14:35:36 +0100 Subject: [PATCH 025/194] `memcache` -> `memcached` https://github.com/memcached/memcached --- conf/defaults.ini | 2 +- pkg/infra/distcache/distcache.go | 4 ++-- pkg/infra/distcache/memcached_storage.go | 12 ++++++------ .../distcache/memcached_storage_integration_test.go | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index d77f980f806..3386e552b8a 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -108,7 +108,7 @@ cache_mode = private #################################### Cache server ############################# [cache_server] -# Either "memory", "redis", "memcache" or "database" default is "database" +# Either "memory", "redis", "memcached" or "database" default is "database" type = database # cache connectionstring options diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index 44ab2e08583..c293b62f608 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -36,8 +36,8 @@ func createClient(opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) cacheSto return newRedisStorage(opts) } - if opts.Name == "memcache" { - return newMemcacheStorage(opts) + if opts.Name == "memcached" { + return newMemcachedStorage(opts) } return newDatabaseCache(sqlstore) diff --git a/pkg/infra/distcache/memcached_storage.go b/pkg/infra/distcache/memcached_storage.go index df1346bf350..ea326d759b7 100644 --- a/pkg/infra/distcache/memcached_storage.go +++ b/pkg/infra/distcache/memcached_storage.go @@ -7,12 +7,12 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -type memcacheStorage struct { +type memcachedStorage struct { c *memcache.Client } -func newMemcacheStorage(opts *setting.CacheOpts) *memcacheStorage { - return &memcacheStorage{ +func newMemcachedStorage(opts *setting.CacheOpts) *memcachedStorage { + return &memcachedStorage{ c: memcache.New(opts.ConnStr), } } @@ -26,7 +26,7 @@ func newItem(sid string, data []byte, expire int32) *memcache.Item { } // Put sets value to given key in the cache. -func (s *memcacheStorage) Put(key string, val interface{}, expires time.Duration) error { +func (s *memcachedStorage) Put(key string, val interface{}, expires time.Duration) error { item := &cachedItem{Val: val} bytes, err := encodeGob(item) @@ -40,7 +40,7 @@ func (s *memcacheStorage) Put(key string, val interface{}, expires time.Duration } // Get gets value by given key in the cache. -func (s *memcacheStorage) Get(key string) (interface{}, error) { +func (s *memcachedStorage) Get(key string) (interface{}, error) { i, err := s.c.Get(key) if err != nil && err.Error() == "memcache: cache miss" { @@ -62,6 +62,6 @@ func (s *memcacheStorage) Get(key string) (interface{}, error) { } // Delete delete a key from the cache -func (s *memcacheStorage) Delete(key string) error { +func (s *memcachedStorage) Delete(key string) error { return s.c.Delete(key) } diff --git a/pkg/infra/distcache/memcached_storage_integration_test.go b/pkg/infra/distcache/memcached_storage_integration_test.go index 128abb6923f..125bf8d2bf1 100644 --- a/pkg/infra/distcache/memcached_storage_integration_test.go +++ b/pkg/infra/distcache/memcached_storage_integration_test.go @@ -9,6 +9,6 @@ import ( ) func TestMemcachedCacheStorage(t *testing.T) { - opts := &setting.CacheOpts{Name: "memcache", ConnStr: "localhost:11211"} + opts := &setting.CacheOpts{Name: "memcached", ConnStr: "localhost:11211"} runTestsForClient(t, createTestClient(t, opts, nil)) } From 6231095f72b0305a50b8d7e926b17db0df7a69eb Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Mar 2019 14:57:45 +0100 Subject: [PATCH 026/194] reverts package.json I made during the flight >.> --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index af270d47ad0..a937ba6f717 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,11 @@ "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p", "cli": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts" }, + "husky": { + "hooks": { + "pre-commit": "lint-staged && grunt precommit" + } + }, "lint-staged": { "*.{ts,tsx,json,scss}": [ "prettier --write", From 9a78c231653bd3b4fb6b412ffa8d9dc6de06778a Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Mar 2019 15:15:05 +0100 Subject: [PATCH 027/194] rename put -> set --- pkg/infra/distcache/database_storage.go | 2 +- pkg/infra/distcache/database_storage_test.go | 10 +++++----- pkg/infra/distcache/distcache.go | 14 +++++++++----- pkg/infra/distcache/distcache_test.go | 16 ++++++++-------- pkg/infra/distcache/memcached_storage.go | 4 ++-- pkg/infra/distcache/redis_storage.go | 2 +- 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/pkg/infra/distcache/database_storage.go b/pkg/infra/distcache/database_storage.go index f4365383b82..0cf613471db 100644 --- a/pkg/infra/distcache/database_storage.go +++ b/pkg/infra/distcache/database_storage.go @@ -79,7 +79,7 @@ type cacheData struct { CreatedAt int64 } -func (dc *databaseCache) Put(key string, value interface{}, expire time.Duration) error { +func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration) error { item := &cachedItem{Val: value} data, err := encodeGob(item) if err != nil { diff --git a/pkg/infra/distcache/database_storage_test.go b/pkg/infra/distcache/database_storage_test.go index 931fbc81c7f..24d8cea16bb 100644 --- a/pkg/infra/distcache/database_storage_test.go +++ b/pkg/infra/distcache/database_storage_test.go @@ -22,15 +22,15 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) { //set time.now to 2 weeks ago getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - db.Put("key1", obj, 1000*time.Second) - db.Put("key2", obj, 1000*time.Second) - db.Put("key3", obj, 1000*time.Second) + db.Set("key1", obj, 1000*time.Second) + db.Set("key2", obj, 1000*time.Second) + db.Set("key3", obj, 1000*time.Second) // insert object that should never expire - db.Put("key4", obj, 0) + db.Set("key4", obj, 0) getTime = time.Now - db.Put("key5", obj, 1000*time.Second) + db.Set("key5", obj, 1000*time.Second) //run GC db.internalRunGC() diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index c293b62f608..549774b848b 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -31,7 +31,7 @@ func (ds *DistributedCache) Init() error { return nil } -func createClient(opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { +func createClient(opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) CacheStorage { if opts.Name == "redis" { return newRedisStorage(opts) } @@ -46,7 +46,7 @@ func createClient(opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) cacheSto // DistributedCache allows Grafana to cache data outside its own process type DistributedCache struct { log log.Logger - Client cacheStorage + Client CacheStorage SQLStore *sqlstore.SqlStore `inject:""` Cfg *setting.Cfg `inject:""` } @@ -66,12 +66,16 @@ func decodeGob(data []byte, out *cachedItem) error { return gob.NewDecoder(buf).Decode(&out) } -type cacheStorage interface { +// CacheStorage allows the caller to set, get and delete items in the cache. +// Cached items are stored as byte arrays and marshalled using "encoding/gob" +// so any struct added to the cache needs to be registred with `gob.Register` +// ex `gob.Register(CacheableStruct{})`` +type CacheStorage interface { // Get reads object from Cache Get(key string) (interface{}, error) - // Puts an object into the cache - Put(key string, value interface{}, expire time.Duration) error + // Set sets an object into the cache + Set(key string, value interface{}, expire time.Duration) error // Delete object from cache Delete(key string) error diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index f6ed13d4f06..a4a596fd930 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -20,7 +20,7 @@ func init() { gob.Register(CacheableStruct{}) } -func createTestClient(t *testing.T, opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) cacheStorage { +func createTestClient(t *testing.T, opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) CacheStorage { t.Helper() dc := &DistributedCache{ @@ -50,16 +50,16 @@ func TestCachedBasedOnConfig(t *testing.T) { runTestsForClient(t, client) } -func runTestsForClient(t *testing.T, client cacheStorage) { +func runTestsForClient(t *testing.T, client CacheStorage) { canPutGetAndDeleteCachedObjects(t, client) canNotFetchExpiredItems(t, client) canSetInfiniteCacheExpiration(t, client) } -func canPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { +func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} - err := client.Put("key", cacheableStruct, 0) + err := client.Set("key", cacheableStruct, 0) assert.Equal(t, err, nil) data, err := client.Get("key") @@ -76,10 +76,10 @@ func canPutGetAndDeleteCachedObjects(t *testing.T, client cacheStorage) { assert.Equal(t, err, ErrCacheItemNotFound) } -func canNotFetchExpiredItems(t *testing.T, client cacheStorage) { +func canNotFetchExpiredItems(t *testing.T, client CacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} - err := client.Put("key", cacheableStruct, time.Second) + err := client.Set("key", cacheableStruct, time.Second) assert.Equal(t, err, nil) //not sure how this can be avoided when testing redis/memcached :/ @@ -90,12 +90,12 @@ func canNotFetchExpiredItems(t *testing.T, client cacheStorage) { assert.Equal(t, err, ErrCacheItemNotFound) } -func canSetInfiniteCacheExpiration(t *testing.T, client cacheStorage) { +func canSetInfiniteCacheExpiration(t *testing.T, client CacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} // insert cache item one day back getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - err := client.Put("key", cacheableStruct, 0) + err := client.Set("key", cacheableStruct, 0) assert.Equal(t, err, nil) // should not be able to read that value since its expired diff --git a/pkg/infra/distcache/memcached_storage.go b/pkg/infra/distcache/memcached_storage.go index ea326d759b7..7a29eec0e5d 100644 --- a/pkg/infra/distcache/memcached_storage.go +++ b/pkg/infra/distcache/memcached_storage.go @@ -25,8 +25,8 @@ func newItem(sid string, data []byte, expire int32) *memcache.Item { } } -// Put sets value to given key in the cache. -func (s *memcachedStorage) Put(key string, val interface{}, expires time.Duration) error { +// Set sets value to given key in the cache. +func (s *memcachedStorage) Set(key string, val interface{}, expires time.Duration) error { item := &cachedItem{Val: val} bytes, err := encodeGob(item) diff --git a/pkg/infra/distcache/redis_storage.go b/pkg/infra/distcache/redis_storage.go index 4e6a8b6d325..1414671f05b 100644 --- a/pkg/infra/distcache/redis_storage.go +++ b/pkg/infra/distcache/redis_storage.go @@ -20,7 +20,7 @@ func newRedisStorage(opts *setting.CacheOpts) *redisStorage { } // Set sets value to given key in session. -func (s *redisStorage) Put(key string, val interface{}, expires time.Duration) error { +func (s *redisStorage) Set(key string, val interface{}, expires time.Duration) error { item := &cachedItem{Val: val} value, err := encodeGob(item) if err != nil { From daa3b17951f3c149ecb8434a61a86b4422749589 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Mar 2019 15:34:51 +0100 Subject: [PATCH 028/194] code layouts and comments --- conf/defaults.ini | 3 +- pkg/cmd/grafana-server/server.go | 1 + pkg/infra/distcache/database_storage.go | 56 +++++++++++---------- pkg/infra/distcache/distcache.go | 63 ++++++++++++++++-------- pkg/infra/distcache/distcache_test.go | 3 +- pkg/infra/distcache/memcached_storage.go | 11 ++--- pkg/setting/setting.go | 2 - 7 files changed, 79 insertions(+), 60 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 3386e552b8a..91a58243c04 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -108,11 +108,10 @@ cache_mode = private #################################### Cache server ############################# [cache_server] -# Either "memory", "redis", "memcached" or "database" default is "database" +# Either "redis", "memcached" or "database" default is "database" type = database # cache connectionstring options -# memory: no config required. Should only be used on single install grafana. # database: will use Grafana primary database. # redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` # memcache: 127.0.0.1:11211 diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 53218147ae0..d2852e0b8ca 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -28,6 +28,7 @@ import ( // self registering services _ "github.com/grafana/grafana/pkg/extensions" + _ "github.com/grafana/grafana/pkg/infra/distcache" _ "github.com/grafana/grafana/pkg/infra/metrics" _ "github.com/grafana/grafana/pkg/infra/serverlock" _ "github.com/grafana/grafana/pkg/infra/tracing" diff --git a/pkg/infra/distcache/database_storage.go b/pkg/infra/distcache/database_storage.go index 0cf613471db..6a357005a21 100644 --- a/pkg/infra/distcache/database_storage.go +++ b/pkg/infra/distcache/database_storage.go @@ -1,6 +1,7 @@ package distcache import ( + "context" "time" "github.com/grafana/grafana/pkg/log" @@ -18,32 +19,33 @@ func newDatabaseCache(sqlstore *sqlstore.SqlStore) *databaseCache { log: log.New("distcache.database"), } - //go dc.StartGC() //TODO: start the GC somehow return dc } +func (dc *databaseCache) Run(ctx context.Context) error { + ticker := time.NewTicker(time.Minute * 10) + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + dc.internalRunGC() + } + } +} + var getTime = time.Now func (dc *databaseCache) internalRunGC() { now := getTime().Unix() - sql := `DELETE FROM cache_data WHERE (? - created) >= expire` + sql := `DELETE FROM cache_data WHERE (? - created_at) >= expires AND expires <> 0` - //EXTRACT(EPOCH FROM NOW()) - created >= expire - //UNIX_TIMESTAMP(NOW()) - created >= expire _, err := dc.SQLStore.NewSession().Exec(sql, now) if err != nil { dc.log.Error("failed to run garbage collect", "error", err) } } -func (dc *databaseCache) StartGC() { - dc.internalRunGC() - - time.AfterFunc(time.Second*10, func() { - dc.StartGC() - }) -} - func (dc *databaseCache) Get(key string) (interface{}, error) { cacheHits := []cacheData{} err := dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) @@ -57,8 +59,10 @@ func (dc *databaseCache) Get(key string) (interface{}, error) { } cacheHit = cacheHits[0] + // if Expires is set. Make sure its still valid. if cacheHit.Expires > 0 { - if getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires { + existedButExpired := getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires + if existedButExpired { dc.Delete(key) return nil, ErrCacheItemNotFound } @@ -72,13 +76,6 @@ func (dc *databaseCache) Get(key string) (interface{}, error) { return item.Val, nil } -type cacheData struct { - Key string - Data []byte - Expires int64 - CreatedAt int64 -} - func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration) error { item := &cachedItem{Val: value} data, err := encodeGob(item) @@ -87,22 +84,23 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration } now := getTime().Unix() - cacheHits := []cacheData{} err = dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) if err != nil { return err } - var expiresInEpoch int64 + var expiresAtEpoch int64 if expire != 0 { - expiresInEpoch = int64(expire) / int64(time.Second) + expiresAtEpoch = int64(expire) / int64(time.Second) } + session := dc.SQLStore.NewSession() + // insert or update depending on if item already exist if len(cacheHits) > 0 { - _, err = dc.SQLStore.NewSession().Exec("UPDATE cache_data SET data=?, created=?, expire=? WHERE key=?", data, now, expiresInEpoch, key) + _, err = session.Exec("UPDATE cache_data SET data=?, created=?, expire=? WHERE key=?", data, now, expiresAtEpoch, key) } else { - _, err = dc.SQLStore.NewSession().Exec("INSERT INTO cache_data(key,data,created_at,expires) VALUES(?,?,?,?)", key, data, now, expiresInEpoch) + _, err = session.Exec("INSERT INTO cache_data(key,data,created_at,expires) VALUES(?,?,?,?)", key, data, now, expiresAtEpoch) } return err @@ -110,8 +108,14 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration func (dc *databaseCache) Delete(key string) error { sql := `DELETE FROM cache_data WHERE key = ?` - _, err := dc.SQLStore.NewSession().Exec(sql, key) return err } + +type cacheData struct { + Key string + Data []byte + Expires int64 + CreatedAt int64 +} diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/distcache/distcache.go index 549774b848b..a8f12adaa27 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/distcache/distcache.go @@ -2,6 +2,7 @@ package distcache import ( "bytes" + "context" "encoding/gob" "errors" "time" @@ -22,6 +23,29 @@ func init() { registry.RegisterService(&DistributedCache{}) } +// CacheStorage allows the caller to set, get and delete items in the cache. +// Cached items are stored as byte arrays and marshalled using "encoding/gob" +// so any struct added to the cache needs to be registred with `distcache.Register` +// ex `distcache.Register(CacheableStruct{})`` +type CacheStorage interface { + // Get reads object from Cache + Get(key string) (interface{}, error) + + // Set sets an object into the cache + Set(key string, value interface{}, expire time.Duration) error + + // Delete object from cache + Delete(key string) error +} + +// DistributedCache allows Grafana to cache data outside its own process +type DistributedCache struct { + log log.Logger + Client CacheStorage + SQLStore *sqlstore.SqlStore `inject:""` + Cfg *setting.Cfg `inject:""` +} + // Init initializes the service func (ds *DistributedCache) Init() error { ds.log = log.New("distributed.cache") @@ -31,6 +55,16 @@ func (ds *DistributedCache) Init() error { return nil } +func (ds *DistributedCache) Run(ctx context.Context) error { + backgroundjob, ok := ds.Client.(registry.BackgroundService) + if ok { + return backgroundjob.Run(ctx) + } + + <-ctx.Done() + return ctx.Err() +} + func createClient(opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) CacheStorage { if opts.Name == "redis" { return newRedisStorage(opts) @@ -43,12 +77,14 @@ func createClient(opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) CacheSto return newDatabaseCache(sqlstore) } -// DistributedCache allows Grafana to cache data outside its own process -type DistributedCache struct { - log log.Logger - Client CacheStorage - SQLStore *sqlstore.SqlStore `inject:""` - Cfg *setting.Cfg `inject:""` +// Register records a type, identified by a value for that type, under its +// internal type name. That name will identify the concrete type of a value +// sent or received as an interface variable. Only types that will be +// transferred as implementations of interface values need to be registered. +// Expecting to be used only during initialization, it panics if the mapping +// between types and names is not a bijection. +func Register(value interface{}) { + gob.Register(value) } type cachedItem struct { @@ -65,18 +101,3 @@ func decodeGob(data []byte, out *cachedItem) error { buf := bytes.NewBuffer(data) return gob.NewDecoder(buf).Decode(&out) } - -// CacheStorage allows the caller to set, get and delete items in the cache. -// Cached items are stored as byte arrays and marshalled using "encoding/gob" -// so any struct added to the cache needs to be registred with `gob.Register` -// ex `gob.Register(CacheableStruct{})`` -type CacheStorage interface { - // Get reads object from Cache - Get(key string) (interface{}, error) - - // Set sets an object into the cache - Set(key string, value interface{}, expire time.Duration) error - - // Delete object from cache - Delete(key string) error -} diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index a4a596fd930..b631a6283ac 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -1,7 +1,6 @@ package distcache import ( - "encoding/gob" "testing" "time" @@ -17,7 +16,7 @@ type CacheableStruct struct { } func init() { - gob.Register(CacheableStruct{}) + Register(CacheableStruct{}) } func createTestClient(t *testing.T, opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) CacheStorage { diff --git a/pkg/infra/distcache/memcached_storage.go b/pkg/infra/distcache/memcached_storage.go index 7a29eec0e5d..998d05621c9 100644 --- a/pkg/infra/distcache/memcached_storage.go +++ b/pkg/infra/distcache/memcached_storage.go @@ -28,21 +28,18 @@ func newItem(sid string, data []byte, expire int32) *memcache.Item { // Set sets value to given key in the cache. func (s *memcachedStorage) Set(key string, val interface{}, expires time.Duration) error { item := &cachedItem{Val: val} - bytes, err := encodeGob(item) if err != nil { return err } - memcacheItem := newItem(key, bytes, int32(expires)) - - return s.c.Set(memcacheItem) + memcachedItem := newItem(key, bytes, int32(expires)) + return s.c.Set(memcachedItem) } // Get gets value by given key in the cache. func (s *memcachedStorage) Get(key string) (interface{}, error) { - i, err := s.c.Get(key) - + memcachedItem, err := s.c.Get(key) if err != nil && err.Error() == "memcache: cache miss" { return nil, ErrCacheItemNotFound } @@ -53,7 +50,7 @@ func (s *memcachedStorage) Get(key string) (interface{}, error) { item := &cachedItem{} - err = decodeGob(i.Value, item) + err = decodeGob(memcachedItem.Value, item) if err != nil { return nil, err } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index f25f2211b40..864c29fb382 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -783,8 +783,6 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.EnterpriseLicensePath = enterprise.Key("license_path").MustString(filepath.Join(cfg.DataPath, "license.jwt")) cacheServer := iniFile.Section("cache_server") - //cfg.DistCacheType = cacheServer.Key("type").MustString("database") - //cfg.DistCacheConnStr = cacheServer.Key("connstr").MustString("") cfg.CacheOptions = &CacheOpts{ Name: cacheServer.Key("type").MustString("database"), ConnStr: cacheServer.Key("connstr").MustString(""), From dbc1315d6f69bb6ce154e5b907d1077d5301c7f7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 7 Mar 2019 17:16:08 +0100 Subject: [PATCH 029/194] build steps for cache servers --- .circleci/config.yml | 18 ++++++++++++++++++ scripts/circle-test-cache-servers.sh | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100755 scripts/circle-test-cache-servers.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 69cea87dccd..9ec8b9dc05d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,6 +56,23 @@ jobs: name: postgres integration tests command: './scripts/circle-test-postgres.sh' + cache-server-test: + docker: + - image: circleci/golang:1.11.5 + - image: circleci/redis:4-alpine + - image: memcached + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + #- run: sudo apt update + #- run: sudo apt install -y postgresql-client + - run: dockerize -wait tcp://127.0.0.1:11211 -timeout 120s + - run: dockerize -wait tcp://127.0.0.1:6739 -timeout 120s + #- run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql' + - run: + name: cache server tests + command: './scripts/circle-test-cache-servers.sh' + codespell: docker: - image: circleci/python @@ -554,4 +571,5 @@ workflows: - gometalinter - mysql-integration-test - postgres-integration-test + - cache-server-test filters: *filter-not-release-or-master diff --git a/scripts/circle-test-cache-servers.sh b/scripts/circle-test-cache-servers.sh new file mode 100755 index 00000000000..6b29be15f42 --- /dev/null +++ b/scripts/circle-test-cache-servers.sh @@ -0,0 +1,17 @@ +#!/bin/bash +function exit_if_fail { + command=$@ + echo "Executing '$command'" + eval $command + rc=$? + if [ $rc -ne 0 ]; then + echo "'$command' returned $rc." + exit $rc + fi +} + +echo "running redis and memcache tests" +#set -e +#time for d in $(go list ./pkg/...); do +time exit_if_fail go test -tags="redis memcached" ./pkg/infra/distcache/... +#done From 66e71b66dd94d6a6ccafae16f8c0cb8fc1da8603 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 7 Mar 2019 19:07:11 +0100 Subject: [PATCH 030/194] renames key to cache_key apparently key is a reserved keyword in mysql. and the error messages doesnt mention that. can I please have 6h back? --- .circleci/config.yml | 7 ++-- pkg/infra/distcache/database_storage.go | 37 ++++++++++--------- pkg/infra/distcache/database_storage_test.go | 16 +++++--- pkg/infra/distcache/distcache_test.go | 18 ++++----- .../sqlstore/migrations/cache_data_mig.go | 6 +-- scripts/circle-test-cache-servers.sh | 3 +- 6 files changed, 48 insertions(+), 39 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9ec8b9dc05d..da0e0665285 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,11 +64,8 @@ jobs: working_directory: /go/src/github.com/grafana/grafana steps: - checkout - #- run: sudo apt update - #- run: sudo apt install -y postgresql-client - run: dockerize -wait tcp://127.0.0.1:11211 -timeout 120s - - run: dockerize -wait tcp://127.0.0.1:6739 -timeout 120s - #- run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql' + - run: dockerize -wait tcp://127.0.0.1:6379 -timeout 120s - run: name: cache server tests command: './scripts/circle-test-cache-servers.sh' @@ -562,6 +559,8 @@ workflows: filters: *filter-not-release-or-master - postgres-integration-test: filters: *filter-not-release-or-master + - cache-server-test: + filters: *filter-not-release-or-master - grafana-docker-pr: requires: - build diff --git a/pkg/infra/distcache/database_storage.go b/pkg/infra/distcache/database_storage.go index 6a357005a21..9883751569f 100644 --- a/pkg/infra/distcache/database_storage.go +++ b/pkg/infra/distcache/database_storage.go @@ -8,6 +8,8 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" ) +var getTime = time.Now + type databaseCache struct { SQLStore *sqlstore.SqlStore log log.Logger @@ -34,8 +36,6 @@ func (dc *databaseCache) Run(ctx context.Context) error { } } -var getTime = time.Now - func (dc *databaseCache) internalRunGC() { now := getTime().Unix() sql := `DELETE FROM cache_data WHERE (? - created_at) >= expires AND expires <> 0` @@ -47,19 +47,20 @@ func (dc *databaseCache) internalRunGC() { } func (dc *databaseCache) Get(key string) (interface{}, error) { - cacheHits := []cacheData{} - err := dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) + cacheHits := []CacheData{} + sess := dc.SQLStore.NewSession() + defer sess.Close() + err := sess.Where("cache_key= ?", key).Find(&cacheHits) + if err != nil { return nil, err } - var cacheHit cacheData if len(cacheHits) == 0 { return nil, ErrCacheItemNotFound } - cacheHit = cacheHits[0] - // if Expires is set. Make sure its still valid. + cacheHit := cacheHits[0] if cacheHit.Expires > 0 { existedButExpired := getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires if existedButExpired { @@ -83,9 +84,10 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration return err } - now := getTime().Unix() - cacheHits := []cacheData{} - err = dc.SQLStore.NewSession().Where(`key = ?`, key).Find(&cacheHits) + session := dc.SQLStore.NewSession() + + var cacheHit CacheData + has, err := session.Where("cache_key = ?", key).Get(&cacheHit) if err != nil { return err } @@ -95,27 +97,28 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration expiresAtEpoch = int64(expire) / int64(time.Second) } - session := dc.SQLStore.NewSession() // insert or update depending on if item already exist - if len(cacheHits) > 0 { - _, err = session.Exec("UPDATE cache_data SET data=?, created=?, expire=? WHERE key=?", data, now, expiresAtEpoch, key) + if has { + _, err = session.Exec(`UPDATE cache_data SET data=?, created=?, expire=? WHERE cache_key='?'`, data, getTime().Unix(), expiresAtEpoch, key) } else { - _, err = session.Exec("INSERT INTO cache_data(key,data,created_at,expires) VALUES(?,?,?,?)", key, data, now, expiresAtEpoch) + _, err = session.Exec(`INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`, key, data, getTime().Unix(), expiresAtEpoch) } return err } func (dc *databaseCache) Delete(key string) error { - sql := `DELETE FROM cache_data WHERE key = ?` + sql := "DELETE FROM cache_data WHERE cache_key=?" _, err := dc.SQLStore.NewSession().Exec(sql, key) return err } -type cacheData struct { - Key string +type CacheData struct { + CacheKey string Data []byte Expires int64 CreatedAt int64 } + +// func (cd CacheData) TableName() string { return "cache_data" } diff --git a/pkg/infra/distcache/database_storage_test.go b/pkg/infra/distcache/database_storage_test.go index 24d8cea16bb..fc526996c89 100644 --- a/pkg/infra/distcache/database_storage_test.go +++ b/pkg/infra/distcache/database_storage_test.go @@ -21,10 +21,16 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) { obj := &CacheableStruct{String: "foolbar"} //set time.now to 2 weeks ago + var err error getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - db.Set("key1", obj, 1000*time.Second) - db.Set("key2", obj, 1000*time.Second) - db.Set("key3", obj, 1000*time.Second) + err = db.Set("key1", obj, 1000*time.Second) + assert.Equal(t, err, nil) + + err = db.Set("key2", obj, 1000*time.Second) + assert.Equal(t, err, nil) + + err = db.Set("key3", obj, 1000*time.Second) + assert.Equal(t, err, nil) // insert object that should never expire db.Set("key4", obj, 0) @@ -36,8 +42,8 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) { db.internalRunGC() //try to read values - _, err := db.Get("key1") - assert.Equal(t, err, ErrCacheItemNotFound) + _, err = db.Get("key1") + assert.Equal(t, err, ErrCacheItemNotFound, "expected cache item not found. got: ", err) _, err = db.Get("key2") assert.Equal(t, err, ErrCacheItemNotFound) _, err = db.Get("key3") diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/distcache/distcache_test.go index b631a6283ac..62b07027a05 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/distcache/distcache_test.go @@ -58,34 +58,34 @@ func runTestsForClient(t *testing.T, client CacheStorage) { func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} - err := client.Set("key", cacheableStruct, 0) - assert.Equal(t, err, nil) + err := client.Set("key1", cacheableStruct, 0) + assert.Equal(t, err, nil, "expected nil. got: ", err) - data, err := client.Get("key") + data, err := client.Get("key1") s, ok := data.(CacheableStruct) assert.Equal(t, ok, true) assert.Equal(t, s.String, "hej") assert.Equal(t, s.Int64, int64(2000)) - err = client.Delete("key") + err = client.Delete("key1") assert.Equal(t, err, nil) - _, err = client.Get("key") + _, err = client.Get("key1") assert.Equal(t, err, ErrCacheItemNotFound) } func canNotFetchExpiredItems(t *testing.T, client CacheStorage) { cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} - err := client.Set("key", cacheableStruct, time.Second) + err := client.Set("key1", cacheableStruct, time.Second) assert.Equal(t, err, nil) //not sure how this can be avoided when testing redis/memcached :/ <-time.After(time.Second + time.Millisecond) // should not be able to read that value since its expired - _, err = client.Get("key") + _, err = client.Get("key1") assert.Equal(t, err, ErrCacheItemNotFound) } @@ -94,12 +94,12 @@ func canSetInfiniteCacheExpiration(t *testing.T, client CacheStorage) { // insert cache item one day back getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - err := client.Set("key", cacheableStruct, 0) + err := client.Set("key1", cacheableStruct, 0) assert.Equal(t, err, nil) // should not be able to read that value since its expired getTime = time.Now - data, err := client.Get("key") + data, err := client.Get("key1") s, ok := data.(CacheableStruct) assert.Equal(t, ok, true) diff --git a/pkg/services/sqlstore/migrations/cache_data_mig.go b/pkg/services/sqlstore/migrations/cache_data_mig.go index f12f7f797c8..3467b88962b 100644 --- a/pkg/services/sqlstore/migrations/cache_data_mig.go +++ b/pkg/services/sqlstore/migrations/cache_data_mig.go @@ -6,17 +6,17 @@ func addCacheMigration(mg *migrator.Migrator) { var cacheDataV1 = migrator.Table{ Name: "cache_data", Columns: []*migrator.Column{ - {Name: "key", Type: migrator.DB_NVarchar, IsPrimaryKey: true, Length: 168}, + {Name: "cache_key", Type: migrator.DB_NVarchar, IsPrimaryKey: true, Length: 168}, {Name: "data", Type: migrator.DB_Blob}, {Name: "expires", Type: migrator.DB_Integer, Length: 255, Nullable: false}, {Name: "created_at", Type: migrator.DB_Integer, Length: 255, Nullable: false}, }, Indices: []*migrator.Index{ - {Cols: []string{"key"}, Type: migrator.UniqueIndex}, + {Cols: []string{"cache_key"}, Type: migrator.UniqueIndex}, }, } mg.AddMigration("create cache_data table", migrator.NewAddTableMigration(cacheDataV1)) - mg.AddMigration("add unique index cache_data.key", migrator.NewAddIndexMigration(cacheDataV1, cacheDataV1.Indices[0])) + mg.AddMigration("add unique index cache_data.cache_key", migrator.NewAddIndexMigration(cacheDataV1, cacheDataV1.Indices[0])) } diff --git a/scripts/circle-test-cache-servers.sh b/scripts/circle-test-cache-servers.sh index 6b29be15f42..a75b7235763 100755 --- a/scripts/circle-test-cache-servers.sh +++ b/scripts/circle-test-cache-servers.sh @@ -13,5 +13,6 @@ function exit_if_fail { echo "running redis and memcache tests" #set -e #time for d in $(go list ./pkg/...); do -time exit_if_fail go test -tags="redis memcached" ./pkg/infra/distcache/... +time exit_if_fail go test -tags=redis ./pkg/infra/distcache/... +time exit_if_fail go test -tags=memcached ./pkg/infra/distcache/... #done From 7e7427637cf67e385934a3cc11f04aa641179139 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 8 Mar 2019 20:49:16 +0100 Subject: [PATCH 031/194] renames distcache -> remotecache --- conf/defaults.ini | 2 +- conf/sample.ini | 11 ++++++++++ pkg/cmd/grafana-server/server.go | 2 +- .../database_storage.go | 2 +- .../database_storage_test.go | 2 +- .../memcached_storage.go | 4 ++-- .../memcached_storage_integration_test.go | 4 ++-- .../redis_storage.go | 4 ++-- .../redis_storage_integration_test.go | 4 ++-- .../remotecache.go} | 20 ++++++++++--------- .../remotecache_test.go} | 10 +++++----- pkg/setting/setting.go | 8 ++++---- scripts/circle-test-cache-servers.sh | 4 ++-- 13 files changed, 45 insertions(+), 32 deletions(-) rename pkg/infra/{distcache => remotecache}/database_storage.go (99%) rename pkg/infra/{distcache => remotecache}/database_storage_test.go (98%) rename pkg/infra/{distcache => remotecache}/memcached_storage.go (92%) rename pkg/infra/{distcache => remotecache}/memcached_storage_integration_test.go (64%) rename pkg/infra/{distcache => remotecache}/redis_storage.go (92%) rename pkg/infra/{distcache => remotecache}/redis_storage_integration_test.go (65%) rename pkg/infra/{distcache/distcache.go => remotecache/remotecache.go} (79%) rename pkg/infra/{distcache/distcache_test.go => remotecache/remotecache_test.go} (90%) diff --git a/conf/defaults.ini b/conf/defaults.ini index 91a58243c04..74bb8b057ad 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -107,7 +107,7 @@ path = grafana.db cache_mode = private #################################### Cache server ############################# -[cache_server] +[remote_cache] # Either "redis", "memcached" or "database" default is "database" type = database diff --git a/conf/sample.ini b/conf/sample.ini index 57ff82181de..860efab0140 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -102,6 +102,17 @@ log_queries = # For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared) ;cache_mode = private +#################################### Cache server ############################# +[remote_cache] +# Either "redis", "memcached" or "database" default is "database" +;type = database + +# cache connectionstring options +# database: will use Grafana primary database. +# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` +# memcache: 127.0.0.1:11211 +;connstr = + #################################### Session #################################### [session] # Either "memory", "file", "redis", "mysql", "postgres", default is "file" diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index d2852e0b8ca..c10212329cf 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -28,8 +28,8 @@ import ( // self registering services _ "github.com/grafana/grafana/pkg/extensions" - _ "github.com/grafana/grafana/pkg/infra/distcache" _ "github.com/grafana/grafana/pkg/infra/metrics" + _ "github.com/grafana/grafana/pkg/infra/remotecache" _ "github.com/grafana/grafana/pkg/infra/serverlock" _ "github.com/grafana/grafana/pkg/infra/tracing" _ "github.com/grafana/grafana/pkg/infra/usagestats" diff --git a/pkg/infra/distcache/database_storage.go b/pkg/infra/remotecache/database_storage.go similarity index 99% rename from pkg/infra/distcache/database_storage.go rename to pkg/infra/remotecache/database_storage.go index 9883751569f..cb6c95ce157 100644 --- a/pkg/infra/distcache/database_storage.go +++ b/pkg/infra/remotecache/database_storage.go @@ -1,4 +1,4 @@ -package distcache +package remotecache import ( "context" diff --git a/pkg/infra/distcache/database_storage_test.go b/pkg/infra/remotecache/database_storage_test.go similarity index 98% rename from pkg/infra/distcache/database_storage_test.go rename to pkg/infra/remotecache/database_storage_test.go index fc526996c89..7fde3d325e5 100644 --- a/pkg/infra/distcache/database_storage_test.go +++ b/pkg/infra/remotecache/database_storage_test.go @@ -1,4 +1,4 @@ -package distcache +package remotecache import ( "testing" diff --git a/pkg/infra/distcache/memcached_storage.go b/pkg/infra/remotecache/memcached_storage.go similarity index 92% rename from pkg/infra/distcache/memcached_storage.go rename to pkg/infra/remotecache/memcached_storage.go index 998d05621c9..7356849c1ef 100644 --- a/pkg/infra/distcache/memcached_storage.go +++ b/pkg/infra/remotecache/memcached_storage.go @@ -1,4 +1,4 @@ -package distcache +package remotecache import ( "time" @@ -11,7 +11,7 @@ type memcachedStorage struct { c *memcache.Client } -func newMemcachedStorage(opts *setting.CacheOpts) *memcachedStorage { +func newMemcachedStorage(opts *setting.RemoteCacheOptions) *memcachedStorage { return &memcachedStorage{ c: memcache.New(opts.ConnStr), } diff --git a/pkg/infra/distcache/memcached_storage_integration_test.go b/pkg/infra/remotecache/memcached_storage_integration_test.go similarity index 64% rename from pkg/infra/distcache/memcached_storage_integration_test.go rename to pkg/infra/remotecache/memcached_storage_integration_test.go index 125bf8d2bf1..d55d78ff482 100644 --- a/pkg/infra/distcache/memcached_storage_integration_test.go +++ b/pkg/infra/remotecache/memcached_storage_integration_test.go @@ -1,6 +1,6 @@ // +build memcached -package distcache +package remotecache import ( "testing" @@ -9,6 +9,6 @@ import ( ) func TestMemcachedCacheStorage(t *testing.T) { - opts := &setting.CacheOpts{Name: "memcached", ConnStr: "localhost:11211"} + opts := &setting.RemoteCacheOptions{Name: "memcached", ConnStr: "localhost:11211"} runTestsForClient(t, createTestClient(t, opts, nil)) } diff --git a/pkg/infra/distcache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go similarity index 92% rename from pkg/infra/distcache/redis_storage.go rename to pkg/infra/remotecache/redis_storage.go index 1414671f05b..9d54020fe79 100644 --- a/pkg/infra/distcache/redis_storage.go +++ b/pkg/infra/remotecache/redis_storage.go @@ -1,4 +1,4 @@ -package distcache +package remotecache import ( "time" @@ -11,7 +11,7 @@ type redisStorage struct { c *redis.Client } -func newRedisStorage(opts *setting.CacheOpts) *redisStorage { +func newRedisStorage(opts *setting.RemoteCacheOptions) *redisStorage { opt := &redis.Options{ Network: "tcp", Addr: opts.ConnStr, diff --git a/pkg/infra/distcache/redis_storage_integration_test.go b/pkg/infra/remotecache/redis_storage_integration_test.go similarity index 65% rename from pkg/infra/distcache/redis_storage_integration_test.go rename to pkg/infra/remotecache/redis_storage_integration_test.go index 289a3ff4e2d..bd834fb89ff 100644 --- a/pkg/infra/distcache/redis_storage_integration_test.go +++ b/pkg/infra/remotecache/redis_storage_integration_test.go @@ -1,6 +1,6 @@ // +build redis -package distcache +package remotecache import ( "testing" @@ -10,6 +10,6 @@ import ( func TestRedisCacheStorage(t *testing.T) { - opts := &setting.CacheOpts{Name: "redis", ConnStr: "localhost:6379"} + opts := &setting.RemoteCacheOptions{Name: "redis", ConnStr: "localhost:6379"} runTestsForClient(t, createTestClient(t, opts, nil)) } diff --git a/pkg/infra/distcache/distcache.go b/pkg/infra/remotecache/remotecache.go similarity index 79% rename from pkg/infra/distcache/distcache.go rename to pkg/infra/remotecache/remotecache.go index a8f12adaa27..761a2b3d337 100644 --- a/pkg/infra/distcache/distcache.go +++ b/pkg/infra/remotecache/remotecache.go @@ -1,4 +1,4 @@ -package distcache +package remotecache import ( "bytes" @@ -20,7 +20,7 @@ var ( ) func init() { - registry.RegisterService(&DistributedCache{}) + registry.RegisterService(&RemoteCache{}) } // CacheStorage allows the caller to set, get and delete items in the cache. @@ -38,8 +38,8 @@ type CacheStorage interface { Delete(key string) error } -// DistributedCache allows Grafana to cache data outside its own process -type DistributedCache struct { +// RemoteCache allows Grafana to cache data outside its own process +type RemoteCache struct { log log.Logger Client CacheStorage SQLStore *sqlstore.SqlStore `inject:""` @@ -47,15 +47,17 @@ type DistributedCache struct { } // Init initializes the service -func (ds *DistributedCache) Init() error { - ds.log = log.New("distributed.cache") +func (ds *RemoteCache) Init() error { + ds.log = log.New("cache.remote") - ds.Client = createClient(ds.Cfg.CacheOptions, ds.SQLStore) + ds.Client = createClient(ds.Cfg.RemoteCacheOptions, ds.SQLStore) return nil } -func (ds *DistributedCache) Run(ctx context.Context) error { +// Run start the backend processes for cache clients +func (ds *RemoteCache) Run(ctx context.Context) error { + //create new interface if more clients need GC jobs backgroundjob, ok := ds.Client.(registry.BackgroundService) if ok { return backgroundjob.Run(ctx) @@ -65,7 +67,7 @@ func (ds *DistributedCache) Run(ctx context.Context) error { return ctx.Err() } -func createClient(opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) CacheStorage { +func createClient(opts *setting.RemoteCacheOptions, sqlstore *sqlstore.SqlStore) CacheStorage { if opts.Name == "redis" { return newRedisStorage(opts) } diff --git a/pkg/infra/distcache/distcache_test.go b/pkg/infra/remotecache/remotecache_test.go similarity index 90% rename from pkg/infra/distcache/distcache_test.go rename to pkg/infra/remotecache/remotecache_test.go index 62b07027a05..8887686c3a1 100644 --- a/pkg/infra/distcache/distcache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -1,4 +1,4 @@ -package distcache +package remotecache import ( "testing" @@ -19,13 +19,13 @@ func init() { Register(CacheableStruct{}) } -func createTestClient(t *testing.T, opts *setting.CacheOpts, sqlstore *sqlstore.SqlStore) CacheStorage { +func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore *sqlstore.SqlStore) CacheStorage { t.Helper() - dc := &DistributedCache{ + dc := &RemoteCache{ SQLStore: sqlstore, Cfg: &setting.Cfg{ - CacheOptions: opts, + RemoteCacheOptions: opts, }, } @@ -44,7 +44,7 @@ func TestCachedBasedOnConfig(t *testing.T) { HomePath: "../../../", }) - client := createTestClient(t, cfg.CacheOptions, sqlstore.InitTestDB(t)) + client := createTestClient(t, cfg.RemoteCacheOptions, sqlstore.InitTestDB(t)) runTestsForClient(t, client) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 864c29fb382..9d135ca3aae 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -242,7 +242,7 @@ type Cfg struct { EditorsCanOwn bool // DistributedCache - CacheOptions *CacheOpts + RemoteCacheOptions *RemoteCacheOptions } type CommandLineArgs struct { @@ -782,8 +782,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { enterprise := iniFile.Section("enterprise") cfg.EnterpriseLicensePath = enterprise.Key("license_path").MustString(filepath.Join(cfg.DataPath, "license.jwt")) - cacheServer := iniFile.Section("cache_server") - cfg.CacheOptions = &CacheOpts{ + cacheServer := iniFile.Section("remote_cache") + cfg.RemoteCacheOptions = &RemoteCacheOptions{ Name: cacheServer.Key("type").MustString("database"), ConnStr: cacheServer.Key("connstr").MustString(""), } @@ -791,7 +791,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { return nil } -type CacheOpts struct { +type RemoteCacheOptions struct { Name string ConnStr string } diff --git a/scripts/circle-test-cache-servers.sh b/scripts/circle-test-cache-servers.sh index a75b7235763..3ec5dbf1069 100755 --- a/scripts/circle-test-cache-servers.sh +++ b/scripts/circle-test-cache-servers.sh @@ -13,6 +13,6 @@ function exit_if_fail { echo "running redis and memcache tests" #set -e #time for d in $(go list ./pkg/...); do -time exit_if_fail go test -tags=redis ./pkg/infra/distcache/... -time exit_if_fail go test -tags=memcached ./pkg/infra/distcache/... +time exit_if_fail go test -tags=redis ./pkg/infra/remotecache/... +time exit_if_fail go test -tags=memcached ./pkg/infra/remotecache/... #done From 085b63109945b5ae43d71f9ee194e1c3f4285f99 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 11 Mar 2019 09:20:30 +0100 Subject: [PATCH 032/194] add docs about remote cache settings --- docs/sources/installation/configuration.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index f0418ad31a6..9705dd2001c 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -162,9 +162,9 @@ executed with working directory set to the installation path. ### enable_gzip -Set this option to `true` to enable HTTP compression, this can improve -transfer speed and bandwidth utilization. It is recommended that most -users set it to `true`. By default it is set to `false` for compatibility +Set this option to `true` to enable HTTP compression, this can improve +transfer speed and bandwidth utilization. It is recommended that most +users set it to `true`. By default it is set to `false` for compatibility reasons. ### cert_file @@ -179,7 +179,6 @@ Path to the certificate key file (if `protocol` is set to `https`). Set to true for Grafana to log all HTTP requests (not just errors). These are logged as Info level events to grafana log. -

@@ -262,6 +261,19 @@ Set to `true` to log the sql calls and execution times. For "sqlite3" only. [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database. (private, shared) Defaults to private. +
+ +## [remote_cache] + +### type + +Either `redis`, `memcached` or `database` default is `database` + +### connstr + +The remote cache connection string. Leave empty when using `database` since it will use the primary database. +Redis example config: `addr=127.0.0.1:6379,pool_size=100,db=grafana` +Memcache example: `127.0.0.1:11211`
From b2967fbb3747a32a4548eebc4a6fba580f2fa7d3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 11 Mar 2019 10:44:16 +0100 Subject: [PATCH 033/194] avoid exposing cache client directly --- pkg/infra/remotecache/remotecache.go | 20 ++++++++++++++++---- pkg/infra/remotecache/remotecache_test.go | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/infra/remotecache/remotecache.go b/pkg/infra/remotecache/remotecache.go index 761a2b3d337..1b9d67b9358 100644 --- a/pkg/infra/remotecache/remotecache.go +++ b/pkg/infra/remotecache/remotecache.go @@ -31,7 +31,7 @@ type CacheStorage interface { // Get reads object from Cache Get(key string) (interface{}, error) - // Set sets an object into the cache + // Set sets an object into the cache. if `expire` is set to zero it never expires. Set(key string, value interface{}, expire time.Duration) error // Delete object from cache @@ -41,16 +41,28 @@ type CacheStorage interface { // RemoteCache allows Grafana to cache data outside its own process type RemoteCache struct { log log.Logger - Client CacheStorage + client CacheStorage SQLStore *sqlstore.SqlStore `inject:""` Cfg *setting.Cfg `inject:""` } +func (ds *RemoteCache) Get(key string) (interface{}, error) { + return ds.client.Get(key) +} + +func (ds *RemoteCache) Set(key string, value interface{}, expire time.Duration) error { + return ds.client.Set(key, value, expire) +} + +func (ds *RemoteCache) Delete(key string) error { + return ds.client.Delete(key) +} + // Init initializes the service func (ds *RemoteCache) Init() error { ds.log = log.New("cache.remote") - ds.Client = createClient(ds.Cfg.RemoteCacheOptions, ds.SQLStore) + ds.client = createClient(ds.Cfg.RemoteCacheOptions, ds.SQLStore) return nil } @@ -58,7 +70,7 @@ func (ds *RemoteCache) Init() error { // Run start the backend processes for cache clients func (ds *RemoteCache) Run(ctx context.Context) error { //create new interface if more clients need GC jobs - backgroundjob, ok := ds.Client.(registry.BackgroundService) + backgroundjob, ok := ds.client.(registry.BackgroundService) if ok { return backgroundjob.Run(ctx) } diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go index 8887686c3a1..ac22607ee70 100644 --- a/pkg/infra/remotecache/remotecache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -34,7 +34,7 @@ func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore * t.Fatalf("failed to init client for test. error: %v", err) } - return dc.Client + return dc.client } func TestCachedBasedOnConfig(t *testing.T) { From 7aeab0a235a515accca2cb7eaae6061cba97a51c Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 11 Mar 2019 10:59:55 +0100 Subject: [PATCH 034/194] use `Get` instead of `Find` --- pkg/infra/remotecache/database_storage.go | 22 +++++++++++----------- scripts/circle-test-cache-servers.sh | 4 +--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pkg/infra/remotecache/database_storage.go b/pkg/infra/remotecache/database_storage.go index cb6c95ce157..2e34ecd1c73 100644 --- a/pkg/infra/remotecache/database_storage.go +++ b/pkg/infra/remotecache/database_storage.go @@ -47,24 +47,24 @@ func (dc *databaseCache) internalRunGC() { } func (dc *databaseCache) Get(key string) (interface{}, error) { - cacheHits := []CacheData{} - sess := dc.SQLStore.NewSession() - defer sess.Close() - err := sess.Where("cache_key= ?", key).Find(&cacheHits) + cacheHit := CacheData{} + session := dc.SQLStore.NewSession() + defer session.Close() + + exist, err := session.Where("cache_key= ?", key).Get(&cacheHit) if err != nil { return nil, err } - if len(cacheHits) == 0 { + if !exist { return nil, ErrCacheItemNotFound } - cacheHit := cacheHits[0] if cacheHit.Expires > 0 { existedButExpired := getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires if existedButExpired { - dc.Delete(key) + _ = dc.Delete(key) //ignore this error since we will return `ErrCacheItemNotFound` anyway return nil, ErrCacheItemNotFound } } @@ -99,9 +99,11 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration // insert or update depending on if item already exist if has { - _, err = session.Exec(`UPDATE cache_data SET data=?, created=?, expire=? WHERE cache_key='?'`, data, getTime().Unix(), expiresAtEpoch, key) + sql := `UPDATE cache_data SET data=?, created=?, expire=? WHERE cache_key='?'` + _, err = session.Exec(sql, data, getTime().Unix(), expiresAtEpoch, key) } else { - _, err = session.Exec(`INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`, key, data, getTime().Unix(), expiresAtEpoch) + sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)` + _, err = session.Exec(sql, key, data, getTime().Unix(), expiresAtEpoch) } return err @@ -120,5 +122,3 @@ type CacheData struct { Expires int64 CreatedAt int64 } - -// func (cd CacheData) TableName() string { return "cache_data" } diff --git a/scripts/circle-test-cache-servers.sh b/scripts/circle-test-cache-servers.sh index 3ec5dbf1069..bacd9928362 100755 --- a/scripts/circle-test-cache-servers.sh +++ b/scripts/circle-test-cache-servers.sh @@ -11,8 +11,6 @@ function exit_if_fail { } echo "running redis and memcache tests" -#set -e -#time for d in $(go list ./pkg/...); do + time exit_if_fail go test -tags=redis ./pkg/infra/remotecache/... time exit_if_fail go test -tags=memcached ./pkg/infra/remotecache/... -#done From 455a33bd8eaaf9e26192861040309e5e986f58a9 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 11 Mar 2019 09:24:25 -0700 Subject: [PATCH 035/194] cleanup after review --- .../grafana-ui/src/utils/processTimeSeries.ts | 31 ++++++++++--------- public/app/core/table_model.ts | 9 ++++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index a56fe004b05..8202d2f6187 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -193,6 +193,22 @@ export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Opt return vmSeries; } +function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData { + return { + columns: [ + { + text: timeSeries.target || 'Value', + unit: timeSeries.unit, + }, + { + text: 'Time', + type: 'time', + }, + ], + rows: timeSeries.datapoints, + }; +} + export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); export const toTableData = (results?: any[]): TableData[] => { @@ -207,20 +223,7 @@ export const toTableData = (results?: any[]): TableData[] => { return data as TableData; } if (data.hasOwnProperty('datapoints')) { - const ts = data as TimeSeries; - return { - columns: [ - { - text: ts.target || 'Value', - unit: ts.unit, - }, - { - text: 'Time', - type: 'time', - }, - ], - rows: ts.datapoints, - } as TableData; + return convertTimeSeriesToTableData(data); } // TODO, try to convert JSON to table? console.warn('Can not convert', data); diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 988c3b1992e..3e8389e4be8 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -1,15 +1,18 @@ import _ from 'lodash'; import { Column, TableData } from '@grafana/ui'; -// This class mutates and uses the extra column fields -interface ColumnEX extends Column { +/** + * Extends the standard Column class with variables that get + * mutated in the angular table panel. + */ +interface AngularTableColumn extends Column { title?: string; sort?: boolean; desc?: boolean; } export default class TableModel implements TableData { - columns: ColumnEX[]; + columns: AngularTableColumn[]; rows: any[]; type: string; columnMap: any; From 00942ec882d55c8bc34a1fb559e2d1239c4b089a Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 11 Mar 2019 09:29:30 -0700 Subject: [PATCH 036/194] MutableColumn --- public/app/core/table_model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 3e8389e4be8..291689941f7 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -5,14 +5,14 @@ import { Column, TableData } from '@grafana/ui'; * Extends the standard Column class with variables that get * mutated in the angular table panel. */ -interface AngularTableColumn extends Column { +interface MutableColumn extends Column { title?: string; sort?: boolean; desc?: boolean; } export default class TableModel implements TableData { - columns: AngularTableColumn[]; + columns: MutableColumn[]; rows: any[]; type: string; columnMap: any; From 8cd54c94e99e9221c0901d15bdcf74c51764c246 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 11 Mar 2019 14:47:54 -0700 Subject: [PATCH 037/194] make value processing reusable --- .../src/components/Gauge/Gauge.test.tsx | 92 +-------------- .../grafana-ui/src/components/Gauge/Gauge.tsx | 77 ++----------- .../src/utils/valueProcessor.test.ts | 107 ++++++++++++++++++ .../grafana-ui/src/utils/valueProcessor.ts | 97 ++++++++++++++++ .../panel/gauge/DisplayValueEditor.tsx | 64 +++++++++++ public/app/plugins/panel/gauge/GaugePanel.tsx | 42 ++++--- .../plugins/panel/gauge/GaugePanelEditor.tsx | 13 ++- .../panel/gauge/SingleStatValueEditor.tsx | 49 +------- public/app/plugins/panel/gauge/types.ts | 22 ++-- 9 files changed, 342 insertions(+), 221 deletions(-) create mode 100644 packages/grafana-ui/src/utils/valueProcessor.test.ts create mode 100644 packages/grafana-ui/src/utils/valueProcessor.ts create mode 100644 public/app/plugins/panel/gauge/DisplayValueEditor.tsx diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 70e29abc221..c6a49eb5b55 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Gauge, Props } from './Gauge'; -import { ValueMapping, MappingType } from '../../types'; import { getTheme } from '../../themes'; jest.mock('jquery', () => ({ @@ -12,19 +11,16 @@ jest.mock('jquery', () => ({ const setup = (propOverrides?: object) => { const props: Props = { maxValue: 100, - valueMappings: [], minValue: 0, - prefix: '', showThresholdMarkers: true, showThresholdLabels: false, - suffix: '', thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }], - unit: 'none', - stat: 'avg', height: 300, width: 300, - value: 25, - decimals: 0, + value: { + text: '25', + numeric: 25, + }, theme: getTheme(), }; @@ -39,38 +35,6 @@ const setup = (propOverrides?: object) => { }; }; -describe('Get font color', () => { - it('should get first threshold color when only one threshold', () => { - const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] }); - - expect(instance.getFontColor(49)).toEqual('#7EB26D'); - }); - - it('should get the threshold color if value is same as a threshold', () => { - const { instance } = setup({ - thresholds: [ - { index: 2, value: 75, color: '#6ED0E0' }, - { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#7EB26D' }, - ], - }); - - expect(instance.getFontColor(50)).toEqual('#EAB839'); - }); - - it('should get the nearest threshold color between thresholds', () => { - const { instance } = setup({ - thresholds: [ - { index: 2, value: 75, color: '#6ED0E0' }, - { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#7EB26D' }, - ], - }); - - expect(instance.getFontColor(55)).toEqual('#EAB839'); - }); -}); - describe('Get thresholds formatted', () => { it('should return first thresholds color for min and max', () => { const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] }); @@ -98,51 +62,3 @@ describe('Get thresholds formatted', () => { ]); }); }); - -describe('Format value', () => { - it('should return if value isNaN', () => { - const valueMappings: ValueMapping[] = []; - const value = 'N/A'; - const { instance } = setup({ valueMappings }); - - const result = instance.formatValue(value); - - expect(result).toEqual('N/A'); - }); - - it('should return formatted value if there are no value mappings', () => { - const valueMappings: ValueMapping[] = []; - const value = '6'; - const { instance } = setup({ valueMappings, decimals: 1 }); - - const result = instance.formatValue(value); - - expect(result).toEqual('6.0'); - }); - - it('should return formatted value if there are no matching value mappings', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' }, - ]; - const value = '10'; - const { instance } = setup({ valueMappings, decimals: 1 }); - - const result = instance.formatValue(value); - - expect(result).toEqual('10.0'); - }); - - it('should return mapped value if there are matching value mappings', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, - { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - ]; - const value = '11'; - const { instance } = setup({ valueMappings, decimals: 1 }); - - const result = instance.formatValue(value); - - expect(result).toEqual('1-20'); - }); -}); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index d04daae3dab..460547a4d7e 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,28 +1,20 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { getMappedValue } from '../../utils/valueMappings'; import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; import { Themeable, GrafanaThemeType } from '../../types/theme'; -import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel'; -import { getValueFormat } from '../../utils/valueFormats/valueFormats'; - -type TimeSeriesValue = string | number | null; +import { Threshold, BasicGaugeColor } from '../../types/panel'; +import { DisplayValue } from '../../utils/valueProcessor'; export interface Props extends Themeable { - decimals?: number | null; + width: number; height: number; - valueMappings: ValueMapping[]; maxValue: number; minValue: number; - prefix: string; thresholds: Threshold[]; showThresholdMarkers: boolean; showThresholdLabels: boolean; - stat: string; - suffix: string; - unit: string; - width: number; - value: number; + + value: DisplayValue; } const FONT_SCALE = 1; @@ -32,15 +24,10 @@ export class Gauge extends PureComponent { static defaultProps = { maxValue: 100, - valueMappings: [], minValue: 0, - prefix: '', showThresholdMarkers: true, showThresholdLabels: false, - suffix: '', thresholds: [], - unit: 'none', - stat: 'avg', theme: GrafanaThemeType.Dark, }; @@ -52,49 +39,6 @@ export class Gauge extends PureComponent { this.draw(); } - formatValue(value: TimeSeriesValue) { - const { decimals, valueMappings, prefix, suffix, unit } = this.props; - - if (isNaN(value as number)) { - return value; - } - - if (valueMappings.length > 0) { - const valueMappedValue = getMappedValue(valueMappings, value); - if (valueMappedValue) { - return `${prefix && prefix + ' '}${valueMappedValue.text}${suffix && ' ' + suffix}`; - } - } - - const formatFunc = getValueFormat(unit); - const formattedValue = formatFunc(value as number, decimals); - const handleNoValueValue = formattedValue || 'no value'; - - return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`; - } - - getFontColor(value: TimeSeriesValue) { - const { thresholds, theme } = this.props; - - if (thresholds.length === 1) { - return getColorFromHexRgbOrName(thresholds[0].color, theme.type); - } - - const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0]; - if (atThreshold) { - return getColorFromHexRgbOrName(atThreshold.color, theme.type); - } - - const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value); - - if (belowThreshold.length > 0) { - const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; - return getColorFromHexRgbOrName(nearestThreshold.color, theme.type); - } - - return BasicGaugeColor.Red; - } - getFormattedThresholds() { const { maxValue, minValue, thresholds, theme } = this.props; @@ -123,15 +67,13 @@ export class Gauge extends PureComponent { draw() { const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props; - const formattedValue = this.formatValue(value) as string; const dimension = Math.min(width, height * 1.3); const backgroundColor = theme.type === GrafanaThemeType.Light ? 'rgb(230,230,230)' : theme.colors.dark3; const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio; const thresholdMarkersWidth = gaugeWidth / 5; - const fontSize = - Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1); + const fontSize = Math.min(dimension / 5, 100) * this.getFontScale(value.text.length); const thresholdLabelFontSize = fontSize / 2.5; const options = { @@ -160,9 +102,9 @@ export class Gauge extends PureComponent { width: thresholdMarkersWidth, }, value: { - color: this.getFontColor(value), + color: value.color ? value.color : BasicGaugeColor.Red, formatter: () => { - return formattedValue; + return value.text; }, font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' }, }, @@ -171,7 +113,8 @@ export class Gauge extends PureComponent { }, }; - const plotSeries = { data: [[0, value]] }; + const numeric = value.numeric !== null ? value.numeric : 0; + const plotSeries = { data: [[0, numeric]] }; try { $.plot(this.canvasElement, [plotSeries], options); diff --git a/packages/grafana-ui/src/utils/valueProcessor.test.ts b/packages/grafana-ui/src/utils/valueProcessor.test.ts new file mode 100644 index 00000000000..76c18f9e93c --- /dev/null +++ b/packages/grafana-ui/src/utils/valueProcessor.test.ts @@ -0,0 +1,107 @@ +import { getValueProcessor, getColorFromThreshold } from './valueProcessor'; +import { getTheme } from '../themes/index'; +import { GrafanaThemeType } from '../types/theme'; +import { MappingType, ValueMapping } from '../types/panel'; + +describe('Process values', () => { + const basicConversions = [ + { value: null, text: '' }, + { value: undefined, text: '' }, + { value: 1.23, text: '1.23' }, + { value: 1, text: '1' }, + { value: 'hello', text: 'hello' }, + { value: {}, text: '[object Object]' }, + { value: [], text: '' }, + { value: [1, 2, 3], text: '1,2,3' }, + { value: ['a', 'b', 'c'], text: 'a,b,c' }, + ]; + + it('should return return a string for any input value', () => { + const processor = getValueProcessor(); + basicConversions.forEach(item => { + expect(processor(item.value).text).toBe(item.text); + }); + }); + + it('should add a suffix to any value', () => { + const processor = getValueProcessor({ + prefix: 'xxx', + theme: getTheme(GrafanaThemeType.Dark), + }); + basicConversions.forEach(item => { + expect(processor(item.value).text).toBe('xxx' + item.text); + }); + }); +}); + +describe('Get color from threshold', () => { + it('should get first threshold color when only one threshold', () => { + const thresholds = [{ index: 0, value: -Infinity, color: '#7EB26D' }]; + expect(getColorFromThreshold(49, thresholds)).toEqual('#7EB26D'); + }); + + it('should get the threshold color if value is same as a threshold', () => { + const thresholds = [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]; + expect(getColorFromThreshold(50, thresholds)).toEqual('#EAB839'); + }); + + it('should get the nearest threshold color between thresholds', () => { + const thresholds = [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]; + expect(getColorFromThreshold(55, thresholds)).toEqual('#EAB839'); + }); +}); + +describe('Format value', () => { + it('should return if value isNaN', () => { + const valueMappings: ValueMapping[] = []; + const value = 'N/A'; + const instance = getValueProcessor({ mappings: valueMappings }); + + const result = instance(value); + + expect(result.text).toEqual('N/A'); + }); + + it('should return formatted value if there are no value mappings', () => { + const valueMappings: ValueMapping[] = []; + const value = '6'; + + const instance = getValueProcessor({ mappings: valueMappings, decimals: 1 }); + + const result = instance(value); + + expect(result.text).toEqual('6.0'); + }); + + it('should return formatted value if there are no matching value mappings', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' }, + ]; + const value = '10'; + const instance = getValueProcessor({ mappings: valueMappings, decimals: 1 }); + + const result = instance(value); + + expect(result.text).toEqual('10.0'); + }); + + it('should return mapped value if there are matching value mappings', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = '11'; + const instance = getValueProcessor({ mappings: valueMappings, decimals: 1 }); + + expect(instance(value).text).toEqual('1-20'); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueProcessor.ts b/packages/grafana-ui/src/utils/valueProcessor.ts new file mode 100644 index 00000000000..243904c269c --- /dev/null +++ b/packages/grafana-ui/src/utils/valueProcessor.ts @@ -0,0 +1,97 @@ +import { ValueMapping, Threshold } from '../types/panel'; +import _ from 'lodash'; +import { getValueFormat, DecimalCount } from './valueFormats/valueFormats'; +import { getMappedValue } from './valueMappings'; +import { GrafanaTheme, GrafanaThemeType } from '../types/theme'; +import { getColorFromHexRgbOrName } from './namedColorsPalette'; + +export interface DisplayValue { + text: string; // How the value should be displayed + numeric?: number; // the value as a number + color?: string; // suggested color +} + +export interface DisplayValueOptions { + unit?: string; + decimals?: DecimalCount; + scaledDecimals?: DecimalCount; + isUtc?: boolean; + + color?: string; + mappings?: ValueMapping[]; + thresholds?: Threshold[]; + prefix?: string; + suffix?: string; + + noValue?: string; + theme?: GrafanaTheme; // Will pick 'dark' if not defined +} + +export type ValueProcessor = (value: any) => DisplayValue; + +export function getValueProcessor(options?: DisplayValueOptions): ValueProcessor { + if (options && !_.isEmpty(options)) { + const formatFunc = getValueFormat(options.unit || 'none'); + return (value: any) => { + const { prefix, suffix, mappings, thresholds, theme } = options; + let color = options.color; + + let text = _.toString(value); + const numeric = _.toNumber(value); + + if (mappings && mappings.length > 0) { + const mappedValue = getMappedValue(mappings, value); + if (mappedValue) { + text = mappedValue.text; + // TODO? convert the mapped value back to a number? + } + } + + if (_.isNumber(numeric)) { + text = formatFunc(numeric, options.decimals, options.scaledDecimals, options.isUtc); + if (thresholds && thresholds.length > 0) { + color = getColorFromThreshold(numeric, thresholds, theme); + } + } + + if (!text) { + text = options.noValue ? options.noValue : ''; + } + if (prefix) { + text = prefix + text; + } + if (suffix) { + text = text + suffix; + } + return { text, numeric, color }; + }; + } + return toStringProcessor; +} + +function toStringProcessor(value: any): DisplayValue { + return { text: _.toString(value), numeric: _.toNumber(value) }; +} + +export function getColorFromThreshold(value: number, thresholds: Threshold[], theme?: GrafanaTheme): string { + const themeType = theme ? theme.type : GrafanaThemeType.Dark; + + if (thresholds.length === 1) { + return getColorFromHexRgbOrName(thresholds[0].color, themeType); + } + + const atThreshold = thresholds.filter(threshold => value === threshold.value)[0]; + if (atThreshold) { + return getColorFromHexRgbOrName(atThreshold.color, themeType); + } + + const belowThreshold = thresholds.filter(threshold => value > threshold.value); + + if (belowThreshold.length > 0) { + const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; + return getColorFromHexRgbOrName(nearestThreshold.color, themeType); + } + + // Use the first threshold as the default color + return getColorFromHexRgbOrName(thresholds[0].color, themeType); +} diff --git a/public/app/plugins/panel/gauge/DisplayValueEditor.tsx b/public/app/plugins/panel/gauge/DisplayValueEditor.tsx new file mode 100644 index 00000000000..51c956a9529 --- /dev/null +++ b/public/app/plugins/panel/gauge/DisplayValueEditor.tsx @@ -0,0 +1,64 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Components +import { FormField, FormLabel, PanelOptionsGroup, UnitPicker } from '@grafana/ui'; + +// Types +import { DisplayValueOptions } from '@grafana/ui/src/utils/valueProcessor'; + +const labelWidth = 6; + +export interface Props { + options: DisplayValueOptions; + onChange: (options: DisplayValueOptions) => void; +} + +export class DisplayValueEditor extends PureComponent { + onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); + + onDecimalChange = event => { + if (!isNaN(event.target.value)) { + this.props.onChange({ + ...this.props.options, + decimals: parseInt(event.target.value, 10), + }); + } else { + this.props.onChange({ + ...this.props.options, + decimals: null, + }); + } + }; + + onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value }); + onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value }); + + render() { + const { unit, decimals, prefix, suffix } = this.props.options; + + let decimalsString = ''; + if (Number.isFinite(decimals)) { + decimalsString = decimals.toString(); + } + + return ( + +
+ Unit + +
+ + + +
+ ); + } +} diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index b75d4a1c7f3..425ccb00356 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -9,30 +9,50 @@ import { Gauge } from '@grafana/ui'; // Types import { GaugeOptions } from './types'; -import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types'; +import { PanelProps, NullValueMode, BasicGaugeColor } from '@grafana/ui/src/types'; +import { DisplayValue, getValueProcessor } from '@grafana/ui/src/utils/valueProcessor'; interface Props extends PanelProps {} interface State { - value: TimeSeriesValue; + value: DisplayValue; } export class GaugePanel extends Component { constructor(props: Props) { super(props); + + if (props.options.valueOptions) { + console.warn('TODO!! how do we best migration options?'); + } + this.state = { - value: this.findValue(props), + value: this.findDisplayValue(props), }; } componentDidUpdate(prevProps: Props) { if (this.props.panelData !== prevProps.panelData) { - this.setState({ value: this.findValue(this.props) }); + this.setState({ value: this.findDisplayValue(this.props) }); } } + findDisplayValue(props: Props): DisplayValue { + const { replaceVariables, options } = this.props; + const { displayOptions } = options; + + const prefix = replaceVariables(displayOptions.prefix); + const suffix = replaceVariables(displayOptions.suffix); + return getValueProcessor({ + color: BasicGaugeColor.Red, // The default color + ...displayOptions, + prefix, + suffix, + // ??? theme:getTheme(GrafanaThemeType.Dark), !! how do I get it here??? + })(this.findValue(props)); + } + findValue(props: Props): number | null { const { panelData, options } = props; - const { valueOptions } = options; if (panelData.timeSeries) { const vmSeries = processTimeSeries({ @@ -41,7 +61,7 @@ export class GaugePanel extends Component { }); if (vmSeries[0]) { - return vmSeries[0].stats[valueOptions.stat]; + return vmSeries[0].stats[options.stat]; } } else if (panelData.tableData) { return panelData.tableData.rows[0].find(prop => prop > 0); @@ -50,12 +70,9 @@ export class GaugePanel extends Component { } render() { - const { width, height, replaceVariables, options } = this.props; - const { valueOptions } = options; + const { width, height, options } = this.props; const { value } = this.state; - const prefix = replaceVariables(valueOptions.prefix); - const suffix = replaceVariables(valueOptions.suffix); return ( {theme => ( @@ -63,12 +80,7 @@ export class GaugePanel extends Component { value={value} width={width} height={height} - prefix={prefix} - suffix={suffix} - unit={valueOptions.unit} - decimals={valueOptions.decimals} thresholds={options.thresholds} - valueMappings={options.valueMappings} showThresholdLabels={options.showThresholdLabels} showThresholdMarkers={options.showThresholdMarkers} minValue={options.minValue} diff --git a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx index f226be7328c..55a0377848d 100644 --- a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx @@ -11,6 +11,8 @@ import { import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor'; import { GaugeOptionsBox } from './GaugeOptionsBox'; import { GaugeOptions, SingleStatValueOptions } from './types'; +import { DisplayValueEditor } from './DisplayValueEditor'; +import { DisplayValueOptions } from '@grafana/ui/src/utils/valueProcessor'; export class GaugePanelEditor extends PureComponent> { onThresholdsChanged = (thresholds: Threshold[]) => @@ -31,13 +33,22 @@ export class GaugePanelEditor extends PureComponent + this.props.onOptionsChange({ + ...this.props.options, + displayOptions, + }); + render() { const { onOptionsChange, options } = this.props; return ( <> - + {/* This just sets the 'stats', that should be moved to somethign more general */} + + + diff --git a/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx index e711df6a2d3..414a606b108 100644 --- a/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx +++ b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx @@ -2,10 +2,10 @@ import React, { PureComponent } from 'react'; // Components -import { FormField, FormLabel, PanelOptionsGroup, Select, UnitPicker } from '@grafana/ui'; +import { FormLabel, PanelOptionsGroup, Select } from '@grafana/ui'; // Types -import { SingleStatValueOptions } from './types'; +import { GaugeOptions } from './types'; const statOptions = [ { value: 'min', label: 'Min' }, @@ -24,41 +24,18 @@ const statOptions = [ const labelWidth = 6; export interface Props { - options: SingleStatValueOptions; - onChange: (valueOptions: SingleStatValueOptions) => void; + options: GaugeOptions; + onChange: (options: GaugeOptions) => void; } export class SingleStatValueEditor extends PureComponent { - onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value }); - onDecimalChange = event => { - if (!isNaN(event.target.value)) { - this.props.onChange({ - ...this.props.options, - decimals: parseInt(event.target.value, 10), - }); - } else { - this.props.onChange({ - ...this.props.options, - decimals: null, - }); - } - }; - - onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value }); - onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value }); - render() { - const { stat, unit, decimals, prefix, suffix } = this.props.options; - - let decimalsString = ''; - if (Number.isFinite(decimals)) { - decimalsString = decimals.toString(); - } + const { stat } = this.props.options; return ( - +
Stat item.value === options.displayMode)} + /> +
diff --git a/public/app/plugins/panel/bargauge/types.ts b/public/app/plugins/panel/bargauge/types.ts index 962d18d0e3d..58694ab43ec 100644 --- a/public/app/plugins/panel/bargauge/types.ts +++ b/public/app/plugins/panel/bargauge/types.ts @@ -8,6 +8,7 @@ export interface BarGaugeOptions { valueOptions: SingleStatValueOptions; valueMappings: ValueMapping[]; thresholds: Threshold[]; + displayMode: 'simple' | 'lcd'; } export const orientationOptions: SelectOptionItem[] = [ @@ -15,9 +16,12 @@ export const orientationOptions: SelectOptionItem[] = [ { value: VizOrientation.Vertical, label: 'Vertical' }, ]; +export const displayModes: SelectOptionItem[] = [{ value: 'simple', label: 'Simple' }, { value: 'lcd', label: 'LCD' }]; + export const defaults: BarGaugeOptions = { minValue: 0, maxValue: 100, + displayMode: 'simple', orientation: VizOrientation.Horizontal, valueOptions: { unit: 'none', From 985f057ab381a00ddf1f97e697013d8fbd85656c Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 14 Mar 2019 13:20:24 -0700 Subject: [PATCH 069/194] revert most options sharing --- packages/grafana-ui/src/utils/singlestat.ts | 6 -- .../app/features/plugins/built_in_plugins.ts | 2 + .../plugins/panel/bargauge/BarGaugePanel.tsx | 14 +--- .../panel/bargauge/BarGaugePanelEditor.tsx | 49 +++++------ public/app/plugins/panel/bargauge/module.tsx | 4 +- public/app/plugins/panel/bargauge/types.ts | 25 ++++-- public/app/plugins/panel/gauge/GaugePanel.tsx | 7 +- .../plugins/panel/gauge/GaugePanelEditor.tsx | 34 ++++---- .../panel/gauge/SingleStatValueEditor.tsx | 51 ------------ .../gauge/__snapshots__/module.test.ts.snap | 31 ------- public/app/plugins/panel/gauge/module.test.ts | 27 ------ public/app/plugins/panel/gauge/module.tsx | 52 +----------- public/app/plugins/panel/gauge/types.ts | 15 ++-- .../app/plugins/panel/singlestat2/README.md | 9 ++ .../SingleStatBase.tsx} | 40 ++++----- .../panel/singlestat2/SingleStatEditor.tsx | 48 +++++++++++ .../panel/singlestat2/SingleStatPanel.tsx | 17 ++++ .../SingleStatValueEditor.tsx} | 38 +++++++-- .../singlestat2/img/icn-singlestat-panel.svg | 83 +++++++++++++++++++ .../app/plugins/panel/singlestat2/module.tsx | 29 +++++++ .../app/plugins/panel/singlestat2/plugin.json | 20 +++++ public/app/plugins/panel/singlestat2/types.ts | 33 ++++++++ 22 files changed, 360 insertions(+), 274 deletions(-) delete mode 100644 public/app/plugins/panel/gauge/SingleStatValueEditor.tsx delete mode 100644 public/app/plugins/panel/gauge/__snapshots__/module.test.ts.snap delete mode 100644 public/app/plugins/panel/gauge/module.test.ts create mode 100644 public/app/plugins/panel/singlestat2/README.md rename public/app/plugins/panel/{gauge/SingleStatPanel.tsx => singlestat2/SingleStatBase.tsx} (64%) create mode 100644 public/app/plugins/panel/singlestat2/SingleStatEditor.tsx create mode 100644 public/app/plugins/panel/singlestat2/SingleStatPanel.tsx rename public/app/plugins/panel/{gauge/DisplayValueEditor.tsx => singlestat2/SingleStatValueEditor.tsx} (55%) create mode 100644 public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg create mode 100644 public/app/plugins/panel/singlestat2/module.tsx create mode 100644 public/app/plugins/panel/singlestat2/plugin.json create mode 100644 public/app/plugins/panel/singlestat2/types.ts diff --git a/packages/grafana-ui/src/utils/singlestat.ts b/packages/grafana-ui/src/utils/singlestat.ts index 95938069b40..5f5fbb8f247 100644 --- a/packages/grafana-ui/src/utils/singlestat.ts +++ b/packages/grafana-ui/src/utils/singlestat.ts @@ -1,17 +1,11 @@ import { PanelData, NullValueMode, SingleStatValueInfo } from '../types'; import { processTimeSeries } from './processTimeSeries'; -import { DisplayValueOptions } from './displayValue'; export interface SingleStatProcessingOptions { panelData: PanelData; stat: string; } -export interface SingleStatOptions { - stat: string; - display: DisplayValueOptions; -} - // // This is a temporary thing, waiting for a better data model and maybe unification between time series & table data // diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 9a156652a65..ab9d9aba08a 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -25,6 +25,7 @@ import * as heatmapPanel from 'app/plugins/panel/heatmap/module'; import * as tablePanel from 'app/plugins/panel/table/module'; import * as table2Panel from 'app/plugins/panel/table2/module'; import * as singlestatPanel from 'app/plugins/panel/singlestat/module'; +import * as singlestatPanel2 from 'app/plugins/panel/singlestat2/module'; import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module'; import * as gaugePanel from 'app/plugins/panel/gauge/module'; import * as barGaugePanel from 'app/plugins/panel/bargauge/module'; @@ -57,6 +58,7 @@ const builtInPlugins = { 'app/plugins/panel/table/module': tablePanel, 'app/plugins/panel/table2/module': table2Panel, 'app/plugins/panel/singlestat/module': singlestatPanel, + 'app/plugins/panel/singlestat2/module': singlestatPanel2, 'app/plugins/panel/gettingstarted/module': gettingStartedPanel, 'app/plugins/panel/gauge/module': gaugePanel, 'app/plugins/panel/bargauge/module': barGaugePanel, diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index 8102d16de40..2fa48b4572a 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -2,7 +2,7 @@ import React from 'react'; // Services & Utils -import { DisplayValue, VizOrientation } from '@grafana/ui'; +import { DisplayValue } from '@grafana/ui'; import { config } from 'app/core/config'; // Components @@ -10,17 +10,11 @@ import { BarGauge } from '@grafana/ui'; // Types import { BarGaugeOptions } from './types'; -import { SingleStatPanel } from '../gauge/SingleStatPanel'; - -export class BarGaugePanel extends SingleStatPanel { - getOrientation(): VizOrientation { - const { options } = this.props; - return options.orientation; - } +import { SingleStatBase } from '../singlestat2/SingleStatBase'; +export class BarGaugePanel extends SingleStatBase { renderStat(value: DisplayValue, width: number, height: number) { const { options } = this.props; - const { display } = options; return ( { width={width} height={height} orientation={options.orientation} - thresholds={display.thresholds} + thresholds={options.thresholds} theme={config.theme} /> ); diff --git a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx index 2420a4c1ec2..cccd4e88b8e 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx @@ -2,38 +2,31 @@ import React, { PureComponent } from 'react'; // Components -import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor'; -import { - PanelOptionsGrid, - PanelOptionsGroup, - FormField, - DisplayValueOptions, - ThresholdsEditor, - Threshold, -} from '@grafana/ui'; +import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui'; // Types -import { FormLabel, PanelEditorProps, Select, ValueMappingsEditor, ValueMapping } from '@grafana/ui'; +import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui'; import { BarGaugeOptions, orientationOptions } from './types'; -import { DisplayValueEditor } from '../gauge/DisplayValueEditor'; +import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor'; +import { SingleStatValueOptions } from '../singlestat2/types'; export class BarGaugePanelEditor extends PureComponent> { - onDisplayOptionsChanged = (displayOptions: DisplayValueOptions) => + onThresholdsChanged = (thresholds: Threshold[]) => this.props.onOptionsChange({ ...this.props.options, - display: displayOptions, - }); - - onThresholdsChanged = (thresholds: Threshold[]) => - this.onDisplayOptionsChanged({ - ...this.props.options.display, thresholds, }); onValueMappingsChanged = (valueMappings: ValueMapping[]) => - this.onDisplayOptionsChanged({ - ...this.props.options.display, - mappings: valueMappings, + this.props.onOptionsChange({ + ...this.props.options, + valueMappings, + }); + + onValueOptionsChanged = (valueOptions: SingleStatValueOptions) => + this.props.onOptionsChange({ + ...this.props.options, + valueOptions, }); onMinValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, minValue: target.value }); @@ -41,17 +34,12 @@ export class BarGaugePanelEditor extends PureComponent this.props.onOptionsChange({ ...this.props.options, orientation: value }); render() { - const { onOptionsChange, options } = this.props; - const { display } = options; + const { options } = this.props; return ( <> - {/* This just sets the 'stats', that should be moved to somethign more general */} - - - - + @@ -66,9 +54,10 @@ export class BarGaugePanelEditor extends PureComponent
- - + + + ); } diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index f3dab902dd1..5ca355b3110 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/public/app/plugins/panel/bargauge/module.tsx @@ -3,10 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui'; import { BarGaugePanel } from './BarGaugePanel'; import { BarGaugePanelEditor } from './BarGaugePanelEditor'; import { BarGaugeOptions, defaults } from './types'; -import { gaugePanelTypeChangedHook } from '../gauge/module'; +import { singleStatOptionsCheck } from '../singlestat2/module'; export const reactPanel = new ReactPanelPlugin(BarGaugePanel); reactPanel.setEditor(BarGaugePanelEditor); reactPanel.setDefaults(defaults); -reactPanel.setPanelTypeChangedHook(gaugePanelTypeChangedHook); +reactPanel.setPanelTypeChangedHook(singleStatOptionsCheck); diff --git a/public/app/plugins/panel/bargauge/types.ts b/public/app/plugins/panel/bargauge/types.ts index 562d4b03323..ea6f1887501 100644 --- a/public/app/plugins/panel/bargauge/types.ts +++ b/public/app/plugins/panel/bargauge/types.ts @@ -1,17 +1,28 @@ -import { SelectOptionItem, VizOrientation } from '@grafana/ui'; +import { VizOrientation, SelectOptionItem } from '@grafana/ui'; -import { GaugeOptions, defaults as gaugeDefaults } from '../gauge/types'; - -export interface BarGaugeOptions extends GaugeOptions { - orientation: VizOrientation; -} +import { SingleStatBaseOptions } from '../singlestat2/types'; export const orientationOptions: SelectOptionItem[] = [ { value: VizOrientation.Horizontal, label: 'Horizontal' }, { value: VizOrientation.Vertical, label: 'Vertical' }, ]; +export interface BarGaugeOptions extends SingleStatBaseOptions { + minValue: number; + maxValue: number; +} + export const defaults: BarGaugeOptions = { - ...gaugeDefaults, + minValue: 0, + maxValue: 100, orientation: VizOrientation.Horizontal, + valueOptions: { + unit: 'none', + stat: 'avg', + prefix: '', + suffix: '', + decimals: null, + }, + thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }], + valueMappings: [], }; diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 387e0116239..72b4756c0b6 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -10,19 +10,18 @@ import { Gauge } from '@grafana/ui'; // Types import { GaugeOptions } from './types'; import { DisplayValue } from '@grafana/ui/src/utils/displayValue'; -import { SingleStatPanel } from './SingleStatPanel'; +import { SingleStatBase } from '../singlestat2/SingleStatBase'; -export class GaugePanel extends SingleStatPanel { +export class GaugePanel extends SingleStatBase { renderStat(value: DisplayValue, width: number, height: number) { const { options } = this.props; - const { display } = options; return ( > { - onDisplayOptionsChanged = (displayOptions: DisplayValueOptions) => + onThresholdsChanged = (thresholds: Threshold[]) => this.props.onOptionsChange({ ...this.props.options, - display: displayOptions, - }); - - onThresholdsChanged = (thresholds: Threshold[]) => - this.onDisplayOptionsChanged({ - ...this.props.options.display, thresholds, }); onValueMappingsChanged = (valueMappings: ValueMapping[]) => - this.onDisplayOptionsChanged({ - ...this.props.options.display, - mappings: valueMappings, + this.props.onOptionsChange({ + ...this.props.options, + valueMappings, + }); + + onValueOptionsChanged = (valueOptions: SingleStatValueOptions) => + this.props.onOptionsChange({ + ...this.props.options, + valueOptions, }); render() { const { onOptionsChange, options } = this.props; - const { display } = options; return ( <> - {/* This just sets the 'stats', that should be moved to somethign more general */} - - + - + - + ); } diff --git a/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx deleted file mode 100644 index 414a606b108..00000000000 --- a/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// Libraries -import React, { PureComponent } from 'react'; - -// Components -import { FormLabel, PanelOptionsGroup, Select } from '@grafana/ui'; - -// Types -import { GaugeOptions } from './types'; - -const statOptions = [ - { value: 'min', label: 'Min' }, - { value: 'max', label: 'Max' }, - { value: 'avg', label: 'Average' }, - { value: 'current', label: 'Current' }, - { value: 'total', label: 'Total' }, - { value: 'name', label: 'Name' }, - { value: 'first', label: 'First' }, - { value: 'delta', label: 'Delta' }, - { value: 'diff', label: 'Difference' }, - { value: 'range', label: 'Range' }, - { value: 'last_time', label: 'Time of last point' }, -]; - -const labelWidth = 6; - -export interface Props { - options: GaugeOptions; - onChange: (options: GaugeOptions) => void; -} - -export class SingleStatValueEditor extends PureComponent { - onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value }); - - render() { - const { stat } = this.props.options; - - return ( - -
- Stat - option.value === stat)} + /> +
Unit diff --git a/public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg b/public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg new file mode 100644 index 00000000000..746687d360f --- /dev/null +++ b/public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx new file mode 100644 index 00000000000..c07e24e5198 --- /dev/null +++ b/public/app/plugins/panel/singlestat2/module.tsx @@ -0,0 +1,29 @@ +import { ReactPanelPlugin } from '@grafana/ui'; +import { SingleStatOptions, defaults } from './types'; +import { SingleStatPanel } from './SingleStatPanel'; +import cloneDeep from 'lodash/cloneDeep'; +import { SingleStatEditor } from './SingleStatEditor'; + +export const reactPanel = new ReactPanelPlugin(SingleStatPanel); + +const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings']; + +export const singleStatOptionsCheck = ( + options: Partial, + prevPluginId?: string, + prevOptions?: any +) => { + if (prevOptions) { + optionsToKeep.forEach(v => { + if (prevOptions.hasOwnProperty(v)) { + options[v] = cloneDeep(prevOptions.display); + } + }); + } + + return options; +}; + +reactPanel.setEditor(SingleStatEditor); +reactPanel.setDefaults(defaults); +reactPanel.setPanelTypeChangedHook(singleStatOptionsCheck); diff --git a/public/app/plugins/panel/singlestat2/plugin.json b/public/app/plugins/panel/singlestat2/plugin.json new file mode 100644 index 00000000000..6828399ec2b --- /dev/null +++ b/public/app/plugins/panel/singlestat2/plugin.json @@ -0,0 +1,20 @@ +{ + "type": "panel", + "name": "Singlestat (react)", + "id": "singlestat2", + "state": "alpha", + + "dataFormats": ["time_series", "table"], + + "info": { + "description": "Singlestat Panel for Grafana", + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-singlestat-panel.svg", + "large": "img/icn-singlestat-panel.svg" + } + } +} diff --git a/public/app/plugins/panel/singlestat2/types.ts b/public/app/plugins/panel/singlestat2/types.ts new file mode 100644 index 00000000000..1f31783e814 --- /dev/null +++ b/public/app/plugins/panel/singlestat2/types.ts @@ -0,0 +1,33 @@ +import { VizOrientation, ValueMapping, Threshold } from '@grafana/ui'; + +export interface SingleStatBaseOptions { + valueMappings: ValueMapping[]; + thresholds: Threshold[]; + valueOptions: SingleStatValueOptions; + orientation: VizOrientation; +} + +export interface SingleStatValueOptions { + unit: string; + suffix: string; + stat: string; + prefix: string; + decimals?: number | null; +} + +export interface SingleStatOptions extends SingleStatBaseOptions { + // TODO, fill in with options from angular +} + +export const defaults: SingleStatOptions = { + valueOptions: { + prefix: '', + suffix: '', + decimals: null, + stat: 'avg', + unit: 'none', + }, + valueMappings: [], + thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }], + orientation: VizOrientation.Auto, +}; From 8be56f8a0e1e44581caf3e73326d429d87bc1576 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 14 Mar 2019 13:31:50 -0700 Subject: [PATCH 070/194] improve single stat display --- .../plugins/panel/singlestat2/SingleStatPanel.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx index f9f20487c95..10c5523e7c1 100644 --- a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx +++ b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx @@ -1,5 +1,5 @@ // Libraries -import React from 'react'; +import React, { CSSProperties } from 'react'; // Types import { SingleStatOptions } from './types'; @@ -8,9 +8,17 @@ import { SingleStatBase } from './SingleStatBase'; export class SingleStatPanel extends SingleStatBase { renderStat(value: DisplayValue, width: number, height: number) { + const style: CSSProperties = {}; + style.margin = '0 auto'; + style.fontSize = '250%'; + style.textAlign = 'center'; + if (value.color) { + style.color = value.color; + } + return (
- {value.text} +
{value.text}
); } From 1fa07a8254a55f65a4556b961c98a823a6ee6deb Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 14 Mar 2019 14:47:01 -0700 Subject: [PATCH 071/194] no inheratance --- .../plugins/panel/bargauge/BarGaugePanel.tsx | 33 +++++++--- public/app/plugins/panel/gauge/GaugePanel.tsx | 30 +++++++-- .../singlestat2/ProcessedValuesRepeater.tsx | 48 +++++++++++++++ .../panel/singlestat2/SingleStatBase.tsx | 61 ------------------- .../panel/singlestat2/SingleStatPanel.tsx | 51 ++++++++++++++-- 5 files changed, 144 insertions(+), 79 deletions(-) create mode 100644 public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx delete mode 100644 public/app/plugins/panel/singlestat2/SingleStatBase.tsx diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index 2fa48b4572a..e738475afc8 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -1,19 +1,17 @@ // Libraries -import React from 'react'; +import React, { PureComponent } from 'react'; // Services & Utils -import { DisplayValue } from '@grafana/ui'; +import { DisplayValue, PanelProps, BarGauge } from '@grafana/ui'; import { config } from 'app/core/config'; -// Components -import { BarGauge } from '@grafana/ui'; - // Types import { BarGaugeOptions } from './types'; -import { SingleStatBase } from '../singlestat2/SingleStatBase'; +import { getSingleStatValues } from '../singlestat2/SingleStatPanel'; +import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater'; -export class BarGaugePanel extends SingleStatBase { - renderStat(value: DisplayValue, width: number, height: number) { +export class BarGaugePanel extends PureComponent> { + renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => { const { options } = this.props; return ( @@ -26,5 +24,24 @@ export class BarGaugePanel extends SingleStatBase { theme={config.theme} /> ); + }; + + getProcessedValues = (): DisplayValue[] => { + return getSingleStatValues(this.props); + }; + + render() { + const { height, width, options, panelData } = this.props; + const { orientation } = options; + return ( + + ); } } diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 72b4756c0b6..b83dc9ad440 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,5 +1,5 @@ // Libraries -import React from 'react'; +import React, { PureComponent } from 'react'; // Services & Utils import { config } from 'app/core/config'; @@ -9,11 +9,12 @@ import { Gauge } from '@grafana/ui'; // Types import { GaugeOptions } from './types'; -import { DisplayValue } from '@grafana/ui/src/utils/displayValue'; -import { SingleStatBase } from '../singlestat2/SingleStatBase'; +import { DisplayValue, PanelProps } from '@grafana/ui'; +import { getSingleStatValues } from '../singlestat2/SingleStatPanel'; +import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater'; -export class GaugePanel extends SingleStatBase { - renderStat(value: DisplayValue, width: number, height: number) { +export class GaugePanel extends PureComponent> { + renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => { const { options } = this.props; return ( @@ -29,5 +30,24 @@ export class GaugePanel extends SingleStatBase { theme={config.theme} /> ); + }; + + getProcessedValues = (): DisplayValue[] => { + return getSingleStatValues(this.props); + }; + + render() { + const { height, width, options, panelData } = this.props; + const { orientation } = options; + return ( + + ); } } diff --git a/public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx b/public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx new file mode 100644 index 00000000000..d42c033eac2 --- /dev/null +++ b/public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx @@ -0,0 +1,48 @@ +import React, { PureComponent } from 'react'; +import { VizOrientation } from '@grafana/ui'; +import { VizRepeater } from '@grafana/ui'; + +export interface Props { + width: number; + height: number; + orientation: VizOrientation; + source: any; // If this changes, the values will be processed + processFlag?: boolean; // change to force processing + + getProcessedValues: () => T[]; + renderValue: (value: T, width: number, height: number) => JSX.Element; +} + +interface State { + values: T[]; +} + +/** + * This is essentially a cache of processed values. This checks for changes + * to the source and then saves the processed values in the State + */ +export class ProcessedValuesRepeater extends PureComponent, State> { + constructor(props: Props) { + super(props); + this.state = { + values: props.getProcessedValues(), + }; + } + + componentDidUpdate(prevProps: Props) { + const { processFlag, source } = this.props; + if (processFlag !== prevProps.processFlag || source !== prevProps.source) { + this.setState({ values: this.props.getProcessedValues() }); + } + } + + render() { + const { orientation, height, width, renderValue } = this.props; + const { values } = this.state; + return ( + + {({ vizHeight, vizWidth, value }) => renderValue(value, vizWidth, vizHeight)} + + ); + } +} diff --git a/public/app/plugins/panel/singlestat2/SingleStatBase.tsx b/public/app/plugins/panel/singlestat2/SingleStatBase.tsx deleted file mode 100644 index fb5e54b68ed..00000000000 --- a/public/app/plugins/panel/singlestat2/SingleStatBase.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { PureComponent } from 'react'; -import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui'; -import { config } from 'app/core/config'; -import { VizRepeater, getDisplayProcessor } from '@grafana/ui'; -import { SingleStatBaseOptions } from './types'; - -export interface State { - values: DisplayValue[]; -} - -export class SingleStatBase extends PureComponent, State> { - constructor(props: PanelProps) { - super(props); - this.state = { - values: this.findDisplayValues(props), - }; - } - - componentDidUpdate(prevProps: PanelProps) { - if (this.props.panelData !== prevProps.panelData) { - this.setState({ values: this.findDisplayValues(this.props) }); - } - } - - findDisplayValues(props: PanelProps): DisplayValue[] { - const { panelData, replaceVariables, options } = this.props; - const { valueOptions, valueMappings } = options; - const processor = getDisplayProcessor({ - unit: valueOptions.unit, - decimals: valueOptions.decimals, - mappings: valueMappings, - thresholds: options.thresholds, - - prefix: replaceVariables(valueOptions.prefix), - suffix: replaceVariables(valueOptions.suffix), - theme: config.theme, - }); - return processSingleStatPanelData({ - panelData: panelData, - stat: valueOptions.stat, - }).map(stat => processor(stat.value)); - } - - /** - * Subclasses will fill in appropriatly - */ - renderStat(value: DisplayValue, width: number, height: number) { - return
{value.text}
; - } - - render() { - const { height, width, options } = this.props; - const { orientation } = options; - const { values } = this.state; - return ( - - {({ vizHeight, vizWidth, value }) => this.renderStat(value, vizWidth, vizHeight)} - - ); - } -} diff --git a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx index 10c5523e7c1..323a0be5658 100644 --- a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx +++ b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx @@ -1,13 +1,35 @@ // Libraries -import React, { CSSProperties } from 'react'; +import React, { PureComponent, CSSProperties } from 'react'; // Types import { SingleStatOptions } from './types'; -import { DisplayValue } from '@grafana/ui/src/utils/displayValue'; -import { SingleStatBase } from './SingleStatBase'; -export class SingleStatPanel extends SingleStatBase { - renderStat(value: DisplayValue, width: number, height: number) { +import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui'; +import { config } from 'app/core/config'; +import { getDisplayProcessor } from '@grafana/ui'; +import { ProcessedValuesRepeater } from './ProcessedValuesRepeater'; + +export const getSingleStatValues = (props: PanelProps): DisplayValue[] => { + const { panelData, replaceVariables, options } = props; + const { valueOptions, valueMappings } = options; + const processor = getDisplayProcessor({ + unit: valueOptions.unit, + decimals: valueOptions.decimals, + mappings: valueMappings, + thresholds: options.thresholds, + + prefix: replaceVariables(valueOptions.prefix), + suffix: replaceVariables(valueOptions.suffix), + theme: config.theme, + }); + return processSingleStatPanelData({ + panelData: panelData, + stat: valueOptions.stat, + }).map(stat => processor(stat.value)); +}; + +export class SingleStatPanel extends PureComponent> { + renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => { const style: CSSProperties = {}; style.margin = '0 auto'; style.fontSize = '250%'; @@ -21,5 +43,24 @@ export class SingleStatPanel extends SingleStatBase {
{value.text}
); + }; + + getProcessedValues = (): DisplayValue[] => { + return getSingleStatValues(this.props); + }; + + render() { + const { height, width, options, panelData } = this.props; + const { orientation } = options; + return ( + + ); } } From 75710d0f2b1444c21a85fc05802c437b74f0302b Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 14 Mar 2019 14:58:46 -0700 Subject: [PATCH 072/194] add partial --- packages/grafana-ui/src/types/panel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 2ac38b6253a..2307f446a98 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -30,10 +30,10 @@ export interface PanelEditorProps { * Called before a panel is initalized */ export type PanelTypeChangedHook = ( - options: TOptions, + options: Partial, prevPluginId?: string, prevOptions?: any -) => TOptions; +) => Partial; export class ReactPanelPlugin { panel: ComponentClass>; From 1ddf6dafb63066d37d0033687aa4f30d0a9f158a Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Mar 2019 07:31:55 +0100 Subject: [PATCH 073/194] changelog: adds note about closing #6359 and #15931 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc566e5f596..abddad1f58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683) * **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683) * **VictorOps**: Adds more information to the victor ops notifiers [#15744](https://github.com/grafana/grafana/issues/15744), thx [@zhulongcheng](https://github.com/zhulongcheng) +* **Dataproxy**: Make it possible to add user details to requests sent to the dataproxy [#6359](https://github.com/grafana/grafana/issues/6359) and [#15931](https://github.com/grafana/grafana/issues/15931) ### Bug Fixes * **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506) From e3b3b35dca4ea3c8deae022ecbe57b59a1c8d867 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 15 Mar 2019 10:07:20 +0300 Subject: [PATCH 074/194] panels: fix loading panels with non-array targets --- public/app/features/dashboard/state/PanelModel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 88065fdf208..1bac7cae3d6 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -126,7 +126,8 @@ export class PanelModel { ensureQueryIds() { if (this.targets) { - for (const query of this.targets) { + for (let i = 0; i < this.targets.length; i++) { + const query = this.targets[i]; if (!query.refId) { query.refId = this.getNextQueryLetter(); } From 707d188428a4a45cb99bd1ddc89fdb3eaf5d4577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 Mar 2019 08:29:11 +0100 Subject: [PATCH 075/194] Bar gauge styling tweaks --- .../src/components/BarGauge/BarGauge.tsx | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 0de46709c16..2ac5a9e287e 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -149,7 +149,7 @@ export class BarGauge extends PureComponent { ); } - getCellColor(positionValue: TimeSeriesValue): string { + getCellColor(positionValue: TimeSeriesValue): CellColors { const { thresholds, theme, value } = this.props; const activeThreshold = getThresholdForValue(thresholds, positionValue); @@ -158,17 +158,33 @@ export class BarGauge extends PureComponent { // if we are past real value the cell is not "on" if (value === null || (positionValue !== null && positionValue > value)) { - return tinycolor(color) - .setAlpha(0.15) - .toRgbString(); + return { + background: tinycolor(color) + .setAlpha(0.15) + .toRgbString(), + border: 'transparent', + isLit: false, + }; } else { - return tinycolor(color) - .setAlpha(0.7) - .toRgbString(); + return { + background: tinycolor(color) + .setAlpha(0.85) + .toRgbString(), + backgroundShade: tinycolor(color) + .setAlpha(0.55) + .toRgbString(), + border: tinycolor(color) + .setAlpha(0.9) + .toRgbString(), + isLit: true, + }; } } - return 'gray'; + return { + background: 'gray', + border: 'gray', + }; } renderLcdMode(valueFormatted: string, valuePercent: number): ReactNode { @@ -176,8 +192,8 @@ export class BarGauge extends PureComponent { const valueRange = maxValue - minValue; const maxSize = this.size * BAR_SIZE_RATIO; - const cellSpacing = 4; - const cellCount = 30; + const cellSpacing = 5; + const cellCount = 25; const cellSize = (maxSize - cellSpacing * cellCount) / cellCount; const colors = this.getValueColors(); const valueStyles = this.getValueStyles(valueFormatted, colors.value, this.size - maxSize); @@ -191,9 +207,11 @@ export class BarGauge extends PureComponent { if (orientation === VizOrientation.Horizontal) { containerStyles.flexDirection = 'row'; containerStyles.alignItems = 'center'; + valueStyles.marginLeft = '20px'; } else { containerStyles.flexDirection = 'column-reverse'; containerStyles.alignItems = 'center'; + valueStyles.marginBottom = '20px'; } const cells: JSX.Element[] = []; @@ -202,18 +220,26 @@ export class BarGauge extends PureComponent { const currentValue = (valueRange / cellCount) * i; const cellColor = this.getCellColor(currentValue); const cellStyles: CSSProperties = { - backgroundColor: cellColor, borderRadius: '2px', }; + if (cellColor.isLit) { + cellStyles.boxShadow = `0 0 4px ${cellColor.border}`; + // cellStyles.border = `1px solid ${cellColor.border}`; + // cellStyles.background = `${cellColor.backgroundShade}`; + cellStyles.backgroundImage = `radial-gradient(${cellColor.background} 10%, ${cellColor.backgroundShade})`; + } else { + cellStyles.backgroundColor = cellColor.background; + } + if (orientation === VizOrientation.Horizontal) { cellStyles.width = `${cellSize}px`; cellStyles.height = `${height}px`; - cellStyles.marginRight = '4px'; + cellStyles.marginRight = `${cellSpacing}px`; } else { cellStyles.height = `${cellSize}px`; cellStyles.width = `${width}px`; - cellStyles.marginTop = '4px'; + cellStyles.marginTop = `${cellSpacing}px`; } cells.push(
); @@ -235,3 +261,10 @@ interface BarColors { bar: string; border: string; } + +interface CellColors { + background: string; + backgroundShade?: string; + border: string; + isLit?: boolean; +} From fe798239b225298550e3d052aaba59ae6da07c22 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 15 Mar 2019 10:26:56 +0300 Subject: [PATCH 076/194] panels: fix loading panels with non-array targets (refactor) --- public/app/features/dashboard/state/PanelModel.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 1bac7cae3d6..16d1f64f750 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -125,9 +125,8 @@ export class PanelModel { } ensureQueryIds() { - if (this.targets) { - for (let i = 0; i < this.targets.length; i++) { - const query = this.targets[i]; + if (this.targets && _.isArray(this.targets)) { + for (const query of this.targets) { if (!query.refId) { query.refId = this.getNextQueryLetter(); } From 6ca1ae309a4cfbfa7f85c691d63c72f0a8277772 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 15 Mar 2019 08:52:25 +0100 Subject: [PATCH 077/194] set correct return type --- public/app/core/utils/explore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 06456fef0ba..45e26e79ebf 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -226,7 +226,7 @@ export function generateKey(index = 0): string { return `Q-${Date.now()}-${Math.random()}-${index}`; } -export function generateEmptyQuery(queries: DataQuery[], index = 0): { refId: string; key: string } { +export function generateEmptyQuery(queries: DataQuery[], index = 0): DataQuery { return { refId: getNextRefIdLetter(queries), key: generateKey(index) }; } From 1db7913a1c51d391924c915b32ea497bb83aecdf Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Mar 2019 09:25:58 +0100 Subject: [PATCH 078/194] changelog: adds note about closing #15836 --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abddad1f58f..490954554a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,15 +3,14 @@ ### New Features * **Prometheus**: adhoc filter support [#8253](https://github.com/grafana/grafana/issues/8253), thx [@mtanda](https://github.com/mtanda) -### Tech -* **Cache**: Adds support for using out of proc caching in the backend [#10816](https://github.com/grafana/grafana/issues/10816) - ### Minor * **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow) * **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683) * **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683) * **VictorOps**: Adds more information to the victor ops notifiers [#15744](https://github.com/grafana/grafana/issues/15744), thx [@zhulongcheng](https://github.com/zhulongcheng) +* **Cache**: Adds support for using out of proc caching in the backend [#10816](https://github.com/grafana/grafana/issues/10816) * **Dataproxy**: Make it possible to add user details to requests sent to the dataproxy [#6359](https://github.com/grafana/grafana/issues/6359) and [#15931](https://github.com/grafana/grafana/issues/15931) +* **Auth**: Support listing and revoking auth tokens via API [#15836](https://github.com/grafana/grafana/issues/15836) ### Bug Fixes * **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506) From c028d410ecef70addb9f1acfbbed21c0750b03df Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 15 Mar 2019 10:49:41 +0300 Subject: [PATCH 079/194] panels: fix loading panels with non-array targets (add tests) --- .../features/dashboard/state/PanelModel.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index 686a8ba6d28..82af0804029 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -3,9 +3,10 @@ import { PanelModel } from './PanelModel'; describe('PanelModel', () => { describe('when creating new panel model', () => { let model; + let modelJson; beforeEach(() => { - model = new PanelModel({ + modelJson = { type: 'table', showColumns: true, targets: [{ refId: 'A' }, { noRefId: true }], @@ -23,7 +24,8 @@ describe('PanelModel', () => { }, ], }, - }); + }; + model = new PanelModel(modelJson); }); it('should apply defaults', () => { @@ -38,6 +40,15 @@ describe('PanelModel', () => { expect(model.targets[1].refId).toBe('B'); }); + it("shouldn't break panel with non-array targets", () => { + modelJson.targets = { + 0: { refId: 'A' }, + foo: { bar: 'baz' }, + }; + model = new PanelModel(modelJson); + expect(model.targets[0].refId).toBe('A'); + }); + it('getSaveModel should remove defaults', () => { const saveModel = model.getSaveModel(); expect(saveModel.gridPos).toBe(undefined); From aa4b593dfa0685448d55c0c900dd1e8cef77461e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 15 Mar 2019 07:19:34 +0100 Subject: [PATCH 080/194] chore: Cleaning up implicit anys in app.ts progress: #14714 --- public/app/app.ts | 78 ++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/public/app/app.ts b/public/app/app.ts index 03f332e357b..a54c9270e2c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -17,12 +17,13 @@ import 'vendor/angular-other/angular-strap'; import $ from 'jquery'; import angular from 'angular'; import config from 'app/core/config'; +// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move import _ from 'lodash'; import moment from 'moment'; import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar'; // add move to lodash for backward compatabiltiy -_.move = (array, fromIndex, toIndex) => { +_.move = (array: [], fromIndex: number, toIndex: number) => { array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); return array; }; @@ -36,7 +37,7 @@ import 'app/features/all'; // import symlinked extensions const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/); -extensionsIndex.keys().forEach(key => { +extensionsIndex.keys().forEach((key: any) => { extensionsIndex(key); }); @@ -52,7 +53,7 @@ export class GrafanaApp { this.ngModuleDependencies = []; } - useModule(module) { + useModule(module: angular.IModule) { if (this.preBootModules) { this.preBootModules.push(module); } else { @@ -67,40 +68,49 @@ export class GrafanaApp { moment.locale(config.bootData.user.locale); - app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $httpProvider, $provide) => { - // pre assing bindings before constructor calls - $compileProvider.preAssignBindingsEnabled(true); + app.config( + ( + $locationProvider: angular.ILocationProvider, + $controllerProvider: angular.IControllerProvider, + $compileProvider: angular.ICompileProvider, + $filterProvider: angular.IFilterProvider, + $httpProvider: angular.IHttpProvider, + $provide: angular.auto.IProvideService + ) => { + // pre assing bindings before constructor calls + $compileProvider.preAssignBindingsEnabled(true); - if (config.buildInfo.env !== 'development') { - $compileProvider.debugInfoEnabled(false); - } + if (config.buildInfo.env !== 'development') { + $compileProvider.debugInfoEnabled(false); + } - $httpProvider.useApplyAsync(true); + $httpProvider.useApplyAsync(true); - this.registerFunctions.controller = $controllerProvider.register; - this.registerFunctions.directive = $compileProvider.directive; - this.registerFunctions.factory = $provide.factory; - this.registerFunctions.service = $provide.service; - this.registerFunctions.filter = $filterProvider.register; + this.registerFunctions.controller = $controllerProvider.register; + this.registerFunctions.directive = $compileProvider.directive; + this.registerFunctions.factory = $provide.factory; + this.registerFunctions.service = $provide.service; + this.registerFunctions.filter = $filterProvider.register; - $provide.decorator('$http', [ - '$delegate', - '$templateCache', - ($delegate, $templateCache) => { - const get = $delegate.get; - $delegate.get = (url, config) => { - if (url.match(/\.html$/)) { - // some template's already exist in the cache - if (!$templateCache.get(url)) { - url += '?v=' + new Date().getTime(); + $provide.decorator('$http', [ + '$delegate', + '$templateCache', + ($delegate: any, $templateCache: any) => { + const get = $delegate.get; + $delegate.get = (url: string, config: any) => { + if (url.match(/\.html$/)) { + // some template's already exist in the cache + if (!$templateCache.get(url)) { + url += '?v=' + new Date().getTime(); + } } - } - return get(url, config); - }; - return $delegate; - }, - ]); - }); + return get(url, config); + }; + return $delegate; + }, + ]); + } + ); this.ngModuleDependencies = [ 'grafana.core', @@ -116,7 +126,7 @@ export class GrafanaApp { ]; // makes it possible to add dynamic stuff - _.each(angularModules, m => { + _.each(angularModules, (m: angular.IModule) => { this.useModule(m); }); @@ -129,7 +139,7 @@ export class GrafanaApp { // bootstrap the app angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { - _.each(this.preBootModules, module => { + _.each(this.preBootModules, (module: angular.IModule) => { _.extend(module, this.registerFunctions); }); From 96af051cb23a550a9c515dd9b850da44548e7969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 15 Mar 2019 08:47:52 +0100 Subject: [PATCH 081/194] chore: Cleaning up implicit anys in manage_dashboard.ts and manage_dashboard.test.ts progress: #14714 --- .../manage_dashboards/manage_dashboards.ts | 68 ++++++--- .../app/core/specs/manage_dashboards.test.ts | 144 +++++++++++------- 2 files changed, 137 insertions(+), 75 deletions(-) diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 25a69b1f5e4..3f6dacd311d 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -1,7 +1,30 @@ +// @ts-ignore import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import { SearchSrv } from 'app/core/services/search_srv'; +import { BackendSrv } from 'app/core/services/backend_srv'; +import { NavModelSrv } from 'app/core/nav_model_srv'; +import { ContextSrv } from 'app/core/services/context_srv'; + +export interface Section { + id: number; + uid: string; + title: string; + expanded: false; + items: any[]; + url: string; + icon: string; + score: number; + checked: boolean; + hideHeader: boolean; + toggle: Function; +} + +export interface FoldersAndDashboardUids { + folderUids: string[]; + dashboardUids: string[]; +} class Query { query: string; @@ -14,7 +37,7 @@ class Query { } export class ManageDashboardsCtrl { - sections: any[]; + sections: Section[]; query: Query; navModel: any; @@ -45,7 +68,12 @@ export class ManageDashboardsCtrl { hasEditPermissionInFolders: boolean; /** @ngInject */ - constructor(private backendSrv, navModelSrv, private searchSrv: SearchSrv, private contextSrv) { + constructor( + private backendSrv: BackendSrv, + navModelSrv: NavModelSrv, + private searchSrv: SearchSrv, + private contextSrv: ContextSrv + ) { this.isEditor = this.contextSrv.isEditor; this.hasEditPermissionInFolders = this.contextSrv.hasEditPermissionInFolders; @@ -73,7 +101,7 @@ export class ManageDashboardsCtrl { refreshList() { return this.searchSrv .search(this.query) - .then(result => { + .then((result: Section[]) => { return this.initDashboardList(result); }) .then(() => { @@ -81,7 +109,7 @@ export class ManageDashboardsCtrl { return; } - return this.backendSrv.getFolderByUid(this.folderUid).then(folder => { + return this.backendSrv.getFolderByUid(this.folderUid).then((folder: any) => { this.canSave = folder.canSave; if (!this.canSave) { this.hasEditPermissionInFolders = false; @@ -90,7 +118,7 @@ export class ManageDashboardsCtrl { }); } - initDashboardList(result: any) { + initDashboardList(result: Section[]) { this.canMove = false; this.canDelete = false; this.selectAllChecked = false; @@ -128,25 +156,25 @@ export class ManageDashboardsCtrl { this.canDelete = selectedDashboards > 0 || selectedFolders > 0; } - getFoldersAndDashboardsToDelete() { - const selectedDashboards = { - folders: [], - dashboards: [], + getFoldersAndDashboardsToDelete(): FoldersAndDashboardUids { + const selectedDashboards: FoldersAndDashboardUids = { + folderUids: [], + dashboardUids: [], }; for (const section of this.sections) { if (section.checked && section.id !== 0) { - selectedDashboards.folders.push(section.uid); + selectedDashboards.folderUids.push(section.uid); } else { const selected = _.filter(section.items, { checked: true }); - selectedDashboards.dashboards.push(..._.map(selected, 'uid')); + selectedDashboards.dashboardUids.push(..._.map(selected, 'uid')); } } return selectedDashboards; } - getFolderIds(sections) { + getFolderIds(sections: Section[]) { const ids = []; for (const s of sections) { if (s.checked) { @@ -158,8 +186,8 @@ export class ManageDashboardsCtrl { delete() { const data = this.getFoldersAndDashboardsToDelete(); - const folderCount = data.folders.length; - const dashCount = data.dashboards.length; + const folderCount = data.folderUids.length; + const dashCount = data.dashboardUids.length; let text = 'Do you want to delete the '; let text2; @@ -179,12 +207,12 @@ export class ManageDashboardsCtrl { icon: 'fa-trash', yesText: 'Delete', onConfirm: () => { - this.deleteFoldersAndDashboards(data.folders, data.dashboards); + this.deleteFoldersAndDashboards(data.folderUids, data.dashboardUids); }, }); } - private deleteFoldersAndDashboards(folderUids, dashboardUids) { + private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) { this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => { this.refreshList(); }); @@ -219,13 +247,13 @@ export class ManageDashboardsCtrl { } initTagFilter() { - return this.searchSrv.getDashboardTags().then(results => { + return this.searchSrv.getDashboardTags().then((results: any) => { this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); this.selectedTagFilter = this.tagFilterOptions[0]; }); } - filterByTag(tag) { + filterByTag(tag: any) { if (_.indexOf(this.query.tag, tag) === -1) { this.query.tag.push(tag); } @@ -243,7 +271,7 @@ export class ManageDashboardsCtrl { return res; } - removeTag(tag, evt) { + removeTag(tag: any, evt: Event) { this.query.tag = _.without(this.query.tag, tag); this.refreshList(); if (evt) { @@ -269,7 +297,7 @@ export class ManageDashboardsCtrl { section.checked = this.selectAllChecked; } - section.items = _.map(section.items, item => { + section.items = _.map(section.items, (item: any) => { item.checked = this.selectAllChecked; return item; }); diff --git a/public/app/core/specs/manage_dashboards.test.ts b/public/app/core/specs/manage_dashboards.test.ts index 5af0ebade02..ef5e240fd36 100644 --- a/public/app/core/specs/manage_dashboards.test.ts +++ b/public/app/core/specs/manage_dashboards.test.ts @@ -1,12 +1,39 @@ -import { ManageDashboardsCtrl } from 'app/core/components/manage_dashboards/manage_dashboards'; -import { SearchSrv } from 'app/core/services/search_srv'; +// @ts-ignore import q from 'q'; +import { + ManageDashboardsCtrl, + Section, + FoldersAndDashboardUids, +} from 'app/core/components/manage_dashboards/manage_dashboards'; +import { SearchSrv } from 'app/core/services/search_srv'; +import { BackendSrv } from '../services/backend_srv'; +import { NavModelSrv } from '../nav_model_srv'; +import { ContextSrv } from '../services/context_srv'; + +const mockSection = (overides?: object): Section => { + const defaultSection: Section = { + id: 0, + items: [], + checked: false, + expanded: false, + hideHeader: false, + icon: '', + score: 0, + title: 'Some Section', + toggle: jest.fn(), + uid: 'someuid', + url: '/some/url/', + }; + + return { ...defaultSection, ...overides }; +}; describe('ManageDashboards', () => { - let ctrl; + let ctrl: ManageDashboardsCtrl; describe('when browsing dashboards', () => { beforeEach(() => { + const tags: any[] = []; const response = [ { id: 410, @@ -18,11 +45,11 @@ describe('ManageDashboards', () => { title: 'Dashboard Test', url: 'dashboard/db/dashboard-test', icon: 'fa fa-folder', - tags: [], + tags, isStarred: false, }, ], - tags: [], + tags, isStarred: false, }, { @@ -37,11 +64,11 @@ describe('ManageDashboards', () => { title: 'Dashboard Test', url: 'dashboard/db/dashboard-test', icon: 'fa fa-folder', - tags: [], + tags, isStarred: false, }, ], - tags: [], + tags, isStarred: false, }, ]; @@ -61,6 +88,7 @@ describe('ManageDashboards', () => { describe('when browsing dashboards for a folder', () => { beforeEach(() => { + const tags: any[] = []; const response = [ { id: 410, @@ -72,11 +100,11 @@ describe('ManageDashboards', () => { title: 'Dashboard Test', url: 'dashboard/db/dashboard-test', icon: 'fa fa-folder', - tags: [], + tags, isStarred: false, }, ], - tags: [], + tags, isStarred: false, }, ]; @@ -92,6 +120,7 @@ describe('ManageDashboards', () => { describe('when searching dashboards', () => { beforeEach(() => { + const tags: any[] = []; const response = [ { checked: false, @@ -103,7 +132,7 @@ describe('ManageDashboards', () => { title: 'Dashboard Test', url: 'dashboard/db/dashboard-test', icon: 'fa fa-folder', - tags: [], + tags, isStarred: false, folderId: 410, folderUid: 'uid', @@ -115,7 +144,7 @@ describe('ManageDashboards', () => { title: 'Dashboard Test', url: 'dashboard/db/dashboard-test', icon: 'fa fa-folder', - tags: [], + tags, folderId: 499, isStarred: false, }, @@ -245,7 +274,7 @@ describe('ManageDashboards', () => { }); describe('when selecting dashboards', () => { - let ctrl; + let ctrl: ManageDashboardsCtrl; beforeEach(() => { ctrl = createCtrlWithStubs([]); @@ -254,16 +283,16 @@ describe('ManageDashboards', () => { describe('and no dashboards are selected', () => { beforeEach(() => { ctrl.sections = [ - { + mockSection({ id: 1, items: [{ id: 2, checked: false }], checked: false, - }, - { + }), + mockSection({ id: 0, items: [{ id: 3, checked: false }], checked: false, - }, + }), ]; ctrl.selectionChanged(); }); @@ -302,16 +331,16 @@ describe('ManageDashboards', () => { describe('and all folders and dashboards are selected', () => { beforeEach(() => { ctrl.sections = [ - { + mockSection({ id: 1, items: [{ id: 2, checked: true }], checked: true, - }, - { + }), + mockSection({ id: 0, items: [{ id: 3, checked: true }], checked: true, - }, + }), ]; ctrl.selectionChanged(); }); @@ -350,18 +379,18 @@ describe('ManageDashboards', () => { describe('and one dashboard in root is selected', () => { beforeEach(() => { ctrl.sections = [ - { + mockSection({ id: 1, title: 'folder', items: [{ id: 2, checked: false }], checked: false, - }, - { + }), + mockSection({ id: 0, title: 'General', items: [{ id: 3, checked: true }], checked: false, - }, + }), ]; ctrl.selectionChanged(); }); @@ -378,18 +407,18 @@ describe('ManageDashboards', () => { describe('and one child dashboard is selected', () => { beforeEach(() => { ctrl.sections = [ - { + mockSection({ id: 1, title: 'folder', items: [{ id: 2, checked: true }], checked: false, - }, - { + }), + mockSection({ id: 0, title: 'General', items: [{ id: 3, checked: false }], checked: false, - }, + }), ]; ctrl.selectionChanged(); @@ -407,18 +436,18 @@ describe('ManageDashboards', () => { describe('and one child dashboard and one dashboard is selected', () => { beforeEach(() => { ctrl.sections = [ - { + mockSection({ id: 1, title: 'folder', items: [{ id: 2, checked: true }], checked: false, - }, - { + }), + mockSection({ id: 0, title: 'General', items: [{ id: 3, checked: true }], checked: false, - }, + }), ]; ctrl.selectionChanged(); @@ -436,24 +465,24 @@ describe('ManageDashboards', () => { describe('and one child dashboard and one folder is selected', () => { beforeEach(() => { ctrl.sections = [ - { + mockSection({ id: 1, title: 'folder', items: [{ id: 2, checked: false }], checked: true, - }, - { + }), + mockSection({ id: 3, title: 'folder', items: [{ id: 4, checked: true }], checked: false, - }, - { + }), + mockSection({ id: 0, title: 'General', items: [{ id: 3, checked: false }], checked: false, - }, + }), ]; ctrl.selectionChanged(); @@ -470,55 +499,55 @@ describe('ManageDashboards', () => { }); describe('when deleting dashboards', () => { - let toBeDeleted: any; + let toBeDeleted: FoldersAndDashboardUids; beforeEach(() => { ctrl = createCtrlWithStubs([]); ctrl.sections = [ - { + mockSection({ id: 1, uid: 'folder', title: 'folder', items: [{ id: 2, checked: true, uid: 'folder-dash' }], checked: true, - }, - { + }), + mockSection({ id: 3, title: 'folder-2', items: [{ id: 3, checked: true, uid: 'folder-2-dash' }], checked: false, uid: 'folder-2', - }, - { + }), + mockSection({ id: 0, title: 'General', items: [{ id: 3, checked: true, uid: 'root-dash' }], checked: true, - }, + }), ]; toBeDeleted = ctrl.getFoldersAndDashboardsToDelete(); }); it('should return 1 folder', () => { - expect(toBeDeleted.folders.length).toEqual(1); + expect(toBeDeleted.folderUids.length).toEqual(1); }); it('should return 2 dashboards', () => { - expect(toBeDeleted.dashboards.length).toEqual(2); + expect(toBeDeleted.dashboardUids.length).toEqual(2); }); it('should filter out children if parent is checked', () => { - expect(toBeDeleted.folders[0]).toEqual('folder'); + expect(toBeDeleted.folderUids[0]).toEqual('folder'); }); it('should not filter out children if parent not is checked', () => { - expect(toBeDeleted.dashboards[0]).toEqual('folder-2-dash'); + expect(toBeDeleted.dashboardUids[0]).toEqual('folder-2-dash'); }); it('should not filter out children if parent is checked and root', () => { - expect(toBeDeleted.dashboards[1]).toEqual('root-dash'); + expect(toBeDeleted.dashboardUids[1]).toEqual('root-dash'); }); }); @@ -527,19 +556,19 @@ describe('ManageDashboards', () => { ctrl = createCtrlWithStubs([]); ctrl.sections = [ - { + mockSection({ id: 1, title: 'folder', items: [{ id: 2, checked: true, uid: 'dash' }], checked: false, uid: 'folder', - }, - { + }), + mockSection({ id: 0, title: 'General', items: [{ id: 3, checked: true, uid: 'dash-2' }], checked: false, - }, + }), ]; }); @@ -562,5 +591,10 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) { }, }; - return new ManageDashboardsCtrl({}, { getNav: () => {} }, searchSrvStub as SearchSrv, { isEditor: true }); + return new ManageDashboardsCtrl( + {} as BackendSrv, + { getNav: () => {} } as NavModelSrv, + searchSrvStub as SearchSrv, + { isEditor: true } as ContextSrv + ); } From e2fd85854c84cee9697626d94348b6e3a819d0f0 Mon Sep 17 00:00:00 2001 From: JacobEriksson <48587724+JacobEriksson@users.noreply.github.com> Date: Fri, 15 Mar 2019 11:07:22 +0100 Subject: [PATCH 082/194] Update index.md Added information about Amazon Timestream and Oracle Database --- docs/sources/enterprise/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 5d524dcbee2..421e94cbf9f 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -38,6 +38,8 @@ With a Grafana Enterprise license you will get access to premium plugins, includ * [DataDog](https://grafana.com/plugins/grafana-datadog-datasource) * [Dynatrace](https://grafana.com/plugins/grafana-dynatrace-datasource) * [New Relic](https://grafana.com/plugins/grafana-newrelic-datasource) +* [Amazon Timestream](https://grafana.com/plugins/grafana-timestream-datasource) +* [Oracle Database](https://grafana.com/plugins/grafana-oracle-datasource) ## Try Grafana Enterprise From 09b9b595b2877febd1b530059bec466ff7a6b873 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Fri, 15 Mar 2019 11:53:30 +0100 Subject: [PATCH 083/194] Add check for Env before log --- pkg/tsdb/mssql/mssql.go | 5 ++++- pkg/tsdb/mysql/mysql.go | 5 ++++- pkg/tsdb/postgres/postgres.go | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index 12f2b6c03c9..c740d6cbe77 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -3,6 +3,7 @@ package mssql import ( "database/sql" "fmt" + "github.com/grafana/grafana/pkg/setting" "strconv" _ "github.com/denisenkom/go-mssqldb" @@ -24,7 +25,9 @@ func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin if err != nil { return nil, err } - logger.Debug("getEngine", "connection", cnnstr) + if setting.Env == setting.DEV { + logger.Debug("getEngine", "connection", cnnstr) + } config := tsdb.SqlQueryEndpointConfiguration{ DriverName: "mssql", diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index d307e12166c..0451f8f0dc1 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -3,6 +3,7 @@ package mysql import ( "database/sql" "fmt" + "github.com/grafana/grafana/pkg/setting" "reflect" "strconv" "strings" @@ -44,7 +45,9 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin cnnstr += "&tls=" + tlsConfigString } - logger.Debug("getEngine", "connection", cnnstr) + if setting.Env == setting.DEV { + logger.Debug("getEngine", "connection", cnnstr) + } config := tsdb.SqlQueryEndpointConfiguration{ DriverName: "mysql", diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index 4bcf06638f4..ae6b165e731 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -2,6 +2,7 @@ package postgres import ( "database/sql" + "github.com/grafana/grafana/pkg/setting" "net/url" "strconv" @@ -19,7 +20,9 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp logger := log.New("tsdb.postgres") cnnstr := generateConnectionString(datasource) - logger.Debug("getEngine", "connection", cnnstr) + if setting.Env == setting.DEV { + logger.Debug("getEngine", "connection", cnnstr) + } config := tsdb.SqlQueryEndpointConfiguration{ DriverName: "postgres", From 714e03c1623352037ce167ad3e5feabd5d13502f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 Mar 2019 12:45:37 +0100 Subject: [PATCH 084/194] Bar gauge auto lcd cell count --- packages/grafana-ui/src/components/BarGauge/BarGauge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 2ac5a9e287e..448c86ac78d 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -193,7 +193,7 @@ export class BarGauge extends PureComponent { const valueRange = maxValue - minValue; const maxSize = this.size * BAR_SIZE_RATIO; const cellSpacing = 5; - const cellCount = 25; + const cellCount = maxSize / 20; const cellSize = (maxSize - cellSpacing * cellCount) / cellCount; const colors = this.getValueColors(); const valueStyles = this.getValueStyles(valueFormatted, colors.value, this.size - maxSize); From 91ff146d7d6a9148f22727e26e058b6393e3e283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 Mar 2019 13:34:38 +0100 Subject: [PATCH 085/194] Bar gauge gradient mode --- .../src/components/BarGauge/BarGauge.tsx | 79 +++++++++++++------ .../ThresholdsEditor/ThresholdsEditor.tsx | 6 +- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 448c86ac78d..9cdeb158663 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -99,17 +99,51 @@ export class BarGauge extends PureComponent { * Return width or height depending on viz orientation * */ get size() { - const { height, width, orientation } = this.props; - return orientation === VizOrientation.Horizontal ? width : height; + const { height, width } = this.props; + return this.isVertical ? height : width; + } + + get isVertical() { + return this.props.orientation === VizOrientation.Vertical; + } + + getBarGradient(maxSize: number): string { + const { minValue, maxValue, thresholds } = this.props; + const cssDirection = this.isVertical ? '0deg' : '90deg'; + const currentValue = this.getNumericValue(); + + let gradient = ''; + let lastpos = 0; + + for (let i = 0; i < thresholds.length; i++) { + const threshold = thresholds[i]; + const color = getColorFromHexRgbOrName(threshold.color); + const valuePercent = Math.min(threshold.value / (maxValue - minValue), 1); + const pos = valuePercent * maxSize; + const offset = Math.round(pos - (pos - lastpos) / 2); + + if (gradient === '') { + gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`; + } else if (currentValue < threshold.value) { + break; + } else { + lastpos = pos; + gradient += ` ${offset}px, ${color}`; + } + } + + console.log(gradient); + return gradient + ')'; } renderSimpleMode(valueFormatted: string, valuePercent: number): ReactNode { - const { height, width, orientation } = this.props; + const { height, width } = this.props; const maxSize = this.size * BAR_SIZE_RATIO; const barSize = Math.max(valuePercent * maxSize, 0); const colors = this.getValueColors(); - const valueStyles = this.getValueStyles(valueFormatted, colors.value, this.size - maxSize); + const spaceForText = this.isVertical ? width : this.size - maxSize; + const valueStyles = this.getValueStyles(valueFormatted, colors.value, spaceForText); const containerStyles: CSSProperties = { width: `${width}px`, @@ -117,17 +151,16 @@ export class BarGauge extends PureComponent { display: 'flex', }; - const barStyles: CSSProperties = { - backgroundColor: colors.bar, - }; + const barStyles: CSSProperties = {}; // Custom styles for vertical orientation - if (orientation === VizOrientation.Vertical) { + if (this.isVertical) { containerStyles.flexDirection = 'column'; containerStyles.justifyContent = 'flex-end'; barStyles.height = `${barSize}px`; barStyles.width = `${width}px`; - barStyles.borderTop = `1px solid ${colors.border}`; + // barStyles.borderTop = `1px solid ${colors.border}`; + barStyles.background = this.getBarGradient(maxSize); } else { // Custom styles for horizontal orientation containerStyles.flexDirection = 'row-reverse'; @@ -136,7 +169,8 @@ export class BarGauge extends PureComponent { barStyles.height = `${height}px`; barStyles.width = `${barSize}px`; barStyles.marginRight = '10px'; - barStyles.borderRight = `1px solid ${colors.border}`; + // barStyles.borderRight = `1px solid ${colors.border}`; + barStyles.background = this.getBarGradient(maxSize); } return ( @@ -188,7 +222,7 @@ export class BarGauge extends PureComponent { } renderLcdMode(valueFormatted: string, valuePercent: number): ReactNode { - const { height, width, maxValue, minValue, orientation } = this.props; + const { height, width, maxValue, minValue } = this.props; const valueRange = maxValue - minValue; const maxSize = this.size * BAR_SIZE_RATIO; @@ -196,7 +230,8 @@ export class BarGauge extends PureComponent { const cellCount = maxSize / 20; const cellSize = (maxSize - cellSpacing * cellCount) / cellCount; const colors = this.getValueColors(); - const valueStyles = this.getValueStyles(valueFormatted, colors.value, this.size - maxSize); + const spaceForText = this.isVertical ? width : this.size - maxSize; + const valueStyles = this.getValueStyles(valueFormatted, colors.value, spaceForText); const containerStyles: CSSProperties = { width: `${width}px`, @@ -204,14 +239,14 @@ export class BarGauge extends PureComponent { display: 'flex', }; - if (orientation === VizOrientation.Horizontal) { - containerStyles.flexDirection = 'row'; - containerStyles.alignItems = 'center'; - valueStyles.marginLeft = '20px'; - } else { + if (this.isVertical) { containerStyles.flexDirection = 'column-reverse'; containerStyles.alignItems = 'center'; valueStyles.marginBottom = '20px'; + } else { + containerStyles.flexDirection = 'row'; + containerStyles.alignItems = 'center'; + valueStyles.marginLeft = '20px'; } const cells: JSX.Element[] = []; @@ -232,14 +267,14 @@ export class BarGauge extends PureComponent { cellStyles.backgroundColor = cellColor.background; } - if (orientation === VizOrientation.Horizontal) { - cellStyles.width = `${cellSize}px`; - cellStyles.height = `${height}px`; - cellStyles.marginRight = `${cellSpacing}px`; - } else { + if (this.isVertical) { cellStyles.height = `${cellSize}px`; cellStyles.width = `${width}px`; cellStyles.marginTop = `${cellSpacing}px`; + } else { + cellStyles.width = `${cellSize}px`; + cellStyles.height = `${height}px`; + cellStyles.marginRight = `${cellSpacing}px`; } cells.push(
); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 3361e1bee46..3f1d2973c1f 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -166,7 +166,11 @@ export class ThresholdsEditor extends PureComponent {
{threshold.color && (
- this.onChangeThresholdColor(threshold, color)} /> + this.onChangeThresholdColor(threshold, color)} + enableNamedColors={true} + />
)}
From 38f82cfb0e2ff1db654485c2e427ad4487aabfb9 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Fri, 15 Mar 2019 13:34:39 +0100 Subject: [PATCH 086/194] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 490954554a1..1f09ead1fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * **Cache**: Adds support for using out of proc caching in the backend [#10816](https://github.com/grafana/grafana/issues/10816) * **Dataproxy**: Make it possible to add user details to requests sent to the dataproxy [#6359](https://github.com/grafana/grafana/issues/6359) and [#15931](https://github.com/grafana/grafana/issues/15931) * **Auth**: Support listing and revoking auth tokens via API [#15836](https://github.com/grafana/grafana/issues/15836) +* **Datasource**: Only log connection string in dev environment [#16001](https://github.com/grafana/grafana/issues/16001) ### Bug Fixes * **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506) From fff6e0a52298425f6de1c45466f92e71e0d96781 Mon Sep 17 00:00:00 2001 From: Steven Sheehy Date: Fri, 15 Mar 2019 08:11:40 -0500 Subject: [PATCH 087/194] feature(explore/table): Add tooltips to explore table (#16007) Longer labels are now viewable as a tooltip in the Explore table Signed-off-by: Steven Sheehy --- public/app/features/explore/Table.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/app/features/explore/Table.tsx b/public/app/features/explore/Table.tsx index 4946a6a505d..bbf338df8f9 100644 --- a/public/app/features/explore/Table.tsx +++ b/public/app/features/explore/Table.tsx @@ -40,11 +40,15 @@ export default class Table extends PureComponent { const tableModel = data || EMPTY_TABLE; const columnNames = tableModel.columns.map(({ text }) => text); const columns = tableModel.columns.map(({ filterable, text }) => ({ - Header: text, + Header: () => {text}, accessor: text, className: VALUE_REGEX.test(text) ? 'text-right' : '', show: text !== 'Time', - Cell: row => {row.value}, + Cell: row => ( + + {row.value} + + ), })); const noDataText = data ? 'The queries returned no data for a table.' : ''; From b1b5e8d74cc4021ca951dc1b5c788d69ff5e0544 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 15 Mar 2019 08:38:29 -0700 Subject: [PATCH 088/194] use singlestat base where appropriate --- packages/grafana-ui/src/utils/displayValue.test.ts | 4 ++-- public/app/plugins/panel/bargauge/module.tsx | 4 ++-- public/app/plugins/panel/gauge/module.tsx | 4 ++-- public/app/plugins/panel/singlestat2/module.tsx | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/grafana-ui/src/utils/displayValue.test.ts b/packages/grafana-ui/src/utils/displayValue.test.ts index 1f426aad5b0..bf0e7a89bba 100644 --- a/packages/grafana-ui/src/utils/displayValue.test.ts +++ b/packages/grafana-ui/src/utils/displayValue.test.ts @@ -1,8 +1,8 @@ import { getDisplayProcessor, getColorFromThreshold, DisplayProcessor, DisplayValue } from './displayValue'; import { MappingType, ValueMapping } from '../types/panel'; -function assertSame(input: any, processorss: DisplayProcessor[], match: DisplayValue) { - processorss.forEach(processor => { +function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) { + processors.forEach(processor => { const value = processor(input); expect(value.text).toEqual(match.text); if (match.hasOwnProperty('numeric')) { diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index 5ca355b3110..3c46adeb4f9 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/public/app/plugins/panel/bargauge/module.tsx @@ -3,10 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui'; import { BarGaugePanel } from './BarGaugePanel'; import { BarGaugePanelEditor } from './BarGaugePanelEditor'; import { BarGaugeOptions, defaults } from './types'; -import { singleStatOptionsCheck } from '../singlestat2/module'; +import { singleStatBaseOptionsCheck } from '../singlestat2/module'; export const reactPanel = new ReactPanelPlugin(BarGaugePanel); reactPanel.setEditor(BarGaugePanelEditor); reactPanel.setDefaults(defaults); -reactPanel.setPanelTypeChangedHook(singleStatOptionsCheck); +reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck); diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 7d56ac5641b..340af06a080 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -3,10 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui'; import { GaugePanelEditor } from './GaugePanelEditor'; import { GaugePanel } from './GaugePanel'; import { GaugeOptions, defaults } from './types'; -import { singleStatOptionsCheck } from '../singlestat2/module'; +import { singleStatBaseOptionsCheck } from '../singlestat2/module'; export const reactPanel = new ReactPanelPlugin(GaugePanel); reactPanel.setEditor(GaugePanelEditor); reactPanel.setDefaults(defaults); -reactPanel.setPanelTypeChangedHook(singleStatOptionsCheck); +reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck); diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx index c07e24e5198..283b32802e1 100644 --- a/public/app/plugins/panel/singlestat2/module.tsx +++ b/public/app/plugins/panel/singlestat2/module.tsx @@ -1,5 +1,5 @@ import { ReactPanelPlugin } from '@grafana/ui'; -import { SingleStatOptions, defaults } from './types'; +import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types'; import { SingleStatPanel } from './SingleStatPanel'; import cloneDeep from 'lodash/cloneDeep'; import { SingleStatEditor } from './SingleStatEditor'; @@ -8,8 +8,8 @@ export const reactPanel = new ReactPanelPlugin(SingleStatPane const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings']; -export const singleStatOptionsCheck = ( - options: Partial, +export const singleStatBaseOptionsCheck = ( + options: Partial, prevPluginId?: string, prevOptions?: any ) => { @@ -26,4 +26,4 @@ export const singleStatOptionsCheck = ( reactPanel.setEditor(SingleStatEditor); reactPanel.setDefaults(defaults); -reactPanel.setPanelTypeChangedHook(singleStatOptionsCheck); +reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck); From 0bb772aba77ba304da57c865746118af248cccd7 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 15 Mar 2019 09:03:47 -0700 Subject: [PATCH 089/194] get values from base options --- public/app/plugins/panel/singlestat2/SingleStatPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx index 323a0be5658..1c731e0a0c7 100644 --- a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx +++ b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx @@ -2,14 +2,14 @@ import React, { PureComponent, CSSProperties } from 'react'; // Types -import { SingleStatOptions } from './types'; +import { SingleStatOptions, SingleStatBaseOptions } from './types'; import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui'; import { config } from 'app/core/config'; import { getDisplayProcessor } from '@grafana/ui'; import { ProcessedValuesRepeater } from './ProcessedValuesRepeater'; -export const getSingleStatValues = (props: PanelProps): DisplayValue[] => { +export const getSingleStatValues = (props: PanelProps): DisplayValue[] => { const { panelData, replaceVariables, options } = props; const { valueOptions, valueMappings } = options; const processor = getDisplayProcessor({ From e787176bcacd0647ee6dc552da964f66e7a777c6 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 15 Mar 2019 11:02:41 -0700 Subject: [PATCH 090/194] add startAt to random walk scenario --- pkg/tsdb/testdata/scenarios.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go index 421a907b5e9..a061119a764 100644 --- a/pkg/tsdb/testdata/scenarios.go +++ b/pkg/tsdb/testdata/scenarios.go @@ -251,7 +251,7 @@ func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResu series := newSeriesForQuery(query) points := make(tsdb.TimeSeriesPoints, 0) - walker := rand.Float64() * 100 + walker := query.Model.Get("startValue").MustFloat64(rand.Float64() * 100) for i := int64(0); i < 10000 && timeWalkerMs < to; i++ { points = append(points, tsdb.NewTimePoint(null.FloatFrom(walker), float64(timeWalkerMs))) From 2b5ac6bafad9ed19923c044ffc2039721ee9e091 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 15 Mar 2019 11:07:20 -0700 Subject: [PATCH 091/194] add test file --- pkg/tsdb/testdata/scenarios_test.go | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 pkg/tsdb/testdata/scenarios_test.go diff --git a/pkg/tsdb/testdata/scenarios_test.go b/pkg/tsdb/testdata/scenarios_test.go new file mode 100644 index 00000000000..f97a43a1138 --- /dev/null +++ b/pkg/tsdb/testdata/scenarios_test.go @@ -0,0 +1,37 @@ +package testdata + +import ( + "testing" + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" + . "github.com/smartystreets/goconvey/convey" +) + +func TestTestdataScenarios(t *testing.T) { + Convey("random walk ", t, func() { + if scenario, exist := ScenarioRegistry["random_walk"]; exist { + + Convey("Should start at the requested value", func() { + req := &tsdb.TsdbQuery{ + TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()), + Queries: []*tsdb.Query{ + {RefId: "A", IntervalMs: 100, MaxDataPoints: 10, Model: simplejson.New()}, + }, + } + query := req.Queries[0] + query.Model.Set("startValue", 1.234) + + result := scenario.Handler(req.Queries[0], req) + points := result.Series[0].Points + + So(result.Series, ShouldNotBeNil) + So(points[0][0].Float64, ShouldEqual, 1.234) + }) + + } else { + t.Fail() + } + }) +} From 98911e0708d23ed18bfdae844341a22d9547901d Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 14 Mar 2019 16:17:20 +0100 Subject: [PATCH 092/194] adds backend code style guide --- style_guides/backend.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 style_guides/backend.md diff --git a/style_guides/backend.md b/style_guides/backend.md new file mode 100644 index 00000000000..8150530071b --- /dev/null +++ b/style_guides/backend.md @@ -0,0 +1,30 @@ +# Backend style guide + +Grafanas backend has been developed for a long time with a mix of code styles. + +This style guide is a guide for how we want to write Go code in the future. Generally, we want to follow the style guides used in Go [Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) and Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style) + + +## Global state +Global state makes testing and debugging software harder and its something we want to avoid when possible. +Unfortunately, there is quite a lot of global state in Grafana. The way we want to migrate away from this +is to use the `inject` package to wire up all dependencies either in `pkg/cmd/grafana-server/main.go` or +self registering using `registry.RegisterService` ex https://github.com/grafana/grafana/blob/master/pkg/services/cleanup/cleanup.go#L25 + +### the `bus` +`bus.Dispatch` is used in many places and something we want to avoid in the future since it refers to a global instance. +The preferred solution, in this case, is to inject the `bus` into services or take the bus instance as a parameter into functions. + +### settings package +In the `setting` packages there are many global variables which Grafana sets at startup. This is also something we want to move +away from and move as much configuration as possible to the `setting.Cfg` struct and pass the around just like the bus + +## Linting and formatting +We enforce strict `gofmt` formating and use some linters on our codebase. You can find the current list of linters at https://github.com/grafana/grafana/blob/master/scripts/gometalinter.sh#L23 + +We don't enforce `golint` but we encourage it and we will test so the number of linting errors does not increase over time. + +## Testing +We use GoConvey for BDD/scenario based testing. Which we think is useful for testing certain chain or interactions. Ex https://github.com/grafana/grafana/blob/master/pkg/services/auth/auth_token_test.go + +For smaller tests its preferred to use standard library testing. \ No newline at end of file From 24ead3a4a464d807b3fdedaefb3c9e1fd158a7e5 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 15 Mar 2019 12:33:45 -0700 Subject: [PATCH 093/194] add random_walk_table scenario --- pkg/tsdb/testdata/scenarios.go | 68 ++++++++++++++++++++ pkg/tsdb/testdata/scenarios_test.go | 97 +++++++++++++++++++++++------ 2 files changed, 146 insertions(+), 19 deletions(-) diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go index a061119a764..4780dd6e662 100644 --- a/pkg/tsdb/testdata/scenarios.go +++ b/pkg/tsdb/testdata/scenarios.go @@ -2,6 +2,7 @@ package testdata import ( "encoding/json" + "math" "math/rand" "strconv" "strings" @@ -100,6 +101,15 @@ func init() { }, }) + registerScenario(&Scenario{ + Id: "random_walk_table", + Name: "Random Walk Table", + + Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult { + return getRandomWalkTable(query, context) + }, + }) + registerScenario(&Scenario{ Id: "slow_query", Name: "Slow Query", @@ -267,6 +277,64 @@ func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResu return queryRes } +func getRandomWalkTable(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResult { + timeWalkerMs := tsdbQuery.TimeRange.GetFromAsMsEpoch() + to := tsdbQuery.TimeRange.GetToAsMsEpoch() + + table := tsdb.Table{ + Columns: []tsdb.TableColumn{ + {Text: "Time"}, + {Text: "Value"}, + {Text: "Min"}, + {Text: "Max"}, + {Text: "Info"}, + }, + Rows: []tsdb.RowValues{}, + } + + withNil := query.Model.Get("withNil").MustBool(false) + walker := query.Model.Get("startValue").MustFloat64(rand.Float64() * 100) + spread := 2.5 + var info strings.Builder + + for i := int64(0); i < query.MaxDataPoints && timeWalkerMs < to; i++ { + delta := rand.Float64() - 0.5 + walker += delta + + info.Reset() + if delta > 0 { + info.WriteString("up") + } else { + info.WriteString("down") + } + if math.Abs(delta) > .4 { + info.WriteString(" fast") + } + row := tsdb.RowValues{ + float64(timeWalkerMs), + walker, + walker - ((rand.Float64() * spread) + 0.01), // Min + walker + ((rand.Float64() * spread) + 0.01), // Max + info.String(), + } + + // Add some random null values + if withNil && rand.Float64() > 0.8 { + for i := 1; i < 4; i++ { + if rand.Float64() > .2 { + row[i] = nil + } + } + } + + table.Rows = append(table.Rows, row) + timeWalkerMs += query.IntervalMs + } + queryRes := tsdb.NewQueryResult() + queryRes.Tables = append(queryRes.Tables, &table) + return queryRes +} + func registerScenario(scenario *Scenario) { ScenarioRegistry[scenario.Id] = scenario } diff --git a/pkg/tsdb/testdata/scenarios_test.go b/pkg/tsdb/testdata/scenarios_test.go index f97a43a1138..8f734235f46 100644 --- a/pkg/tsdb/testdata/scenarios_test.go +++ b/pkg/tsdb/testdata/scenarios_test.go @@ -11,27 +11,86 @@ import ( func TestTestdataScenarios(t *testing.T) { Convey("random walk ", t, func() { - if scenario, exist := ScenarioRegistry["random_walk"]; exist { + scenario, exist := ScenarioRegistry["random_walk"] + So(exist, ShouldBeTrue) - Convey("Should start at the requested value", func() { - req := &tsdb.TsdbQuery{ - TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()), - Queries: []*tsdb.Query{ - {RefId: "A", IntervalMs: 100, MaxDataPoints: 10, Model: simplejson.New()}, - }, + Convey("Should start at the requested value", func() { + req := &tsdb.TsdbQuery{ + TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()), + Queries: []*tsdb.Query{ + {RefId: "A", IntervalMs: 100, MaxDataPoints: 100, Model: simplejson.New()}, + }, + } + query := req.Queries[0] + query.Model.Set("startValue", 1.234) + + result := scenario.Handler(req.Queries[0], req) + points := result.Series[0].Points + + So(result.Series, ShouldNotBeNil) + So(points[0][0].Float64, ShouldEqual, 1.234) + }) + }) + + Convey("random walk table", t, func() { + scenario, exist := ScenarioRegistry["random_walk_table"] + So(exist, ShouldBeTrue) + + Convey("Should return a table that looks like value/min/max", func() { + req := &tsdb.TsdbQuery{ + TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()), + Queries: []*tsdb.Query{ + {RefId: "A", IntervalMs: 100, MaxDataPoints: 100, Model: simplejson.New()}, + }, + } + + result := scenario.Handler(req.Queries[0], req) + table := result.Tables[0] + + So(len(table.Rows), ShouldBeGreaterThan, 50) + for _, row := range table.Rows { + value := row[1] + min := row[2] + max := row[3] + + So(min, ShouldBeLessThan, value) + So(max, ShouldBeGreaterThan, value) + } + }) + + Convey("Should return a table with some nil values", func() { + req := &tsdb.TsdbQuery{ + TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()), + Queries: []*tsdb.Query{ + {RefId: "A", IntervalMs: 100, MaxDataPoints: 100, Model: simplejson.New()}, + }, + } + query := req.Queries[0] + query.Model.Set("withNil", true) + + result := scenario.Handler(req.Queries[0], req) + table := result.Tables[0] + + nil1 := false + nil2 := false + nil3 := false + + So(len(table.Rows), ShouldBeGreaterThan, 50) + for _, row := range table.Rows { + if row[1] == nil { + nil1 = true } - query := req.Queries[0] - query.Model.Set("startValue", 1.234) + if row[2] == nil { + nil2 = true + } + if row[3] == nil { + nil3 = true + } + } - result := scenario.Handler(req.Queries[0], req) - points := result.Series[0].Points - - So(result.Series, ShouldNotBeNil) - So(points[0][0].Float64, ShouldEqual, 1.234) - }) - - } else { - t.Fail() - } + So(nil1, ShouldBeTrue) + So(nil2, ShouldBeTrue) + So(nil3, ShouldBeTrue) + }) }) } From bd811b4a952ba5e0e202c589cebce387632e8ec1 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 15 Mar 2019 12:36:55 -0700 Subject: [PATCH 094/194] dont test exists in the test... it will fail if not found --- pkg/tsdb/testdata/scenarios_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/tsdb/testdata/scenarios_test.go b/pkg/tsdb/testdata/scenarios_test.go index 8f734235f46..307f5e4affe 100644 --- a/pkg/tsdb/testdata/scenarios_test.go +++ b/pkg/tsdb/testdata/scenarios_test.go @@ -11,8 +11,7 @@ import ( func TestTestdataScenarios(t *testing.T) { Convey("random walk ", t, func() { - scenario, exist := ScenarioRegistry["random_walk"] - So(exist, ShouldBeTrue) + scenario, _ := ScenarioRegistry["random_walk"] Convey("Should start at the requested value", func() { req := &tsdb.TsdbQuery{ @@ -33,8 +32,7 @@ func TestTestdataScenarios(t *testing.T) { }) Convey("random walk table", t, func() { - scenario, exist := ScenarioRegistry["random_walk_table"] - So(exist, ShouldBeTrue) + scenario, _ := ScenarioRegistry["random_walk_table"] Convey("Should return a table that looks like value/min/max", func() { req := &tsdb.TsdbQuery{ From e6cba97b45c38de38308fc5ca215c55036b232eb Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 15 Mar 2019 13:31:40 -0700 Subject: [PATCH 095/194] disable react table cell measure --- packages/grafana-ui/src/components/Table/Table.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index a551d6d221d..80f833520e0 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -77,6 +77,8 @@ export class Table extends Component { this.measurer = new CellMeasurerCache({ defaultHeight: 30, defaultWidth: 150, + fixedWidth: true, + fixedHeight: true, }); } From 9bcc9b062c62b5c6375507943b12beb8b58ffd4d Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 15 Mar 2019 13:52:32 -0700 Subject: [PATCH 096/194] calculate the column width --- .../grafana-ui/src/components/Table/Table.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 80f833520e0..c32c46f8b88 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -8,6 +8,7 @@ import { CellMeasurerCache, CellMeasurer, GridCellProps, + Index, } from 'react-virtualized'; import { Themeable } from '../../types/theme'; @@ -26,6 +27,7 @@ import { stringToJsRegex } from '../../utils/index'; export interface Props extends Themeable { data: TableData; + minColumnWidth: number; showHeader: boolean; fixedHeader: boolean; fixedColumns: number; @@ -46,6 +48,7 @@ interface State { interface ColumnRenderInfo { header: string; + width: number; builder: TableCellBuilder; } @@ -64,6 +67,7 @@ export class Table extends Component { fixedHeader: true, fixedColumns: 0, rotate: false, + minColumnWidth: 150, }; constructor(props: Props) { @@ -76,9 +80,7 @@ export class Table extends Component { this.renderer = this.initColumns(props); this.measurer = new CellMeasurerCache({ defaultHeight: 30, - defaultWidth: 150, fixedWidth: true, - fixedHeight: true, }); } @@ -112,7 +114,8 @@ export class Table extends Component { /** Given the configuration, setup how each column gets rendered */ initColumns(props: Props): ColumnRenderInfo[] { - const { styles, data } = props; + const { styles, data, width, minColumnWidth } = props; + const columnWidth = Math.max(width / data.columns.length, minColumnWidth); return data.columns.map((col, index) => { let title = col.text; @@ -133,6 +136,7 @@ export class Table extends Component { return { header: title, + width: columnWidth, builder: getCellBuilder(col, style, this.props), }; }); @@ -230,6 +234,10 @@ export class Table extends Component { ); }; + getColumnWidth = (col: Index): number => { + return this.renderer[col.index].width; + }; + render() { const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props; const { data } = this.state; @@ -271,7 +279,7 @@ export class Table extends Component { rowCount={rowCount} overscanColumnCount={8} overscanRowCount={8} - columnWidth={this.measurer.columnWidth} + columnWidth={this.getColumnWidth} deferredMeasurementCache={this.measurer} cellRenderer={this.cellRenderer} rowHeight={this.measurer.rowHeight} From ec34099ea7d3b76f1785bf5484fc92be732837c5 Mon Sep 17 00:00:00 2001 From: Steven Sheehy Date: Sat, 16 Mar 2019 00:55:10 -0500 Subject: [PATCH 097/194] Explore: Fix log stats for long labels Signed-off-by: Steven Sheehy --- public/app/features/explore/LogLabelStats.tsx | 4 +++- public/sass/components/_panel_logs.scss | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/LogLabelStats.tsx b/public/app/features/explore/LogLabelStats.tsx index d689b6e70b2..466cc050e43 100644 --- a/public/app/features/explore/LogLabelStats.tsx +++ b/public/app/features/explore/LogLabelStats.tsx @@ -11,7 +11,9 @@ function LogLabelStatsRow(logLabelStatsModel: LogLabelStatsModel) { return (
-
{value}
+
+ {value} +
{count}
{percent}
diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 367d25ada6b..22c82461e85 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -299,6 +299,8 @@ $column-horizontal-spacing: 10px; &__value { flex: 1; + text-overflow: ellipsis; + overflow: hidden; } &__count, From 1303a66725e244ba5285e3c4438b9acc6ea4fe1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 17 Mar 2019 13:59:26 +0100 Subject: [PATCH 098/194] Great progress on bar gauge look --- .../src/components/BarGauge/BarGauge.test.tsx | 2 +- .../src/components/BarGauge/BarGauge.tsx | 67 ++++++++++++------- public/app/plugins/panel/bargauge/types.ts | 10 ++- 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx index 07a640ee7f7..eca2e8079e2 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -15,7 +15,7 @@ const setup = (propOverrides?: object) => { minValue: 0, prefix: '', suffix: '', - displayMode: 'simple', + displayMode: 'basic', thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }], unit: 'none', height: 300, diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 9cdeb158663..e4f0f37b4a1 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -23,7 +23,7 @@ export interface Props extends Themeable { prefix?: string; suffix?: string; decimals?: number; - displayMode: 'simple' | 'lcd'; + displayMode: 'basic' | 'lcd' | 'gradient'; } export class BarGauge extends PureComponent { @@ -32,7 +32,7 @@ export class BarGauge extends PureComponent { minValue: 0, value: 100, unit: 'none', - displayMode: 'simple', + displayMode: 'basic', orientation: VizOrientation.Horizontal, thresholds: [], valueMappings: [], @@ -47,10 +47,13 @@ export class BarGauge extends PureComponent { const formatFunc = getValueFormat(unit); const valueFormatted = formatFunc(numericValue, decimals); - if (displayMode === 'lcd') { - return this.renderLcdMode(valueFormatted, valuePercent); - } else { - return this.renderSimpleMode(valueFormatted, valuePercent); + switch (displayMode) { + case 'lcd': + return this.renderRetroBars(valueFormatted, valuePercent); + case 'basic': + case 'gradient': + default: + return this.renderBasicAndGradientBars(valueFormatted, valuePercent); } } @@ -72,15 +75,15 @@ export class BarGauge extends PureComponent { return { value: color, border: color, - bar: tinycolor(color) - .setAlpha(0.3) + background: tinycolor(color) + .setAlpha(0.15) .toRgbString(), }; } return { value: getColorFromHexRgbOrName('gray', theme.type), - bar: getColorFromHexRgbOrName('gray', theme.type), + background: getColorFromHexRgbOrName('gray', theme.type), border: getColorFromHexRgbOrName('gray', theme.type), }; } @@ -136,14 +139,15 @@ export class BarGauge extends PureComponent { return gradient + ')'; } - renderSimpleMode(valueFormatted: string, valuePercent: number): ReactNode { - const { height, width } = this.props; + renderBasicAndGradientBars(valueFormatted: string, valuePercent: number): ReactNode { + const { height, width, displayMode } = this.props; const maxSize = this.size * BAR_SIZE_RATIO; const barSize = Math.max(valuePercent * maxSize, 0); const colors = this.getValueColors(); - const spaceForText = this.isVertical ? width : this.size - maxSize; + const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height); const valueStyles = this.getValueStyles(valueFormatted, colors.value, spaceForText); + const isBasic = displayMode === 'basic'; const containerStyles: CSSProperties = { width: `${width}px`, @@ -151,26 +155,45 @@ export class BarGauge extends PureComponent { display: 'flex', }; - const barStyles: CSSProperties = {}; + const barStyles: CSSProperties = { + borderRadius: '3px', + }; - // Custom styles for vertical orientation if (this.isVertical) { + // Custom styles for vertical orientation containerStyles.flexDirection = 'column'; containerStyles.justifyContent = 'flex-end'; + barStyles.transition = 'height 1s'; barStyles.height = `${barSize}px`; barStyles.width = `${width}px`; - // barStyles.borderTop = `1px solid ${colors.border}`; - barStyles.background = this.getBarGradient(maxSize); + if (isBasic) { + // Basic styles + barStyles.background = `${colors.background}`; + barStyles.border = `1px solid ${colors.border}`; + barStyles.boxShadow = `0 0 4px ${colors.border}`; + } else { + // Gradient styles + barStyles.background = this.getBarGradient(maxSize); + } } else { // Custom styles for horizontal orientation containerStyles.flexDirection = 'row-reverse'; containerStyles.justifyContent = 'flex-end'; containerStyles.alignItems = 'center'; + barStyles.transition = 'width 1s'; barStyles.height = `${height}px`; barStyles.width = `${barSize}px`; barStyles.marginRight = '10px'; - // barStyles.borderRight = `1px solid ${colors.border}`; - barStyles.background = this.getBarGradient(maxSize); + + if (isBasic) { + // Basic styles + barStyles.background = `${colors.background}`; + barStyles.border = `1px solid ${colors.border}`; + barStyles.boxShadow = `0 0 4px ${colors.border}`; + } else { + // Gradient styles + barStyles.background = this.getBarGradient(maxSize); + } } return ( @@ -221,7 +244,7 @@ export class BarGauge extends PureComponent { }; } - renderLcdMode(valueFormatted: string, valuePercent: number): ReactNode { + renderRetroBars(valueFormatted: string, valuePercent: number): ReactNode { const { height, width, maxValue, minValue } = this.props; const valueRange = maxValue - minValue; @@ -230,7 +253,7 @@ export class BarGauge extends PureComponent { const cellCount = maxSize / 20; const cellSize = (maxSize - cellSpacing * cellCount) / cellCount; const colors = this.getValueColors(); - const spaceForText = this.isVertical ? width : this.size - maxSize; + const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height); const valueStyles = this.getValueStyles(valueFormatted, colors.value, spaceForText); const containerStyles: CSSProperties = { @@ -260,8 +283,6 @@ export class BarGauge extends PureComponent { if (cellColor.isLit) { cellStyles.boxShadow = `0 0 4px ${cellColor.border}`; - // cellStyles.border = `1px solid ${cellColor.border}`; - // cellStyles.background = `${cellColor.backgroundShade}`; cellStyles.backgroundImage = `radial-gradient(${cellColor.background} 10%, ${cellColor.backgroundShade})`; } else { cellStyles.backgroundColor = cellColor.background; @@ -293,7 +314,7 @@ export class BarGauge extends PureComponent { interface BarColors { value: string; - bar: string; + background: string; border: string; } diff --git a/public/app/plugins/panel/bargauge/types.ts b/public/app/plugins/panel/bargauge/types.ts index 58694ab43ec..6c45b535b36 100644 --- a/public/app/plugins/panel/bargauge/types.ts +++ b/public/app/plugins/panel/bargauge/types.ts @@ -8,7 +8,7 @@ export interface BarGaugeOptions { valueOptions: SingleStatValueOptions; valueMappings: ValueMapping[]; thresholds: Threshold[]; - displayMode: 'simple' | 'lcd'; + displayMode: 'basic' | 'lcd' | 'gradient'; } export const orientationOptions: SelectOptionItem[] = [ @@ -16,12 +16,16 @@ export const orientationOptions: SelectOptionItem[] = [ { value: VizOrientation.Vertical, label: 'Vertical' }, ]; -export const displayModes: SelectOptionItem[] = [{ value: 'simple', label: 'Simple' }, { value: 'lcd', label: 'LCD' }]; +export const displayModes: SelectOptionItem[] = [ + { value: 'gradient', label: 'Gradient' }, + { value: 'lcd', label: 'Retro LCD' }, + { value: 'basic', label: 'Basic' }, +]; export const defaults: BarGaugeOptions = { minValue: 0, maxValue: 100, - displayMode: 'simple', + displayMode: 'basic', orientation: VizOrientation.Horizontal, valueOptions: { unit: 'none', From 0eb2ca3ffd86ba165b5d8d755915cf782189a155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 18 Mar 2019 07:42:43 +0100 Subject: [PATCH 099/194] chore: Cleaning up implicit anys in DashboardExporter and tests progress: #14714 --- packages/grafana-ui/src/types/plugin.ts | 1 + public/app/core/config.ts | 2 +- .../DashExportModal/DashboardExporter.test.ts | 17 +++--- .../DashExportModal/DashboardExporter.ts | 61 ++++++++++++++----- public/test/specs/helpers.ts | 3 +- 5 files changed, 61 insertions(+), 23 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index e2dda8ad407..bb1794a5154 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -91,6 +91,7 @@ export interface PluginMeta { includes: PluginInclude[]; // Datasource-specific + builtIn?: boolean; metrics?: boolean; tables?: boolean; logs?: boolean; diff --git a/public/app/core/config.ts b/public/app/core/config.ts index bbf7fb88d62..9789888e60f 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -13,7 +13,7 @@ export interface BuildInfo { export class Settings { datasources: any; - panels: PanelPlugin[]; + panels: { [key: string]: PanelPlugin }; appSubUrl: string; windowTitlePrefix: string; buildInfo: BuildInfo; diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index ac1b5f08632..1562953fd1b 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -4,13 +4,16 @@ jest.mock('app/core/store', () => { }; }); +// @ts-ignore import _ from 'lodash'; import config from 'app/core/config'; import { DashboardExporter } from './DashboardExporter'; import { DashboardModel } from '../../state/DashboardModel'; +import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { PanelPlugin } from 'app/types'; describe('given dashboard with repeated panels', () => { - let dash, exported; + let dash: any, exported: any; beforeEach(done => { dash = { @@ -89,25 +92,25 @@ describe('given dashboard with repeated panels', () => { config.buildInfo.version = '3.0.2'; //Stubs test function calls - const datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) }; + const datasourceSrvStub = ({ get: jest.fn(arg => getStub(arg)) } as any) as DatasourceSrv; config.panels['graph'] = { id: 'graph', name: 'Graph', info: { version: '1.1.0' }, - }; + } as PanelPlugin; config.panels['table'] = { id: 'table', name: 'Table', info: { version: '1.1.1' }, - }; + } as PanelPlugin; config.panels['heatmap'] = { id: 'heatmap', name: 'Heatmap', info: { version: '1.1.2' }, - }; + } as PanelPlugin; dash = new DashboardModel(dash, {}); const exporter = new DashboardExporter(datasourceSrvStub); @@ -213,7 +216,7 @@ describe('given dashboard with repeated panels', () => { }); // Stub responses -const stubs = []; +const stubs: { [key: string]: {} } = {}; stubs['gfdb'] = { name: 'gfdb', meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' }, @@ -249,6 +252,6 @@ stubs['-- Grafana --'] = { }, }; -function getStub(arg) { +function getStub(arg: string) { return Promise.resolve(stubs[arg || 'gfdb']); } diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts index 6cf14f81c86..4150cb9a848 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts @@ -1,9 +1,42 @@ -import config from 'app/core/config'; +// @ts-ignore import _ from 'lodash'; + +import config from 'app/core/config'; import { DashboardModel } from '../../state/DashboardModel'; +import DatasourceSrv from 'app/features/plugins/datasource_srv'; +import { PanelModel } from 'app/features/dashboard/state'; +import { PanelPlugin } from 'app/types/plugins'; + +interface Input { + name: string; + type: string; + label: string; + value: any; + description: string; +} + +interface Requires { + [key: string]: { + type: string; + id: string; + name: string; + version: string; + }; +} + +interface DataSources { + [key: string]: { + name: string; + label: string; + description: string; + type: string; + pluginId: string; + pluginName: string; + }; +} export class DashboardExporter { - constructor(private datasourceSrv) {} + constructor(private datasourceSrv: DatasourceSrv) {} makeExportable(dashboard: DashboardModel) { // clean up repeated rows and panels, @@ -18,19 +51,19 @@ export class DashboardExporter { // undo repeat cleanup dashboard.processRepeats(); - const inputs = []; - const requires = {}; - const datasources = {}; - const promises = []; - const variableLookup: any = {}; + const inputs: Input[] = []; + const requires: Requires = {}; + const datasources: DataSources = {}; + const promises: Array> = []; + const variableLookup: { [key: string]: any } = {}; for (const variable of saveModel.templating.list) { variableLookup[variable.name] = variable; } - const templateizeDatasourceUsage = obj => { - let datasource = obj.datasource; - let datasourceVariable = null; + const templateizeDatasourceUsage = (obj: any) => { + let datasource: string = obj.datasource; + let datasourceVariable: any = null; // ignore data source properties that contain a variable if (datasource && datasource.indexOf('$') === 0) { @@ -74,7 +107,7 @@ export class DashboardExporter { ); }; - const processPanel = panel => { + const processPanel = (panel: PanelModel) => { if (panel.datasource !== undefined) { templateizeDatasourceUsage(panel); } @@ -87,7 +120,7 @@ export class DashboardExporter { } } - const panelDef = config.panels[panel.type]; + const panelDef: PanelPlugin = config.panels[panel.type]; if (panelDef) { requires['panel' + panelDef.id] = { type: 'panel', @@ -135,7 +168,7 @@ export class DashboardExporter { return Promise.all(promises) .then(() => { - _.each(datasources, (value, key) => { + _.each(datasources, (value: any) => { inputs.push(value); }); @@ -160,7 +193,7 @@ export class DashboardExporter { } // make inputs and requires a top thing - const newObj = {}; + const newObj: { [key: string]: {} } = {}; newObj['__inputs'] = inputs; newObj['__requires'] = _.sortBy(requires, ['id']); diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 58403ac7ed7..f9124773c97 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -3,6 +3,7 @@ import config from 'app/core/config'; import * as dateMath from 'app/core/utils/datemath'; import { angularMocks, sinon } from '../lib/common'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; +import { PanelPlugin } from 'app/types'; export function ControllerTestContext(this: any) { const self = this; @@ -62,7 +63,7 @@ export function ControllerTestContext(this: any) { $rootScope.colors.push('#' + i); } - config.panels['test'] = { info: {} }; + config.panels['test'] = { info: {} } as PanelPlugin; self.ctrl = $controller( Ctrl, { $scope: self.scope }, From 854644f46cdc10387ef27399bbde7a61f835e9bf Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Mon, 18 Mar 2019 10:39:14 +0100 Subject: [PATCH 100/194] Add more patterns to no-only-test task --- scripts/grunt/default_task.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js index 8a71ea26627..a656e0c60af 100644 --- a/scripts/grunt/default_task.js +++ b/scripts/grunt/default_task.js @@ -34,14 +34,17 @@ module.exports = function (grunt) { ]); grunt.registerTask('no-only-tests', function () { - var files = grunt.file.expand('public/**/*_specs\.ts', 'public/**/*_specs\.js'); + var files = grunt.file.expand( + 'public/**/*@(_specs|\.test)\.@(ts|js|tsx|jsx)', + 'packages/grafana-ui/**/*@(_specs|\.test)\.@(ts|js|tsx|jsx)' + ); files.forEach(function (spec) { var rows = grunt.file.read(spec).split('\n'); rows.forEach(function (row) { if (row.indexOf('.only(') > 0) { grunt.log.errorlns(row); - grunt.fail.warn('found only statement in test: ' + spec) + grunt.fail.warn('found only statement in test: ' + spec); } }); }); From dcc5373e770035035889d55c0a543eea59ed26c8 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Mon, 18 Mar 2019 10:40:04 +0100 Subject: [PATCH 101/194] Remove .only function --- .../src/components/ThresholdsEditor/ThresholdsEditor.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index ea94537c429..38cd8e5c763 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -161,7 +161,7 @@ describe('change threshold value', () => { }); describe('on blur threshold value', () => { - it.only('should resort rows and update indexes', () => { + it('should resort rows and update indexes', () => { const { instance } = setup(); const thresholds = [ { index: 0, value: -Infinity, color: '#7EB26D' }, From c0eb1402972f4c7112c19295896d6a968d73e721 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 18 Mar 2019 10:41:28 +0100 Subject: [PATCH 102/194] moving --- .../grafana-ui/src/components/Input/Input.tsx | 93 +++++++++++++++++++ packages/grafana-ui/src/types/forms.ts | 26 ++++++ packages/grafana-ui/src/types/index.ts | 1 + packages/grafana-ui/src/utils/index.ts | 1 + packages/grafana-ui/src/utils/validate.ts | 15 +++ 5 files changed, 136 insertions(+) create mode 100644 packages/grafana-ui/src/components/Input/Input.tsx create mode 100644 packages/grafana-ui/src/types/forms.ts create mode 100644 packages/grafana-ui/src/utils/validate.ts diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx new file mode 100644 index 00000000000..57d6a753b17 --- /dev/null +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -0,0 +1,93 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; +import { ValidationEvents, ValidationRule } from '../../types/forms'; + +export enum InputStatus { + Invalid = 'invalid', + Valid = 'valid', +} + +export enum InputTypes { + Text = 'text', + Number = 'number', + Password = 'password', + Email = 'email', +} + +export enum EventsWithValidation { + onBlur = 'onBlur', + onFocus = 'onFocus', + onChange = 'onChange', +} + +interface Props extends React.HTMLProps { + validationEvents?: ValidationEvents; + hideErrorMessage?: boolean; + + // Override event props and append status as argument + onBlur?: (event: React.FocusEvent, status?: InputStatus) => void; + onFocus?: (event: React.FocusEvent, status?: InputStatus) => void; + onChange?: (event: React.FormEvent, status?: InputStatus) => void; +} + +export class Input extends PureComponent { + static defaultProps = { + className: '', + }; + + state = { + error: null, + }; + + get status() { + return this.state.error ? InputStatus.Invalid : InputStatus.Valid; + } + + get isInvalid() { + return this.status === InputStatus.Invalid; + } + + validatorAsync = (validationRules: ValidationRule[]) => { + return evt => { + const errors = validate(evt.target.value, validationRules); + this.setState(prevState => { + return { + ...prevState, + error: errors ? errors[0] : null, + }; + }); + }; + }; + + populateEventPropsWithStatus = (restProps, validationEvents: ValidationEvents) => { + const inputElementProps = { ...restProps }; + Object.keys(EventsWithValidation).forEach((eventName: EventsWithValidation) => { + if (hasValidationEvent(eventName, validationEvents) || restProps[eventName]) { + inputElementProps[eventName] = async evt => { + evt.persist(); // Needed for async. https://reactjs.org/docs/events.html#event-pooling + if (hasValidationEvent(eventName, validationEvents)) { + await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]); + } + if (restProps[eventName]) { + restProps[eventName].apply(null, [evt, this.status]); + } + }; + } + }); + return inputElementProps; + }; + + render() { + const { validationEvents, className, hideErrorMessage, ...restProps } = this.props; + const { error } = this.state; + const inputClassName = classNames('gf-form-input', { invalid: this.isInvalid }, className); + const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents); + + return ( +
+ + {error && !hideErrorMessage && {error}} +
+ ); + } +} diff --git a/packages/grafana-ui/src/types/forms.ts b/packages/grafana-ui/src/types/forms.ts new file mode 100644 index 00000000000..602ee434ee5 --- /dev/null +++ b/packages/grafana-ui/src/types/forms.ts @@ -0,0 +1,26 @@ +export enum InputStatus { + Invalid = 'invalid', + Valid = 'valid', +} + +export enum InputTypes { + Text = 'text', + Number = 'number', + Password = 'password', + Email = 'email', +} + +export enum EventsWithValidation { + onBlur = 'onBlur', + onFocus = 'onFocus', + onChange = 'onChange', +} + +export interface ValidationRule { + rule: (valueToValidate: string) => boolean; + errorMessage: string; +} + +export interface ValidationEvents { + [eventName: string]: ValidationRule[]; +} diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index b09d88bab4d..390f8a4db29 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -5,3 +5,4 @@ export * from './plugin'; export * from './datasource'; export * from './theme'; export * from './threshold'; +export * from './forms'; diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index a08b9ce1a89..00a6c20d4d1 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -7,3 +7,4 @@ export * from './thresholds'; export * from './string'; export * from './deprecationWarning'; export { getMappedValue } from './valueMappings'; +export * from './validate'; diff --git a/packages/grafana-ui/src/utils/validate.ts b/packages/grafana-ui/src/utils/validate.ts new file mode 100644 index 00000000000..20979ae33ff --- /dev/null +++ b/packages/grafana-ui/src/utils/validate.ts @@ -0,0 +1,15 @@ +import { EventsWithValidation, ValidationEvents, ValidationRule } from '../types'; + +export const validate = (value: string, validationRules: ValidationRule[]) => { + const errors = validationRules.reduce((acc, currentRule) => { + if (!currentRule.rule(value)) { + return acc.concat(currentRule.errorMessage); + } + return acc; + }, []); + return errors.length > 0 ? errors : null; +}; + +export const hasValidationEvent = (event: EventsWithValidation, validationEvents?: ValidationEvents) => { + return validationEvents && validationEvents[event]; +}; From 515fb5903ee772a1f43823d53c13c2ba2dea3e74 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 18 Mar 2019 10:44:00 +0100 Subject: [PATCH 103/194] sorting imports --- public/app/features/dashboard/state/PanelModel.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 5aca2bad462..f49ed2c0785 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -1,11 +1,13 @@ // Libraries import _ from 'lodash'; -// Types +// Utils import { Emitter } from 'app/core/utils/emitter'; +import { getNextRefIdLetter } from 'app/core/utils/query'; + +// Types import { DataQuery, TimeSeries, Threshold, ScopedVars, PanelTypeChangedHook } from '@grafana/ui'; import { TableData } from '@grafana/ui/src'; -import { getNextRefIdLetter } from '../../../core/utils/query'; export interface GridPos { x: number; From 39728c885b8c9567ad5887895cb21bd88c877ab8 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 18 Mar 2019 11:17:58 +0100 Subject: [PATCH 104/194] rename to char --- public/app/core/utils/explore.ts | 4 ++-- public/app/core/utils/query.ts | 2 +- public/app/features/dashboard/state/PanelModel.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 45e26e79ebf..2e79610c3c6 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -23,7 +23,7 @@ import { ResultGetter, } from 'app/types/explore'; import { LogsDedupStrategy } from 'app/core/logs_model'; -import { getNextRefIdLetter } from './query'; +import { getNextRefIdChar } from './query'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -227,7 +227,7 @@ export function generateKey(index = 0): string { } export function generateEmptyQuery(queries: DataQuery[], index = 0): DataQuery { - return { refId: getNextRefIdLetter(queries), key: generateKey(index) }; + return { refId: getNextRefIdChar(queries), key: generateKey(index) }; } /** diff --git a/public/app/core/utils/query.ts b/public/app/core/utils/query.ts index 304dcf1846f..933a73138a8 100644 --- a/public/app/core/utils/query.ts +++ b/public/app/core/utils/query.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import { DataQuery } from '@grafana/ui/'; -export const getNextRefIdLetter = (queries: DataQuery[]): string => { +export const getNextRefIdChar = (queries: DataQuery[]): string => { const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; return _.find(letters, refId => { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index f49ed2c0785..8ffce0f1e3b 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; // Utils import { Emitter } from 'app/core/utils/emitter'; -import { getNextRefIdLetter } from 'app/core/utils/query'; +import { getNextRefIdChar } from 'app/core/utils/query'; // Types import { DataQuery, TimeSeries, Threshold, ScopedVars, PanelTypeChangedHook } from '@grafana/ui'; @@ -131,7 +131,7 @@ export class PanelModel { if (this.targets) { for (const query of this.targets) { if (!query.refId) { - query.refId = getNextRefIdLetter(this.targets); + query.refId = getNextRefIdChar(this.targets); } } } @@ -269,7 +269,7 @@ export class PanelModel { addQuery(query?: Partial) { query = query || { refId: 'A' }; - query.refId = getNextRefIdLetter(this.targets); + query.refId = getNextRefIdChar(this.targets); this.targets.push(query as DataQuery); } From cb9bda810fae9bb49ee25d2059dddbec53766ad7 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 18 Mar 2019 11:21:40 +0100 Subject: [PATCH 105/194] test --- public/app/core/utils/query.test.ts | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 public/app/core/utils/query.test.ts diff --git a/public/app/core/utils/query.test.ts b/public/app/core/utils/query.test.ts new file mode 100644 index 00000000000..a69162751a4 --- /dev/null +++ b/public/app/core/utils/query.test.ts @@ -0,0 +1,30 @@ +import { DataQuery } from '@grafana/ui'; +import { getNextRefIdChar } from './query'; + +const dataQueries: DataQuery[] = [ + { + refId: 'A', + }, + { + refId: 'B', + }, + { + refId: 'C', + }, + { + refId: 'D', + }, + { + refId: 'E', + }, +]; + +describe('Get next refId char', () => { + it('should return next char', () => { + expect(getNextRefIdChar(dataQueries)).toEqual('F'); + }); + + it('should get first char', () => { + expect(getNextRefIdChar([])).toEqual('A'); + }); +}); From be7a5dab69cefe7e2e6dac258fc31cbeb1ee99d8 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 18 Mar 2019 11:23:40 +0100 Subject: [PATCH 106/194] reorder imports --- public/app/core/utils/explore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 2e79610c3c6..fdc63b931f7 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -9,6 +9,7 @@ import store from 'app/core/store'; import { parse as parseDate } from 'app/core/utils/datemath'; import { colors } from '@grafana/ui'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; +import { getNextRefIdChar } from './query'; // Types import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui'; @@ -23,7 +24,6 @@ import { ResultGetter, } from 'app/types/explore'; import { LogsDedupStrategy } from 'app/core/logs_model'; -import { getNextRefIdChar } from './query'; export const DEFAULT_RANGE = { from: 'now-6h', From 2b9cf1132f987ee3f1db9a606d5ec7fc09f471bb Mon Sep 17 00:00:00 2001 From: Oleg Gaidarenko Date: Mon, 18 Mar 2019 13:31:57 +0100 Subject: [PATCH 107/194] Use ora#fail instead of console.log Since with ora#fail you can stderr it instead of using the stdout, and it's a bit nicer since it will show that cross sign :) --- scripts/cli/utils/useSpinner.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/cli/utils/useSpinner.ts b/scripts/cli/utils/useSpinner.ts index 81ed9bb6fcf..298a6516689 100644 --- a/scripts/cli/utils/useSpinner.ts +++ b/scripts/cli/utils/useSpinner.ts @@ -10,8 +10,7 @@ export const useSpinner = (spinnerLabel: string, fn: FnToSpin, killProcess await fn(options); spinner.succeed(); } catch (e) { - spinner.fail(); - console.log(e); + spinner.fail(e); if (killProcess) { process.exit(1); } From f3b9ce317e793900f23dd27055a545f4aafbee17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 15 Mar 2019 11:51:13 +0100 Subject: [PATCH 108/194] docs: intial draft for frontend review doc --- style_guides/frontend-review-checklist.md | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 style_guides/frontend-review-checklist.md diff --git a/style_guides/frontend-review-checklist.md b/style_guides/frontend-review-checklist.md new file mode 100644 index 00000000000..39c1dea8ee4 --- /dev/null +++ b/style_guides/frontend-review-checklist.md @@ -0,0 +1,67 @@ +# Frontend Review Checklist + +## High level checks + +- [ ] The pull request adds value and the impact of the change is in line with [Frontend Style Guide](https://github.com/grafana/grafana/blob/master/style_guides/frontend.md). +- [ ] The pull request works the way it says it should do. +- [ ] The pull request does not increase the Angular code base. + > We are in the process of migrating to React so any increment of Angular code is generally discouraged from. (there are a few exceptions) +- [ ] The pull request closes one issue if possible and does not fix unrelated issues within the same pull request. +- [ ] The pull request contains necessary tests. + +## Low level checks + +- [ ] The pull request contains a title that explains the PR. +- [ ] The pull request contains necessary link(s) to issue(s). +- [ ] The pull request contains commits with commit messages that are small and understandable. +- [ ] The pull request does not contain magic strings or numbers that could be replaced with an `Enum` or `const` instead. +- [ ] The pull request does not increase the number of `implicit any` errors. +- [ ] The pull request does not contain uses of `any` or `{}` that are unexplainable. +- [ ] The pull request does not contain large React component that could easily be split into several smaller components. +- [ ] The pull request does not contain back end calls directly from components, use actions and Redux instead. + +### Bug specific checks + +- [ ] The pull request contains only one commit if possible. +- [ ] The pull request contains `closes: #Issue` or `fixes: #Issue` in pull request description. + +### Redux specific checks (skip if pull request does not contain Redux changes) + +- [ ] The pull request does not contain code that mutate state in reducers or thunks. +- [ ] The pull request uses helpers `actionCreatorFactory` and `reducerFactory` instead of traditional `switch statement` reducers in Redux. +- [ ] The pull request uses `reducerTester` to test reducers. +- [ ] The pull request does not contain code that access reducers state slice directly, instead the code uses state selectors to access state. + +## Common bad practices + +### 1. Missing Props/State type + +- React Component definitions + + ```jsx + // good + export class YourClass extends PureComponent<{},{}> { ... } + + // bad + export class YourClass extends PureComponent { ... } + ``` + +- React Component constructor + + ```typescript + // good + constructor(props:Props) {...} + + // bad + constructor(props) {...} + ``` + +- React Component defaultProps + + ```typescript + // good + static defaultProps: Partial = { ... } + + // bad + static defaultProps = { ... } + ``` From f251345b6804d3dad328fa60008e81d94323db16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 18 Mar 2019 07:55:44 +0100 Subject: [PATCH 109/194] docs: moved examples to frontend.md --- style_guides/frontend-review-checklist.md | 34 ------------ style_guides/frontend.md | 65 ++++++++++++++++------- 2 files changed, 46 insertions(+), 53 deletions(-) diff --git a/style_guides/frontend-review-checklist.md b/style_guides/frontend-review-checklist.md index 39c1dea8ee4..139c963b42d 100644 --- a/style_guides/frontend-review-checklist.md +++ b/style_guides/frontend-review-checklist.md @@ -31,37 +31,3 @@ - [ ] The pull request uses helpers `actionCreatorFactory` and `reducerFactory` instead of traditional `switch statement` reducers in Redux. - [ ] The pull request uses `reducerTester` to test reducers. - [ ] The pull request does not contain code that access reducers state slice directly, instead the code uses state selectors to access state. - -## Common bad practices - -### 1. Missing Props/State type - -- React Component definitions - - ```jsx - // good - export class YourClass extends PureComponent<{},{}> { ... } - - // bad - export class YourClass extends PureComponent { ... } - ``` - -- React Component constructor - - ```typescript - // good - constructor(props:Props) {...} - - // bad - constructor(props) {...} - ``` - -- React Component defaultProps - - ```typescript - // good - static defaultProps: Partial = { ... } - - // bad - static defaultProps = { ... } - ``` diff --git a/style_guides/frontend.md b/style_guides/frontend.md index caef4f711ef..18069183e66 100644 --- a/style_guides/frontend.md +++ b/style_guides/frontend.md @@ -1,36 +1,36 @@ # Frontend Style Guide -Generally we follow the Airbnb [React Style Guide](https://github.com/airbnb/javascript/tree/master/react). +Generally we follow the Airbnb [React Style Guide](https://github.com/airbnb/javascript/tree/master/react). ## Table of Contents - 1. [Basic Rules](#basic-rules) - 1. [File & Component Organization](#Organization) - 1. [Naming](#naming) - 1. [Declaration](#declaration) - 1. [Props](#props) - 1. [Refs](#refs) - 1. [Methods](#methods) - 1. [Ordering](#ordering) +1. [Basic Rules](#basic-rules) +1. [File & Component Organization](#Organization) +1. [Naming](#naming) +1. [Declaration](#declaration) +1. [Props](#props) +1. [Refs](#refs) +1. [Methods](#methods) +1. [Ordering](#ordering) ## Basic rules -* Try to keep files small and focused and break large components up into sub components. +- Try to keep files small and focused and break large components up into sub components. ## Organization -* Components and types that needs to be used by external plugins needs to go into @grafana/ui -* Components should get their own folder under features/xxx/components - * Sub components can live in that component folders, so small component do not need their own folder - * Place test next to their component file (same dir) - * Component sass should live in the same folder as component code -* State logic & domain models should live in features/xxx/state -* Containers (pages) can live in feature root features/xxx - * up for debate? +- Components and types that needs to be used by external plugins needs to go into @grafana/ui +- Components should get their own folder under features/xxx/components + - Sub components can live in that component folders, so small component do not need their own folder + - Place test next to their component file (same dir) + - Component sass should live in the same folder as component code +- State logic & domain models should live in features/xxx/state +- Containers (pages) can live in feature root features/xxx + - up for debate? ## Props -* Name callback props & handlers with a "on" prefix. +- Name callback props & handlers with a "on" prefix. ```tsx // good @@ -56,5 +56,32 @@ render() { } ``` +- React Component definitions +```jsx +// good +export class YourClass extends PureComponent<{},{}> { ... } +// bad +export class YourClass extends PureComponent { ... } +``` + +- React Component constructor + +```typescript +// good +constructor(props:Props) {...} + +// bad +constructor(props) {...} +``` + +- React Component defaultProps + +```typescript +// good +static defaultProps: Partial = { ... } + +// bad +static defaultProps = { ... } +``` From ed1b00190479a5053ed3e7c14d18fb5d2f5479b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 18 Mar 2019 12:03:34 +0100 Subject: [PATCH 110/194] docs: renamed file and added redux framework file --- ...st.md => pull-request-review-checklist.md} | 17 +- style_guides/redux.md | 158 ++++++++++++++++++ 2 files changed, 168 insertions(+), 7 deletions(-) rename style_guides/{frontend-review-checklist.md => pull-request-review-checklist.md} (82%) create mode 100644 style_guides/redux.md diff --git a/style_guides/frontend-review-checklist.md b/style_guides/pull-request-review-checklist.md similarity index 82% rename from style_guides/frontend-review-checklist.md rename to style_guides/pull-request-review-checklist.md index 139c963b42d..2fd017386ea 100644 --- a/style_guides/frontend-review-checklist.md +++ b/style_guides/pull-request-review-checklist.md @@ -1,4 +1,4 @@ -# Frontend Review Checklist +# Pull Request Review Checklist ## High level checks @@ -15,19 +15,22 @@ - [ ] The pull request contains necessary link(s) to issue(s). - [ ] The pull request contains commits with commit messages that are small and understandable. - [ ] The pull request does not contain magic strings or numbers that could be replaced with an `Enum` or `const` instead. -- [ ] The pull request does not increase the number of `implicit any` errors. -- [ ] The pull request does not contain uses of `any` or `{}` that are unexplainable. -- [ ] The pull request does not contain large React component that could easily be split into several smaller components. -- [ ] The pull request does not contain back end calls directly from components, use actions and Redux instead. ### Bug specific checks - [ ] The pull request contains only one commit if possible. - [ ] The pull request contains `closes: #Issue` or `fixes: #Issue` in pull request description. +## Frontend specific checks + +- [ ] The pull request does not increase the number of `implicit any` errors. +- [ ] The pull request does not contain uses of `any` or `{}` without comments describing why. +- [ ] The pull request does not contain large React component that could easily be split into several smaller components. +- [ ] The pull request does not contain back end calls directly from components, use actions and Redux instead. + ### Redux specific checks (skip if pull request does not contain Redux changes) - [ ] The pull request does not contain code that mutate state in reducers or thunks. -- [ ] The pull request uses helpers `actionCreatorFactory` and `reducerFactory` instead of traditional `switch statement` reducers in Redux. -- [ ] The pull request uses `reducerTester` to test reducers. +- [ ] The pull request uses helpers `actionCreatorFactory` and `reducerFactory` instead of traditional `switch statement` reducers in Redux. ([Redux framework](https://github.com/grafana/grafana/blob/master/style_guides/redux.md)) +- [ ] The pull request uses `reducerTester` to test reducers.([Redux framework](https://github.com/grafana/grafana/blob/master/style_guides/redux.md)) - [ ] The pull request does not contain code that access reducers state slice directly, instead the code uses state selectors to access state. diff --git a/style_guides/redux.md b/style_guides/redux.md new file mode 100644 index 00000000000..ff64fe400f3 --- /dev/null +++ b/style_guides/redux.md @@ -0,0 +1,158 @@ +# Redux framework + +To reduce the amount of boilerplate code used to create a strongly typed redux solution with actions, action creators, reducers and tests we've introduced a small framework around Redux. + +`+` Much less boilerplate code +`-` Non Redux standard api + +## New core functionality + +### actionCreatorFactory + +Used to create an action creator with the following signature + +```typescript +{ type: string , (payload: T): {type: string; payload: T;} } +``` + +where the `type` string will be ensured to be unique and `T` is the type supplied to the factory. + +#### Example + +```typescript +export const someAction = actionCreatorFactory('SOME_ACTION').create(); + +// later when dispatched +someAction('this rocks!'); +``` + +```typescript +// best practices, always use an interface as type +interface SomeAction { + data: string; +} +export const someAction = actionCreatorFactory('SOME_ACTION').create(); + +// later when dispatched +someAction({ data: 'best practices' }); +``` + +```typescript +// declaring an action creator with a type string that has already been defined will throw +export const someAction = actionCreatorFactory('SOME_ACTION').create(); +export const theAction = actionCreatorFactory('SOME_ACTION').create(); // will throw +``` + +### noPayloadActionCreatorFactory + +Used when you don't need to supply a payload for your action. Will create an action creator with the following signature + +```typescript +{ type: string , (): {type: string; payload: undefined;} } +``` + +where the `type` string will be ensured to be unique. + +#### Example + +```typescript +export const noPayloadAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create(); + +// later when dispatched +noPayloadAction(); +``` + +```typescript +// declaring an action creator with a type string that has already been defined will throw +export const noPayloadAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create(); +export const noAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create(); // will throw +``` + +### reducerFactory + +Fluent API used to create a reducer. (same as implementing the standard switch statement in Redux) + +#### Example + +```typescript +interface ExampleReducerState { + data: string[]; +} + +const intialState: ExampleReducerState = { data: [] }; + +export const someAction = actionCreatorFactory('SOME_ACTION').create(); +export const otherAction = actionCreatorFactory('Other_ACTION').create(); + +export const exampleReducer = reducerFactory(intialState) + // addMapper is the function that ties an action creator to a state change + .addMapper({ + // action creator to filter out which mapper to use + filter: someAction, + // mapper function where the state change occurs + mapper: (state, action) => ({ ...state, data: state.data.concat(action.payload) }), + }) + // a developer can just chain addMapper functions until reducer is done + .addMapper({ + filter: otherAction, + mapper: (state, action) => ({ ...state, data: action.payload }), + }) + .create(); // this will return the reducer +``` + +#### Typing limitations + +There is a challenge left with the mapper function that I can not solve with TypeScript. The signature of a mapper is + +```typescript +(state: State, action: ActionOf) => State; +``` + +If you would to return an object that is not of the state type like the following mapper + +```typescript +mapper: (state, action) => ({ nonExistingProperty: ''}), +``` + +Then you would receive the following compile error + +```shell +[ts] Property 'data' is missing in type '{ nonExistingProperty: string; }' but required in type 'ExampleReducerState'. [2741] +``` + +But if you return an object that is spreading state and add a non existing property type like the following mapper + +```typescript +mapper: (state, action) => ({ ...state, nonExistingProperty: ''}), +``` + +Then you would not receive any compile error. + +If you want to make sure that never happens you can just supply the State type to the mapper callback like the following mapper: + +```typescript +mapper: (state, action): ExampleReducerState => ({ ...state, nonExistingProperty: 'kalle' }), +``` + +Then you would receive the following compile error + +```shell +[ts] +Type '{ nonExistingProperty: string; data: string[]; }' is not assignable to type 'ExampleReducerState'. + Object literal may only specify known properties, and 'nonExistingProperty' does not exist in type 'ExampleReducerState'. [2322] +``` + +## New test functionality + +### reducerTester + +Fluent API that simplifies the testing of reducers + +#### Example + +```typescript +reducerTester() + .givenReducer(someReducer, initialState) + .whenActionIsDispatched(someAction('reducer tests')) + .thenStateShouldEqual({ ...initialState, data: 'reducer tests' }); +``` From 384e11fd6832d06c26af3873a12ef6337bcc7d3b Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 18 Mar 2019 15:41:46 +0100 Subject: [PATCH 111/194] Copied from new timepicker and unified component branch --- .../src/components/Input}/Input.test.tsx | 12 +-- .../grafana-ui/src/components/Input/Input.tsx | 40 +++----- .../Input}/__snapshots__/Input.test.tsx.snap | 0 packages/grafana-ui/src/components/index.ts | 1 + packages/grafana-ui/src/types/forms.ts | 26 ----- packages/grafana-ui/src/types/index.ts | 2 +- .../grafana-ui/src/types/input.ts | 0 packages/grafana-ui/src/utils/validate.ts | 25 +++-- public/app/core/components/Form/Input.tsx | 94 ------------------- public/app/core/components/Form/index.ts | 1 - public/app/core/utils/validate.ts | 16 ---- .../dashboard/panel_editor/QueryOptions.tsx | 9 +- public/app/types/index.ts | 1 - 13 files changed, 40 insertions(+), 187 deletions(-) rename {public/app/core/components/Form => packages/grafana-ui/src/components/Input}/Input.test.tsx (83%) rename {public/app/core/components/Form => packages/grafana-ui/src/components/Input}/__snapshots__/Input.test.tsx.snap (100%) delete mode 100644 packages/grafana-ui/src/types/forms.ts rename public/app/types/form.ts => packages/grafana-ui/src/types/input.ts (100%) delete mode 100644 public/app/core/components/Form/Input.tsx delete mode 100644 public/app/core/components/Form/index.ts delete mode 100644 public/app/core/utils/validate.ts diff --git a/public/app/core/components/Form/Input.test.tsx b/packages/grafana-ui/src/components/Input/Input.test.tsx similarity index 83% rename from public/app/core/components/Form/Input.test.tsx rename to packages/grafana-ui/src/components/Input/Input.test.tsx index 9e903208e80..1d39b594b1c 100644 --- a/public/app/core/components/Form/Input.test.tsx +++ b/packages/grafana-ui/src/components/Input/Input.test.tsx @@ -1,18 +1,16 @@ -import React from 'react'; +import React from 'react'; import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; -import { Input, EventsWithValidation } from './Input'; -import { ValidationEvents } from 'app/types'; +import { Input } from './Input'; +import { EventsWithValidation } from '../../utils'; +import { ValidationEvents } from '../../types'; const TEST_ERROR_MESSAGE = 'Value must be empty or less than 3 chars'; const testBlurValidation: ValidationEvents = { [EventsWithValidation.onBlur]: [ { rule: (value: string) => { - if (!value || value.length < 3) { - return true; - } - return false; + return !value || value.length < 3; }, errorMessage: TEST_ERROR_MESSAGE, }, diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index 57d6a753b17..f5f59e265c0 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -1,25 +1,13 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, ChangeEvent } from 'react'; import classNames from 'classnames'; -import { ValidationEvents, ValidationRule } from '../../types/forms'; +import { validate, EventsWithValidation, hasValidationEvent } from '../../utils'; +import { ValidationEvents, ValidationRule } from '../../types'; export enum InputStatus { Invalid = 'invalid', Valid = 'valid', } -export enum InputTypes { - Text = 'text', - Number = 'number', - Password = 'password', - Email = 'email', -} - -export enum EventsWithValidation { - onBlur = 'onBlur', - onFocus = 'onFocus', - onChange = 'onChange', -} - interface Props extends React.HTMLProps { validationEvents?: ValidationEvents; hideErrorMessage?: boolean; @@ -27,7 +15,7 @@ interface Props extends React.HTMLProps { // Override event props and append status as argument onBlur?: (event: React.FocusEvent, status?: InputStatus) => void; onFocus?: (event: React.FocusEvent, status?: InputStatus) => void; - onChange?: (event: React.FormEvent, status?: InputStatus) => void; + onChange?: (event: React.ChangeEvent, status?: InputStatus) => void; } export class Input extends PureComponent { @@ -48,24 +36,24 @@ export class Input extends PureComponent { } validatorAsync = (validationRules: ValidationRule[]) => { - return evt => { + return (evt: ChangeEvent) => { const errors = validate(evt.target.value, validationRules); this.setState(prevState => { - return { - ...prevState, - error: errors ? errors[0] : null, - }; + return { ...prevState, error: errors ? errors[0] : null }; }); }; }; - populateEventPropsWithStatus = (restProps, validationEvents: ValidationEvents) => { + populateEventPropsWithStatus = (restProps: any, validationEvents: ValidationEvents | undefined) => { const inputElementProps = { ...restProps }; - Object.keys(EventsWithValidation).forEach((eventName: EventsWithValidation) => { - if (hasValidationEvent(eventName, validationEvents) || restProps[eventName]) { - inputElementProps[eventName] = async evt => { + if (!validationEvents) { + return inputElementProps; + } + Object.keys(EventsWithValidation).forEach(eventName => { + if (hasValidationEvent(eventName as EventsWithValidation, validationEvents) || restProps[eventName]) { + inputElementProps[eventName] = async (evt: ChangeEvent) => { evt.persist(); // Needed for async. https://reactjs.org/docs/events.html#event-pooling - if (hasValidationEvent(eventName, validationEvents)) { + if (hasValidationEvent(eventName as EventsWithValidation, validationEvents)) { await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]); } if (restProps[eventName]) { diff --git a/public/app/core/components/Form/__snapshots__/Input.test.tsx.snap b/packages/grafana-ui/src/components/Input/__snapshots__/Input.test.tsx.snap similarity index 100% rename from public/app/core/components/Form/__snapshots__/Input.test.tsx.snap rename to packages/grafana-ui/src/components/Input/__snapshots__/Input.test.tsx.snap diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index b8c8d66cead..e20a52f6485 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -25,6 +25,7 @@ export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor'; export { Switch } from './Switch/Switch'; export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult'; export { UnitPicker } from './UnitPicker/UnitPicker'; +export { Input, InputStatus } from './Input/Input'; // Visualizations export { Gauge } from './Gauge/Gauge'; diff --git a/packages/grafana-ui/src/types/forms.ts b/packages/grafana-ui/src/types/forms.ts deleted file mode 100644 index 602ee434ee5..00000000000 --- a/packages/grafana-ui/src/types/forms.ts +++ /dev/null @@ -1,26 +0,0 @@ -export enum InputStatus { - Invalid = 'invalid', - Valid = 'valid', -} - -export enum InputTypes { - Text = 'text', - Number = 'number', - Password = 'password', - Email = 'email', -} - -export enum EventsWithValidation { - onBlur = 'onBlur', - onFocus = 'onFocus', - onChange = 'onChange', -} - -export interface ValidationRule { - rule: (valueToValidate: string) => boolean; - errorMessage: string; -} - -export interface ValidationEvents { - [eventName: string]: ValidationRule[]; -} diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index 390f8a4db29..c0aede431d0 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -5,4 +5,4 @@ export * from './plugin'; export * from './datasource'; export * from './theme'; export * from './threshold'; -export * from './forms'; +export * from './input'; diff --git a/public/app/types/form.ts b/packages/grafana-ui/src/types/input.ts similarity index 100% rename from public/app/types/form.ts rename to packages/grafana-ui/src/types/input.ts diff --git a/packages/grafana-ui/src/utils/validate.ts b/packages/grafana-ui/src/utils/validate.ts index 20979ae33ff..286ec700577 100644 --- a/packages/grafana-ui/src/utils/validate.ts +++ b/packages/grafana-ui/src/utils/validate.ts @@ -1,15 +1,24 @@ -import { EventsWithValidation, ValidationEvents, ValidationRule } from '../types'; +import { ValidationRule, ValidationEvents } from '../types/input'; + +export enum EventsWithValidation { + onBlur = 'onBlur', + onFocus = 'onFocus', + onChange = 'onChange', +} export const validate = (value: string, validationRules: ValidationRule[]) => { - const errors = validationRules.reduce((acc, currentRule) => { - if (!currentRule.rule(value)) { - return acc.concat(currentRule.errorMessage); - } - return acc; - }, []); + const errors = validationRules.reduce( + (acc, currRule) => { + if (!currRule.rule(value)) { + return acc.concat(currRule.errorMessage); + } + return acc; + }, + [] as string[] + ); return errors.length > 0 ? errors : null; }; -export const hasValidationEvent = (event: EventsWithValidation, validationEvents?: ValidationEvents) => { +export const hasValidationEvent = (event: EventsWithValidation, validationEvents: ValidationEvents | undefined) => { return validationEvents && validationEvents[event]; }; diff --git a/public/app/core/components/Form/Input.tsx b/public/app/core/components/Form/Input.tsx deleted file mode 100644 index 7940f3b1104..00000000000 --- a/public/app/core/components/Form/Input.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { PureComponent } from 'react'; -import classNames from 'classnames'; -import { ValidationEvents, ValidationRule } from 'app/types'; -import { validate, hasValidationEvent } from 'app/core/utils/validate'; - -export enum InputStatus { - Invalid = 'invalid', - Valid = 'valid', -} - -export enum InputTypes { - Text = 'text', - Number = 'number', - Password = 'password', - Email = 'email', -} - -export enum EventsWithValidation { - onBlur = 'onBlur', - onFocus = 'onFocus', - onChange = 'onChange', -} - -interface Props extends React.HTMLProps { - validationEvents?: ValidationEvents; - hideErrorMessage?: boolean; - - // Override event props and append status as argument - onBlur?: (event: React.FocusEvent, status?: InputStatus) => void; - onFocus?: (event: React.FocusEvent, status?: InputStatus) => void; - onChange?: (event: React.FormEvent, status?: InputStatus) => void; -} - -export class Input extends PureComponent { - static defaultProps = { - className: '', - }; - - state = { - error: null, - }; - - get status() { - return this.state.error ? InputStatus.Invalid : InputStatus.Valid; - } - - get isInvalid() { - return this.status === InputStatus.Invalid; - } - - validatorAsync = (validationRules: ValidationRule[]) => { - return evt => { - const errors = validate(evt.target.value, validationRules); - this.setState(prevState => { - return { - ...prevState, - error: errors ? errors[0] : null, - }; - }); - }; - }; - - populateEventPropsWithStatus = (restProps, validationEvents: ValidationEvents) => { - const inputElementProps = { ...restProps }; - Object.keys(EventsWithValidation).forEach((eventName: EventsWithValidation) => { - if (hasValidationEvent(eventName, validationEvents) || restProps[eventName]) { - inputElementProps[eventName] = async evt => { - evt.persist(); // Needed for async. https://reactjs.org/docs/events.html#event-pooling - if (hasValidationEvent(eventName, validationEvents)) { - await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]); - } - if (restProps[eventName]) { - restProps[eventName].apply(null, [evt, this.status]); - } - }; - } - }); - return inputElementProps; - }; - - render() { - const { validationEvents, className, hideErrorMessage, ...restProps } = this.props; - const { error } = this.state; - const inputClassName = classNames('gf-form-input', { invalid: this.isInvalid }, className); - const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents); - - return ( -
- - {error && !hideErrorMessage && {error}} -
- ); - } -} diff --git a/public/app/core/components/Form/index.ts b/public/app/core/components/Form/index.ts deleted file mode 100644 index 6322cf3241a..00000000000 --- a/public/app/core/components/Form/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Input } from './Input'; diff --git a/public/app/core/utils/validate.ts b/public/app/core/utils/validate.ts deleted file mode 100644 index c6663882808..00000000000 --- a/public/app/core/utils/validate.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ValidationRule, ValidationEvents } from 'app/types'; -import { EventsWithValidation } from 'app/core/components/Form/Input'; - -export const validate = (value: string, validationRules: ValidationRule[]) => { - const errors = validationRules.reduce((acc, currRule) => { - if (!currRule.rule(value)) { - return acc.concat(currRule.errorMessage); - } - return acc; - }, []); - return errors.length > 0 ? errors : null; -}; - -export const hasValidationEvent = (event: EventsWithValidation, validationEvents: ValidationEvents) => { - return validationEvents && validationEvents[event]; -}; diff --git a/public/app/features/dashboard/panel_editor/QueryOptions.tsx b/public/app/features/dashboard/panel_editor/QueryOptions.tsx index 0d031cb12ba..377582d7ce5 100644 --- a/public/app/features/dashboard/panel_editor/QueryOptions.tsx +++ b/public/app/features/dashboard/panel_editor/QueryOptions.tsx @@ -5,17 +5,12 @@ import React, { PureComponent, ChangeEvent, FocusEvent } from 'react'; import { isValidTimeSpan } from 'app/core/utils/rangeutil'; // Components -import { Switch } from '@grafana/ui'; -import { Input } from 'app/core/components/Form'; -import { EventsWithValidation } from 'app/core/components/Form/Input'; -import { InputStatus } from 'app/core/components/Form/Input'; +import { DataSourceSelectItem, EventsWithValidation, Input, InputStatus, Switch, ValidationEvents } from '@grafana/ui'; import { DataSourceOption } from './DataSourceOption'; import { FormLabel } from '@grafana/ui'; // Types -import { PanelModel } from '../state/PanelModel'; -import { DataSourceSelectItem } from '@grafana/ui/src/types'; -import { ValidationEvents } from 'app/types'; +import { PanelModel } from '../state'; const timeRangeValidationEvents: ValidationEvents = { [EventsWithValidation.onBlur]: [ diff --git a/public/app/types/index.ts b/public/app/types/index.ts index eefba746c61..3bf76aeb3c3 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -12,6 +12,5 @@ export * from './plugins'; export * from './organization'; export * from './appNotifications'; export * from './search'; -export * from './form'; export * from './explore'; export * from './store'; From 6673915f2ebe62334e418bb7c74e70f1ea394498 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 19 Mar 2019 08:16:19 +0100 Subject: [PATCH 112/194] Update style_guides/backend.md Co-Authored-By: bergquist --- style_guides/backend.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/style_guides/backend.md b/style_guides/backend.md index 8150530071b..1c6c86efc0b 100644 --- a/style_guides/backend.md +++ b/style_guides/backend.md @@ -17,7 +17,7 @@ The preferred solution, in this case, is to inject the `bus` into services or ta ### settings package In the `setting` packages there are many global variables which Grafana sets at startup. This is also something we want to move -away from and move as much configuration as possible to the `setting.Cfg` struct and pass the around just like the bus +away from and move as much configuration as possible to the `setting.Cfg` struct and pass it around, just like the bus. ## Linting and formatting We enforce strict `gofmt` formating and use some linters on our codebase. You can find the current list of linters at https://github.com/grafana/grafana/blob/master/scripts/gometalinter.sh#L23 @@ -27,4 +27,4 @@ We don't enforce `golint` but we encourage it and we will test so the number of ## Testing We use GoConvey for BDD/scenario based testing. Which we think is useful for testing certain chain or interactions. Ex https://github.com/grafana/grafana/blob/master/pkg/services/auth/auth_token_test.go -For smaller tests its preferred to use standard library testing. \ No newline at end of file +For smaller tests its preferred to use standard library testing. From 39e75d75b40a7a3cc570ac8fa36e762b9c8639d7 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 19 Mar 2019 10:48:41 +0100 Subject: [PATCH 113/194] build: crcmod speedups rsync to gcp for deploy. --- scripts/build/ci-deploy/Dockerfile | 2 +- scripts/build/ci-deploy/build-deploy.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build/ci-deploy/Dockerfile b/scripts/build/ci-deploy/Dockerfile index dd4987b96c3..e608d9156e7 100644 --- a/scripts/build/ci-deploy/Dockerfile +++ b/scripts/build/ci-deploy/Dockerfile @@ -10,7 +10,7 @@ FROM circleci/python:2.7-stretch USER root -RUN pip install awscli && \ +RUN pip install -U awscli crcmod && \ curl https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-222.0.0-linux-x86_64.tar.gz | \ tar xvzf - -C /opt && \ apt update && \ diff --git a/scripts/build/ci-deploy/build-deploy.sh b/scripts/build/ci-deploy/build-deploy.sh index 8dedeead009..ed9c9e5459e 100755 --- a/scripts/build/ci-deploy/build-deploy.sh +++ b/scripts/build/ci-deploy/build-deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash -_version="1.2.0" +_version="1.2.1" _tag="grafana/grafana-ci-deploy:${_version}" docker build -t $_tag . From 4152e5c16ca2059692cd18071128c1e6beac0b18 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 19 Mar 2019 10:57:17 +0100 Subject: [PATCH 114/194] build: updated deploy container with crcmod. --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index da0e0665285..49fb3776534 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -322,7 +322,7 @@ jobs: deploy-enterprise-master: docker: - - image: grafana/grafana-ci-deploy:1.2.0 + - image: grafana/grafana-ci-deploy:1.2.1 steps: - attach_workspace: at: . @@ -345,7 +345,7 @@ jobs: deploy-enterprise-release: docker: - - image: grafana/grafana-ci-deploy:1.2.0 + - image: grafana/grafana-ci-deploy:1.2.1 steps: - checkout - attach_workspace: @@ -378,7 +378,7 @@ jobs: deploy-master: docker: - - image: grafana/grafana-ci-deploy:1.2.0 + - image: grafana/grafana-ci-deploy:1.2.1 steps: - attach_workspace: at: . @@ -409,7 +409,7 @@ jobs: deploy-release: docker: - - image: grafana/grafana-ci-deploy:1.2.0 + - image: grafana/grafana-ci-deploy:1.2.1 steps: - checkout - attach_workspace: From 4dceb60d204cb9de6849037838f42d8e84f3fc36 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 19 Mar 2019 11:34:01 +0100 Subject: [PATCH 115/194] build: migrates the build container into the main repo. --- scripts/build/ci-build/Dockerfile | 114 +++++++++++++++++++++++++ scripts/build/ci-build/Makefile | 54 ++++++++++++ scripts/build/ci-build/README.md | 20 +++++ scripts/build/ci-build/bootstrap.sh | 5 ++ scripts/build/ci-build/build-deploy.sh | 7 ++ 5 files changed, 200 insertions(+) create mode 100644 scripts/build/ci-build/Dockerfile create mode 100644 scripts/build/ci-build/Makefile create mode 100644 scripts/build/ci-build/README.md create mode 100755 scripts/build/ci-build/bootstrap.sh create mode 100755 scripts/build/ci-build/build-deploy.sh diff --git a/scripts/build/ci-build/Dockerfile b/scripts/build/ci-build/Dockerfile new file mode 100644 index 00000000000..7c6ed58c0e4 --- /dev/null +++ b/scripts/build/ci-build/Dockerfile @@ -0,0 +1,114 @@ +FROM ubuntu:14.04 as toolchain + +ENV OSX_SDK_URL=https://s3.dockerproject.org/darwin/v2/ \ + OSX_SDK=MacOSX10.10.sdk \ + OSX_MIN=10.10 \ + CTNG=1.23.0 + +# FIRST PART +# build osx64 toolchain (stripped of man documentation) +# the toolchain produced is not self contained, it needs clang at runtime +# +# SECOND PART +# build gcc (no g++) centos6-x64 toolchain +# doc: https://crosstool-ng.github.io/docs/ +# apt-get should be all dep to build toolchain +# sed and 1st echo are for convenience to get the toolchain in /tmp/x86_64-centos6-linux-gnu +# other echo are to enable build by root (crosstool-NG refuse to do that by default) +# the last 2 rm are just to save some time and space writing docker layers +# +# THIRD PART +# build fpm and creates a set of deb from gem +# ruby2.0 depends on ruby1.9.3 which is install as default ruby +# rm/ln are here to change that +# created deb depends on rubygem-json but json gem is not build +# so do by hand + + +# might wanna make sure osx cross and the other tarball as well as the packages ends up somewhere other than tmp +# might also wanna put them as their own layer to not have to unpack them every time? + +RUN apt-get update && \ + apt-get install -y \ + clang-3.8 patch libxml2-dev \ + ca-certificates \ + curl \ + git \ + make \ + xz-utils && \ + git clone https://github.com/tpoechtrager/osxcross.git /tmp/osxcross && \ + curl -L ${OSX_SDK_URL}/${OSX_SDK}.tar.xz -o /tmp/osxcross/tarballs/${OSX_SDK}.tar.xz && \ + ln -s /usr/bin/clang-3.8 /usr/bin/clang && \ + ln -s /usr/bin/clang++-3.8 /usr/bin/clang++ && \ + ln -s /usr/bin/llvm-dsymutil-3.8 /usr/bin/dsymutil && \ + UNATTENDED=yes OSX_VERSION_MIN=${OSX_MIN} /tmp/osxcross/build.sh && \ + rm -rf /tmp/osxcross/target/SDK/${OSX_SDK}/usr/share && \ + cd /tmp && \ + tar cfJ osxcross.tar.xz osxcross/target && \ + rm -rf /tmp/osxcross && \ + apt-get install -y \ + bison curl flex gawk gcc g++ gperf help2man libncurses5-dev make patch python-dev texinfo xz-utils && \ + curl -L http://crosstool-ng.org/download/crosstool-ng/crosstool-ng-${CTNG}.tar.xz \ + | tar -xJ -C /tmp/ && \ + cd /tmp/crosstool-ng-${CTNG} && \ + ./configure --enable-local && \ + make && \ + ./ct-ng x86_64-centos6-linux-gnu && \ + sed -i '/CT_PREFIX_DIR=/d' .config && \ + echo 'CT_PREFIX_DIR="/tmp/${CT_HOST:+HOST-${CT_HOST}/}${CT_TARGET}"' >> .config && \ + echo 'CT_EXPERIMENTAL=y' >> .config && \ + echo 'CT_ALLOW_BUILD_AS_ROOT=y' >> .config && \ + echo 'CT_ALLOW_BUILD_AS_ROOT_SURE=y' >> .config && \ + ./ct-ng build && \ + cd /tmp && \ + rm /tmp/x86_64-centos6-linux-gnu/build.log.bz2 && \ + tar cfJ x86_64-centos6-linux-gnu.tar.xz x86_64-centos6-linux-gnu/ && \ + rm -rf /tmp/x86_64-centos6-linux-gnu/ && \ + rm -rf /tmp/crosstool-ng-${CTNG} + +# base image to crossbuild grafana +FROM ubuntu:14.04 + +ENV GOVERSION=1.11.5 \ + PATH=/usr/local/go/bin:$PATH \ + GOPATH=/go \ + NODEVERSION=10.14.2 + +COPY --from=toolchain /tmp/x86_64-centos6-linux-gnu.tar.xz /tmp/ +COPY --from=toolchain /tmp/osxcross.tar.xz /tmp/ + +RUN apt-get update && \ + apt-get install -y \ + clang-3.8 gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf gcc-mingw-w64-x86-64 \ + apt-transport-https \ + ca-certificates \ + curl \ + libfontconfig1 \ + gcc \ + g++ \ + git \ + make \ + rpm \ + xz-utils \ + expect \ + gnupg2 \ + unzip && \ + ln -s /usr/bin/clang-3.8 /usr/bin/clang && \ + ln -s /usr/bin/clang++-3.8 /usr/bin/clang++ && \ + ln -s /usr/bin/llvm-dsymutil-3.8 /usr/bin/dsymutil && \ + curl -L https://nodejs.org/dist/v${NODEVERSION}/node-v${NODEVERSION}-linux-x64.tar.xz \ + | tar -xJ --strip-components=1 -C /usr/local && \ + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb [arch=amd64] https://dl.yarnpkg.com/debian/ stable main" \ + | tee /etc/apt/sources.list.d/yarn.list && \ + apt-get update && apt-get install --no-install-recommends yarn && \ + curl -L https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz \ + | tar -xz -C /usr/local + +RUN apt-get install -y \ + gcc libc-dev make && \ + gpg2 --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB && \ + curl -sSL https://get.rvm.io | bash -s stable && \ + /bin/bash -l -c "rvm requirements && rvm install 2.2 && gem install -N fpm" + +ADD ./bootstrap.sh /tmp/bootstrap.sh \ No newline at end of file diff --git a/scripts/build/ci-build/Makefile b/scripts/build/ci-build/Makefile new file mode 100644 index 00000000000..64fa376d7cf --- /dev/null +++ b/scripts/build/ci-build/Makefile @@ -0,0 +1,54 @@ +VERSION="dev" +TAG="grafana/build-container" +USER_ID=$(shell id -u) +GROUP_ID=$(shell id -g) + +all: build deploy + +build: + docker build -t "${TAG}:${VERSION}" . + +deploy: + docker push "${TAG}:${VERSION}" + +run: + docker run -ti \ + -e "CIRCLE_BRANCH=local" \ + -e "CIRCLE_BUILD_NUM=472" \ + ${TAG}:${VERSION} \ + bash + +run-with-local-source-live: + docker run -d \ + -e "CIRCLE_BRANCH=local" \ + -e "CIRCLE_BUILD_NUM=472" \ + -w "/go/src/github.com/grafana/grafana" \ + --name grafana-build \ + -v "${GOPATH}/src/github.com/grafana/grafana:/go/src/github.com/grafana/grafana" \ + ${TAG}:${VERSION} \ + bash -c "/tmp/bootstrap.sh; mkdir /.cache; chown "${USER_ID}:${GROUP_ID}" /.cache; tail -f /dev/null" + docker exec -ti --user "${USER_ID}:${GROUP_ID}" grafana-build bash + +run-with-local-source-copy: + docker run -d \ + -e "CIRCLE_BRANCH=local" \ + -e "CIRCLE_BUILD_NUM=472" \ + -w "/go/src/github.com/grafana/grafana" \ + --name grafana-build \ + ${TAG}:${VERSION} \ + bash -c "/tmp/bootstrap.sh; tail -f /dev/null" + docker cp "${GOPATH}/src/github.com/grafana/grafana" grafana-build:/go/src/github.com/grafana/ + docker exec -ti grafana-build bash + +update-source: + docker cp "${GOPATH}/src/github.com/grafana/grafana" grafana-build:/go/src/github.com/grafana/ + +attach: + docker exec -ti grafana-build bash + +attach-live: + docker exec -ti --user "${USER_ID}:${GROUP_ID}" grafana-build bash + +stop: + docker kill grafana-build + docker rm grafana-build diff --git a/scripts/build/ci-build/README.md b/scripts/build/ci-build/README.md new file mode 100644 index 00000000000..e66ec1b3cf7 --- /dev/null +++ b/scripts/build/ci-build/README.md @@ -0,0 +1,20 @@ +# grafana-build-container +Grafana build container + +## Description + +This is a container for cross-platform builds of Grafana. You can run it locally using the Makefile. + +## Makefile targets + +* `make run-with-local-source-copy` + - Starts the container locally and copies your local sources into the container +* `make run-with-local-source-live` + - Starts the container (as your user) locally and maps your Grafana project dir into the container +* `make update-source` + - Updates the sources in the container from your local sources +* `make stop` + - Kills the container +* `make attach` + - Opens bash within the running container + diff --git a/scripts/build/ci-build/bootstrap.sh b/scripts/build/ci-build/bootstrap.sh new file mode 100755 index 00000000000..2eda345b5ab --- /dev/null +++ b/scripts/build/ci-build/bootstrap.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +cd /tmp +tar xfJ x86_64-centos6-linux-gnu.tar.xz +tar xfJ osxcross.tar.xz diff --git a/scripts/build/ci-build/build-deploy.sh b/scripts/build/ci-build/build-deploy.sh new file mode 100755 index 00000000000..c2a33e4a9e4 --- /dev/null +++ b/scripts/build/ci-build/build-deploy.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +_version="1.2.3" +_tag="grafana/build-container:${_version}" + +docker build -t $_tag . +docker push $_tag From d075af2b674176ecace3613a1e5d6fdc0d0e7671 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 19 Mar 2019 13:14:32 +0100 Subject: [PATCH 116/194] adding story and fixing tests --- .../ThresholdsEditor.story.tsx | 16 + .../ThresholdsEditor.test.tsx | 31 +- .../ThresholdsEditor/ThresholdsEditor.tsx | 4 +- .../ThresholdsEditor.test.tsx.snap | 446 +++++++++++++++++- 4 files changed, 477 insertions(+), 20 deletions(-) create mode 100644 packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.story.tsx diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.story.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.story.tsx new file mode 100644 index 00000000000..8d6112130e7 --- /dev/null +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.story.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; + +import { ThresholdsEditor } from './ThresholdsEditor'; + +const ThresholdsEditorStories = storiesOf('UI/ThresholdsEditor', module); +const thresholds = [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 50, color: 'red' }]; + +ThresholdsEditorStories.add('default', () => { + return ; +}); + +ThresholdsEditorStories.add('with thresholds', () => { + return ; +}); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 38cd8e5c763..db494053d6e 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -1,6 +1,7 @@ import React, { ChangeEvent } from 'react'; import { mount } from 'enzyme'; import { ThresholdsEditor, Props } from './ThresholdsEditor'; +import { colors } from '../../utils'; const setup = (propOverrides?: Partial) => { const props: Props = { @@ -31,7 +32,7 @@ describe('Initialization', () => { it('should add a base threshold if missing', () => { const { instance } = setup(); - expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); + expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]); }); }); @@ -41,7 +42,7 @@ describe('Add threshold', () => { instance.onAddThreshold(0); - expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); + expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]); }); it('should add threshold', () => { @@ -50,41 +51,41 @@ describe('Add threshold', () => { instance.onAddThreshold(1); expect(instance.state.thresholds).toEqual([ - { index: 0, value: -Infinity, color: '#7EB26D' }, - { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: colors[0] }, + { index: 1, value: 50, color: colors[2] }, ]); }); it('should add another threshold above a first', () => { const { instance } = setup({ - thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }], + thresholds: [{ index: 0, value: -Infinity, color: colors[0] }, { index: 1, value: 50, color: colors[2] }], }); instance.onAddThreshold(2); expect(instance.state.thresholds).toEqual([ - { index: 0, value: -Infinity, color: '#7EB26D' }, - { index: 1, value: 50, color: '#EAB839' }, - { index: 2, value: 75, color: '#6ED0E0' }, + { index: 0, value: -Infinity, color: colors[0] }, + { index: 1, value: 50, color: colors[2] }, + { index: 2, value: 75, color: colors[3] }, ]); }); it('should add another threshold between first and second index', () => { const { instance } = setup({ thresholds: [ - { index: 0, value: -Infinity, color: '#7EB26D' }, - { index: 1, value: 50, color: '#EAB839' }, - { index: 2, value: 75, color: '#6ED0E0' }, + { index: 0, value: -Infinity, color: colors[0] }, + { index: 1, value: 50, color: colors[2] }, + { index: 2, value: 75, color: colors[3] }, ], }); instance.onAddThreshold(2); expect(instance.state.thresholds).toEqual([ - { index: 0, value: -Infinity, color: '#7EB26D' }, - { index: 1, value: 50, color: '#EAB839' }, - { index: 2, value: 62.5, color: '#EF843C' }, - { index: 3, value: 75, color: '#6ED0E0' }, + { index: 0, value: -Infinity, color: colors[0] }, + { index: 1, value: 50, color: colors[2] }, + { index: 2, value: 62.5, color: colors[4] }, + { index: 3, value: 75, color: colors[3] }, ]); }); }); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 3361e1bee46..adacf393a09 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -3,8 +3,8 @@ import { Threshold } from '../../types'; import { ColorPicker } from '..'; import { PanelOptionsGroup } from '..'; import { colors } from '../../utils'; -import { ThemeContext } from '../../themes/ThemeContext'; -import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; +import { ThemeContext } from '../../themes'; +import { getColorFromHexRgbOrName } from '../../utils'; export interface Props { thresholds: Threshold[]; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap index b0dc025090b..bd0ab03bf51 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap +++ b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap @@ -1,7 +1,447 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render with base threshold 1`] = ` - - - + + +
+
+ + Thresholds + +
+
+
+
+
+ +
+
+
+
+ +
+
+ + + + } + hideAfter={300} + > + +
+
+
+
+
+ + + + +
+
+
+ +
+
+
+
+
+
+
+ + `; From 9f6b793563c9f6fee7a1fc74eda86309b25a2773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 19 Mar 2019 13:41:47 +0100 Subject: [PATCH 117/194] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f09ead1fe3..83d58bb81a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,16 @@ * **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619) * **Heatmap**: legend shows wrong colors for small values [#14019](https://github.com/grafana/grafana/issues/14019) +# 6.0.2 (unreleased) + +### Bug Fixes +* **Alerting**: Fixed issue with AlertList panel links resulting in panel not found errors. [#15975](https://github.com/grafana/grafana/pull/15975), [@torkelo](https://github.com/torkelo) +* **Dashboard**: Improved error handling when rendering dashboard panels. [#15970](https://github.com/grafana/grafana/pull/15970), [@torkelo](https://github.com/torkelo) +* **LDAP**: Fix allow anonymous server bind for ldap search. [#15872](https://github.com/grafana/grafana/pull/15872), [@marefr](https://github.com/marefr) +* **Discord**: Fix discord notifier so it doesn't crash when there are no image generated. [#15833](https://github.com/grafana/grafana/pull/15833), [@marefr](https://github.com/marefr) +* **Panel Edit**: Prevent search in VizPicker from stealing focus. [#15802](https://github.com/grafana/grafana/pull/15802), [@peterholmberg](https://github.com/peterholmberg) +* **Datasource admin**: Fixed url of back button in datasource edit page, when root_url configured. [#15759](https://github.com/grafana/grafana/pull/15759), [@dprokop](https://github.com/dprokop) + # 6.0.1 (2019-03-06) ### Bug Fixes From abbb7b81c760a07c0571823c1db7d55550ed9330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Mar 2019 13:42:25 +0100 Subject: [PATCH 118/194] fix(ci): frontend tests was accidentially commented out --- scripts/circle-test-frontend.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/circle-test-frontend.sh b/scripts/circle-test-frontend.sh index 9d945a03b7f..423dee84954 100755 --- a/scripts/circle-test-frontend.sh +++ b/scripts/circle-test-frontend.sh @@ -14,7 +14,7 @@ function exit_if_fail { start=$(date +%s) exit_if_fail npm run prettier:check -# exit_if_fail npm run test +exit_if_fail npm run test end=$(date +%s) seconds=$((end - start)) From e294252e926232b93290d07c4b851622169942c1 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 5 Mar 2019 15:07:09 +0100 Subject: [PATCH 119/194] dashboards: user automatically becomes admin for created dashboards --- pkg/services/dashboards/dashboard_service.go | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index f8df6763994..424980c1a86 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -238,6 +238,49 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da return nil, err } + // TODO: check if dashboard exists already. could have id set but not exist + if dto.Dashboard.Id == 0 && dto.Dashboard.Uid == "" { + rtEditor := models.ROLE_EDITOR + rtViewer := models.ROLE_VIEWER + + items := []*models.DashboardAcl{ + { + OrgId: dr.orgId, + DashboardId: cmd.Result.Id, + UserId: cmd.Result.CreatedBy, + Permission: models.PERMISSION_ADMIN, + Created: time.Now(), + Updated: time.Now(), + }, + { + OrgId: dr.orgId, + DashboardId: cmd.Result.Id, + Role: &rtEditor, + Permission: models.PERMISSION_EDIT, + Created: time.Now(), + Updated: time.Now(), + }, + { + OrgId: dr.orgId, + DashboardId: cmd.Result.Id, + Role: &rtViewer, + Permission: models.PERMISSION_VIEW, + Created: time.Now(), + Updated: time.Now(), + }, + } + + aclCmd := &models.UpdateDashboardAclCommand{ + DashboardId: cmd.Result.Id, + Items: items, + } + + if err = bus.Dispatch(aclCmd); err != nil { + return cmd.Result, err + } + + } + return cmd.Result, nil } From e174f7c20bd26fba79442a480528c54bf6060716 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 5 Mar 2019 15:25:02 +0100 Subject: [PATCH 120/194] folders: admin for created folders --- pkg/services/dashboards/folder_service.go | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/folder_service.go index b521b0e5213..917852f4781 100644 --- a/pkg/services/dashboards/folder_service.go +++ b/pkg/services/dashboards/folder_service.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/search" + "time" ) // FolderService service for operating on folders @@ -114,6 +115,45 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er return toFolderError(err) } + rtEditor := models.ROLE_EDITOR + rtViewer := models.ROLE_VIEWER + + items := []*models.DashboardAcl{ + { + OrgId: dr.orgId, + DashboardId: saveDashboardCmd.Result.Id, + UserId: saveDashboardCmd.Result.CreatedBy, + Permission: models.PERMISSION_ADMIN, + Created: time.Now(), + Updated: time.Now(), + }, + { + OrgId: dr.orgId, + DashboardId: saveDashboardCmd.Result.Id, + Role: &rtEditor, + Permission: models.PERMISSION_EDIT, + Created: time.Now(), + Updated: time.Now(), + }, + { + OrgId: dr.orgId, + DashboardId: saveDashboardCmd.Result.Id, + Role: &rtViewer, + Permission: models.PERMISSION_VIEW, + Created: time.Now(), + Updated: time.Now(), + }, + } + + aclCmd := &models.UpdateDashboardAclCommand{ + DashboardId: saveDashboardCmd.Result.Id, + Items: items, + } + + if err = bus.Dispatch(aclCmd); err != nil { + return err + } + query := models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id} dashFolder, err = getFolder(query) if err != nil { From c8c004095cc5707761560bfc4163a056ddb5d96a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 5 Mar 2019 16:44:41 +0100 Subject: [PATCH 121/194] permissions: broken out func for making creator admin. --- pkg/api/api.go | 2 +- pkg/api/dashboard.go | 11 +++- pkg/api/folder.go | 2 +- pkg/services/dashboards/acl_service.go | 62 ++++++++++++++++++++ pkg/services/dashboards/dashboard_service.go | 43 -------------- 5 files changed, 74 insertions(+), 46 deletions(-) create mode 100644 pkg/services/dashboards/acl_service.go diff --git a/pkg/api/api.go b/pkg/api/api.go index f3dc35b6b06..b5214f93d35 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -265,7 +265,7 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) { folderRoute.Get("/", Wrap(GetFolders)) folderRoute.Get("/id/:id", Wrap(GetFolderByID)) - folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(CreateFolder)) + folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(hs.CreateFolder)) folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { folderUidRoute.Get("/", Wrap(GetFolderByUID)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 07c4f75778d..016146a5c61 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -213,7 +213,8 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) dash := cmd.GetDashboardModel() - if dash.Id == 0 && dash.Uid == "" { + newDashboard := dash.Id == 0 && dash.Uid == "" + if newDashboard { limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard") if err != nil { return Error(500, "failed to get quota", err) @@ -276,6 +277,14 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) return Error(500, "Failed to save dashboard", err) } + if hs.Cfg.EditorsCanOwn && newDashboard { + aclService := dashboards.NewAclService() + err := aclService.MakeUserAdmin(cmd.OrgId, cmd.UserId, dashboard.Id) + if err != nil { + hs.log.Error("Could not make user admin", "error", err) + } + } + c.TimeRequest(metrics.M_Api_Dashboard_Save) return JSON(200, util.DynMap{ "status": "success", diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 0e08343b556..4e106dc6452 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -54,7 +54,7 @@ func GetFolderByID(c *m.ReqContext) Response { return JSON(200, toFolderDto(g, folder)) } -func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response { +func (hs *HTTPServer) CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) err := s.CreateFolder(&cmd) if err != nil { diff --git a/pkg/services/dashboards/acl_service.go b/pkg/services/dashboards/acl_service.go new file mode 100644 index 00000000000..79b55470093 --- /dev/null +++ b/pkg/services/dashboards/acl_service.go @@ -0,0 +1,62 @@ +package dashboards + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" + "time" +) + +// NewService factory for creating a new dashboard service +var NewAclService = func() *AclService { + return &AclService{ + log: log.New("dashboard-acl-service"), + } +} + +type AclService struct { + log log.Logger +} + +func (as *AclService) MakeUserAdmin(orgId int64, userId int64, dashboardId int64) error { + rtEditor := models.ROLE_EDITOR + rtViewer := models.ROLE_VIEWER + + items := []*models.DashboardAcl{ + { + OrgId: orgId, + DashboardId: dashboardId, + UserId: userId, + Permission: models.PERMISSION_ADMIN, + Created: time.Now(), + Updated: time.Now(), + }, + { + OrgId: orgId, + DashboardId: dashboardId, + Role: &rtEditor, + Permission: models.PERMISSION_EDIT, + Created: time.Now(), + Updated: time.Now(), + }, + { + OrgId: orgId, + DashboardId: dashboardId, + Role: &rtViewer, + Permission: models.PERMISSION_VIEW, + Created: time.Now(), + Updated: time.Now(), + }, + } + + aclCmd := &models.UpdateDashboardAclCommand{ + DashboardId: dashboardId, + Items: items, + } + + if err := bus.Dispatch(aclCmd); err != nil { + return err + } + + return nil +} diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 424980c1a86..f8df6763994 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -238,49 +238,6 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da return nil, err } - // TODO: check if dashboard exists already. could have id set but not exist - if dto.Dashboard.Id == 0 && dto.Dashboard.Uid == "" { - rtEditor := models.ROLE_EDITOR - rtViewer := models.ROLE_VIEWER - - items := []*models.DashboardAcl{ - { - OrgId: dr.orgId, - DashboardId: cmd.Result.Id, - UserId: cmd.Result.CreatedBy, - Permission: models.PERMISSION_ADMIN, - Created: time.Now(), - Updated: time.Now(), - }, - { - OrgId: dr.orgId, - DashboardId: cmd.Result.Id, - Role: &rtEditor, - Permission: models.PERMISSION_EDIT, - Created: time.Now(), - Updated: time.Now(), - }, - { - OrgId: dr.orgId, - DashboardId: cmd.Result.Id, - Role: &rtViewer, - Permission: models.PERMISSION_VIEW, - Created: time.Now(), - Updated: time.Now(), - }, - } - - aclCmd := &models.UpdateDashboardAclCommand{ - DashboardId: cmd.Result.Id, - Items: items, - } - - if err = bus.Dispatch(aclCmd); err != nil { - return cmd.Result, err - } - - } - return cmd.Result, nil } From da3dcd19184dacf4533b99d8532c6ea217378d25 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 5 Mar 2019 16:53:16 +0100 Subject: [PATCH 122/194] folder: uses service to make user admin of created folder. --- pkg/api/folder.go | 5 +++ pkg/services/dashboards/folder_service.go | 40 ----------------------- 2 files changed, 5 insertions(+), 40 deletions(-) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 4e106dc6452..4e66439219d 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -61,6 +61,11 @@ func (hs *HTTPServer) CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) R return toFolderError(err) } + if hs.Cfg.EditorsCanOwn { + aclService := dashboards.NewAclService() + aclService.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, cmd.Result.Id) + } + g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser) return JSON(200, toFolderDto(g, cmd.Result)) } diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/folder_service.go index 917852f4781..b521b0e5213 100644 --- a/pkg/services/dashboards/folder_service.go +++ b/pkg/services/dashboards/folder_service.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/search" - "time" ) // FolderService service for operating on folders @@ -115,45 +114,6 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er return toFolderError(err) } - rtEditor := models.ROLE_EDITOR - rtViewer := models.ROLE_VIEWER - - items := []*models.DashboardAcl{ - { - OrgId: dr.orgId, - DashboardId: saveDashboardCmd.Result.Id, - UserId: saveDashboardCmd.Result.CreatedBy, - Permission: models.PERMISSION_ADMIN, - Created: time.Now(), - Updated: time.Now(), - }, - { - OrgId: dr.orgId, - DashboardId: saveDashboardCmd.Result.Id, - Role: &rtEditor, - Permission: models.PERMISSION_EDIT, - Created: time.Now(), - Updated: time.Now(), - }, - { - OrgId: dr.orgId, - DashboardId: saveDashboardCmd.Result.Id, - Role: &rtViewer, - Permission: models.PERMISSION_VIEW, - Created: time.Now(), - Updated: time.Now(), - }, - } - - aclCmd := &models.UpdateDashboardAclCommand{ - DashboardId: saveDashboardCmd.Result.Id, - Items: items, - } - - if err = bus.Dispatch(aclCmd); err != nil { - return err - } - query := models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id} dashFolder, err = getFolder(query) if err != nil { From 124fb743e8d9edfdaf58559e1925dc4a6dd13489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 6 Mar 2019 08:09:34 +0100 Subject: [PATCH 123/194] teams: make test cases pass again --- pkg/api/dashboard_test.go | 4 ++++ pkg/api/folder_test.go | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 923bf57ce8a..d58e2246eec 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -972,8 +972,12 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() + cfg := setting.NewCfg() + cfg.EditorsCanOwn = false + hs := HTTPServer{ Bus: bus.GetBus(), + Cfg: cfg, } sc := setupScenarioContext(url) diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 880de338c8f..914acf5797e 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" ) @@ -141,12 +142,20 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() + cfg := setting.NewCfg() + cfg.EditorsCanOwn = false + + hs := HTTPServer{ + Bus: bus.GetBus(), + Cfg: cfg, + } + sc := setupScenarioContext(url) sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID} - return CreateFolder(c, cmd) + return hs.CreateFolder(c, cmd) }) origNewFolderService := dashboards.NewFolderService From efbd93f824ff9c83bed73dd30150dd15c95f3546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 6 Mar 2019 08:40:42 +0100 Subject: [PATCH 124/194] teams: show teams and plugins for editors that can own --- pkg/api/folder_test.go | 2 +- pkg/api/index.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 914acf5797e..d5e4ee418cd 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -143,7 +143,7 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa defer bus.ClearBusHandlers() cfg := setting.NewCfg() - cfg.EditorsCanOwn = false + cfg.EditorsCanOwn = true hs := HTTPServer{ Bus: bus.GetBus(), diff --git a/pkg/api/index.go b/pkg/api/index.go index 904a885b171..88c4b7e929d 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -327,6 +327,34 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er }) } + if c.OrgRole == m.ROLE_EDITOR && hs.Cfg.EditorsCanOwn { + cfgNode := &dtos.NavLink{ + Id: "cfg", + Text: "Configuration", + SubTitle: "Organization: " + c.OrgName, + Icon: "gicon gicon-cog", + Url: setting.AppSubUrl + "/org/teams", + Children: []*dtos.NavLink{ + { + Text: "Teams", + Id: "teams", + Description: "Manage org groups", + Icon: "gicon gicon-team", + Url: setting.AppSubUrl + "/org/teams", + }, + { + Text: "Plugins", + Id: "plugins", + Description: "View and configure plugins", + Icon: "gicon gicon-plugins", + Url: setting.AppSubUrl + "/plugins", + }, + }, + } + + data.NavTree = append(data.NavTree, cfgNode) + } + data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Help", SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit), From 22e098b83019bb048212a704a406e84316f499c0 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 6 Mar 2019 10:27:38 +0100 Subject: [PATCH 125/194] teams: editors can work with teams. --- pkg/api/api.go | 7 ++++--- pkg/middleware/auth.go | 17 +++++++++++++++++ public/app/routes/routes.ts | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index b5214f93d35..50700108394 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -14,6 +14,7 @@ func (hs *HTTPServer) registerRoutes() { reqGrafanaAdmin := middleware.ReqGrafanaAdmin reqEditorRole := middleware.ReqEditorRole reqOrgAdmin := middleware.ReqOrgAdmin + reqAdminOrEditorCanAdmin := middleware.EditorCanAdmin(hs.Cfg.EditorsCanOwn) redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL() quota := middleware.Quota(hs.QuotaService) @@ -41,8 +42,8 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/org/users", reqOrgAdmin, hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index) r.Get("/org/users/invite", reqOrgAdmin, hs.Index) - r.Get("/org/teams", reqOrgAdmin, hs.Index) - r.Get("/org/teams/*", reqOrgAdmin, hs.Index) + r.Get("/org/teams", reqAdminOrEditorCanAdmin, hs.Index) + r.Get("/org/teams/*", reqAdminOrEditorCanAdmin, hs.Index) r.Get("/org/apikeys/", reqOrgAdmin, hs.Index) r.Get("/dashboard/import/", reqSignedIn, hs.Index) r.Get("/configuration", reqGrafanaAdmin, hs.Index) @@ -161,7 +162,7 @@ func (hs *HTTPServer) registerRoutes() { teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember)) teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences)) teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences)) - }, reqOrgAdmin) + }, reqAdminOrEditorCanAdmin) // team without requirement of user to be org admin apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) { diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index e06409211eb..6bf37e7fd50 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -86,3 +86,20 @@ func Auth(options *AuthOptions) macaron.Handler { } } } + +func EditorCanAdmin(enabled bool) macaron.Handler { + return func(c *m.ReqContext) { + ok := false + if c.OrgRole == m.ROLE_ADMIN { + ok = true + } + + if c.OrgRole == m.ROLE_EDITOR && enabled { + ok = true + } + + if !ok { + accessForbidden(c) + } + } +} diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 442fb5acb0c..06af66d7d5d 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -207,7 +207,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/org/teams/edit/:id/:page?', { template: '', resolve: { - roles: () => ['Admin'], + roles: () => (config.editorsCanOwn ? ['Editor', 'Admin'] : ['Admin']), component: () => TeamPages, }, }) From af4994ba1623482d42b2b79af80b6d9ed01b46b5 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 6 Mar 2019 11:47:18 +0100 Subject: [PATCH 126/194] teams: editor added as admin for created teams. --- pkg/api/api.go | 2 +- pkg/api/team.go | 13 ++++++++++++- pkg/models/team_member.go | 20 +++++++++++--------- pkg/services/sqlstore/migrations/team_mig.go | 6 ++++++ pkg/services/sqlstore/team.go | 13 +++++++------ 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 50700108394..3cbcc8029a3 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -154,7 +154,7 @@ func (hs *HTTPServer) registerRoutes() { // team (admin permission required) apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) { - teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(CreateTeam)) + teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(hs.CreateTeam)) teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(UpdateTeam)) teamsRoute.Delete("/:teamId", Wrap(DeleteTeamByID)) teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers)) diff --git a/pkg/api/team.go b/pkg/api/team.go index 32265e5d018..5c58a0df71c 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -8,7 +8,7 @@ import ( ) // POST /api/teams -func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response { +func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNameTaken { @@ -17,6 +17,17 @@ func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response { return Error(500, "Failed to create Team", err) } + if c.OrgRole == m.ROLE_EDITOR && hs.Cfg.EditorsCanOwn { + addMemberCmd := m.AddTeamMemberCommand{ + UserId: c.SignedInUser.UserId, + OrgId: cmd.OrgId, + TeamId: cmd.Result.Id, + Permission: int64(m.PERMISSION_ADMIN), + } + err := bus.Dispatch(&addMemberCmd) + c.Logger.Error("Could not add creator to team.", "error", err) + } + return JSON(200, &util.DynMap{ "teamId": cmd.Result.Id, "message": "Team created", diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index dd64787f465..01659cb0347 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -12,11 +12,12 @@ var ( // TeamMember model type TeamMember struct { - Id int64 - OrgId int64 - TeamId int64 - UserId int64 - External bool + Id int64 + OrgId int64 + TeamId int64 + UserId int64 + External bool + Permission int64 Created time.Time Updated time.Time @@ -26,10 +27,11 @@ type TeamMember struct { // COMMANDS type AddTeamMemberCommand struct { - UserId int64 `json:"userId" binding:"Required"` - OrgId int64 `json:"-"` - TeamId int64 `json:"-"` - External bool `json:"-"` + UserId int64 `json:"userId" binding:"Required"` + OrgId int64 `json:"-"` + TeamId int64 `json:"-"` + External bool `json:"-"` + Permission int64 `json:"-"` } type RemoveTeamMemberCommand struct { diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go index 34c46ad13cf..1ec27ee926d 100644 --- a/pkg/services/sqlstore/migrations/team_mig.go +++ b/pkg/services/sqlstore/migrations/team_mig.go @@ -54,4 +54,10 @@ func addTeamMigrations(mg *Migrator) { mg.AddMigration("Add column external to team_member table", NewAddColumnMigration(teamMemberV1, &Column{ Name: "external", Type: DB_Bool, Nullable: true, })) + + mg.AddMigration("Add column permission to team_member table", NewAddColumnMigration(teamMemberV1, &Column{ + Name: "permission", + Type: DB_BigInt, + Nullable: true, + })) } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 83593e6f2d7..c11a2d077ed 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -240,12 +240,13 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error { } entity := m.TeamMember{ - OrgId: cmd.OrgId, - TeamId: cmd.TeamId, - UserId: cmd.UserId, - External: cmd.External, - Created: time.Now(), - Updated: time.Now(), + OrgId: cmd.OrgId, + TeamId: cmd.TeamId, + UserId: cmd.UserId, + External: cmd.External, + Created: time.Now(), + Updated: time.Now(), + Permission: cmd.Permission, } _, err := sess.Insert(&entity) From 7888457aaee0aa233bd2696b75436d6fc686ff06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 6 Mar 2019 12:27:18 +0100 Subject: [PATCH 127/194] teams: basic ui for permission in team members view --- pkg/models/team_member.go | 17 +- pkg/services/sqlstore/team.go | 2 +- public/app/features/teams/TeamMembers.tsx | 18 +- .../app/features/teams/__mocks__/teamMocks.ts | 2 + .../__snapshots__/TeamMembers.test.tsx.snap | 429 ++++++++++++++++++ public/app/types/acl.ts | 20 + public/app/types/teams.ts | 1 + 7 files changed, 478 insertions(+), 11 deletions(-) diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 01659cb0347..813455d3d2b 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -55,12 +55,13 @@ type GetTeamMembersQuery struct { // Projections and DTOs type TeamMemberDTO struct { - OrgId int64 `json:"orgId"` - TeamId int64 `json:"teamId"` - UserId int64 `json:"userId"` - External bool `json:"-"` - Email string `json:"email"` - Login string `json:"login"` - AvatarUrl string `json:"avatarUrl"` - Labels []string `json:"labels"` + OrgId int64 `json:"orgId"` + TeamId int64 `json:"teamId"` + UserId int64 `json:"userId"` + External bool `json:"-"` + Email string `json:"email"` + Login string `json:"login"` + AvatarUrl string `json:"avatarUrl"` + Labels []string `json:"labels"` + Permission int64 `json:"permission"` } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index c11a2d077ed..546e0231706 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -294,7 +294,7 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error { if query.External { sess.Where("team_member.external=?", dialect.BooleanStr(true)) } - sess.Cols("team_member.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login", "team_member.external") + sess.Cols("team_member.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login", "team_member.external", "team_member.permission") sess.Asc("user.login", "user.email") err := sess.Find(&query.Result) diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index e5c3aaafef0..341d9311b53 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -2,9 +2,9 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker } from 'app/core/components/Select/UserPicker'; -import { DeleteButton } from '@grafana/ui'; +import { DeleteButton, Select } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; -import { TeamMember, User } from 'app/types'; +import { TeamMember, User, teamsPermissionLevels } from 'app/types'; import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; @@ -70,6 +70,7 @@ export class TeamMembers extends PureComponent { } renderMember(member: TeamMember, syncEnabled: boolean) { + const currentPermissionLevel = teamsPermissionLevels.find(dp => dp.value === member.permission); return ( @@ -77,6 +78,18 @@ export class TeamMembers extends PureComponent { {member.login} {member.email} + +
+ +
+ + @@ -205,6 +253,48 @@ exports[`Render should render team members 1`] = ` test@test.com + +
+ +
+ + @@ -255,6 +387,48 @@ exports[`Render should render team members 1`] = ` test@test.com + +
+ +
+ + @@ -363,6 +579,9 @@ exports[`Render should render team members when sync enabled 1`] = ` Email + + Permission + test@test.com + +
+ +
+ + test@test.com + +
+ +
+ + test@test.com + +
+ {}} - className="gf-form-select-box__control--menu-right" - value={currentPermissionLevel} - isDisabled={true} - /> -
- {' '} + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ {}} + onChange={item => this.onPermissionChange(item, member)} className="gf-form-select-box__control--menu-right" - value={currentPermissionLevel} - isDisabled={true} + value={teamsPermissionLevels.find(dp => dp.value === member.permission)} />
@@ -176,6 +188,7 @@ const mapDispatchToProps = { addTeamMember, removeTeamMember, setSearchMemberQuery, + updateTeamMember, }; export default connect( diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap index fc168457334..01d7b40ec61 100644 --- a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -206,7 +206,7 @@ exports[`Render should render team members 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} @@ -276,7 +276,7 @@ exports[`Render should render team members 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} @@ -346,7 +346,7 @@ exports[`Render should render team members 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} @@ -416,7 +416,7 @@ exports[`Render should render team members 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} @@ -486,7 +486,7 @@ exports[`Render should render team members 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} @@ -649,7 +649,7 @@ exports[`Render should render team members when sync enabled 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} @@ -735,7 +735,7 @@ exports[`Render should render team members when sync enabled 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} @@ -821,7 +821,7 @@ exports[`Render should render team members when sync enabled 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} @@ -907,7 +907,7 @@ exports[`Render should render team members when sync enabled 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} @@ -993,7 +993,7 @@ exports[`Render should render team members when sync enabled 1`] = ` backspaceRemovesValue={true} className="gf-form-select-box__control--menu-right" isClearable={false} - isDisabled={true} + isDisabled={false} isLoading={false} isMulti={false} isSearchable={false} diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index d948dc1c5a3..e2582839233 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -160,3 +160,12 @@ export function deleteTeam(id: number): ThunkResult { dispatch(loadTeams()); }; } + +export function updateTeamMember(member: TeamMember): ThunkResult { + return async dispatch => { + await getBackendSrv().put(`/api/teams/${member.teamId}/members/${member.userId}`, { + permission: member.permission, + }); + dispatch(loadTeamMembers()); + }; +} From 074ebf0f482e9f1a5e445c19fe947d6e8e2bd989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 8 Mar 2019 11:56:48 +0100 Subject: [PATCH 132/194] teams: only write error message if error --- pkg/api/team.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/api/team.go b/pkg/api/team.go index 5c58a0df71c..da72bda472b 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -24,8 +24,10 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo TeamId: cmd.Result.Id, Permission: int64(m.PERMISSION_ADMIN), } - err := bus.Dispatch(&addMemberCmd) - c.Logger.Error("Could not add creator to team.", "error", err) + + if err := bus.Dispatch(&addMemberCmd); err != nil { + c.Logger.Error("Could not add creator to team.", "error", err) + } } return JSON(200, &util.DynMap{ From 3c74ac304490aa3ef46f045fd7d61d718253c5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 8 Mar 2019 12:25:10 +0100 Subject: [PATCH 133/194] teams: update only the selected user --- pkg/services/sqlstore/team.go | 2 +- public/app/types/acl.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 4822af7009c..0a5383d993f 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -272,7 +272,7 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { } member.Permission = cmd.Permission - _, err = sess.Update(member) + _, err = sess.Where("org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId).Update(member) return err }) diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index 8134ddb1749..12016732222 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -100,7 +100,7 @@ export const dashboardPermissionLevels: DashboardPermissionInfo[] = [ ]; export enum TeamPermissionLevel { - Member = 0, + Member = 1, Admin = 4, } From 1315a67022c5e117e36cfb14cd59cd0586c4c079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 8 Mar 2019 13:10:03 +0100 Subject: [PATCH 134/194] teams: make sure we use TeamPermissionLevel enum --- .../app/features/teams/__mocks__/teamMocks.ts | 6 +-- .../__snapshots__/TeamMembers.test.tsx.snap | 40 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 6d4b5ea3aad..3f0830eda16 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -1,4 +1,4 @@ -import { Team, TeamGroup, TeamMember } from 'app/types'; +import { Team, TeamGroup, TeamMember, TeamPermissionLevel } from 'app/types'; export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { const teams: Team[] = []; @@ -36,7 +36,7 @@ export const getMockTeamMembers = (amount: number): TeamMember[] => { email: 'test@test.com', login: `testUser-${i}`, labels: ['label 1', 'label 2'], - permission: 0, + permission: TeamPermissionLevel.Member, }); } @@ -51,7 +51,7 @@ export const getMockTeamMember = (): TeamMember => { email: 'test@test.com', login: 'testUser', labels: [], - permission: 0, + permission: TeamPermissionLevel.Member, }; }; diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap index 01d7b40ec61..c356727ebeb 100644 --- a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -218,7 +218,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -231,7 +231,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} @@ -288,7 +288,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -301,7 +301,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} @@ -358,7 +358,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -371,7 +371,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} @@ -428,7 +428,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -441,7 +441,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} @@ -498,7 +498,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -511,7 +511,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} @@ -661,7 +661,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -674,7 +674,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} @@ -747,7 +747,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -760,7 +760,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} @@ -833,7 +833,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -846,7 +846,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} @@ -919,7 +919,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -932,7 +932,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} @@ -1005,7 +1005,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, }, Object { "description": "Can add/remove permissions and delete team.", @@ -1018,7 +1018,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 0, + "value": 1, } } width={null} From 3c46b786d2a58f23ebe9ae6fd8b846ffbcc21cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 8 Mar 2019 13:53:42 +0100 Subject: [PATCH 135/194] teams: change back to permissionlevel for Member to 0 --- pkg/services/sqlstore/team.go | 2 +- .../__snapshots__/TeamMembers.test.tsx.snap | 40 +++++++++---------- public/app/types/acl.ts | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 0a5383d993f..f7f7d7fc2cb 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -272,7 +272,7 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { } member.Permission = cmd.Permission - _, err = sess.Where("org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId).Update(member) + _, err = sess.Cols("permission").Where("org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId).Update(member) return err }) diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap index c356727ebeb..01d7b40ec61 100644 --- a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -218,7 +218,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -231,7 +231,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} @@ -288,7 +288,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -301,7 +301,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} @@ -358,7 +358,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -371,7 +371,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} @@ -428,7 +428,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -441,7 +441,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} @@ -498,7 +498,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -511,7 +511,7 @@ exports[`Render should render team members 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} @@ -661,7 +661,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -674,7 +674,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} @@ -747,7 +747,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -760,7 +760,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} @@ -833,7 +833,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -846,7 +846,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} @@ -919,7 +919,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -932,7 +932,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} @@ -1005,7 +1005,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, }, Object { "description": "Can add/remove permissions and delete team.", @@ -1018,7 +1018,7 @@ exports[`Render should render team members when sync enabled 1`] = ` Object { "description": "Is team member", "label": "Member", - "value": 1, + "value": 0, } } width={null} diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index 12016732222..8134ddb1749 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -100,7 +100,7 @@ export const dashboardPermissionLevels: DashboardPermissionInfo[] = [ ]; export enum TeamPermissionLevel { - Member = 1, + Member = 0, Admin = 4, } From 5adde259d307ac36277b419475712503cf25d63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 8 Mar 2019 14:37:21 +0100 Subject: [PATCH 136/194] teams: team update test --- pkg/api/team.go | 3 ++- pkg/services/teams/team.go | 10 ++++++++ pkg/services/teams/teams_test.go | 42 ++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 pkg/services/teams/team.go create mode 100644 pkg/services/teams/teams_test.go diff --git a/pkg/api/team.go b/pkg/api/team.go index da72bda472b..3d357fa9763 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -4,6 +4,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/teams" "github.com/grafana/grafana/pkg/util" ) @@ -40,7 +41,7 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":teamId") - if err := bus.Dispatch(&cmd); err != nil { + if err := teams.UpdateTeam(c.SignedInUser, &cmd); err != nil { if err == m.ErrTeamNameTaken { return Error(400, "Team name taken", err) } diff --git a/pkg/services/teams/team.go b/pkg/services/teams/team.go new file mode 100644 index 00000000000..4bd4b78d587 --- /dev/null +++ b/pkg/services/teams/team.go @@ -0,0 +1,10 @@ +package teams + +import ( + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func UpdateTeam(user m.SignedInUser, cmd *m.UpdateTeamCommand) error { + return bus.Dispatch(cmd) +} diff --git a/pkg/services/teams/teams_test.go b/pkg/services/teams/teams_test.go new file mode 100644 index 00000000000..aaa19440bb4 --- /dev/null +++ b/pkg/services/teams/teams_test.go @@ -0,0 +1,42 @@ +package teams + +import ( + . "github.com/smartystreets/goconvey/convey" + m "github.com/grafana/grafana/pkg/models" +) + + +func TestUpdateTeam(t *testing.T) { + Convey("Updating a team as an editor", t, func() { + Convey("Given an editor and a team he isn't a member of", func() { + + UpdateTeam(editor, m.UpdateTeamCommand{ + Id: 0, + Name: "", + Email: "", + OrgId: 0, + }) + }) + + // the editor should not be able to update the team if they aren't members of it + + fakeDash := m.NewDashboard("Child dash") + fakeDash.Id = 1 + fakeDash.FolderId = 1 + fakeDash.HasAcl = false + + bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error { + dashboards := []*m.Dashboard{fakeDash} + query.Result = dashboards + return nil + }) + + var getDashboardQueries []*m.GetDashboardQuery + + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = fakeDash + getDashboardQueries = append(getDashboardQueries, query) + return nil + }) + + bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { From 90e9fda90c9904e273c30ed6563eb4e15de915e6 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 8 Mar 2019 15:46:15 +0100 Subject: [PATCH 137/194] teams: start of team update guardian for editors --- pkg/models/team.go | 8 +- pkg/services/teams/team.go | 35 ++++++ pkg/services/teams/teams_test.go | 176 +++++++++++++++++++++++++------ 3 files changed, 183 insertions(+), 36 deletions(-) diff --git a/pkg/models/team.go b/pkg/models/team.go index 61285db3a5f..bd0d803d9d3 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -7,9 +7,11 @@ import ( // Typed errors var ( - ErrTeamNotFound = errors.New("Team not found") - ErrTeamNameTaken = errors.New("Team name is taken") - ErrTeamMemberNotFound = errors.New("Team member not found") + ErrTeamNotFound = errors.New("Team not found") + ErrTeamNameTaken = errors.New("Team name is taken") + ErrTeamMemberNotFound = errors.New("Team member not found") + ErrNotAllowedToUpdateTeam = errors.New("User not allowed to update team") + ErrNotAllowedToUpdateTeamInDifferentOrg = errors.New("User not allowed to update team in another org") ) // Team model diff --git a/pkg/services/teams/team.go b/pkg/services/teams/team.go index 4bd4b78d587..7ff18820b62 100644 --- a/pkg/services/teams/team.go +++ b/pkg/services/teams/team.go @@ -5,6 +5,41 @@ import ( m "github.com/grafana/grafana/pkg/models" ) +func canUpdateTeam(orgId int64, teamId int64, user m.SignedInUser) error { + if user.OrgRole == m.ROLE_ADMIN { + return nil + } + + if user.OrgId != orgId { + return m.ErrNotAllowedToUpdateTeamInDifferentOrg + } + + cmd := m.GetTeamMembersQuery{ + OrgId: orgId, + TeamId: teamId, + UserId: user.UserId, + // TODO: do we need to do something special about external users + // External: false, + } + + if err := bus.Dispatch(&cmd); err != nil { + // TODO: look into how we want to do logging + return err + } + + for _, member := range cmd.Result { + if member.UserId == user.UserId && member.Permission == int64(m.PERMISSION_ADMIN) { + return nil + } + } + + return m.ErrNotAllowedToUpdateTeam +} + func UpdateTeam(user m.SignedInUser, cmd *m.UpdateTeamCommand) error { + if err := canUpdateTeam(cmd.OrgId, cmd.Id, user); err != nil { + return err + } + return bus.Dispatch(cmd) } diff --git a/pkg/services/teams/teams_test.go b/pkg/services/teams/teams_test.go index aaa19440bb4..9dd42e1add5 100644 --- a/pkg/services/teams/teams_test.go +++ b/pkg/services/teams/teams_test.go @@ -1,42 +1,152 @@ package teams import ( - . "github.com/smartystreets/goconvey/convey" + "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/pkg/errors" + . "github.com/smartystreets/goconvey/convey" + "testing" ) - func TestUpdateTeam(t *testing.T) { - Convey("Updating a team as an editor", t, func() { + Convey("Updating a team", t, func() { + bus.ClearBusHandlers() Convey("Given an editor and a team he isn't a member of", func() { - - UpdateTeam(editor, m.UpdateTeamCommand{ - Id: 0, - Name: "", - Email: "", - OrgId: 0, + editor := m.SignedInUser{ + UserId: 1, + OrgId: 1, + OrgRole: m.ROLE_EDITOR, + } + + Convey("Should not be able to update the team", func() { + cmd := m.UpdateTeamCommand{ + Id: 1, + OrgId: editor.OrgId, + } + + bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { + return errors.New("Editor not allowed to update team.") + }) + bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error { + cmd.Result = []*m.TeamMemberDTO{} + return nil + }) + + err := UpdateTeam(editor, &cmd) + + So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) + }) + }) + + Convey("Given an editor and a team he is a member of", func() { + editor := m.SignedInUser{ + UserId: 1, + OrgId: 1, + OrgRole: m.ROLE_EDITOR, + } + + testTeam := m.Team{ + Id: 1, + OrgId: 1, + } + + Convey("Should be able to update the team", func() { + cmd := m.UpdateTeamCommand{ + Id: testTeam.Id, + OrgId: testTeam.OrgId, + } + + teamUpdated := false + + bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { + teamUpdated = true + return nil + }) + + bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error { + cmd.Result = []*m.TeamMemberDTO{{ + OrgId: testTeam.OrgId, + TeamId: testTeam.Id, + UserId: editor.UserId, + Permission: int64(m.PERMISSION_ADMIN), + }} + return nil + }) + + err := UpdateTeam(editor, &cmd) + + So(teamUpdated, ShouldBeTrue) + So(err, ShouldBeNil) + }) + }) + + Convey("Given an editor and a team in another org", func() { + editor := m.SignedInUser{ + UserId: 1, + OrgId: 1, + OrgRole: m.ROLE_EDITOR, + } + + testTeam := m.Team{ + Id: 1, + OrgId: 2, + } + + Convey("Shouldn't be able to update the team", func() { + cmd := m.UpdateTeamCommand{ + Id: testTeam.Id, + OrgId: testTeam.OrgId, + } + + bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { + return errors.New("Can't update a team in a different org.") + }) + bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error { + cmd.Result = []*m.TeamMemberDTO{{ + OrgId: testTeam.OrgId, + TeamId: testTeam.Id, + UserId: editor.UserId, + Permission: int64(m.PERMISSION_ADMIN), + }} + return nil + }) + + err := UpdateTeam(editor, &cmd) + + So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg) + }) + }) + + Convey("Given an org admin and a team", func() { + editor := m.SignedInUser{ + UserId: 1, + OrgId: 1, + OrgRole: m.ROLE_ADMIN, + } + + testTeam := m.Team{ + Id: 1, + OrgId: 1, + } + + Convey("Should be able to update the team", func() { + cmd := m.UpdateTeamCommand{ + Id: testTeam.Id, + OrgId: testTeam.OrgId, + } + + teamUpdated := false + + bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { + teamUpdated = true + return nil + }) + + err := UpdateTeam(editor, &cmd) + + So(teamUpdated, ShouldBeTrue) + So(err, ShouldBeNil) + }) + }) }) - }) - - // the editor should not be able to update the team if they aren't members of it - - fakeDash := m.NewDashboard("Child dash") - fakeDash.Id = 1 - fakeDash.FolderId = 1 - fakeDash.HasAcl = false - - bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error { - dashboards := []*m.Dashboard{fakeDash} - query.Result = dashboards - return nil - }) - - var getDashboardQueries []*m.GetDashboardQuery - - bus.AddHandler("test", func(query *m.GetDashboardQuery) error { - query.Result = fakeDash - getDashboardQueries = append(getDashboardQueries, query) - return nil - }) - - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { +} From 319879cfa8a069f9905031d9fc9dcc8ae5d5f483 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 8 Mar 2019 15:58:32 +0100 Subject: [PATCH 138/194] teams: bugfix, user pointer. --- pkg/services/teams/team.go | 4 ++-- pkg/services/teams/teams_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/services/teams/team.go b/pkg/services/teams/team.go index 7ff18820b62..6adf03b8b21 100644 --- a/pkg/services/teams/team.go +++ b/pkg/services/teams/team.go @@ -5,7 +5,7 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func canUpdateTeam(orgId int64, teamId int64, user m.SignedInUser) error { +func canUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser) error { if user.OrgRole == m.ROLE_ADMIN { return nil } @@ -36,7 +36,7 @@ func canUpdateTeam(orgId int64, teamId int64, user m.SignedInUser) error { return m.ErrNotAllowedToUpdateTeam } -func UpdateTeam(user m.SignedInUser, cmd *m.UpdateTeamCommand) error { +func UpdateTeam(user *m.SignedInUser, cmd *m.UpdateTeamCommand) error { if err := canUpdateTeam(cmd.OrgId, cmd.Id, user); err != nil { return err } diff --git a/pkg/services/teams/teams_test.go b/pkg/services/teams/teams_test.go index 9dd42e1add5..12b773568c7 100644 --- a/pkg/services/teams/teams_test.go +++ b/pkg/services/teams/teams_test.go @@ -32,7 +32,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := UpdateTeam(editor, &cmd) + err := UpdateTeam(&editor, &cmd) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) }) @@ -73,7 +73,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := UpdateTeam(editor, &cmd) + err := UpdateTeam(&editor, &cmd) So(teamUpdated, ShouldBeTrue) So(err, ShouldBeNil) @@ -111,7 +111,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := UpdateTeam(editor, &cmd) + err := UpdateTeam(&editor, &cmd) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg) }) @@ -142,7 +142,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := UpdateTeam(editor, &cmd) + err := UpdateTeam(&editor, &cmd) So(teamUpdated, ShouldBeTrue) So(err, ShouldBeNil) From 3be1d71f1ff1e1ed87e1c9a3896253fa4cdd6df3 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 11:12:52 +0100 Subject: [PATCH 139/194] teams: test refactorings. --- pkg/services/teams/team.go | 1 - pkg/services/teams/teams_test.go | 133 ++++++++++++------------------- 2 files changed, 52 insertions(+), 82 deletions(-) diff --git a/pkg/services/teams/team.go b/pkg/services/teams/team.go index 6adf03b8b21..ae9327699be 100644 --- a/pkg/services/teams/team.go +++ b/pkg/services/teams/team.go @@ -23,7 +23,6 @@ func canUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser) error { } if err := bus.Dispatch(&cmd); err != nil { - // TODO: look into how we want to do logging return err } diff --git a/pkg/services/teams/teams_test.go b/pkg/services/teams/teams_test.go index 12b773568c7..1282eefc611 100644 --- a/pkg/services/teams/teams_test.go +++ b/pkg/services/teams/teams_test.go @@ -11,57 +11,43 @@ import ( func TestUpdateTeam(t *testing.T) { Convey("Updating a team", t, func() { bus.ClearBusHandlers() + + admin := m.SignedInUser{ + UserId: 1, + OrgId: 1, + OrgRole: m.ROLE_ADMIN, + } + editor := m.SignedInUser{ + UserId: 2, + OrgId: 1, + OrgRole: m.ROLE_EDITOR, + } + testTeam := m.Team{ + Id: 1, + OrgId: 1, + } + + updateTeamCmd := m.UpdateTeamCommand{ + Id: testTeam.Id, + OrgId: testTeam.OrgId, + } + Convey("Given an editor and a team he isn't a member of", func() { - editor := m.SignedInUser{ - UserId: 1, - OrgId: 1, - OrgRole: m.ROLE_EDITOR, - } - Convey("Should not be able to update the team", func() { - cmd := m.UpdateTeamCommand{ - Id: 1, - OrgId: editor.OrgId, - } - - bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { - return errors.New("Editor not allowed to update team.") - }) + shouldNotUpdateTeam() bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error { cmd.Result = []*m.TeamMemberDTO{} return nil }) - err := UpdateTeam(&editor, &cmd) - + err := UpdateTeam(&editor, &updateTeamCmd) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) }) }) Convey("Given an editor and a team he is a member of", func() { - editor := m.SignedInUser{ - UserId: 1, - OrgId: 1, - OrgRole: m.ROLE_EDITOR, - } - - testTeam := m.Team{ - Id: 1, - OrgId: 1, - } - Convey("Should be able to update the team", func() { - cmd := m.UpdateTeamCommand{ - Id: testTeam.Id, - OrgId: testTeam.OrgId, - } - - teamUpdated := false - - bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { - teamUpdated = true - return nil - }) + teamUpdatedCallback := updateTeamCalled() bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error { cmd.Result = []*m.TeamMemberDTO{{ @@ -73,38 +59,29 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := UpdateTeam(&editor, &cmd) - - So(teamUpdated, ShouldBeTrue) + err := UpdateTeam(&editor, &updateTeamCmd) + So(teamUpdatedCallback(), ShouldBeTrue) So(err, ShouldBeNil) }) }) Convey("Given an editor and a team in another org", func() { - editor := m.SignedInUser{ - UserId: 1, - OrgId: 1, - OrgRole: m.ROLE_EDITOR, - } - - testTeam := m.Team{ + testTeamOtherOrg := m.Team{ Id: 1, OrgId: 2, } Convey("Shouldn't be able to update the team", func() { cmd := m.UpdateTeamCommand{ - Id: testTeam.Id, - OrgId: testTeam.OrgId, + Id: testTeamOtherOrg.Id, + OrgId: testTeamOtherOrg.OrgId, } - bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { - return errors.New("Can't update a team in a different org.") - }) + shouldNotUpdateTeam() bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error { cmd.Result = []*m.TeamMemberDTO{{ - OrgId: testTeam.OrgId, - TeamId: testTeam.Id, + OrgId: testTeamOtherOrg.OrgId, + TeamId: testTeamOtherOrg.Id, UserId: editor.UserId, Permission: int64(m.PERMISSION_ADMIN), }} @@ -112,41 +89,35 @@ func TestUpdateTeam(t *testing.T) { }) err := UpdateTeam(&editor, &cmd) - So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg) }) }) Convey("Given an org admin and a team", func() { - editor := m.SignedInUser{ - UserId: 1, - OrgId: 1, - OrgRole: m.ROLE_ADMIN, - } - - testTeam := m.Team{ - Id: 1, - OrgId: 1, - } - Convey("Should be able to update the team", func() { - cmd := m.UpdateTeamCommand{ - Id: testTeam.Id, - OrgId: testTeam.OrgId, - } + teamUpdatedCallback := updateTeamCalled() + err := UpdateTeam(&admin, &updateTeamCmd) - teamUpdated := false - - bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { - teamUpdated = true - return nil - }) - - err := UpdateTeam(&editor, &cmd) - - So(teamUpdated, ShouldBeTrue) + So(teamUpdatedCallback(), ShouldBeTrue) So(err, ShouldBeNil) }) }) }) } + +func updateTeamCalled() func() bool { + wasCalled := false + bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { + wasCalled = true + return nil + }) + + return func() bool { return wasCalled } +} + +func shouldNotUpdateTeam() { + bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { + return errors.New("UpdateTeamCommand not expected.") + }) + +} From 0d61f895773fd91f338769700ed70f3968fe528c Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 11:26:01 +0100 Subject: [PATCH 140/194] teams: cleanup. --- pkg/api/team.go | 7 ++++++- pkg/services/teams/team.go | 10 +--------- pkg/services/teams/teams_test.go | 16 +++++++++++----- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/pkg/api/team.go b/pkg/api/team.go index 3d357fa9763..6e62b186f83 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -41,7 +41,12 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":teamId") - if err := teams.UpdateTeam(c.SignedInUser, &cmd); err != nil { + + if err := teams.CanUpdateTeam(cmd.OrgId, cmd.Id, c.SignedInUser); err != nil { + return Error(403, "User not allowed to update team", err) + } + + if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNameTaken { return Error(400, "Team name taken", err) } diff --git a/pkg/services/teams/team.go b/pkg/services/teams/team.go index ae9327699be..9419d649204 100644 --- a/pkg/services/teams/team.go +++ b/pkg/services/teams/team.go @@ -5,7 +5,7 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func canUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser) error { +func CanUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser) error { if user.OrgRole == m.ROLE_ADMIN { return nil } @@ -34,11 +34,3 @@ func canUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser) error { return m.ErrNotAllowedToUpdateTeam } - -func UpdateTeam(user *m.SignedInUser, cmd *m.UpdateTeamCommand) error { - if err := canUpdateTeam(cmd.OrgId, cmd.Id, user); err != nil { - return err - } - - return bus.Dispatch(cmd) -} diff --git a/pkg/services/teams/teams_test.go b/pkg/services/teams/teams_test.go index 1282eefc611..7fac1be6880 100644 --- a/pkg/services/teams/teams_test.go +++ b/pkg/services/teams/teams_test.go @@ -40,12 +40,12 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := UpdateTeam(&editor, &updateTeamCmd) + err := CanUpdateTeam(&editor, &updateTeamCmd) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) }) }) - Convey("Given an editor and a team he is a member of", func() { + Convey("Given an editor and a team he is an admin in", func() { Convey("Should be able to update the team", func() { teamUpdatedCallback := updateTeamCalled() @@ -59,7 +59,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := UpdateTeam(&editor, &updateTeamCmd) + err := CanUpdateTeam(&editor, &updateTeamCmd) So(teamUpdatedCallback(), ShouldBeTrue) So(err, ShouldBeNil) }) @@ -88,7 +88,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := UpdateTeam(&editor, &cmd) + err := CanUpdateTeam(&editor, &cmd) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg) }) }) @@ -96,12 +96,18 @@ func TestUpdateTeam(t *testing.T) { Convey("Given an org admin and a team", func() { Convey("Should be able to update the team", func() { teamUpdatedCallback := updateTeamCalled() - err := UpdateTeam(&admin, &updateTeamCmd) + err := CanUpdateTeam(&admin, &updateTeamCmd) So(teamUpdatedCallback(), ShouldBeTrue) So(err, ShouldBeNil) }) }) + Convey("Given that the editorsCanOwn feature toggle is disabled", func() { + + Convey("Given an editor and a team he is an admin", func() { + + }) + }) }) } From d668550aa2127254d83166c7da90053b9d85728a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 11:45:06 +0100 Subject: [PATCH 141/194] teams: added feature toggle and refactor tests --- pkg/api/team.go | 4 +-- pkg/services/teams/team.go | 6 +++- pkg/services/teams/teams_test.go | 50 +++++--------------------------- 3 files changed, 15 insertions(+), 45 deletions(-) diff --git a/pkg/api/team.go b/pkg/api/team.go index 6e62b186f83..e9239acffa3 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -38,11 +38,11 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo } // PUT /api/teams/:teamId -func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { +func (hs *HTTPServer) UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":teamId") - if err := teams.CanUpdateTeam(cmd.OrgId, cmd.Id, c.SignedInUser); err != nil { + if err := teams.CanUpdateTeam(cmd.OrgId, cmd.Id, c.SignedInUser, hs.Cfg.EditorsCanOwn); err != nil { return Error(403, "User not allowed to update team", err) } diff --git a/pkg/services/teams/team.go b/pkg/services/teams/team.go index 9419d649204..3818b22bca3 100644 --- a/pkg/services/teams/team.go +++ b/pkg/services/teams/team.go @@ -5,11 +5,15 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func CanUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser) error { +func CanUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser, editorCanOwn bool) error { if user.OrgRole == m.ROLE_ADMIN { return nil } + if !editorCanOwn { + return m.ErrNotAllowedToUpdateTeam + } + if user.OrgId != orgId { return m.ErrNotAllowedToUpdateTeamInDifferentOrg } diff --git a/pkg/services/teams/teams_test.go b/pkg/services/teams/teams_test.go index 7fac1be6880..50237af2945 100644 --- a/pkg/services/teams/teams_test.go +++ b/pkg/services/teams/teams_test.go @@ -3,7 +3,6 @@ package teams import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "github.com/pkg/errors" . "github.com/smartystreets/goconvey/convey" "testing" ) @@ -27,28 +26,20 @@ func TestUpdateTeam(t *testing.T) { OrgId: 1, } - updateTeamCmd := m.UpdateTeamCommand{ - Id: testTeam.Id, - OrgId: testTeam.OrgId, - } - Convey("Given an editor and a team he isn't a member of", func() { Convey("Should not be able to update the team", func() { - shouldNotUpdateTeam() bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error { cmd.Result = []*m.TeamMemberDTO{} return nil }) - err := CanUpdateTeam(&editor, &updateTeamCmd) + err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor, true) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) }) }) Convey("Given an editor and a team he is an admin in", func() { Convey("Should be able to update the team", func() { - teamUpdatedCallback := updateTeamCalled() - bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error { cmd.Result = []*m.TeamMemberDTO{{ OrgId: testTeam.OrgId, @@ -59,8 +50,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanUpdateTeam(&editor, &updateTeamCmd) - So(teamUpdatedCallback(), ShouldBeTrue) + err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor, true) So(err, ShouldBeNil) }) }) @@ -72,12 +62,6 @@ func TestUpdateTeam(t *testing.T) { } Convey("Shouldn't be able to update the team", func() { - cmd := m.UpdateTeamCommand{ - Id: testTeamOtherOrg.Id, - OrgId: testTeamOtherOrg.OrgId, - } - - shouldNotUpdateTeam() bus.AddHandler("test", func(cmd *m.GetTeamMembersQuery) error { cmd.Result = []*m.TeamMemberDTO{{ OrgId: testTeamOtherOrg.OrgId, @@ -88,42 +72,24 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanUpdateTeam(&editor, &cmd) + err := CanUpdateTeam(testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor, true) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg) }) }) Convey("Given an org admin and a team", func() { Convey("Should be able to update the team", func() { - teamUpdatedCallback := updateTeamCalled() - err := CanUpdateTeam(&admin, &updateTeamCmd) - - So(teamUpdatedCallback(), ShouldBeTrue) + err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &admin, true) So(err, ShouldBeNil) }) }) + Convey("Given that the editorsCanOwn feature toggle is disabled", func() { + Convey("Editors should not be able to update teams", func() { + err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor, false) - Convey("Given an editor and a team he is an admin", func() { - + So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) }) }) }) } - -func updateTeamCalled() func() bool { - wasCalled := false - bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { - wasCalled = true - return nil - }) - - return func() bool { return wasCalled } -} - -func shouldNotUpdateTeam() { - bus.AddHandler("test", func(cmd *m.UpdateTeamCommand) error { - return errors.New("UpdateTeamCommand not expected.") - }) - -} From 8e7a8282c1f52141c0c9d351a74f54a45c74baef Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 11:51:06 +0100 Subject: [PATCH 142/194] teams: removed feature toggle as it is already in middleware --- pkg/api/api.go | 2 +- pkg/api/team.go | 2 +- pkg/services/teams/team.go | 6 +----- pkg/services/teams/teams_test.go | 16 ++++------------ 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index e5d725342fe..c004d600b1b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -155,7 +155,7 @@ func (hs *HTTPServer) registerRoutes() { // team (admin permission required) apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) { teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(hs.CreateTeam)) - teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(UpdateTeam)) + teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(hs.UpdateTeam)) teamsRoute.Delete("/:teamId", Wrap(DeleteTeamByID)) teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers)) teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember)) diff --git a/pkg/api/team.go b/pkg/api/team.go index e9239acffa3..6d74b11e588 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -42,7 +42,7 @@ func (hs *HTTPServer) UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Respo cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":teamId") - if err := teams.CanUpdateTeam(cmd.OrgId, cmd.Id, c.SignedInUser, hs.Cfg.EditorsCanOwn); err != nil { + if err := teams.CanUpdateTeam(cmd.OrgId, cmd.Id, c.SignedInUser); err != nil { return Error(403, "User not allowed to update team", err) } diff --git a/pkg/services/teams/team.go b/pkg/services/teams/team.go index 3818b22bca3..9419d649204 100644 --- a/pkg/services/teams/team.go +++ b/pkg/services/teams/team.go @@ -5,15 +5,11 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func CanUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser, editorCanOwn bool) error { +func CanUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser) error { if user.OrgRole == m.ROLE_ADMIN { return nil } - if !editorCanOwn { - return m.ErrNotAllowedToUpdateTeam - } - if user.OrgId != orgId { return m.ErrNotAllowedToUpdateTeamInDifferentOrg } diff --git a/pkg/services/teams/teams_test.go b/pkg/services/teams/teams_test.go index 50237af2945..85bbddf014f 100644 --- a/pkg/services/teams/teams_test.go +++ b/pkg/services/teams/teams_test.go @@ -33,7 +33,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor, true) + err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) }) }) @@ -50,7 +50,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor, true) + err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor) So(err, ShouldBeNil) }) }) @@ -72,24 +72,16 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanUpdateTeam(testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor, true) + err := CanUpdateTeam(testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg) }) }) Convey("Given an org admin and a team", func() { Convey("Should be able to update the team", func() { - err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &admin, true) + err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &admin) So(err, ShouldBeNil) }) }) - - Convey("Given that the editorsCanOwn feature toggle is disabled", func() { - Convey("Editors should not be able to update teams", func() { - err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor, false) - - So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) - }) - }) }) } From 23231e6d510b60f5609ee76343f808a5dd5becf6 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 12:03:15 +0100 Subject: [PATCH 143/194] teams: added delete team guard --- pkg/api/api.go | 2 +- pkg/api/team.go | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index c004d600b1b..e5d725342fe 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -155,7 +155,7 @@ func (hs *HTTPServer) registerRoutes() { // team (admin permission required) apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) { teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(hs.CreateTeam)) - teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(hs.UpdateTeam)) + teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(UpdateTeam)) teamsRoute.Delete("/:teamId", Wrap(DeleteTeamByID)) teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers)) teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember)) diff --git a/pkg/api/team.go b/pkg/api/team.go index 6d74b11e588..61d966c2a8b 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -38,12 +38,12 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo } // PUT /api/teams/:teamId -func (hs *HTTPServer) UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { +func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":teamId") if err := teams.CanUpdateTeam(cmd.OrgId, cmd.Id, c.SignedInUser); err != nil { - return Error(403, "User not allowed to update team", err) + return Error(403, "Not allowed to update team", err) } if err := bus.Dispatch(&cmd); err != nil { @@ -58,11 +58,19 @@ func (hs *HTTPServer) UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Respo // DELETE /api/teams/:teamId func DeleteTeamByID(c *m.ReqContext) Response { - if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil { + orgId := c.OrgId + teamId := c.ParamsInt64(":teamId") + user := c.SignedInUser + + if err := teams.CanUpdateTeam(orgId, teamId, user); err != nil { + return Error(403, "Not allowed to delete team", err) + } + + if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: orgId, Id: teamId}); err != nil { if err == m.ErrTeamNotFound { return Error(404, "Failed to delete Team. ID not found", nil) } - return Error(500, "Failed to update Team", err) + return Error(500, "Failed to delete Team", err) } return Success("Team deleted") } From 1f949e58e1f9e15d43cb88daa0f753ca927bd8cd Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 13:14:06 +0100 Subject: [PATCH 144/194] teams: teams guard on all teams update methods. --- pkg/api/team.go | 9 ++++++++- pkg/api/team_members.go | 33 ++++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/pkg/api/team.go b/pkg/api/team.go index 61d966c2a8b..223ad404793 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -131,5 +131,12 @@ func GetTeamPreferences(c *m.ReqContext) Response { // PUT /api/teams/:teamId/preferences func UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response { - return updatePreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"), &dtoCmd) + teamId := c.ParamsInt64(":teamId") + orgId := c.OrgId + + if err := teams.CanUpdateTeam(orgId, teamId, c.SignedInUser); err != nil { + return Error(403, "Not allowed to update team preferences.", err) + } + + return updatePreferencesFor(orgId, 0, teamId, &dtoCmd) } diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index e0919262111..b2bb1781020 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -4,6 +4,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/teams" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -30,8 +31,15 @@ func GetTeamMembers(c *m.ReqContext) Response { // POST /api/teams/:teamId/members func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response { - cmd.TeamId = c.ParamsInt64(":teamId") - cmd.OrgId = c.OrgId + teamId := c.ParamsInt64(":teamId") + orgId := c.OrgId + + if err := teams.CanUpdateTeam(orgId, teamId, c.SignedInUser); err != nil { + return Error(403, "Not allowed to add team member", err) + } + + cmd.TeamId = teamId + cmd.OrgId = orgId if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNotFound { @@ -52,9 +60,16 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response { // PUT /:teamId/members/:userId func UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response { - cmd.TeamId = c.ParamsInt64(":teamId") + teamId := c.ParamsInt64(":teamId") + orgId := c.OrgId + + if err := teams.CanUpdateTeam(orgId, teamId, c.SignedInUser); err != nil { + return Error(403, "Not allowed to update team member", err) + } + + cmd.TeamId = teamId cmd.UserId = c.ParamsInt64(":userId") - cmd.OrgId = c.OrgId + cmd.OrgId = orgId if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamMemberNotFound { @@ -67,7 +82,15 @@ func UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response { // DELETE /api/teams/:teamId/members/:userId func RemoveTeamMember(c *m.ReqContext) Response { - if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil { + orgId := c.OrgId + teamId := c.ParamsInt64(":teamId") + userId := c.ParamsInt64(":userId") + + if err := teams.CanUpdateTeam(orgId, teamId, c.SignedInUser); err != nil { + return Error(403, "Not allowed to remove team member", err) + } + + if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: orgId, TeamId: teamId, UserId: userId}); err != nil { if err == m.ErrTeamNotFound { return Error(404, "Team not found", nil) } From 89d4db8eb6d02cd585cf53cbfeda01dd5a6f1e9a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 14:40:57 +0100 Subject: [PATCH 145/194] teams: team listing shows only your teams (editors). --- pkg/api/team.go | 16 +++++++++++----- pkg/models/team.go | 11 ++++++----- pkg/services/sqlstore/team.go | 4 ++++ public/app/features/teams/state/actions.ts | 6 ++++-- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/pkg/api/team.go b/pkg/api/team.go index 223ad404793..e4adb0bd430 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -86,12 +86,18 @@ func SearchTeams(c *m.ReqContext) Response { page = 1 } + var userIdFilter int64 + if c.QueryBool("showMine") { + userIdFilter = c.SignedInUser.UserId + } + query := m.SearchTeamsQuery{ - OrgId: c.OrgId, - Query: c.Query("query"), - Name: c.Query("name"), - Page: page, - Limit: perPage, + OrgId: c.OrgId, + Query: c.Query("query"), + Name: c.Query("name"), + UserIdFilter: userIdFilter, + Page: page, + Limit: perPage, } if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/models/team.go b/pkg/models/team.go index bd0d803d9d3..bb9289ee5e5 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -61,11 +61,12 @@ type GetTeamsByUserQuery struct { } type SearchTeamsQuery struct { - Query string - Name string - Limit int - Page int - OrgId int64 + Query string + Name string + Limit int + Page int + OrgId int64 + UserIdFilter int64 Result SearchTeamQueryResult } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index f7f7d7fc2cb..a9ee6979406 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -149,6 +149,10 @@ func SearchTeams(query *m.SearchTeamsQuery) error { params := make([]interface{}, 0) sql.WriteString(getTeamSelectSqlBase()) + if query.UserIdFilter > 0 { + sql.WriteString(`INNER JOIN team_member on team.id = team_member.team_id AND team_member.user_id = ?`) + params = append(params, query.UserIdFilter) + } sql.WriteString(` WHERE team.org_id = ?`) params = append(params, query.OrgId) diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index e2582839233..bfccddeefc5 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,9 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { OrgRole, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; import { buildNavModel } from './navModel'; +import { contextSrv } from '../../../core/services/context_srv'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -85,7 +86,8 @@ export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ export function loadTeams(): ThunkResult { return async dispatch => { - const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); + const showMine = contextSrv.user.orgRole === OrgRole.Editor; + const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1, showMine }); dispatch(teamsLoaded(response.teams)); }; } From d593ffe3c1a8e0e0fefd343c769355a02cdecdeb Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 15:05:28 +0100 Subject: [PATCH 146/194] dashboards: better error handling --- pkg/api/dashboard.go | 3 ++- pkg/api/folder.go | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 016146a5c61..deecdf2c1c8 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -281,7 +281,8 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) aclService := dashboards.NewAclService() err := aclService.MakeUserAdmin(cmd.OrgId, cmd.UserId, dashboard.Id) if err != nil { - hs.log.Error("Could not make user admin", "error", err) + hs.log.Error("Could not make user admin", "dashboard", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) + return Error(500, "Failed to make user admin of dashboard", err) } } diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 4e66439219d..a2d6a765b16 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -63,7 +63,10 @@ func (hs *HTTPServer) CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) R if hs.Cfg.EditorsCanOwn { aclService := dashboards.NewAclService() - aclService.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, cmd.Result.Id) + if err := aclService.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, cmd.Result.Id); err != nil { + hs.log.Error("Could not make user admin", "folder", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) + return Error(500, "Failed to make user admin of folder", err) + } } g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser) From 0b209de5d1addb9a31b86aa720778ebf551ac18a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 15:34:32 +0100 Subject: [PATCH 147/194] dashboard: only admin permission added to dashboard in folder. --- pkg/api/dashboard.go | 3 +- pkg/api/folder.go | 2 +- pkg/services/dashboards/acl_service.go | 39 +++++++++++++++----------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index deecdf2c1c8..b7b2383d1f8 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -279,7 +279,8 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) if hs.Cfg.EditorsCanOwn && newDashboard { aclService := dashboards.NewAclService() - err := aclService.MakeUserAdmin(cmd.OrgId, cmd.UserId, dashboard.Id) + inFolder := cmd.FolderId > 0 + err := aclService.MakeUserAdmin(cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) if err != nil { hs.log.Error("Could not make user admin", "dashboard", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) return Error(500, "Failed to make user admin of dashboard", err) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index a2d6a765b16..fd10897fc9a 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -63,7 +63,7 @@ func (hs *HTTPServer) CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) R if hs.Cfg.EditorsCanOwn { aclService := dashboards.NewAclService() - if err := aclService.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, cmd.Result.Id); err != nil { + if err := aclService.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, cmd.Result.Id, true); err != nil { hs.log.Error("Could not make user admin", "folder", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) return Error(500, "Failed to make user admin of folder", err) } diff --git a/pkg/services/dashboards/acl_service.go b/pkg/services/dashboards/acl_service.go index 79b55470093..dae3ec1372b 100644 --- a/pkg/services/dashboards/acl_service.go +++ b/pkg/services/dashboards/acl_service.go @@ -18,7 +18,7 @@ type AclService struct { log log.Logger } -func (as *AclService) MakeUserAdmin(orgId int64, userId int64, dashboardId int64) error { +func (as *AclService) MakeUserAdmin(orgId int64, userId int64, dashboardId int64, setViewAndEditPermissions bool) error { rtEditor := models.ROLE_EDITOR rtViewer := models.ROLE_VIEWER @@ -31,22 +31,27 @@ func (as *AclService) MakeUserAdmin(orgId int64, userId int64, dashboardId int64 Created: time.Now(), Updated: time.Now(), }, - { - OrgId: orgId, - DashboardId: dashboardId, - Role: &rtEditor, - Permission: models.PERMISSION_EDIT, - Created: time.Now(), - Updated: time.Now(), - }, - { - OrgId: orgId, - DashboardId: dashboardId, - Role: &rtViewer, - Permission: models.PERMISSION_VIEW, - Created: time.Now(), - Updated: time.Now(), - }, + } + + if setViewAndEditPermissions { + items = append(items, + &models.DashboardAcl{ + OrgId: orgId, + DashboardId: dashboardId, + Role: &rtEditor, + Permission: models.PERMISSION_EDIT, + Created: time.Now(), + Updated: time.Now(), + }, + &models.DashboardAcl{ + OrgId: orgId, + DashboardId: dashboardId, + Role: &rtViewer, + Permission: models.PERMISSION_VIEW, + Created: time.Now(), + Updated: time.Now(), + }, + ) } aclCmd := &models.UpdateDashboardAclCommand{ From a6a3d698da2f09802ad25363917371fe6bda5237 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 11 Mar 2019 15:48:05 +0100 Subject: [PATCH 148/194] teams: cleanup. --- pkg/services/sqlstore/team.go | 14 ++++---------- pkg/services/teams/team.go | 2 -- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index a9ee6979406..7c5a5f88983 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -92,10 +92,8 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error { // DeleteTeam will delete a team, its member and any permissions connected to the team func DeleteTeam(cmd *m.DeleteTeamCommand) error { return inTransaction(func(sess *DBSession) error { - if teamExists, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil { + if _, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil { return err - } else if !teamExists { - return m.ErrTeamNotFound } deletes := []string{ @@ -118,7 +116,7 @@ func teamExists(orgId int64, teamId int64, sess *DBSession) (bool, error) { if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil { return false, err } else if len(res) != 1 { - return false, nil + return false, m.ErrTeamNotFound } return true, nil @@ -238,10 +236,8 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error { return m.ErrTeamMemberAlreadyAdded } - if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil { + if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil { return err - } else if !teamExists { - return m.ErrTeamNotFound } entity := m.TeamMember{ @@ -285,10 +281,8 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { // RemoveTeamMember removes a member from a team func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error { return inTransaction(func(sess *DBSession) error { - if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil { + if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil { return err - } else if !teamExists { - return m.ErrTeamNotFound } var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?" diff --git a/pkg/services/teams/team.go b/pkg/services/teams/team.go index 9419d649204..080fe961ab6 100644 --- a/pkg/services/teams/team.go +++ b/pkg/services/teams/team.go @@ -18,8 +18,6 @@ func CanUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser) error { OrgId: orgId, TeamId: teamId, UserId: user.UserId, - // TODO: do we need to do something special about external users - // External: false, } if err := bus.Dispatch(&cmd); err != nil { From a90b3e331ecc2c9912802d693d5e8cdcf6ac1657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 12 Mar 2019 07:32:47 +0100 Subject: [PATCH 149/194] config: updated feature toggle name --- conf/defaults.ini | 2 +- conf/sample.ini | 2 +- pkg/api/api.go | 2 +- pkg/api/dashboard.go | 2 +- pkg/api/dashboard_test.go | 2 +- pkg/api/folder.go | 2 +- pkg/api/folder_test.go | 2 +- pkg/api/frontendsettings.go | 2 +- pkg/api/index.go | 2 +- pkg/api/team.go | 2 +- pkg/setting/setting.go | 7 +++---- public/app/core/config.ts | 4 ++-- public/app/features/teams/TeamMembers.tsx | 4 ++-- public/app/routes/routes.ts | 2 +- 14 files changed, 18 insertions(+), 19 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 492525e6b5f..bb415721391 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -259,7 +259,7 @@ external_manage_info = viewers_can_edit = false # Editors can administrate dashboard, folders and teams they create -editors_can_own = false +editors_can_admin = false [auth] # Login cookie name diff --git a/conf/sample.ini b/conf/sample.ini index fd414c2af47..321c1120693 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -239,7 +239,7 @@ log_queries = ;viewers_can_edit = false # Editors can administrate dashboard, folders and teams they create -;editors_can_own = false +;editors_can_admin = false [auth] # Login cookie name diff --git a/pkg/api/api.go b/pkg/api/api.go index e5d725342fe..9ffb0278935 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -14,7 +14,7 @@ func (hs *HTTPServer) registerRoutes() { reqGrafanaAdmin := middleware.ReqGrafanaAdmin reqEditorRole := middleware.ReqEditorRole reqOrgAdmin := middleware.ReqOrgAdmin - reqAdminOrEditorCanAdmin := middleware.EditorCanAdmin(hs.Cfg.EditorsCanOwn) + reqAdminOrEditorCanAdmin := middleware.EditorCanAdmin(hs.Cfg.EditorsCanAdmin) redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL() quota := middleware.Quota(hs.QuotaService) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index b7b2383d1f8..14a1e3baf32 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -277,7 +277,7 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) return Error(500, "Failed to save dashboard", err) } - if hs.Cfg.EditorsCanOwn && newDashboard { + if hs.Cfg.EditorsCanAdmin && newDashboard { aclService := dashboards.NewAclService() inFolder := cmd.FolderId > 0 err := aclService.MakeUserAdmin(cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index d58e2246eec..5411643af96 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -973,7 +973,7 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d defer bus.ClearBusHandlers() cfg := setting.NewCfg() - cfg.EditorsCanOwn = false + cfg.EditorsCanAdmin = false hs := HTTPServer{ Bus: bus.GetBus(), diff --git a/pkg/api/folder.go b/pkg/api/folder.go index fd10897fc9a..66c640f96e8 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -61,7 +61,7 @@ func (hs *HTTPServer) CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) R return toFolderError(err) } - if hs.Cfg.EditorsCanOwn { + if hs.Cfg.EditorsCanAdmin { aclService := dashboards.NewAclService() if err := aclService.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, cmd.Result.Id, true); err != nil { hs.log.Error("Could not make user admin", "folder", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index d5e4ee418cd..15c51e476b5 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -143,7 +143,7 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa defer bus.ClearBusHandlers() cfg := setting.NewCfg() - cfg.EditorsCanOwn = true + cfg.EditorsCanAdmin = true hs := HTTPServer{ Bus: bus.GetBus(), diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 67a511b8b4d..cd61c2f3ebc 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -167,7 +167,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf "externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl, "externalUserMngLinkName": setting.ExternalUserMngLinkName, "viewersCanEdit": setting.ViewersCanEdit, - "editorsCanOwn": hs.Cfg.EditorsCanOwn, + "editorsCanAdmin": hs.Cfg.EditorsCanAdmin, "disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml, "buildInfo": map[string]interface{}{ "version": setting.BuildVersion, diff --git a/pkg/api/index.go b/pkg/api/index.go index 88c4b7e929d..3f60290ea55 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -327,7 +327,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er }) } - if c.OrgRole == m.ROLE_EDITOR && hs.Cfg.EditorsCanOwn { + if c.OrgRole == m.ROLE_EDITOR && hs.Cfg.EditorsCanAdmin { cfgNode := &dtos.NavLink{ Id: "cfg", Text: "Configuration", diff --git a/pkg/api/team.go b/pkg/api/team.go index e4adb0bd430..619d24ea0b1 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -18,7 +18,7 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo return Error(500, "Failed to create Team", err) } - if c.OrgRole == m.ROLE_EDITOR && hs.Cfg.EditorsCanOwn { + if c.OrgRole == m.ROLE_EDITOR && hs.Cfg.EditorsCanAdmin { addMemberCmd := m.AddTeamMemberCommand{ UserId: c.SignedInUser.UserId, OrgId: cmd.OrgId, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index bc57291b5f9..8c6d8c54f11 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -239,14 +239,13 @@ type Cfg struct { LoginMaxLifetimeDays int TokenRotationIntervalMinutes int - // User - EditorsCanOwn bool - // Dataproxy SendUserHeader bool // DistributedCache RemoteCacheOptions *RemoteCacheOptions + + EditorsCanAdmin bool } type CommandLineArgs struct { @@ -670,7 +669,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { ExternalUserMngLinkName = users.Key("external_manage_link_name").String() ExternalUserMngInfo = users.Key("external_manage_info").String() ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false) - cfg.EditorsCanOwn = users.Key("editors_can_own").MustBool(false) + cfg.EditorsCanAdmin = users.Key("editors_can_admin").MustBool(false) // auth auth := iniFile.Section("auth") diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 9789888e60f..fe9005973b8 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -37,7 +37,7 @@ export class Settings { passwordHint: any; loginError: any; viewersCanEdit: boolean; - editorsCanOwn: boolean; + editorsCanAdmin: boolean; disableSanitizeHtml: boolean; theme: GrafanaTheme; @@ -59,7 +59,7 @@ export class Settings { isEnterprise: false, }, viewersCanEdit: false, - editorsCanOwn: false, + editorsCanAdmin: false, disableSanitizeHtml: false, }; diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 29f1e2e4947..bcb2f3fab4b 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -93,7 +93,7 @@ export class TeamMembers extends PureComponent { {member.login} {member.email} - +
this.onPermissionChange(item, member)} + className="gf-form-select-box__control--menu-right" + value={value} + /> + )} + {!isUserTeamAdmin && {value.label}} +
+ +
+ ); + } + renderMember(member: TeamMember, syncEnabled: boolean) { return ( @@ -93,19 +125,7 @@ export class TeamMembers extends PureComponent { {member.login} {member.email} - - -
- + + Member +
@@ -271,41 +239,9 @@ exports[`Render should render team members 1`] = `
- + + Member +
@@ -411,41 +315,9 @@ exports[`Render should render team members 1`] = `
- + + Member +
@@ -644,41 +484,9 @@ exports[`Render should render team members when sync enabled 1`] = `
- + + Member +
@@ -816,41 +592,9 @@ exports[`Render should render team members when sync enabled 1`] = `
- + + Member +
@@ -983,6 +695,152 @@ exports[`Render should render team members when sync enabled 1`] = ` + +
+ + Member + +
+ +
+ + + + + + + + + + +
+
+`; + +exports[`Render when feature toggle editorsCanAdmin is turned on should render permissions select if user is Grafana Admin 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add team member +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + Email + + Permission + +
+ + + testUser-1 + + test@test.com +
- - +
+ + + testUser-2 + + test@test.com + +
+
+ +
+ + + testUser-3 + + test@test.com + +
+
+ +
+ + + testUser-4 + + test@test.com + +
+
+ +
+ + + testUser-5 + + test@test.com + +
+
+ +
+
+
+`; + +exports[`Render when feature toggle editorsCanAdmin is turned on should render permissions select if user is Org Admin 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add team member +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + Email + + Permission + +
+ + + testUser-1 + + test@test.com + +
+
+ +
+ + + testUser-2 + + test@test.com + +
+
+ +
+ + + testUser-3 + + test@test.com + +
+
+ +
+ + + testUser-4 + + test@test.com + +
+
+ +
+ + + testUser-5 + + test@test.com + +
+
+ +
+
+
+`; + +exports[`Render when feature toggle editorsCanAdmin is turned on should render permissions select if user is team admin 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add team member +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - {this.renderPermissionsSelect(member)} + {this.renderPermissions(member)} {syncEnabled && this.renderLabels(member.labels)} ); @@ -152,7 +158,11 @@ export class TeamMembers extends PureComponent {
-
diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 3f0830eda16..f38f8f2b144 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -25,7 +25,7 @@ export const getMockTeam = (): Team => { }; }; -export const getMockTeamMembers = (amount: number): TeamMember[] => { +export const getMockTeamMembers = (amount: number, teamAdminId: number): TeamMember[] => { const teamMembers: TeamMember[] = []; for (let i = 1; i <= amount; i++) { @@ -36,7 +36,7 @@ export const getMockTeamMembers = (amount: number): TeamMember[] => { email: 'test@test.com', login: `testUser-${i}`, labels: ['label 1', 'label 2'], - permission: TeamPermissionLevel.Member, + permission: i === teamAdminId ? TeamPermissionLevel.Admin : TeamPermissionLevel.Member, }); } diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap index 77b50436590..da89d26d191 100644 --- a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -201,9 +201,41 @@ exports[`Render should render team members 1`] = `
- - Member - +
@@ -249,6 +314,7 @@ exports[`Render should render team members 1`] = ` className="text-right" > @@ -277,9 +343,41 @@ exports[`Render should render team members 1`] = `
- - Member - +
@@ -325,6 +456,7 @@ exports[`Render should render team members 1`] = ` className="text-right" > @@ -353,9 +485,41 @@ exports[`Render should render team members 1`] = `
- - Member - +
@@ -510,6 +707,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="text-right" > @@ -538,9 +736,41 @@ exports[`Render should render team members when sync enabled 1`] = `
- - Member - +
@@ -618,6 +881,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="text-right" > @@ -646,9 +910,41 @@ exports[`Render should render team members when sync enabled 1`] = `
- - Member - +
@@ -726,6 +1055,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="text-right" > @@ -888,6 +1218,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -958,6 +1289,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -1028,6 +1360,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -1098,6 +1431,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -1168,6 +1502,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -1330,6 +1665,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -1400,6 +1736,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -1470,6 +1807,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -1540,6 +1878,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -1610,6 +1949,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p className="text-right" > @@ -1641,7 +1981,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p />
+
+ + Name + + Email + + Permission + +
+ + + testUser-1 + + test@test.com + +
+
+ +
+ + + testUser-2 + + test@test.com + +
+
+ +
+ + + testUser-3 + + test@test.com + +
+
+ +
+ + + testUser-4 + + test@test.com + +
+
+ +
+ + + testUser-5 + + test@test.com + +
+
From b783fa7039daff42e0406262c80fdf19c852dc53 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 12 Mar 2019 13:59:53 +0100 Subject: [PATCH 152/194] team: renames teams.CanUpdate teamguardian.CanAdmin --- pkg/api/dashboard_test.go | 1 + pkg/api/folder_test.go | 5 +---- pkg/api/team.go | 17 ++++++++++++----- pkg/api/team_members.go | 8 ++++---- pkg/services/{teams => teamguardian}/team.go | 4 ++-- .../{teams => teamguardian}/teams_test.go | 10 +++++----- public/app/types/acl.ts | 2 +- 7 files changed, 26 insertions(+), 21 deletions(-) rename pkg/services/{teams => teamguardian}/team.go (85%) rename pkg/services/{teams => teamguardian}/teams_test.go (87%) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 5411643af96..c54647d9847 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1028,6 +1028,7 @@ func restoreDashboardVersionScenario(desc string, url string, routePattern strin defer bus.ClearBusHandlers() hs := HTTPServer{ + Cfg: setting.NewCfg(), Bus: bus.GetBus(), } diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 15c51e476b5..5e7184ae0c9 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -142,12 +142,9 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() - cfg := setting.NewCfg() - cfg.EditorsCanAdmin = true - hs := HTTPServer{ Bus: bus.GetBus(), - Cfg: cfg, + Cfg: setting.NewCfg(), } sc := setupScenarioContext(url) diff --git a/pkg/api/team.go b/pkg/api/team.go index 619d24ea0b1..6d5753fdc90 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -4,7 +4,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/teams" + "github.com/grafana/grafana/pkg/services/teamguardian" "github.com/grafana/grafana/pkg/util" ) @@ -42,7 +42,7 @@ func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":teamId") - if err := teams.CanUpdateTeam(cmd.OrgId, cmd.Id, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(cmd.OrgId, cmd.Id, c.SignedInUser); err != nil { return Error(403, "Not allowed to update team", err) } @@ -62,7 +62,7 @@ func DeleteTeamByID(c *m.ReqContext) Response { teamId := c.ParamsInt64(":teamId") user := c.SignedInUser - if err := teams.CanUpdateTeam(orgId, teamId, user); err != nil { + if err := teamguardian.CanAdmin(orgId, teamId, user); err != nil { return Error(403, "Not allowed to delete team", err) } @@ -132,7 +132,14 @@ func GetTeamByID(c *m.ReqContext) Response { // GET /api/teams/:teamId/preferences func GetTeamPreferences(c *m.ReqContext) Response { - return getPreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId")) + teamId := c.ParamsInt64(":teamId") + orgId := c.OrgId + + if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { + return Error(403, "Not allowed to view team preferences.", err) + } + + return getPreferencesFor(orgId, 0, teamId) } // PUT /api/teams/:teamId/preferences @@ -140,7 +147,7 @@ func UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response teamId := c.ParamsInt64(":teamId") orgId := c.OrgId - if err := teams.CanUpdateTeam(orgId, teamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to update team preferences.", err) } diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index b2bb1781020..669326ded18 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -4,7 +4,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/teams" + "github.com/grafana/grafana/pkg/services/teamguardian" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -34,7 +34,7 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response { teamId := c.ParamsInt64(":teamId") orgId := c.OrgId - if err := teams.CanUpdateTeam(orgId, teamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to add team member", err) } @@ -63,7 +63,7 @@ func UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response { teamId := c.ParamsInt64(":teamId") orgId := c.OrgId - if err := teams.CanUpdateTeam(orgId, teamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to update team member", err) } @@ -86,7 +86,7 @@ func RemoveTeamMember(c *m.ReqContext) Response { teamId := c.ParamsInt64(":teamId") userId := c.ParamsInt64(":userId") - if err := teams.CanUpdateTeam(orgId, teamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to remove team member", err) } diff --git a/pkg/services/teams/team.go b/pkg/services/teamguardian/team.go similarity index 85% rename from pkg/services/teams/team.go rename to pkg/services/teamguardian/team.go index 080fe961ab6..9946ae7c734 100644 --- a/pkg/services/teams/team.go +++ b/pkg/services/teamguardian/team.go @@ -1,11 +1,11 @@ -package teams +package teamguardian import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) -func CanUpdateTeam(orgId int64, teamId int64, user *m.SignedInUser) error { +func CanAdmin(orgId int64, teamId int64, user *m.SignedInUser) error { if user.OrgRole == m.ROLE_ADMIN { return nil } diff --git a/pkg/services/teams/teams_test.go b/pkg/services/teamguardian/teams_test.go similarity index 87% rename from pkg/services/teams/teams_test.go rename to pkg/services/teamguardian/teams_test.go index 85bbddf014f..9b1ba7ee4cb 100644 --- a/pkg/services/teams/teams_test.go +++ b/pkg/services/teamguardian/teams_test.go @@ -1,4 +1,4 @@ -package teams +package teamguardian import ( "github.com/grafana/grafana/pkg/bus" @@ -33,7 +33,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor) + err := CanAdmin(testTeam.OrgId, testTeam.Id, &editor) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) }) }) @@ -50,7 +50,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &editor) + err := CanAdmin(testTeam.OrgId, testTeam.Id, &editor) So(err, ShouldBeNil) }) }) @@ -72,14 +72,14 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanUpdateTeam(testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor) + err := CanAdmin(testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg) }) }) Convey("Given an org admin and a team", func() { Convey("Should be able to update the team", func() { - err := CanUpdateTeam(testTeam.OrgId, testTeam.Id, &admin) + err := CanAdmin(testTeam.OrgId, testTeam.Id, &admin) So(err, ShouldBeNil) }) }) diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index 8134ddb1749..55e9bff620b 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -115,6 +115,6 @@ export const teamsPermissionLevels: TeamPermissionInfo[] = [ { value: TeamPermissionLevel.Admin, label: 'Admin', - description: 'Can add/remove permissions and delete team.', + description: 'Can add/remove permissions, members and delete team.', }, ]; From 8593668ab23acf72a6f88abaa6c01ce52c9284cf Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 12 Mar 2019 14:19:12 +0100 Subject: [PATCH 153/194] teams: tests use the new message for modifying team members. --- .../__snapshots__/TeamMembers.test.tsx.snap | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap index d8540ed0615..77b50436590 100644 --- a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -866,7 +866,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -936,7 +936,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1006,7 +1006,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1076,7 +1076,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1146,7 +1146,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1154,7 +1154,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p } value={ Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, } @@ -1308,7 +1308,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1378,7 +1378,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1448,7 +1448,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1518,7 +1518,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1588,7 +1588,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1596,7 +1596,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p } value={ Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, } @@ -1750,7 +1750,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1758,7 +1758,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p } value={ Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, } @@ -1820,7 +1820,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1890,7 +1890,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -1960,7 +1960,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, @@ -2030,7 +2030,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p "value": 0, }, Object { - "description": "Can add/remove permissions and delete team.", + "description": "Can add/remove permissions, members and delete team.", "label": "Admin", "value": 4, }, From 21d3d274523be3817caed94d2e85f6a77f1dc877 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 12 Mar 2019 16:59:39 +0100 Subject: [PATCH 154/194] teams: editors can't remove the last admin from a team. --- pkg/api/api.go | 2 +- pkg/api/team_members.go | 9 ++++++-- pkg/models/team.go | 1 + pkg/models/team_member.go | 7 +++--- pkg/services/sqlstore/team.go | 35 ++++++++++++++++++++++++++++++ pkg/services/sqlstore/team_test.go | 17 +++++++++++++++ 6 files changed, 65 insertions(+), 6 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9ffb0278935..9acd9485312 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -160,7 +160,7 @@ func (hs *HTTPServer) registerRoutes() { teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers)) teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember)) teamsRoute.Put("/:teamId/members/:userId", bind(m.UpdateTeamMemberCommand{}), Wrap(UpdateTeamMember)) - teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember)) + teamsRoute.Delete("/:teamId/members/:userId", Wrap(hs.RemoveTeamMember)) teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences)) teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences)) }, reqAdminOrEditorCanAdmin) diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 669326ded18..72aded688ec 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -81,7 +81,7 @@ func UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response { } // DELETE /api/teams/:teamId/members/:userId -func RemoveTeamMember(c *m.ReqContext) Response { +func (hs *HTTPServer) RemoveTeamMember(c *m.ReqContext) Response { orgId := c.OrgId teamId := c.ParamsInt64(":teamId") userId := c.ParamsInt64(":userId") @@ -90,7 +90,12 @@ func RemoveTeamMember(c *m.ReqContext) Response { return Error(403, "Not allowed to remove team member", err) } - if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: orgId, TeamId: teamId, UserId: userId}); err != nil { + protectLastAdmin := false + if c.OrgRole == m.ROLE_EDITOR { + protectLastAdmin = true + } + + if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: orgId, TeamId: teamId, UserId: userId, ProtectLastAdmin: protectLastAdmin}); err != nil { if err == m.ErrTeamNotFound { return Error(404, "Team not found", nil) } diff --git a/pkg/models/team.go b/pkg/models/team.go index bb9289ee5e5..5b659331601 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -10,6 +10,7 @@ var ( ErrTeamNotFound = errors.New("Team not found") ErrTeamNameTaken = errors.New("Team name is taken") ErrTeamMemberNotFound = errors.New("Team member not found") + ErrLastTeamAdmin = errors.New("Not allowed to remove last admin") ErrNotAllowedToUpdateTeam = errors.New("User not allowed to update team") ErrNotAllowedToUpdateTeamInDifferentOrg = errors.New("User not allowed to update team in another org") ) diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 1140e39b095..0cc39b0f605 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -42,9 +42,10 @@ type UpdateTeamMemberCommand struct { } type RemoveTeamMemberCommand struct { - OrgId int64 `json:"-"` - UserId int64 - TeamId int64 + OrgId int64 `json:"-"` + UserId int64 + TeamId int64 + ProtectLastAdmin bool `json:"-"` } // ---------------------- diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 7c5a5f88983..3848adcc7dc 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -285,6 +285,18 @@ func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error { return err } + if cmd.ProtectLastAdmin { + lastAdmin, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId) + if err != nil { + return err + } + + if lastAdmin { + return m.ErrLastTeamAdmin + } + + } + var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?" res, err := sess.Exec(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId) if err != nil { @@ -299,6 +311,29 @@ func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error { }) } +func isLastAdmin(sess *DBSession, orgId int64, teamId int64, userId int64) (bool, error) { + rawSql := "SELECT user_id FROM team_member WHERE org_id=? and team_id=? and permission=?" + userIds := []*int64{} + err := sess.SQL(rawSql, orgId, teamId, m.PERMISSION_ADMIN).Find(&userIds) + if err != nil { + return false, err + } + + isAdmin := false + for _, adminId := range userIds { + if userId == *adminId { + isAdmin = true + break + } + } + + if isAdmin && len(userIds) == 1 { + return true, nil + } + + return false, err +} + // GetTeamMembers return a list of members for the specified team func GetTeamMembers(query *m.GetTeamMembersQuery) error { query.Result = make([]*m.TeamMemberDTO, 0) diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index 1c5f2024a79..ca5379bae65 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -152,6 +152,23 @@ func TestTeamCommandsAndQueries(t *testing.T) { So(len(q2.Result), ShouldEqual, 0) }) + Convey("When ProtectLastAdmin is set to true", func() { + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: int64(m.PERMISSION_ADMIN)}) + So(err, ShouldBeNil) + + Convey("A user should not be able to remove the last admin", func() { + err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true}) + So(err, ShouldEqual, m.ErrLastTeamAdmin) + }) + + Convey("A user should be able to remove an admin if there are other admins", func() { + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: int64(m.PERMISSION_ADMIN)}) + err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true}) + So(err, ShouldEqual, nil) + }) + + }) + Convey("Should be able to remove a group with users and permissions", func() { groupId := group2.Result.Id err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1]}) From c823ad5de7f1ce2da450b2a65902bcf236c2d113 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 12 Mar 2019 17:24:18 +0100 Subject: [PATCH 155/194] team: uses PermissionType instead of int64 for permissions. --- pkg/api/team.go | 2 +- pkg/models/team_member.go | 20 ++++++++++---------- pkg/services/sqlstore/team.go | 1 + pkg/services/sqlstore/team_test.go | 8 ++++---- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pkg/api/team.go b/pkg/api/team.go index 6d5753fdc90..ab853888f76 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -23,7 +23,7 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo UserId: c.SignedInUser.UserId, OrgId: cmd.OrgId, TeamId: cmd.Result.Id, - Permission: int64(m.PERMISSION_ADMIN), + Permission: m.PERMISSION_ADMIN, } if err := bus.Dispatch(&addMemberCmd); err != nil { diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 0cc39b0f605..9b7c2aeb0a4 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -17,7 +17,7 @@ type TeamMember struct { TeamId int64 UserId int64 External bool - Permission int64 + Permission PermissionType Created time.Time Updated time.Time @@ -27,18 +27,18 @@ type TeamMember struct { // COMMANDS type AddTeamMemberCommand struct { - UserId int64 `json:"userId" binding:"Required"` - OrgId int64 `json:"-"` - TeamId int64 `json:"-"` - External bool `json:"-"` - Permission int64 `json:"-"` + UserId int64 `json:"userId" binding:"Required"` + OrgId int64 `json:"-"` + TeamId int64 `json:"-"` + External bool `json:"-"` + Permission PermissionType `json:"-"` } type UpdateTeamMemberCommand struct { - UserId int64 `json:"-"` - OrgId int64 `json:"-"` - TeamId int64 `json:"-"` - Permission int64 `json:"permission"` + UserId int64 `json:"-"` + OrgId int64 `json:"-"` + TeamId int64 `json:"-"` + Permission PermissionType `json:"permission"` } type RemoveTeamMemberCommand struct { diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 3848adcc7dc..bf993a930f2 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -271,6 +271,7 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { return m.ErrTeamMemberNotFound } + // TODO: check to make sure that permission is a legal value member.Permission = cmd.Permission _, err = sess.Cols("permission").Where("org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId).Update(member) diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index ca5379bae65..ac357c57a53 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -91,7 +91,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { UserId: userId, OrgId: testOrgId, TeamId: team.Id, - Permission: int64(m.PERMISSION_ADMIN), + Permission: m.PERMISSION_ADMIN, }) So(err, ShouldBeNil) @@ -107,7 +107,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { UserId: 1, OrgId: testOrgId, TeamId: group1.Result.Id, - Permission: int64(m.PERMISSION_ADMIN), + Permission: m.PERMISSION_ADMIN, }) So(err, ShouldEqual, m.ErrTeamMemberNotFound) @@ -153,7 +153,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { }) Convey("When ProtectLastAdmin is set to true", func() { - err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: int64(m.PERMISSION_ADMIN)}) + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: m.PERMISSION_ADMIN}) So(err, ShouldBeNil) Convey("A user should not be able to remove the last admin", func() { @@ -162,7 +162,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { }) Convey("A user should be able to remove an admin if there are other admins", func() { - err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: int64(m.PERMISSION_ADMIN)}) + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN}) err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true}) So(err, ShouldEqual, nil) }) From c826f39a8bef2bbfb76c300df655bf4b17536644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 13 Mar 2019 07:18:57 +0100 Subject: [PATCH 156/194] teams: defaulting invalid permission level to member permission level --- pkg/services/sqlstore/team.go | 5 ++++- pkg/services/sqlstore/team_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index bf993a930f2..c36e45ac503 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -271,7 +271,10 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { return m.ErrTeamMemberNotFound } - // TODO: check to make sure that permission is a legal value + if cmd.Permission != int64(m.PERMISSION_ADMIN) { + cmd.Permission = 0 + } + member.Permission = cmd.Permission _, err = sess.Cols("permission").Where("org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId).Update(member) diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index ac357c57a53..5580f5f9fab 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -102,6 +102,34 @@ func TestTeamCommandsAndQueries(t *testing.T) { So(qAfterUpdate.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN) }) + Convey("Should default to member permission level when updating a user with invalid permission level", func() { + userID := userIds[0] + team := group1.Result + addMemberCmd := m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userID} + err = AddTeamMember(&addMemberCmd) + So(err, ShouldBeNil) + + qBeforeUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id} + err = GetTeamMembers(qBeforeUpdate) + So(err, ShouldBeNil) + So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0) + + invalidPermissionLevel := 1337 + err = UpdateTeamMember(&m.UpdateTeamMemberCommand{ + UserId: userID, + OrgId: testOrgId, + TeamId: team.Id, + Permission: int64(invalidPermissionLevel), + }) + + So(err, ShouldBeNil) + + qAfterUpdate := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id} + err = GetTeamMembers(qAfterUpdate) + So(err, ShouldBeNil) + So(qAfterUpdate.Result[0].Permission, ShouldEqual, 0) + }) + Convey("Shouldn't be able to update a user not in the team.", func() { err = UpdateTeamMember(&m.UpdateTeamMemberCommand{ UserId: 1, From 246e1280489432ae0998ec5a8dbb3066ea9e95b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 13 Mar 2019 07:27:32 +0100 Subject: [PATCH 157/194] teams: changed permission to permission type instead of int --- pkg/services/sqlstore/team.go | 2 +- pkg/services/sqlstore/team_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index c36e45ac503..f7cb7b1ce45 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -271,7 +271,7 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { return m.ErrTeamMemberNotFound } - if cmd.Permission != int64(m.PERMISSION_ADMIN) { + if cmd.Permission != m.PERMISSION_ADMIN { cmd.Permission = 0 } diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index 5580f5f9fab..c63b28625b7 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -114,12 +114,12 @@ func TestTeamCommandsAndQueries(t *testing.T) { So(err, ShouldBeNil) So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0) - invalidPermissionLevel := 1337 + invalidPermissionLevel := m.PERMISSION_EDIT err = UpdateTeamMember(&m.UpdateTeamMemberCommand{ UserId: userID, OrgId: testOrgId, TeamId: team.Id, - Permission: int64(invalidPermissionLevel), + Permission: invalidPermissionLevel, }) So(err, ShouldBeNil) From c420af16b14e586b96d190e52f13805e0491e16a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 13 Mar 2019 10:11:53 +0100 Subject: [PATCH 158/194] teams: editor/viewer team admin cant remove the last admin. --- pkg/api/team_members.go | 6 +++++- pkg/models/team_member.go | 9 +++++---- pkg/services/sqlstore/team.go | 12 ++++++++++++ pkg/services/sqlstore/team_test.go | 12 +++++++++++- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 72aded688ec..4e2dd86a959 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -67,6 +67,10 @@ func UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response { return Error(403, "Not allowed to update team member", err) } + if c.OrgRole != m.ROLE_ADMIN { + cmd.ProtectLastAdmin = true + } + cmd.TeamId = teamId cmd.UserId = c.ParamsInt64(":userId") cmd.OrgId = orgId @@ -91,7 +95,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *m.ReqContext) Response { } protectLastAdmin := false - if c.OrgRole == m.ROLE_EDITOR { + if c.OrgRole != m.ROLE_ADMIN { protectLastAdmin = true } diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 9b7c2aeb0a4..6d0ae7793b3 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -35,10 +35,11 @@ type AddTeamMemberCommand struct { } type UpdateTeamMemberCommand struct { - UserId int64 `json:"-"` - OrgId int64 `json:"-"` - TeamId int64 `json:"-"` - Permission PermissionType `json:"permission"` + UserId int64 `json:"-"` + OrgId int64 `json:"-"` + TeamId int64 `json:"-"` + Permission PermissionType `json:"permission"` + ProtectLastAdmin bool `json:"-"` } type RemoveTeamMemberCommand struct { diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index f7cb7b1ce45..85801f42832 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -271,6 +271,18 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { return m.ErrTeamMemberNotFound } + if cmd.ProtectLastAdmin { + lastAdmin, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId) + if err != nil { + return err + } + + if lastAdmin { + return m.ErrLastTeamAdmin + } + + } + if cmd.Permission != m.PERMISSION_ADMIN { cmd.Permission = 0 } diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index c63b28625b7..7ac78733af7 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -190,11 +190,21 @@ func TestTeamCommandsAndQueries(t *testing.T) { }) Convey("A user should be able to remove an admin if there are other admins", func() { - err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN}) + AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN}) err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true}) So(err, ShouldEqual, nil) }) + Convey("A user should not be able to remove the admin permission for the last admin", func() { + err = UpdateTeamMember(&m.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true}) + So(err, ShouldEqual, m.ErrLastTeamAdmin) + }) + + Convey("A user should be able to remove the admin permission if there are other admins", func() { + AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: m.PERMISSION_ADMIN}) + err = UpdateTeamMember(&m.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true}) + So(err, ShouldEqual, nil) + }) }) Convey("Should be able to remove a group with users and permissions", func() { From 782b5b6a3ab5b1965f1cf1a17d03867cfc376cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 13 Mar 2019 10:38:09 +0100 Subject: [PATCH 159/194] teams: viewers and editors can view teams --- pkg/api/api.go | 8 ++++---- pkg/api/index.go | 9 +-------- pkg/api/team.go | 5 +++++ pkg/middleware/auth.go | 11 +++-------- public/app/routes/routes.ts | 4 ++-- 5 files changed, 15 insertions(+), 22 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9acd9485312..24183e63782 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -14,7 +14,7 @@ func (hs *HTTPServer) registerRoutes() { reqGrafanaAdmin := middleware.ReqGrafanaAdmin reqEditorRole := middleware.ReqEditorRole reqOrgAdmin := middleware.ReqOrgAdmin - reqAdminOrEditorCanAdmin := middleware.EditorCanAdmin(hs.Cfg.EditorsCanAdmin) + reqAdminOrCanAdmin := middleware.AdminOrCanAdmin(hs.Cfg.EditorsCanAdmin) redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL() quota := middleware.Quota(hs.QuotaService) @@ -42,8 +42,8 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/org/users", reqOrgAdmin, hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index) r.Get("/org/users/invite", reqOrgAdmin, hs.Index) - r.Get("/org/teams", reqAdminOrEditorCanAdmin, hs.Index) - r.Get("/org/teams/*", reqAdminOrEditorCanAdmin, hs.Index) + r.Get("/org/teams", reqAdminOrCanAdmin, hs.Index) + r.Get("/org/teams/*", reqAdminOrCanAdmin, hs.Index) r.Get("/org/apikeys/", reqOrgAdmin, hs.Index) r.Get("/dashboard/import/", reqSignedIn, hs.Index) r.Get("/configuration", reqGrafanaAdmin, hs.Index) @@ -163,7 +163,7 @@ func (hs *HTTPServer) registerRoutes() { teamsRoute.Delete("/:teamId/members/:userId", Wrap(hs.RemoveTeamMember)) teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences)) teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences)) - }, reqAdminOrEditorCanAdmin) + }, reqAdminOrCanAdmin) // team without requirement of user to be org admin apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) { diff --git a/pkg/api/index.go b/pkg/api/index.go index 3f60290ea55..e7555e14621 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -327,7 +327,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er }) } - if c.OrgRole == m.ROLE_EDITOR && hs.Cfg.EditorsCanAdmin { + if (c.OrgRole == m.ROLE_EDITOR || c.OrgRole == m.ROLE_VIEWER) && hs.Cfg.EditorsCanAdmin { cfgNode := &dtos.NavLink{ Id: "cfg", Text: "Configuration", @@ -342,13 +342,6 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er Icon: "gicon gicon-team", Url: setting.AppSubUrl + "/org/teams", }, - { - Text: "Plugins", - Id: "plugins", - Description: "View and configure plugins", - Icon: "gicon gicon-plugins", - Url: setting.AppSubUrl + "/plugins", - }, }, } diff --git a/pkg/api/team.go b/pkg/api/team.go index ab853888f76..eb7b1df37be 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -11,6 +11,11 @@ import ( // POST /api/teams func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response { cmd.OrgId = c.OrgId + + if c.OrgRole == m.ROLE_VIEWER { + return Error(403, "Not allowed to create team.", nil) + } + if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNameTaken { return Error(409, "Team name taken", err) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 6bf37e7fd50..8c1e5e04ae7 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -87,18 +87,13 @@ func Auth(options *AuthOptions) macaron.Handler { } } -func EditorCanAdmin(enabled bool) macaron.Handler { +func AdminOrCanAdmin(enabled bool) macaron.Handler { return func(c *m.ReqContext) { - ok := false if c.OrgRole == m.ROLE_ADMIN { - ok = true + return } - if c.OrgRole == m.ROLE_EDITOR && enabled { - ok = true - } - - if !ok { + if !enabled { accessForbidden(c) } } diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 6fe0483c100..19bb96be603 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -195,7 +195,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/org/teams', { template: '', resolve: { - roles: () => ['Editor', 'Admin'], + roles: () => (config.editorsCanAdmin ? [] : ['Editor', 'Admin']), component: () => TeamList, }, }) @@ -207,7 +207,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/org/teams/edit/:id/:page?', { template: '', resolve: { - roles: () => (config.editorsCanAdmin ? ['Editor', 'Admin'] : ['Admin']), + roles: () => (config.editorsCanAdmin ? [] : ['Admin']), component: () => TeamPages, }, }) From b60e71c28b0c62c23717a88ba48bde97a97fdeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 13 Mar 2019 14:05:08 +0100 Subject: [PATCH 160/194] teams: moved logic for searchteams to backend --- pkg/api/api.go | 2 +- pkg/api/team.go | 4 ++-- pkg/api/team_test.go | 10 ++++++++-- public/app/features/teams/state/actions.ts | 6 ++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 24183e63782..9d1151a757e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -168,7 +168,7 @@ func (hs *HTTPServer) registerRoutes() { // team without requirement of user to be org admin apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) { teamsRoute.Get("/:teamId", Wrap(GetTeamByID)) - teamsRoute.Get("/search", Wrap(SearchTeams)) + teamsRoute.Get("/search", Wrap(hs.SearchTeams)) }) // org information available to all users. diff --git a/pkg/api/team.go b/pkg/api/team.go index eb7b1df37be..fd34c0ab720 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -81,7 +81,7 @@ func DeleteTeamByID(c *m.ReqContext) Response { } // GET /api/teams/search -func SearchTeams(c *m.ReqContext) Response { +func (hs *HTTPServer) SearchTeams(c *m.ReqContext) Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 @@ -92,7 +92,7 @@ func SearchTeams(c *m.ReqContext) Response { } var userIdFilter int64 - if c.QueryBool("showMine") { + if hs.Cfg.EditorsCanAdmin && c.OrgRole != m.ROLE_ADMIN { userIdFilter = c.SignedInUser.UserId } diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index a1984288870..cab59cc5f98 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -3,6 +3,8 @@ package api import ( "testing" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" @@ -20,6 +22,10 @@ func TestTeamApiEndpoint(t *testing.T) { TotalCount: 2, } + hs := &HTTPServer{ + Cfg: setting.NewCfg(), + } + Convey("When searching with no parameters", func() { loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) { var sentLimit int @@ -33,7 +39,7 @@ func TestTeamApiEndpoint(t *testing.T) { return nil }) - sc.handlerFunc = SearchTeams + sc.handlerFunc = hs.SearchTeams sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() So(sentLimit, ShouldEqual, 1000) @@ -60,7 +66,7 @@ func TestTeamApiEndpoint(t *testing.T) { return nil }) - sc.handlerFunc = SearchTeams + sc.handlerFunc = hs.SearchTeams sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() So(sentLimit, ShouldEqual, 10) diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index bfccddeefc5..e2582839233 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,9 +1,8 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { OrgRole, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { StoreState, Team, TeamGroup, TeamMember } from 'app/types'; import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; import { buildNavModel } from './navModel'; -import { contextSrv } from '../../../core/services/context_srv'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -86,8 +85,7 @@ export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ export function loadTeams(): ThunkResult { return async dispatch => { - const showMine = contextSrv.user.orgRole === OrgRole.Editor; - const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1, showMine }); + const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); dispatch(teamsLoaded(response.teams)); }; } From b82b94a2470e6d6f5889076b20dd4155ec4d473e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 13 Mar 2019 15:34:38 +0100 Subject: [PATCH 161/194] teams: disable buttons for team members --- .../components/DeleteButton/DeleteButton.tsx | 24 +- .../app/features/teams/TeamMembers.test.tsx | 34 +- public/app/features/teams/TeamMembers.tsx | 28 +- .../app/features/teams/__mocks__/teamMocks.ts | 4 +- .../__snapshots__/TeamMembers.test.tsx.snap | 884 ++++++++++++++---- .../features/teams/state/selectors.test.ts | 2 +- 6 files changed, 734 insertions(+), 242 deletions(-) diff --git a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx index df65d156ab3..d262c821968 100644 --- a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx +++ b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx @@ -2,6 +2,7 @@ import React, { PureComponent, SyntheticEvent } from 'react'; interface Props { onConfirm(): void; + disabled?: boolean; } interface State { @@ -33,25 +34,22 @@ export class DeleteButton extends PureComponent { }; render() { - const { onConfirm } = this.props; - let showConfirm; - let showDeleteButton; - - if (this.state.showConfirm) { - showConfirm = 'show'; - showDeleteButton = 'hide'; - } else { - showConfirm = 'hide'; - showDeleteButton = 'show'; - } + const { onConfirm, disabled } = this.props; + const showConfirmClass = this.state.showConfirm ? 'show' : 'hide'; + const showDeleteButtonClass = this.state.showConfirm ? 'hide' : 'show'; + const disabledClass = disabled ? 'disabled btn-inverse' : ''; + const onClick = disabled ? () => {} : this.onClickDelete; return ( - + - + Cancel diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx index 64609f1fd79..f6f0b4a5e49 100644 --- a/public/app/features/teams/TeamMembers.test.tsx +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -6,16 +6,17 @@ import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks'; import { SelectOptionItem } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; +const signedInUserId = 1; +const originalContextSrv = contextSrv; + jest.mock('app/core/services/context_srv', () => ({ contextSrv: { isGrafanaAdmin: false, hasRole: role => false, - user: { id: 1 }, + user: { id: signedInUserId }, }, })); -const originalContextSrv = contextSrv; - interface SetupProps { propOverrides?: object; isGrafanaAdmin?: boolean; @@ -64,7 +65,7 @@ describe('Render', () => { it('should render team members', () => { const { wrapper } = setup({ propOverrides: { - members: getMockTeamMembers(5), + members: getMockTeamMembers(5, 5), }, }); @@ -74,7 +75,7 @@ describe('Render', () => { it('should render team members when sync enabled', () => { const { wrapper } = setup({ propOverrides: { - members: getMockTeamMembers(5), + members: getMockTeamMembers(5, 5), syncEnabled: true, }, }); @@ -84,8 +85,7 @@ describe('Render', () => { describe('when feature toggle editorsCanAdmin is turned on', () => { it('should render permissions select if user is Grafana Admin', () => { - const members = getMockTeamMembers(5); - members[4].permission = TeamPermissionLevel.Admin; + const members = getMockTeamMembers(5, 5); const { wrapper } = setup({ propOverrides: { members, editorsCanAdmin: true }, isGrafanaAdmin: true, @@ -96,8 +96,7 @@ describe('Render', () => { }); it('should render permissions select if user is Org Admin', () => { - const members = getMockTeamMembers(5); - members[4].permission = TeamPermissionLevel.Admin; + const members = getMockTeamMembers(5, 5); const { wrapper } = setup({ propOverrides: { members, editorsCanAdmin: true }, isGrafanaAdmin: false, @@ -108,8 +107,7 @@ describe('Render', () => { }); it('should render permissions select if user is team admin', () => { - const members = getMockTeamMembers(5); - members[0].permission = TeamPermissionLevel.Admin; + const members = getMockTeamMembers(5, signedInUserId); const { wrapper } = setup({ propOverrides: { members, editorsCanAdmin: true }, isGrafanaAdmin: false, @@ -118,6 +116,20 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); + + it('should render span and disable buttons if user is team member', () => { + const members = getMockTeamMembers(5, 5); + const { wrapper } = setup({ + propOverrides: { + members, + editorsCanAdmin: true, + }, + isGrafanaAdmin: false, + isOrgAdmin: false, + }); + + expect(wrapper).toMatchSnapshot(); + }); }); }); diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 89ad24f9c58..fc5706ec68f 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -39,7 +39,7 @@ export class TeamMembers extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newTeamMember: null }; - this.renderPermissionsSelect = this.renderPermissionsSelect.bind(this); + this.renderPermissions = this.renderPermissions.bind(this); } componentDidMount() { @@ -88,13 +88,19 @@ export class TeamMembers extends PureComponent { this.props.updateTeamMember(updatedTeamMember); }; - renderPermissionsSelect(member: TeamMember) { + private isSignedInUserTeamAdmin = () => { const { members, editorsCanAdmin } = this.props; const userInMembers = members.find(m => m.userId === contextSrv.user.id); - const isUserTeamAdmin = - contextSrv.isGrafanaAdmin || contextSrv.hasRole(OrgRole.Admin) - ? true - : userInMembers && userInMembers.permission === TeamPermissionLevel.Admin; + const isAdmin = contextSrv.isGrafanaAdmin || contextSrv.hasRole(OrgRole.Admin); + const userIsTeamAdmin = userInMembers && userInMembers.permission === TeamPermissionLevel.Admin; + const isSignedInUserTeamAdmin = isAdmin || userIsTeamAdmin; + + return isSignedInUserTeamAdmin || !editorsCanAdmin; + }; + + renderPermissions(member: TeamMember) { + const { editorsCanAdmin } = this.props; + const isUserTeamAdmin = this.isSignedInUserTeamAdmin(); const value = teamsPermissionLevels.find(dp => dp.value === member.permission); return ( @@ -125,10 +131,10 @@ export class TeamMembers extends PureComponent { {member.login} {member.email} - this.onRemoveMember(member)} /> + this.onRemoveMember(member)} disabled={!this.isSignedInUserTeamAdmin()} />
+
+
+`; + +exports[`Render when feature toggle editorsCanAdmin is turned on should render span and disable buttons if user is team member 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add team member +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index e88fbdfd4b1..1721437c929 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -40,7 +40,7 @@ describe('Team selectors', () => { }); describe('Get members', () => { - const mockTeamMembers = getMockTeamMembers(5); + const mockTeamMembers = getMockTeamMembers(5, 5); it('should return team members', () => { const mockState: TeamState = { From fc0461134f0e4ff014f0b2b33719abf4eb842987 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 13 Mar 2019 15:47:47 +0100 Subject: [PATCH 162/194] dashboards: simplified code. --- pkg/api/dashboard_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index c54647d9847..ea69c049115 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -972,12 +972,9 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() - cfg := setting.NewCfg() - cfg.EditorsCanAdmin = false - hs := HTTPServer{ Bus: bus.GetBus(), - Cfg: cfg, + Cfg: setting.NewCfg(), } sc := setupScenarioContext(url) From ccfd6789ca1f9ae0ea6b9927cc45813d795c5ad9 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 13 Mar 2019 16:32:59 +0100 Subject: [PATCH 163/194] teams: cleanup. --- pkg/models/team_member.go | 18 +++++++++--------- pkg/services/teamguardian/team.go | 2 +- pkg/services/teamguardian/teams_test.go | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 6d0ae7793b3..c9afd4cd75f 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -64,13 +64,13 @@ type GetTeamMembersQuery struct { // Projections and DTOs type TeamMemberDTO struct { - OrgId int64 `json:"orgId"` - TeamId int64 `json:"teamId"` - UserId int64 `json:"userId"` - External bool `json:"-"` - Email string `json:"email"` - Login string `json:"login"` - AvatarUrl string `json:"avatarUrl"` - Labels []string `json:"labels"` - Permission int64 `json:"permission"` + OrgId int64 `json:"orgId"` + TeamId int64 `json:"teamId"` + UserId int64 `json:"userId"` + External bool `json:"-"` + Email string `json:"email"` + Login string `json:"login"` + AvatarUrl string `json:"avatarUrl"` + Labels []string `json:"labels"` + Permission PermissionType `json:"permission"` } diff --git a/pkg/services/teamguardian/team.go b/pkg/services/teamguardian/team.go index 9946ae7c734..6fddc318f5e 100644 --- a/pkg/services/teamguardian/team.go +++ b/pkg/services/teamguardian/team.go @@ -25,7 +25,7 @@ func CanAdmin(orgId int64, teamId int64, user *m.SignedInUser) error { } for _, member := range cmd.Result { - if member.UserId == user.UserId && member.Permission == int64(m.PERMISSION_ADMIN) { + if member.UserId == user.UserId && member.Permission == m.PERMISSION_ADMIN { return nil } } diff --git a/pkg/services/teamguardian/teams_test.go b/pkg/services/teamguardian/teams_test.go index 9b1ba7ee4cb..2ec86769a29 100644 --- a/pkg/services/teamguardian/teams_test.go +++ b/pkg/services/teamguardian/teams_test.go @@ -45,7 +45,7 @@ func TestUpdateTeam(t *testing.T) { OrgId: testTeam.OrgId, TeamId: testTeam.Id, UserId: editor.UserId, - Permission: int64(m.PERMISSION_ADMIN), + Permission: m.PERMISSION_ADMIN, }} return nil }) @@ -67,7 +67,7 @@ func TestUpdateTeam(t *testing.T) { OrgId: testTeamOtherOrg.OrgId, TeamId: testTeamOtherOrg.Id, UserId: editor.UserId, - Permission: int64(m.PERMISSION_ADMIN), + Permission: m.PERMISSION_ADMIN, }} return nil }) From 3f57a81c4722cbd77328e370272e749fdcdcbc7a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 13 Mar 2019 16:46:35 +0100 Subject: [PATCH 164/194] teams: cleanup. --- pkg/api/team_members.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 4e2dd86a959..1674cc120ce 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -31,16 +31,13 @@ func GetTeamMembers(c *m.ReqContext) Response { // POST /api/teams/:teamId/members func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response { - teamId := c.ParamsInt64(":teamId") - orgId := c.OrgId + cmd.OrgId = c.OrgId + cmd.TeamId = c.ParamsInt64(":teamId") - if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to add team member", err) } - cmd.TeamId = teamId - cmd.OrgId = orgId - if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNotFound { return Error(404, "Team not found", nil) From 6a63725df04fa5a2c717f7af63b485a171cb66fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 13 Mar 2019 16:48:15 +0100 Subject: [PATCH 165/194] teams: comment explaining input validation Co-Authored-By: xlson --- pkg/services/sqlstore/team.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 85801f42832..d76d8401499 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -283,7 +283,7 @@ func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { } - if cmd.Permission != m.PERMISSION_ADMIN { + if cmd.Permission != m.PERMISSION_ADMIN { // make sure we don't get invalid permission levels in store cmd.Permission = 0 } From 178d637b4e5fa21fb89ed102de6e6a6c022fe065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 14 Mar 2019 07:53:31 +0100 Subject: [PATCH 166/194] refactor: splitted TeamMembers to TeamMemberRow --- .../app/features/teams/TeamMemberRow.test.tsx | 82 + public/app/features/teams/TeamMemberRow.tsx | 106 + .../app/features/teams/TeamMembers.test.tsx | 206 +- public/app/features/teams/TeamMembers.tsx | 95 +- .../__snapshots__/TeamMemberRow.test.tsx.snap | 191 ++ .../__snapshots__/TeamMembers.test.tsx.snap | 2430 ++--------------- 6 files changed, 677 insertions(+), 2433 deletions(-) create mode 100644 public/app/features/teams/TeamMemberRow.test.tsx create mode 100644 public/app/features/teams/TeamMemberRow.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap diff --git a/public/app/features/teams/TeamMemberRow.test.tsx b/public/app/features/teams/TeamMemberRow.test.tsx new file mode 100644 index 00000000000..87f771cc833 --- /dev/null +++ b/public/app/features/teams/TeamMemberRow.test.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamMember, TeamPermissionLevel } from '../../types'; +import { getMockTeamMember } from './__mocks__/teamMocks'; +import { TeamMemberRow, Props } from './TeamMemberRow'; +import { SelectOptionItem } from '@grafana/ui'; + +const setup = (propOverrides?: object) => { + const props: Props = { + member: getMockTeamMember(), + syncEnabled: false, + editorsCanAdmin: false, + signedInUserIsTeamAdmin: false, + updateTeamMember: jest.fn(), + removeTeamMember: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamMemberRow; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + describe('when feature toggle editorsCanAdmin is turned on', () => { + it('should render permissions select if user is team admin', () => { + const { wrapper } = setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: true }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render span and disable buttons if user is team member', () => { + const { wrapper } = setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: false }); + + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('when feature toggle editorsCanAdmin is turned off', () => { + it('should not render permissions', () => { + const { wrapper } = setup({ editorsCanAdmin: false, signedInUserIsTeamAdmin: true }); + + expect(wrapper).toMatchSnapshot(); + }); + }); +}); + +describe('Functions', () => { + describe('on remove member', () => { + const member = getMockTeamMember(); + const { instance } = setup({ member }); + + instance.onRemoveMember(member); + + expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1); + }); + + describe('on update permision for user in team', () => { + const member: TeamMember = { + userId: 3, + teamId: 2, + avatarUrl: '', + email: 'user@user.org', + labels: [], + login: 'member', + permission: TeamPermissionLevel.Member, + }; + const { instance } = setup({ member }); + const permission = TeamPermissionLevel.Admin; + const item: SelectOptionItem = { value: permission }; + const expectedTeamMemeber = { ...member, permission }; + + instance.onPermissionChange(item, member); + + expect(instance.props.updateTeamMember).toHaveBeenCalledWith(expectedTeamMemeber); + }); +}); diff --git a/public/app/features/teams/TeamMemberRow.tsx b/public/app/features/teams/TeamMemberRow.tsx new file mode 100644 index 00000000000..e0bd26f4fd7 --- /dev/null +++ b/public/app/features/teams/TeamMemberRow.tsx @@ -0,0 +1,106 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import { DeleteButton, Select, SelectOptionItem } from '@grafana/ui'; + +import { TeamMember, teamsPermissionLevels } from 'app/types'; +import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle'; +import { updateTeamMember, removeTeamMember } from './state/actions'; +import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; + +export interface Props { + member: TeamMember; + syncEnabled: boolean; + editorsCanAdmin: boolean; + signedInUserIsTeamAdmin: boolean; + removeTeamMember?: typeof removeTeamMember; + updateTeamMember?: typeof updateTeamMember; +} + +export class TeamMemberRow extends PureComponent { + constructor(props: Props) { + super(props); + this.renderLabels = this.renderLabels.bind(this); + this.renderPermissions = this.renderPermissions.bind(this); + } + + onRemoveMember(member: TeamMember) { + this.props.removeTeamMember(member.userId); + } + + onPermissionChange = (item: SelectOptionItem, member: TeamMember) => { + const permission = item.value; + const updatedTeamMember = { ...member, permission }; + + this.props.updateTeamMember(updatedTeamMember); + }; + + renderPermissions(member: TeamMember) { + const { editorsCanAdmin, signedInUserIsTeamAdmin } = this.props; + const value = teamsPermissionLevels.find(dp => dp.value === member.permission); + + return ( + + + + ); + } + + renderLabels(labels: string[]) { + if (!labels) { + return + ); + } + + render() { + const { member, syncEnabled, signedInUserIsTeamAdmin } = this.props; + return ( + + + + + {this.renderPermissions(member)} + {syncEnabled && this.renderLabels(member.labels)} + + + ); + } +} + +function mapStateToProps(state) { + return {}; +} + +const mapDispatchToProps = { + removeTeamMember, + updateTeamMember, +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(TeamMemberRow); diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx index f6f0b4a5e49..fc65b0532e1 100644 --- a/public/app/features/teams/TeamMembers.test.tsx +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -1,45 +1,29 @@ import React from 'react'; import { shallow } from 'enzyme'; import { TeamMembers, Props, State } from './TeamMembers'; -import { TeamMember, TeamPermissionLevel } from '../../types'; -import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks'; -import { SelectOptionItem } from '@grafana/ui'; -import { contextSrv } from 'app/core/services/context_srv'; +import { TeamMember, OrgRole } from '../../types'; +import { getMockTeamMembers } from './__mocks__/teamMocks'; +import { User } from 'app/core/services/context_srv'; const signedInUserId = 1; -const originalContextSrv = contextSrv; -jest.mock('app/core/services/context_srv', () => ({ - contextSrv: { - isGrafanaAdmin: false, - hasRole: role => false, - user: { id: signedInUserId }, - }, -})); - -interface SetupProps { - propOverrides?: object; - isGrafanaAdmin?: boolean; - isOrgAdmin?: boolean; -} - -const setup = (setupProps: SetupProps) => { +const setup = (propOverrides?: object) => { const props: Props = { members: [] as TeamMember[], searchMemberQuery: '', setSearchMemberQuery: jest.fn(), loadTeamMembers: jest.fn(), addTeamMember: jest.fn(), - removeTeamMember: jest.fn(), - updateTeamMember: jest.fn(), syncEnabled: false, editorsCanAdmin: false, + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: false, + orgRole: OrgRole.Viewer, + } as User, }; - contextSrv.isGrafanaAdmin = setupProps.isGrafanaAdmin || false; - contextSrv.hasRole = role => setupProps.isOrgAdmin || false; - - Object.assign(props, setupProps.propOverrides); + Object.assign(props, propOverrides); const wrapper = shallow(); const instance = wrapper.instance() as TeamMembers; @@ -51,11 +35,6 @@ const setup = (setupProps: SetupProps) => { }; describe('Render', () => { - beforeEach(() => { - contextSrv.isGrafanaAdmin = originalContextSrv.isGrafanaAdmin; - contextSrv.hasRole = originalContextSrv.hasRole; - }); - it('should render component', () => { const { wrapper } = setup({}); @@ -63,74 +42,16 @@ describe('Render', () => { }); it('should render team members', () => { - const { wrapper } = setup({ - propOverrides: { - members: getMockTeamMembers(5, 5), - }, - }); + const { wrapper } = setup({ members: getMockTeamMembers(5, 5) }); expect(wrapper).toMatchSnapshot(); }); it('should render team members when sync enabled', () => { - const { wrapper } = setup({ - propOverrides: { - members: getMockTeamMembers(5, 5), - syncEnabled: true, - }, - }); + const { wrapper } = setup({ members: getMockTeamMembers(5, 5), syncEnabled: true }); expect(wrapper).toMatchSnapshot(); }); - - describe('when feature toggle editorsCanAdmin is turned on', () => { - it('should render permissions select if user is Grafana Admin', () => { - const members = getMockTeamMembers(5, 5); - const { wrapper } = setup({ - propOverrides: { members, editorsCanAdmin: true }, - isGrafanaAdmin: true, - isOrgAdmin: false, - }); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render permissions select if user is Org Admin', () => { - const members = getMockTeamMembers(5, 5); - const { wrapper } = setup({ - propOverrides: { members, editorsCanAdmin: true }, - isGrafanaAdmin: false, - isOrgAdmin: true, - }); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render permissions select if user is team admin', () => { - const members = getMockTeamMembers(5, signedInUserId); - const { wrapper } = setup({ - propOverrides: { members, editorsCanAdmin: true }, - isGrafanaAdmin: false, - isOrgAdmin: false, - }); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render span and disable buttons if user is team member', () => { - const members = getMockTeamMembers(5, 5); - const { wrapper } = setup({ - propOverrides: { - members, - editorsCanAdmin: true, - }, - isGrafanaAdmin: false, - isOrgAdmin: false, - }); - - expect(wrapper).toMatchSnapshot(); - }); - }); }); describe('Functions', () => { @@ -144,15 +65,6 @@ describe('Functions', () => { }); }); - describe('on remove member', () => { - const { instance } = setup({}); - const mockTeamMember = getMockTeamMember(); - - instance.onRemoveMember(mockTeamMember); - - expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1); - }); - describe('on add user to team', () => { const { wrapper, instance } = setup({}); const state = wrapper.state() as State; @@ -169,23 +81,85 @@ describe('Functions', () => { expect(instance.props.addTeamMember).toHaveBeenCalledWith(1); }); - describe('on update permision for user in team', () => { - const { instance } = setup({}); - const permission = TeamPermissionLevel.Admin; - const item: SelectOptionItem = { value: permission }; - const member: TeamMember = { - userId: 3, - teamId: 2, - avatarUrl: '', - email: 'user@user.org', - labels: [], - login: 'member', - permission: TeamPermissionLevel.Member, - }; - const expectedTeamMemeber = { ...member, permission }; + describe('isSignedInUserTeamAdmin', () => { + describe('when feature toggle editorsCanAdmin is turned off', () => { + it('should return true', () => { + const { instance } = setup({ editorsCanAdmin: false }); - instance.onPermissionChange(item, member); + const result = instance.isSignedInUserTeamAdmin(); - expect(instance.props.updateTeamMember).toHaveBeenCalledWith(expectedTeamMemeber); + expect(result).toBe(true); + }); + }); + + describe('when feature toggle editorsCanAdmin is turned on', () => { + it('should return true if signed in user is grafanaAdmin', () => { + const members = getMockTeamMembers(5, 5); + const { instance } = setup({ + members, + editorsCanAdmin: true, + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: true, + orgRole: OrgRole.Viewer, + }, + }); + + const result = instance.isSignedInUserTeamAdmin(); + + expect(result).toBe(true); + }); + + it('should return true if signed in user is org admin', () => { + const members = getMockTeamMembers(5, 5); + const { instance } = setup({ + members, + editorsCanAdmin: true, + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: false, + orgRole: OrgRole.Admin, + }, + }); + + const result = instance.isSignedInUserTeamAdmin(); + + expect(result).toBe(true); + }); + + it('should return true if signed in user is team admin', () => { + const members = getMockTeamMembers(5, signedInUserId); + const { instance } = setup({ + members, + editorsCanAdmin: true, + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: false, + orgRole: OrgRole.Viewer, + }, + }); + + const result = instance.isSignedInUserTeamAdmin(); + + expect(result).toBe(true); + }); + + it('should return false if signed in user is not grafanaAdmin, org admin or team admin', () => { + const members = getMockTeamMembers(5, 5); + const { instance } = setup({ + members, + editorsCanAdmin: true, + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: false, + orgRole: OrgRole.Viewer, + }, + }); + + const result = instance.isSignedInUserTeamAdmin(); + + expect(result).toBe(false); + }); + }); }); }); diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index fc5706ec68f..db2b9024a1f 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -2,32 +2,25 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker } from 'app/core/components/Select/UserPicker'; -import { DeleteButton, Select, SelectOptionItem } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; -import { TeamMember, User, teamsPermissionLevels, TeamPermissionLevel, OrgRole } from 'app/types'; -import { - loadTeamMembers, - addTeamMember, - removeTeamMember, - setSearchMemberQuery, - updateTeamMember, -} from './state/actions'; +import { TeamMember, User, TeamPermissionLevel, OrgRole } from 'app/types'; +import { loadTeamMembers, addTeamMember, setSearchMemberQuery } from './state/actions'; import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle'; import { config } from 'app/core/config'; -import { contextSrv } from 'app/core/services/context_srv'; +import { contextSrv, User as SignedInUser } from 'app/core/services/context_srv'; +import TeamMemberRow from './TeamMemberRow'; export interface Props { members: TeamMember[]; searchMemberQuery: string; loadTeamMembers: typeof loadTeamMembers; addTeamMember: typeof addTeamMember; - removeTeamMember: typeof removeTeamMember; setSearchMemberQuery: typeof setSearchMemberQuery; - updateTeamMember: typeof updateTeamMember; syncEnabled: boolean; editorsCanAdmin?: boolean; + signedInUser?: SignedInUser; } export interface State { @@ -39,7 +32,6 @@ export class TeamMembers extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newTeamMember: null }; - this.renderPermissions = this.renderPermissions.bind(this); } componentDidMount() { @@ -50,10 +42,6 @@ export class TeamMembers extends PureComponent { this.props.setSearchMemberQuery(value); }; - onRemoveMember(member: TeamMember) { - this.props.removeTeamMember(member.userId); - } - onToggleAdding = () => { this.setState({ isAdding: !this.state.isAdding }); }; @@ -81,65 +69,16 @@ export class TeamMembers extends PureComponent { ); } - onPermissionChange = (item: SelectOptionItem, member: TeamMember) => { - const permission = item.value; - const updatedTeamMember = { ...member, permission }; - - this.props.updateTeamMember(updatedTeamMember); - }; - - private isSignedInUserTeamAdmin = () => { - const { members, editorsCanAdmin } = this.props; - const userInMembers = members.find(m => m.userId === contextSrv.user.id); - const isAdmin = contextSrv.isGrafanaAdmin || contextSrv.hasRole(OrgRole.Admin); + isSignedInUserTeamAdmin = (): boolean => { + const { members, editorsCanAdmin, signedInUser } = this.props; + const userInMembers = members.find(m => m.userId === signedInUser.id); + const isAdmin = signedInUser.isGrafanaAdmin || signedInUser.orgRole === OrgRole.Admin; const userIsTeamAdmin = userInMembers && userInMembers.permission === TeamPermissionLevel.Admin; const isSignedInUserTeamAdmin = isAdmin || userIsTeamAdmin; return isSignedInUserTeamAdmin || !editorsCanAdmin; }; - renderPermissions(member: TeamMember) { - const { editorsCanAdmin } = this.props; - const isUserTeamAdmin = this.isSignedInUserTeamAdmin(); - const value = teamsPermissionLevels.find(dp => dp.value === member.permission); - - return ( - - - - ); - } - - renderMember(member: TeamMember, syncEnabled: boolean) { - return ( - - - - - {this.renderPermissions(member)} - {syncEnabled && this.renderLabels(member.labels)} - - - ); - } - render() { const { isAdding } = this.state; const { searchMemberQuery, members, syncEnabled, editorsCanAdmin } = this.props; @@ -198,7 +137,18 @@ export class TeamMembers extends PureComponent { - {members && members.map(member => this.renderMember(member, syncEnabled))} + + {members && + members.map(member => ( + + ))} +
+ + Name + + Email + + Permission + +
+ + + testUser-1 + + test@test.com + +
+ + Member + +
+
+ +
+ + + testUser-2 + + test@test.com + +
+ + Member + +
+
+ +
+ + + testUser-3 + + test@test.com + +
+ + Member + +
+
+ +
+ + + testUser-4 + + test@test.com + +
+ + Member + +
+
+ +
+ + + testUser-5 + + test@test.com + +
+ + Admin + +
+
+ +
+ {signedInUserIsTeamAdmin && ( +
; + } + + return ( + + {labels.map(label => ( + {}} /> + ))} +
+ + {member.login}{member.email} + this.onRemoveMember(member)} disabled={!signedInUserIsTeamAdmin} /> +
-
- {isUserTeamAdmin && ( -
- - {member.login}{member.email} - this.onRemoveMember(member)} disabled={!this.isSignedInUserTeamAdmin()} /> -
@@ -211,15 +161,14 @@ function mapStateToProps(state) { members: getTeamMembers(state.team), searchMemberQuery: getSearchMemberQuery(state.team), editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests, + signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests, }; } const mapDispatchToProps = { loadTeamMembers, addTeamMember, - removeTeamMember, setSearchMemberQuery, - updateTeamMember, }; export default connect( diff --git a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap new file mode 100644 index 00000000000..3e7630d0618 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap @@ -0,0 +1,191 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render when feature toggle editorsCanAdmin is turned off should not render permissions 1`] = ` + + + + + + testUser + + + test@test.com + + + +
+ +
+ +
+ + + + +`; + +exports[`Render when feature toggle editorsCanAdmin is turned on should render span and disable buttons if user is team member 1`] = ` + + + + + + testUser + + + test@test.com + + + +
+ + Member + +
+ +
+ + + + +`; diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap index da89d26d191..4d35a4a772b 100644 --- a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -177,361 +177,106 @@ exports[`Render should render team members 1`] = ` - - - - - - testUser-1 - - - test@test.com - - - -
- -
- -
- - - - - + - - - - - testUser-3 - - - test@test.com - - - -
- -
- -
- - - - - + - - - - - testUser-5 - - - test@test.com - - - -
- -
- -
- - - - - - - - - - - - - - testUser-2 - - - test@test.com - - - -
- -
- -
- - - - - - - - - - - - - - testUser-4 - - - test@test.com - - - -
- -
- -
- - - - - - - - - - -
-
-`; - -exports[`Render when feature toggle editorsCanAdmin is turned on should render permissions select if user is Grafana Admin 1`] = ` -
-
-
- -
-
- -
- -
- -
- Add team member -
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Name - - Email - - Permission - -
- - - testUser-1 - - test@test.com - -
-
- -
- - - testUser-2 - - test@test.com - -
-
- -
- - - testUser-3 - - test@test.com - -
-
- -
- - - testUser-4 - - test@test.com - -
-
- -
- - - testUser-5 - - test@test.com - -
-
- -
-
-
-`; - -exports[`Render when feature toggle editorsCanAdmin is turned on should render permissions select if user is Org Admin 1`] = ` -
-
-
- -
-
- -
- -
- -
- Add team member -
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Name - - Email - - Permission - -
- - - testUser-1 - - test@test.com - -
-
- -
- - - testUser-2 - - test@test.com - -
-
- -
- - - testUser-3 - - test@test.com - -
-
- -
- - - testUser-4 - - test@test.com - -
-
- -
- - - testUser-5 - - test@test.com - -
-
- -
-
-
-`; - -exports[`Render when feature toggle editorsCanAdmin is turned on should render permissions select if user is team admin 1`] = ` -
-
-
- -
-
- -
- -
- -
- Add team member -
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - -
- - Name - - Email - - Permission - -
- - - testUser-1 - - test@test.com - -
- - Admin - -
-
- -
- - - testUser-2 - - test@test.com - -
- - Member - -
-
- -
- - - testUser-3 - - test@test.com - -
- - Member - -
-
- -
- - - testUser-4 - - test@test.com - -
- - Member - -
-
- -
- - - testUser-5 - - test@test.com - -
- - Member - -
-
- -
-
-
-`; - -exports[`Render when feature toggle editorsCanAdmin is turned on should render span and disable buttons if user is team member 1`] = ` -
-
-
- -
-
- -
- -
- -
- Add team member -
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - + member={ + Object { + "avatarUrl": "some/url/", + "email": "test@test.com", + "labels": Array [ + "label 1", + "label 2", + ], + "login": "testUser-5", + "permission": 4, + "teamId": 1, + "userId": 5, + } + } + signedInUserIsTeamAdmin={true} + syncEnabled={true} + />
- - Name - - Email - - Permission - -
- - - testUser-1 - - test@test.com - -
- - Member - -
-
- -
- - - testUser-2 - - test@test.com - -
- - Member - -
-
- -
- - - testUser-3 - - test@test.com - -
- - Member - -
-
- -
- - - testUser-4 - - test@test.com - -
- - Member - -
-
- -
- - - testUser-5 - - test@test.com - -
- - Admin - -
-
- -
From e3fc61b326e8bdfd4f01cdbf25b6927454477e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 14 Mar 2019 08:02:24 +0100 Subject: [PATCH 167/194] refactor: moved test from TeamMembers to TeamMemberRow --- .../app/features/teams/TeamMemberRow.test.tsx | 8 + .../app/features/teams/TeamMembers.test.tsx | 6 - .../__snapshots__/TeamMemberRow.test.tsx.snap | 51 +++++ .../__snapshots__/TeamMembers.test.tsx.snap | 203 +----------------- 4 files changed, 64 insertions(+), 204 deletions(-) diff --git a/public/app/features/teams/TeamMemberRow.test.tsx b/public/app/features/teams/TeamMemberRow.test.tsx index 87f771cc833..0607825bff3 100644 --- a/public/app/features/teams/TeamMemberRow.test.tsx +++ b/public/app/features/teams/TeamMemberRow.test.tsx @@ -27,6 +27,14 @@ const setup = (propOverrides?: object) => { }; describe('Render', () => { + it('should render team members when sync enabled', () => { + const member = getMockTeamMember(); + member.labels = ['LDAP']; + const { wrapper } = setup({ member, syncEnabled: true }); + + expect(wrapper).toMatchSnapshot(); + }); + describe('when feature toggle editorsCanAdmin is turned on', () => { it('should render permissions select if user is team admin', () => { const { wrapper } = setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: true }); diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx index fc65b0532e1..02bfc4149f0 100644 --- a/public/app/features/teams/TeamMembers.test.tsx +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -46,12 +46,6 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); - - it('should render team members when sync enabled', () => { - const { wrapper } = setup({ members: getMockTeamMembers(5, 5), syncEnabled: true }); - - expect(wrapper).toMatchSnapshot(); - }); }); describe('Functions', () => { diff --git a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap index 3e7630d0618..3dff08ddc1e 100644 --- a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap @@ -1,5 +1,56 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Render should render team members when sync enabled 1`] = ` + + + + + + testUser + + + test@test.com + + + +
+ + Member + +
+ +
+ + + + + + + +`; + exports[`Render when feature toggle editorsCanAdmin is turned off should not render permissions 1`] = ` - - - - -
`; - -exports[`Render should render team members when sync enabled 1`] = ` -
-
-
- -
-
- -
- -
- -
- Add team member -
-
- -
-
-
-
- - - - - - - - - - - - - - - - - -
- - Name - - Email - - Permission - - -
-
-
-`; From 8c34f595f0683d2277e5f907039689348de9e2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 14 Mar 2019 08:21:53 +0100 Subject: [PATCH 168/194] teams: disable new team button if user is viewer --- public/app/features/teams/TeamList.test.tsx | 44 ++- public/app/features/teams/TeamList.tsx | 13 +- .../__snapshots__/TeamList.test.tsx.snap | 250 ++++++++++++++++++ 3 files changed, 303 insertions(+), 4 deletions(-) diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 369771bb340..da5afb58796 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Props, TeamList } from './TeamList'; -import { NavModel, Team } from '../../types'; +import { NavModel, Team, OrgRole } from '../../types'; import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks'; +import { User } from 'app/core/services/context_srv'; const setup = (propOverrides?: object) => { const props: Props = { @@ -21,6 +22,11 @@ const setup = (propOverrides?: object) => { searchQuery: '', teamsCount: 0, hasFetched: false, + editorsCanAdmin: false, + signedInUser: { + id: 1, + orgRole: OrgRole.Viewer, + } as User, }; Object.assign(props, propOverrides); @@ -49,6 +55,42 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); + + describe('when feature toggle editorsCanAdmin is turned on', () => { + describe('and signedin user is not viewer', () => { + it('should enable the new team button', () => { + const { wrapper } = setup({ + teams: getMultipleMockTeams(1), + teamsCount: 1, + hasFetched: true, + editorsCanAdmin: true, + signedInUser: { + id: 1, + orgRole: OrgRole.Editor, + } as User, + }); + + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('and signedin user is a viewer', () => { + it('should disable the new team button', () => { + const { wrapper } = setup({ + teams: getMultipleMockTeams(1), + teamsCount: 1, + hasFetched: true, + editorsCanAdmin: true, + signedInUser: { + id: 1, + orgRole: OrgRole.Viewer, + } as User, + }); + + expect(wrapper).toMatchSnapshot(); + }); + }); + }); }); describe('Life cycle', () => { diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 60921a3378b..f603994b578 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,11 +4,13 @@ import { hot } from 'react-hot-loader'; import Page from 'app/core/components/Page/Page'; import { DeleteButton } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import { NavModel, Team } from 'app/types'; +import { NavModel, Team, OrgRole } from 'app/types'; import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; +import { config } from 'app/core/config'; +import { contextSrv, User } from 'app/core/services/context_srv'; export interface Props { navModel: NavModel; @@ -19,6 +21,8 @@ export interface Props { loadTeams: typeof loadTeams; deleteTeam: typeof deleteTeam; setSearchQuery: typeof setSearchQuery; + editorsCanAdmin?: boolean; + signedInUser?: User; } export class TeamList extends PureComponent { @@ -84,7 +88,8 @@ export class TeamList extends PureComponent { } renderTeamList() { - const { teams, searchQuery } = this.props; + const { teams, searchQuery, editorsCanAdmin, signedInUser } = this.props; + const disabledClass = editorsCanAdmin && signedInUser.orgRole === OrgRole.Viewer ? ' disabled' : ''; return ( <> @@ -101,7 +106,7 @@ export class TeamList extends PureComponent { @@ -152,6 +157,8 @@ function mapStateToProps(state) { searchQuery: getSearchQuery(state.teams), teamsCount: getTeamsCount(state.teams), hasFetched: state.teams.hasFetched, + editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests, + signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests, }; } diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index 76d13e6a3d6..d4dd2170bae 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -343,3 +343,253 @@ exports[`Render should render teams table 1`] = ` `; + +exports[`Render when feature toggle editorsCanAdmin is turned on and signedin user is a viewer should disable the new team button 1`] = ` + + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
+ + Name + + Email + + Members + +
+ + + + + + test-1 + + + + test-1@test.com + + + + 1 + + + +
+
+ + +`; + +exports[`Render when feature toggle editorsCanAdmin is turned on and signedin user is not viewer should enable the new team button 1`] = ` + + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
+ + Name + + Email + + Members + +
+ + + + + + test-1 + + + + test-1@test.com + + + + 1 + + + +
+
+ + +`; From d1481cac50fff0114c833c43be8905f5b505ee4f Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 14 Mar 2019 09:08:48 +0100 Subject: [PATCH 169/194] teams: refactored db code. --- pkg/services/sqlstore/migrations/team_mig.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go index 1ec27ee926d..981a4865caa 100644 --- a/pkg/services/sqlstore/migrations/team_mig.go +++ b/pkg/services/sqlstore/migrations/team_mig.go @@ -56,8 +56,6 @@ func addTeamMigrations(mg *Migrator) { })) mg.AddMigration("Add column permission to team_member table", NewAddColumnMigration(teamMemberV1, &Column{ - Name: "permission", - Type: DB_BigInt, - Nullable: true, + Name: "permission", Type: DB_SmallInt, Nullable: true, })) } From 13ed10495a96927bb31e26fe72d7aec579c4ce66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 14 Mar 2019 09:49:35 +0100 Subject: [PATCH 170/194] teams: hide tabs settings and groupsync for non team admins --- .../app/features/teams/TeamMembers.test.tsx | 82 ---------------- public/app/features/teams/TeamMembers.tsx | 22 ++--- public/app/features/teams/TeamPages.test.tsx | 52 +++++++++- public/app/features/teams/TeamPages.tsx | 32 +++++-- .../__snapshots__/TeamPages.test.tsx.snap | 22 +++++ .../features/teams/state/selectors.test.ts | 96 ++++++++++++++++++- public/app/features/teams/state/selectors.ts | 18 +++- 7 files changed, 215 insertions(+), 109 deletions(-) diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx index 02bfc4149f0..64e679f92ef 100644 --- a/public/app/features/teams/TeamMembers.test.tsx +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -74,86 +74,4 @@ describe('Functions', () => { expect(instance.props.addTeamMember).toHaveBeenCalledWith(1); }); - - describe('isSignedInUserTeamAdmin', () => { - describe('when feature toggle editorsCanAdmin is turned off', () => { - it('should return true', () => { - const { instance } = setup({ editorsCanAdmin: false }); - - const result = instance.isSignedInUserTeamAdmin(); - - expect(result).toBe(true); - }); - }); - - describe('when feature toggle editorsCanAdmin is turned on', () => { - it('should return true if signed in user is grafanaAdmin', () => { - const members = getMockTeamMembers(5, 5); - const { instance } = setup({ - members, - editorsCanAdmin: true, - signedInUser: { - id: signedInUserId, - isGrafanaAdmin: true, - orgRole: OrgRole.Viewer, - }, - }); - - const result = instance.isSignedInUserTeamAdmin(); - - expect(result).toBe(true); - }); - - it('should return true if signed in user is org admin', () => { - const members = getMockTeamMembers(5, 5); - const { instance } = setup({ - members, - editorsCanAdmin: true, - signedInUser: { - id: signedInUserId, - isGrafanaAdmin: false, - orgRole: OrgRole.Admin, - }, - }); - - const result = instance.isSignedInUserTeamAdmin(); - - expect(result).toBe(true); - }); - - it('should return true if signed in user is team admin', () => { - const members = getMockTeamMembers(5, signedInUserId); - const { instance } = setup({ - members, - editorsCanAdmin: true, - signedInUser: { - id: signedInUserId, - isGrafanaAdmin: false, - orgRole: OrgRole.Viewer, - }, - }); - - const result = instance.isSignedInUserTeamAdmin(); - - expect(result).toBe(true); - }); - - it('should return false if signed in user is not grafanaAdmin, org admin or team admin', () => { - const members = getMockTeamMembers(5, 5); - const { instance } = setup({ - members, - editorsCanAdmin: true, - signedInUser: { - id: signedInUserId, - isGrafanaAdmin: false, - orgRole: OrgRole.Viewer, - }, - }); - - const result = instance.isSignedInUserTeamAdmin(); - - expect(result).toBe(false); - }); - }); - }); }); diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index db2b9024a1f..76fbdc87f1c 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -3,9 +3,9 @@ import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker } from 'app/core/components/Select/UserPicker'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; -import { TeamMember, User, TeamPermissionLevel, OrgRole } from 'app/types'; +import { TeamMember, User } from 'app/types'; import { loadTeamMembers, addTeamMember, setSearchMemberQuery } from './state/actions'; -import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; +import { getSearchMemberQuery, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle'; import { config } from 'app/core/config'; @@ -69,19 +69,11 @@ export class TeamMembers extends PureComponent { ); } - isSignedInUserTeamAdmin = (): boolean => { - const { members, editorsCanAdmin, signedInUser } = this.props; - const userInMembers = members.find(m => m.userId === signedInUser.id); - const isAdmin = signedInUser.isGrafanaAdmin || signedInUser.orgRole === OrgRole.Admin; - const userIsTeamAdmin = userInMembers && userInMembers.permission === TeamPermissionLevel.Admin; - const isSignedInUserTeamAdmin = isAdmin || userIsTeamAdmin; - - return isSignedInUserTeamAdmin || !editorsCanAdmin; - }; - render() { const { isAdding } = this.state; - const { searchMemberQuery, members, syncEnabled, editorsCanAdmin } = this.props; + const { searchMemberQuery, members, syncEnabled, editorsCanAdmin, signedInUser } = this.props; + const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser }); + return (
@@ -100,7 +92,7 @@ export class TeamMembers extends PureComponent { @@ -145,7 +137,7 @@ export class TeamMembers extends PureComponent { member={member} syncEnabled={syncEnabled} editorsCanAdmin={editorsCanAdmin} - signedInUserIsTeamAdmin={this.isSignedInUserTeamAdmin()} + signedInUserIsTeamAdmin={isTeamAdmin} /> ))} diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index 5d751f46989..74e9dfa24f2 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { TeamPages, Props } from './TeamPages'; -import { NavModel, Team } from '../../types'; +import { NavModel, Team, TeamMember, OrgRole } from '../../types'; import { getMockTeam } from './__mocks__/teamMocks'; +import { User } from 'app/core/services/context_srv'; jest.mock('app/core/config', () => ({ buildInfo: { isEnterprise: true }, @@ -15,6 +16,13 @@ const setup = (propOverrides?: object) => { loadTeam: jest.fn(), pageName: 'members', team: {} as Team, + members: [] as TeamMember[], + editorsCanAdmin: false, + signedInUser: { + id: 1, + isGrafanaAdmin: false, + orgRole: OrgRole.Viewer, + } as User, }; Object.assign(props, propOverrides); @@ -65,4 +73,46 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); + + describe('when feature toggle editorsCanAdmin is turned on', () => { + it('should render settings page if user is team admin', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'settings', + preferences: { + homeDashboardId: 1, + theme: 'Default', + timezone: 'Default', + }, + editorsCanAdmin: true, + signedInUser: { + id: 1, + isGrafanaAdmin: false, + orgRole: OrgRole.Admin, + } as User, + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should not render settings page if user is team member', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'settings', + preferences: { + homeDashboardId: 1, + theme: 'Default', + timezone: 'Default', + }, + editorsCanAdmin: true, + signedInUser: { + id: 1, + isGrafanaAdmin: false, + orgRole: OrgRole.Viewer, + } as User, + }); + + expect(wrapper).toMatchSnapshot(); + }); + }); }); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 15adc8b3856..a64fe61612d 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -7,12 +7,13 @@ import Page from 'app/core/components/Page/Page'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { NavModel, Team } from 'app/types'; +import { NavModel, Team, TeamMember } from 'app/types'; import { loadTeam } from './state/actions'; -import { getTeam } from './state/selectors'; +import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors'; import { getTeamLoadingNav } from './state/navModel'; import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; +import { contextSrv, User } from 'app/core/services/context_srv'; export interface Props { team: Team; @@ -20,6 +21,9 @@ export interface Props { teamId: number; pageName: string; navModel: NavModel; + members?: TeamMember[]; + editorsCanAdmin?: boolean; + signedInUser?: User; } interface State { @@ -61,7 +65,15 @@ export class TeamPages extends PureComponent { return _.includes(pages, currentPage) ? currentPage : pages[0]; } - renderPage() { + hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => { + if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) { + navModel.main.children = navModel.main.children.filter(navItem => navItem.text === 'Members'); + } + + return navModel; + }; + + renderPage(isSignedInUserTeamAdmin: boolean) { const { isSyncEnabled } = this.state; const currentPage = this.getCurrentPage(); @@ -70,21 +82,22 @@ export class TeamPages extends PureComponent { return ; case PageTypes.Settings: - return ; + return isSignedInUserTeamAdmin && ; case PageTypes.GroupSync: - return isSyncEnabled && ; + return isSignedInUserTeamAdmin && isSyncEnabled && ; } return null; } render() { - const { team, navModel } = this.props; + const { team, navModel, members, editorsCanAdmin, signedInUser } = this.props; + const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser }); return ( - + - {team && Object.keys(team).length !== 0 && this.renderPage()} + {team && Object.keys(team).length !== 0 && this.renderPage(isTeamAdmin)} ); @@ -101,6 +114,9 @@ function mapStateToProps(state) { teamId: teamId, pageName: pageName, team: getTeam(state.team, teamId), + members: getTeamMembers(state.team), + editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests, + signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests, }; } diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 70f37cea4c5..6fdf7d063ba 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -47,3 +47,25 @@ exports[`Render should render settings and preferences page 1`] = ` `; + +exports[`Render when feature toggle editorsCanAdmin is turned on should not render settings page if user is team member 1`] = ` + + + +`; + +exports[`Render when feature toggle editorsCanAdmin is turned on should render settings page if user is team admin 1`] = ` + + + + + +`; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index 1721437c929..5d9981e0403 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,6 +1,7 @@ -import { getTeam, getTeamMembers, getTeams } from './selectors'; +import { getTeam, getTeamMembers, getTeams, isSignedInUserTeamAdmin, Config } from './selectors'; import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks'; -import { Team, TeamGroup, TeamsState, TeamState } from '../../../types'; +import { Team, TeamGroup, TeamsState, TeamState, OrgRole } from '../../../types'; +import { User } from 'app/core/services/context_srv'; describe('Teams selectors', () => { describe('Get teams', () => { @@ -55,3 +56,94 @@ describe('Team selectors', () => { }); }); }); + +const signedInUserId = 1; + +const setup = (configOverrides?: Partial) => { + const defaultConfig: Config = { + editorsCanAdmin: false, + members: getMockTeamMembers(5, 5), + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: false, + orgRole: OrgRole.Viewer, + } as User, + }; + + return { ...defaultConfig, ...configOverrides }; +}; + +describe('isSignedInUserTeamAdmin', () => { + describe('when feature toggle editorsCanAdmin is turned off', () => { + it('should return true', () => { + const config = setup(); + + const result = isSignedInUserTeamAdmin(config); + + expect(result).toBe(true); + }); + }); + + describe('when feature toggle editorsCanAdmin is turned on', () => { + it('should return true if signed in user is grafanaAdmin', () => { + const config = setup({ + editorsCanAdmin: true, + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: true, + orgRole: OrgRole.Viewer, + } as User, + }); + + const result = isSignedInUserTeamAdmin(config); + + expect(result).toBe(true); + }); + + it('should return true if signed in user is org admin', () => { + const config = setup({ + editorsCanAdmin: true, + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: false, + orgRole: OrgRole.Admin, + } as User, + }); + + const result = isSignedInUserTeamAdmin(config); + + expect(result).toBe(true); + }); + + it('should return true if signed in user is team admin', () => { + const config = setup({ + members: getMockTeamMembers(5, signedInUserId), + editorsCanAdmin: true, + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: false, + orgRole: OrgRole.Viewer, + } as User, + }); + + const result = isSignedInUserTeamAdmin(config); + + expect(result).toBe(true); + }); + + it('should return false if signed in user is not grafanaAdmin, org admin or team admin', () => { + const config = setup({ + editorsCanAdmin: true, + signedInUser: { + id: signedInUserId, + isGrafanaAdmin: false, + orgRole: OrgRole.Viewer, + } as User, + }); + + const result = isSignedInUserTeamAdmin(config); + + expect(result).toBe(false); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 9201993bf0d..d8b8220bb44 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,4 +1,5 @@ -import { Team, TeamsState, TeamState } from 'app/types'; +import { Team, TeamsState, TeamState, TeamMember, OrgRole, TeamPermissionLevel } from 'app/types'; +import { User } from 'app/core/services/context_srv'; export const getSearchQuery = (state: TeamsState) => state.searchQuery; export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery; @@ -28,3 +29,18 @@ export const getTeamMembers = (state: TeamState) => { return regex.test(member.login) || regex.test(member.email); }); }; + +export interface Config { + members: TeamMember[]; + editorsCanAdmin: boolean; + signedInUser: User; +} + +export const isSignedInUserTeamAdmin = (config: Config): boolean => { + const userInMembers = config.members.find(m => m.userId === config.signedInUser.id); + const isAdmin = config.signedInUser.isGrafanaAdmin || config.signedInUser.orgRole === OrgRole.Admin; + const userIsTeamAdmin = userInMembers && userInMembers.permission === TeamPermissionLevel.Admin; + const isSignedInUserTeamAdmin = isAdmin || userIsTeamAdmin; + + return isSignedInUserTeamAdmin || !config.editorsCanAdmin; +}; From b796027bc6854d663f0789c34324d99146f3d390 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 14 Mar 2019 09:14:35 +0100 Subject: [PATCH 171/194] teams: refactor. --- pkg/services/sqlstore/team.go | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index d76d8401499..e52983b5918 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -259,28 +259,21 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error { func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { return inTransaction(func(sess *DBSession) error { rawSql := `SELECT * FROM team_member WHERE org_id=? and team_id=? and user_id=?` - var member m.TeamMember exists, err := sess.SQL(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId).Get(&member) if err != nil { return err } - if !exists { return m.ErrTeamMemberNotFound } if cmd.ProtectLastAdmin { - lastAdmin, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId) + _, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId) if err != nil { return err } - - if lastAdmin { - return m.ErrLastTeamAdmin - } - } if cmd.Permission != m.PERMISSION_ADMIN { // make sure we don't get invalid permission levels in store @@ -302,15 +295,10 @@ func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error { } if cmd.ProtectLastAdmin { - lastAdmin, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId) + _, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId) if err != nil { return err } - - if lastAdmin { - return m.ErrLastTeamAdmin - } - } var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?" @@ -344,7 +332,7 @@ func isLastAdmin(sess *DBSession, orgId int64, teamId int64, userId int64) (bool } if isAdmin && len(userIds) == 1 { - return true, nil + return true, m.ErrLastTeamAdmin } return false, err From 9f8e43916dbdc49c69b6a2bdcfe1232d827ce522 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 14 Mar 2019 09:18:06 +0100 Subject: [PATCH 172/194] permissions: refactor. --- pkg/api/dashboard.go | 3 +-- pkg/api/folder.go | 3 +-- pkg/services/dashboards/acl_service.go | 14 +------------- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 14a1e3baf32..e7fcf5d4355 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -278,9 +278,8 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) } if hs.Cfg.EditorsCanAdmin && newDashboard { - aclService := dashboards.NewAclService() inFolder := cmd.FolderId > 0 - err := aclService.MakeUserAdmin(cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) + err := dashboards.MakeUserAdmin(cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) if err != nil { hs.log.Error("Could not make user admin", "dashboard", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) return Error(500, "Failed to make user admin of dashboard", err) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 66c640f96e8..4b64fc1139f 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -62,8 +62,7 @@ func (hs *HTTPServer) CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) R } if hs.Cfg.EditorsCanAdmin { - aclService := dashboards.NewAclService() - if err := aclService.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, cmd.Result.Id, true); err != nil { + if err := dashboards.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, cmd.Result.Id, true); err != nil { hs.log.Error("Could not make user admin", "folder", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) return Error(500, "Failed to make user admin of folder", err) } diff --git a/pkg/services/dashboards/acl_service.go b/pkg/services/dashboards/acl_service.go index dae3ec1372b..6158b190d68 100644 --- a/pkg/services/dashboards/acl_service.go +++ b/pkg/services/dashboards/acl_service.go @@ -2,23 +2,11 @@ package dashboards import ( "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "time" ) -// NewService factory for creating a new dashboard service -var NewAclService = func() *AclService { - return &AclService{ - log: log.New("dashboard-acl-service"), - } -} - -type AclService struct { - log log.Logger -} - -func (as *AclService) MakeUserAdmin(orgId int64, userId int64, dashboardId int64, setViewAndEditPermissions bool) error { +func MakeUserAdmin(orgId int64, userId int64, dashboardId int64, setViewAndEditPermissions bool) error { rtEditor := models.ROLE_EDITOR rtViewer := models.ROLE_VIEWER From 9f33f0034342790bce1d289a3d4e9a3965d26587 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 14 Mar 2019 09:37:56 +0100 Subject: [PATCH 173/194] teams: refactor. --- pkg/services/sqlstore/team.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index e52983b5918..b561f2e00f6 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -255,19 +255,28 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error { }) } +func getTeamMember(sess *DBSession, orgId int64, teamId int64, userId int64) (m.TeamMember, error) { + rawSql := `SELECT * FROM team_member WHERE org_id=? and team_id=? and user_id=?` + var member m.TeamMember + exists, err := sess.SQL(rawSql, orgId, teamId, userId).Get(&member) + + if err != nil { + return member, err + } + if !exists { + return member, m.ErrTeamMemberNotFound + } + + return member, nil +} + // UpdateTeamMember updates a team member func UpdateTeamMember(cmd *m.UpdateTeamMemberCommand) error { return inTransaction(func(sess *DBSession) error { - rawSql := `SELECT * FROM team_member WHERE org_id=? and team_id=? and user_id=?` - var member m.TeamMember - exists, err := sess.SQL(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId).Get(&member) - + member, err := getTeamMember(sess, cmd.OrgId, cmd.TeamId, cmd.UserId) if err != nil { return err } - if !exists { - return m.ErrTeamMemberNotFound - } if cmd.ProtectLastAdmin { _, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId) From 6589a4e55f9ed120ec8d36f461cc7ea07a561074 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 14 Mar 2019 10:02:46 +0100 Subject: [PATCH 174/194] teams: better names for api permissions. --- pkg/api/api.go | 8 ++++---- pkg/middleware/auth.go | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9d1151a757e..32213e3a58a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -14,7 +14,7 @@ func (hs *HTTPServer) registerRoutes() { reqGrafanaAdmin := middleware.ReqGrafanaAdmin reqEditorRole := middleware.ReqEditorRole reqOrgAdmin := middleware.ReqOrgAdmin - reqAdminOrCanAdmin := middleware.AdminOrCanAdmin(hs.Cfg.EditorsCanAdmin) + reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin) redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL() quota := middleware.Quota(hs.QuotaService) @@ -42,8 +42,8 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/org/users", reqOrgAdmin, hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index) r.Get("/org/users/invite", reqOrgAdmin, hs.Index) - r.Get("/org/teams", reqAdminOrCanAdmin, hs.Index) - r.Get("/org/teams/*", reqAdminOrCanAdmin, hs.Index) + r.Get("/org/teams", reqCanAccessTeams, hs.Index) + r.Get("/org/teams/*", reqCanAccessTeams, hs.Index) r.Get("/org/apikeys/", reqOrgAdmin, hs.Index) r.Get("/dashboard/import/", reqSignedIn, hs.Index) r.Get("/configuration", reqGrafanaAdmin, hs.Index) @@ -163,7 +163,7 @@ func (hs *HTTPServer) registerRoutes() { teamsRoute.Delete("/:teamId/members/:userId", Wrap(hs.RemoveTeamMember)) teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences)) teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences)) - }, reqAdminOrCanAdmin) + }, reqCanAccessTeams) // team without requirement of user to be org admin apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) { diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 8c1e5e04ae7..c00241ea34c 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -87,7 +87,12 @@ func Auth(options *AuthOptions) macaron.Handler { } } -func AdminOrCanAdmin(enabled bool) macaron.Handler { +// AdminOrFeatureEnabled creates a middleware that allows access +// if the signed in user is either an Org Admin or if the +// feature flag is enabled. +// Intended for when feature flags open up access to APIs that +// are otherwise only available to admins. +func AdminOrFeatureEnabled(enabled bool) macaron.Handler { return func(c *m.ReqContext) { if c.OrgRole == m.ROLE_ADMIN { return From adf0390b2c8f1beaaf0d904b884f59ff160310ee Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 14 Mar 2019 12:10:10 +0100 Subject: [PATCH 175/194] teams: local access to bus, moving away from dep on global. --- pkg/api/api.go | 12 ++++++------ pkg/api/team.go | 20 ++++++++++---------- pkg/api/team_members.go | 16 ++++++++-------- pkg/services/teamguardian/team.go | 2 +- pkg/services/teamguardian/teams_test.go | 8 ++++---- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 32213e3a58a..86bc83f558b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -155,14 +155,14 @@ func (hs *HTTPServer) registerRoutes() { // team (admin permission required) apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) { teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(hs.CreateTeam)) - teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(UpdateTeam)) - teamsRoute.Delete("/:teamId", Wrap(DeleteTeamByID)) + teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(hs.UpdateTeam)) + teamsRoute.Delete("/:teamId", Wrap(hs.DeleteTeamByID)) teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers)) - teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember)) - teamsRoute.Put("/:teamId/members/:userId", bind(m.UpdateTeamMemberCommand{}), Wrap(UpdateTeamMember)) + teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(hs.AddTeamMember)) + teamsRoute.Put("/:teamId/members/:userId", bind(m.UpdateTeamMemberCommand{}), Wrap(hs.UpdateTeamMember)) teamsRoute.Delete("/:teamId/members/:userId", Wrap(hs.RemoveTeamMember)) - teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences)) - teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences)) + teamsRoute.Get("/:teamId/preferences", Wrap(hs.GetTeamPreferences)) + teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(hs.UpdateTeamPreferences)) }, reqCanAccessTeams) // team without requirement of user to be org admin diff --git a/pkg/api/team.go b/pkg/api/team.go index fd34c0ab720..cb0ff2f25a0 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -43,15 +43,15 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo } // PUT /api/teams/:teamId -func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { +func (hs *HTTPServer) UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":teamId") - if err := teamguardian.CanAdmin(cmd.OrgId, cmd.Id, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(hs.Bus, cmd.OrgId, cmd.Id, c.SignedInUser); err != nil { return Error(403, "Not allowed to update team", err) } - if err := bus.Dispatch(&cmd); err != nil { + if err := hs.Bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNameTaken { return Error(400, "Team name taken", err) } @@ -62,16 +62,16 @@ func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { } // DELETE /api/teams/:teamId -func DeleteTeamByID(c *m.ReqContext) Response { +func (hs *HTTPServer) DeleteTeamByID(c *m.ReqContext) Response { orgId := c.OrgId teamId := c.ParamsInt64(":teamId") user := c.SignedInUser - if err := teamguardian.CanAdmin(orgId, teamId, user); err != nil { + if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, user); err != nil { return Error(403, "Not allowed to delete team", err) } - if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: orgId, Id: teamId}); err != nil { + if err := hs.Bus.Dispatch(&m.DeleteTeamCommand{OrgId: orgId, Id: teamId}); err != nil { if err == m.ErrTeamNotFound { return Error(404, "Failed to delete Team. ID not found", nil) } @@ -136,11 +136,11 @@ func GetTeamByID(c *m.ReqContext) Response { } // GET /api/teams/:teamId/preferences -func GetTeamPreferences(c *m.ReqContext) Response { +func (hs *HTTPServer) GetTeamPreferences(c *m.ReqContext) Response { teamId := c.ParamsInt64(":teamId") orgId := c.OrgId - if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to view team preferences.", err) } @@ -148,11 +148,11 @@ func GetTeamPreferences(c *m.ReqContext) Response { } // PUT /api/teams/:teamId/preferences -func UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response { +func (hs *HTTPServer) UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response { teamId := c.ParamsInt64(":teamId") orgId := c.OrgId - if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to update team preferences.", err) } diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 1674cc120ce..54a4d8220e5 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -30,15 +30,15 @@ func GetTeamMembers(c *m.ReqContext) Response { } // POST /api/teams/:teamId/members -func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response { +func (hs *HTTPServer) AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response { cmd.OrgId = c.OrgId cmd.TeamId = c.ParamsInt64(":teamId") - if err := teamguardian.CanAdmin(cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(hs.Bus, cmd.OrgId, cmd.TeamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to add team member", err) } - if err := bus.Dispatch(&cmd); err != nil { + if err := hs.Bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNotFound { return Error(404, "Team not found", nil) } @@ -56,11 +56,11 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response { } // PUT /:teamId/members/:userId -func UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response { +func (hs *HTTPServer) UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response { teamId := c.ParamsInt64(":teamId") orgId := c.OrgId - if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to update team member", err) } @@ -72,7 +72,7 @@ func UpdateTeamMember(c *m.ReqContext, cmd m.UpdateTeamMemberCommand) Response { cmd.UserId = c.ParamsInt64(":userId") cmd.OrgId = orgId - if err := bus.Dispatch(&cmd); err != nil { + if err := hs.Bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamMemberNotFound { return Error(404, "Team member not found.", nil) } @@ -87,7 +87,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *m.ReqContext) Response { teamId := c.ParamsInt64(":teamId") userId := c.ParamsInt64(":userId") - if err := teamguardian.CanAdmin(orgId, teamId, c.SignedInUser); err != nil { + if err := teamguardian.CanAdmin(hs.Bus, orgId, teamId, c.SignedInUser); err != nil { return Error(403, "Not allowed to remove team member", err) } @@ -96,7 +96,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *m.ReqContext) Response { protectLastAdmin = true } - if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: orgId, TeamId: teamId, UserId: userId, ProtectLastAdmin: protectLastAdmin}); err != nil { + if err := hs.Bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: orgId, TeamId: teamId, UserId: userId, ProtectLastAdmin: protectLastAdmin}); err != nil { if err == m.ErrTeamNotFound { return Error(404, "Team not found", nil) } diff --git a/pkg/services/teamguardian/team.go b/pkg/services/teamguardian/team.go index 6fddc318f5e..70053d12da1 100644 --- a/pkg/services/teamguardian/team.go +++ b/pkg/services/teamguardian/team.go @@ -5,7 +5,7 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func CanAdmin(orgId int64, teamId int64, user *m.SignedInUser) error { +func CanAdmin(bus bus.Bus, orgId int64, teamId int64, user *m.SignedInUser) error { if user.OrgRole == m.ROLE_ADMIN { return nil } diff --git a/pkg/services/teamguardian/teams_test.go b/pkg/services/teamguardian/teams_test.go index 2ec86769a29..8af69569620 100644 --- a/pkg/services/teamguardian/teams_test.go +++ b/pkg/services/teamguardian/teams_test.go @@ -33,7 +33,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanAdmin(testTeam.OrgId, testTeam.Id, &editor) + err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &editor) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeam) }) }) @@ -50,7 +50,7 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanAdmin(testTeam.OrgId, testTeam.Id, &editor) + err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &editor) So(err, ShouldBeNil) }) }) @@ -72,14 +72,14 @@ func TestUpdateTeam(t *testing.T) { return nil }) - err := CanAdmin(testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor) + err := CanAdmin(bus.GetBus(), testTeamOtherOrg.OrgId, testTeamOtherOrg.Id, &editor) So(err, ShouldEqual, m.ErrNotAllowedToUpdateTeamInDifferentOrg) }) }) Convey("Given an org admin and a team", func() { Convey("Should be able to update the team", func() { - err := CanAdmin(testTeam.OrgId, testTeam.Id, &admin) + err := CanAdmin(bus.GetBus(), testTeam.OrgId, testTeam.Id, &admin) So(err, ShouldBeNil) }) }) From a615b78f8a17b193e802628f58a6ff92f6fc63b1 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 14 Mar 2019 12:18:07 +0100 Subject: [PATCH 176/194] permissions: removes global access to bus from MakeUserAdmin. --- pkg/api/dashboard.go | 2 +- pkg/api/folder.go | 2 +- pkg/api/team.go | 4 ++-- pkg/services/dashboards/acl_service.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index e7fcf5d4355..c47e8f31ccc 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -279,7 +279,7 @@ func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) if hs.Cfg.EditorsCanAdmin && newDashboard { inFolder := cmd.FolderId > 0 - err := dashboards.MakeUserAdmin(cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) + err := dashboards.MakeUserAdmin(hs.Bus, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) if err != nil { hs.log.Error("Could not make user admin", "dashboard", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) return Error(500, "Failed to make user admin of dashboard", err) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 4b64fc1139f..0a9a2671071 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -62,7 +62,7 @@ func (hs *HTTPServer) CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) R } if hs.Cfg.EditorsCanAdmin { - if err := dashboards.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, cmd.Result.Id, true); err != nil { + if err := dashboards.MakeUserAdmin(hs.Bus, c.OrgId, c.SignedInUser.UserId, cmd.Result.Id, true); err != nil { hs.log.Error("Could not make user admin", "folder", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) return Error(500, "Failed to make user admin of folder", err) } diff --git a/pkg/api/team.go b/pkg/api/team.go index cb0ff2f25a0..ecfd8028c1b 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -16,7 +16,7 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo return Error(403, "Not allowed to create team.", nil) } - if err := bus.Dispatch(&cmd); err != nil { + if err := hs.Bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNameTaken { return Error(409, "Team name taken", err) } @@ -31,7 +31,7 @@ func (hs *HTTPServer) CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Respo Permission: m.PERMISSION_ADMIN, } - if err := bus.Dispatch(&addMemberCmd); err != nil { + if err := hs.Bus.Dispatch(&addMemberCmd); err != nil { c.Logger.Error("Could not add creator to team.", "error", err) } } diff --git a/pkg/services/dashboards/acl_service.go b/pkg/services/dashboards/acl_service.go index 6158b190d68..864fbb80a6b 100644 --- a/pkg/services/dashboards/acl_service.go +++ b/pkg/services/dashboards/acl_service.go @@ -6,7 +6,7 @@ import ( "time" ) -func MakeUserAdmin(orgId int64, userId int64, dashboardId int64, setViewAndEditPermissions bool) error { +func MakeUserAdmin(bus bus.Bus, orgId int64, userId int64, dashboardId int64, setViewAndEditPermissions bool) error { rtEditor := models.ROLE_EDITOR rtViewer := models.ROLE_VIEWER From 53c74fa2f56a4b1b029be8a71a3ec882d88b86e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 14 Mar 2019 14:24:13 +0100 Subject: [PATCH 177/194] teams: refactor so that you can only delete teams if you are team admin --- pkg/models/team.go | 13 ++++++----- pkg/services/sqlstore/team.go | 19 +++++++++++++--- public/app/features/teams/TeamList.tsx | 7 ++++-- .../app/features/teams/__mocks__/teamMocks.ts | 2 ++ .../__snapshots__/TeamList.test.tsx.snap | 7 ++++++ public/app/features/teams/state/navModel.ts | 3 ++- public/app/features/teams/state/selectors.ts | 22 +++++++++++++++---- public/app/types/teams.ts | 3 +++ 8 files changed, 60 insertions(+), 16 deletions(-) diff --git a/pkg/models/team.go b/pkg/models/team.go index 5b659331601..bc8cbba8100 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -73,12 +73,13 @@ type SearchTeamsQuery struct { } type TeamDTO struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Email string `json:"email"` - AvatarUrl string `json:"avatarUrl"` - MemberCount int64 `json:"memberCount"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` + MemberCount int64 `json:"memberCount"` + Permission PermissionType `json:"permission"` } type SearchTeamQueryResult struct { diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index b561f2e00f6..03fd2df78fc 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -23,13 +23,25 @@ func init() { bus.AddHandler("sql", GetTeamMembers) } +func getTeamSearchSqlBase() string { + return `SELECT + team.id as id, + team.org_id, + team.name as name, + team.email as email, + (SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count, + team_member.permission + FROM team as team + INNER JOIN team_member on team.id = team_member.team_id AND team_member.user_id = ? ` +} + func getTeamSelectSqlBase() string { return `SELECT team.id as id, team.org_id, team.name as name, team.email as email, - (SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count + (SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count FROM team as team ` } @@ -146,10 +158,11 @@ func SearchTeams(query *m.SearchTeamsQuery) error { var sql bytes.Buffer params := make([]interface{}, 0) - sql.WriteString(getTeamSelectSqlBase()) if query.UserIdFilter > 0 { - sql.WriteString(`INNER JOIN team_member on team.id = team_member.team_id AND team_member.user_id = ?`) + sql.WriteString(getTeamSearchSqlBase()) params = append(params, query.UserIdFilter) + } else { + sql.WriteString(getTeamSelectSqlBase()) } sql.WriteString(` WHERE team.org_id = ?`) diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index f603994b578..e9d51785d72 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -6,7 +6,7 @@ import { DeleteButton } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { NavModel, Team, OrgRole } from 'app/types'; import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; -import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors'; +import { getSearchQuery, getTeams, getTeamsCount, isPermissionTeamAdmin } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { config } from 'app/core/config'; @@ -43,7 +43,10 @@ export class TeamList extends PureComponent { }; renderTeam(team: Team) { + const { editorsCanAdmin, signedInUser } = this.props; + const permission = team.permission; const teamUrl = `org/teams/edit/${team.id}`; + const canDelete = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser }); return ( @@ -62,7 +65,7 @@ export class TeamList extends PureComponent { {team.memberCount} - this.deleteTeam(team)} /> + this.deleteTeam(team)} disabled={!canDelete} /> ); diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index f38f8f2b144..abaa5ef555f 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -9,6 +9,7 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { avatarUrl: 'some/url/', email: `test-${i}@test.com`, memberCount: i, + permission: TeamPermissionLevel.Member, }); } @@ -22,6 +23,7 @@ export const getMockTeam = (): Team => { avatarUrl: 'some/url/', email: 'test@test.com', memberCount: 1, + permission: TeamPermissionLevel.Member, }; }; diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index d4dd2170bae..430466559c5 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -133,6 +133,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > @@ -183,6 +184,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > @@ -233,6 +235,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > @@ -283,6 +286,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > @@ -333,6 +337,7 @@ exports[`Render should render teams table 1`] = ` className="text-right" > @@ -458,6 +463,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us className="text-right" > @@ -583,6 +589,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us className="text-right" > diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts index 2fd5a68e680..aeb6b85f91e 100644 --- a/public/app/features/teams/state/navModel.ts +++ b/public/app/features/teams/state/navModel.ts @@ -1,4 +1,4 @@ -import { Team, NavModelItem, NavModel } from 'app/types'; +import { Team, NavModelItem, NavModel, TeamPermissionLevel } from 'app/types'; import config from 'app/core/config'; export function buildNavModel(team: Team): NavModelItem { @@ -47,6 +47,7 @@ export function getTeamLoadingNav(pageName: string): NavModel { name: 'Loading', email: 'loading', memberCount: 0, + permission: TeamPermissionLevel.Member, }); let node: NavModelItem; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index d8b8220bb44..e770abfc093 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -37,10 +37,24 @@ export interface Config { } export const isSignedInUserTeamAdmin = (config: Config): boolean => { - const userInMembers = config.members.find(m => m.userId === config.signedInUser.id); - const isAdmin = config.signedInUser.isGrafanaAdmin || config.signedInUser.orgRole === OrgRole.Admin; - const userIsTeamAdmin = userInMembers && userInMembers.permission === TeamPermissionLevel.Admin; + const { members, signedInUser, editorsCanAdmin } = config; + const userInMembers = members.find(m => m.userId === signedInUser.id); + const permission = userInMembers ? userInMembers.permission : TeamPermissionLevel.Member; + + return isPermissionTeamAdmin({ permission, signedInUser, editorsCanAdmin }); +}; + +export interface PermissionConfig { + permission: TeamPermissionLevel; + editorsCanAdmin: boolean; + signedInUser: User; +} + +export const isPermissionTeamAdmin = (config: PermissionConfig): boolean => { + const { permission, signedInUser, editorsCanAdmin } = config; + const isAdmin = signedInUser.isGrafanaAdmin || signedInUser.orgRole === OrgRole.Admin; + const userIsTeamAdmin = permission === TeamPermissionLevel.Admin; const isSignedInUserTeamAdmin = isAdmin || userIsTeamAdmin; - return isSignedInUserTeamAdmin || !config.editorsCanAdmin; + return isSignedInUserTeamAdmin || !editorsCanAdmin; }; diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index ef804e437d4..707ff97b738 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -1,9 +1,12 @@ +import { TeamPermissionLevel } from './acl'; + export interface Team { id: number; name: string; avatarUrl: string; email: string; memberCount: number; + permission: TeamPermissionLevel; } export interface TeamMember { From b71c9803a9e89c0c9f5b2e55935108ba5949b3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 18 Mar 2019 13:24:47 +0100 Subject: [PATCH 178/194] fix: new team link goes nowhere for viewers --- public/app/features/teams/TeamList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index e9d51785d72..5d3ef005c9e 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -92,7 +92,9 @@ export class TeamList extends PureComponent { renderTeamList() { const { teams, searchQuery, editorsCanAdmin, signedInUser } = this.props; - const disabledClass = editorsCanAdmin && signedInUser.orgRole === OrgRole.Viewer ? ' disabled' : ''; + const isCanAdminAndViewer = editorsCanAdmin && signedInUser.orgRole === OrgRole.Viewer; + const disabledClass = isCanAdminAndViewer ? ' disabled' : ''; + const newTeamHref = isCanAdminAndViewer ? '#' : 'org/teams/new'; return ( <> @@ -109,7 +111,7 @@ export class TeamList extends PureComponent { From e23e6a8bdc1c5c4addbdf4bf76334fcb17b901a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 18 Mar 2019 14:09:15 +0100 Subject: [PATCH 179/194] fix: fixed snapshots and permission select not beeing able to click --- public/app/features/teams/TeamMemberRow.tsx | 2 +- .../teams/__snapshots__/TeamList.test.tsx.snap | 2 +- .../__snapshots__/TeamMemberRow.test.tsx.snap | 16 ++++++++++++---- public/sass/pages/_admin.scss | 6 ++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/public/app/features/teams/TeamMemberRow.tsx b/public/app/features/teams/TeamMemberRow.tsx index e0bd26f4fd7..0111d1efd8e 100644 --- a/public/app/features/teams/TeamMemberRow.tsx +++ b/public/app/features/teams/TeamMemberRow.tsx @@ -40,7 +40,7 @@ export class TeamMemberRow extends PureComponent { return ( - +
{signedInUserIsTeamAdmin && ( + ng-options="f for f in ['custom', 'critical', 'warning', 'ok']" ng-change="ctrl.onThresholdTypeChange($index)" ng-disabled="ctrl.disabled">
@@ -73,4 +73,4 @@
-
\ No newline at end of file +
diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts index 4f480873d5b..c67a030188d 100644 --- a/public/app/plugins/panel/graph/thresholds_form.ts +++ b/public/app/plugins/panel/graph/thresholds_form.ts @@ -1,5 +1,6 @@ import coreModule from 'app/core/core_module'; - +import config from 'app/core/config'; +import tinycolor from 'tinycolor2'; export class ThresholdFormCtrl { panelCtrl: any; panel: any; @@ -30,6 +31,8 @@ export class ThresholdFormCtrl { fill: true, line: true, yaxis: 'left', + fillColor: 'rgba(234,112, 112, 0.12)', + lineColor: 'rgba(237, 46, 24, 0.60)', }); this.panelCtrl.render(); } @@ -56,6 +59,19 @@ export class ThresholdFormCtrl { this.render(); }; } + + onThresholdTypeChange(index) { + // Because of the ng-model binding, threshold's color mode is already set here + if (this.panel.thresholds[index].colorMode === 'custom') { + this.panel.thresholds[index].fillColor = tinycolor(config.theme.colors.blueBase) + .setAlpha(0.2) + .toRgbString(); + this.panel.thresholds[index].lineColor = tinycolor(config.theme.colors.blueShade) + .setAlpha(0.6) + .toRgbString(); + } + this.panelCtrl.render(); + } } coreModule.directive('graphThresholdForm', () => { From c1d585b156c25a76919122400d5f4c2cab9b77f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 19 Mar 2019 14:48:35 +0100 Subject: [PATCH 185/194] chore: cleaning up noimplicit anys in search_srv and tests progress: #14714 --- .../manage_dashboards/manage_dashboards.ts | 3 +- public/app/core/services/backend_srv.ts | 24 +++++++++++++- public/app/core/services/search_srv.ts | 32 ++++++++++++------- .../app/core/specs/manage_dashboards.test.ts | 1 + public/app/core/specs/search_srv.test.ts | 30 +++++++++-------- 5 files changed, 64 insertions(+), 26 deletions(-) diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 3f6dacd311d..5b5e299b1af 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -11,7 +11,8 @@ export interface Section { id: number; uid: string; title: string; - expanded: false; + expanded: boolean; + removable: boolean; items: any[]; url: string; icon: string; diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index c73cc7661f5..53ab3ab6ce7 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -4,6 +4,28 @@ import appEvents from 'app/core/app_events'; import config from 'app/core/config'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; +export enum HitType { + DashHitDB = 'dash-db', + DashHitHome = 'dash-home', + DashHitFolder = 'dash-folder', +} + +export interface Hit { + id: number; + uid: string; + title: string; + uri: string; + url: string; + slug: string; + type: HitType; + tags: string[]; + isStarred: boolean; + folderId: number; + folderUid: string; + folderTitle: string; + folderUrl: string; +} + export class BackendSrv { private inFlightRequests = {}; private HTTP_REQUEST_CANCELED = -1; @@ -237,7 +259,7 @@ export class BackendSrv { return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 }); } - search(query) { + search(query): Promise { return this.get('/api/search', query); } diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 22d33921ebd..4a605d3fc50 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -1,21 +1,31 @@ +// @ts-ignore import _ from 'lodash'; +// @ts-ignore +import { IQService } from 'angular'; + import coreModule from 'app/core/core_module'; import impressionSrv from 'app/core/services/impression_srv'; import store from 'app/core/store'; import { contextSrv } from 'app/core/services/context_srv'; +import { BackendSrv, Hit } from './backend_srv'; +import { Section } from '../components/manage_dashboards/manage_dashboards'; + +interface Sections { + [key: string]: Partial
; +} export class SearchSrv { recentIsOpen: boolean; starredIsOpen: boolean; /** @ngInject */ - constructor(private backendSrv, private $q) { + constructor(private backendSrv: BackendSrv, private $q: IQService) { this.recentIsOpen = store.getBool('search.sections.recent', true); this.starredIsOpen = store.getBool('search.sections.starred', true); } - private getRecentDashboards(sections) { - return this.queryForRecentDashboards().then(result => { + private getRecentDashboards(sections: Sections) { + return this.queryForRecentDashboards().then((result: any[]) => { if (result.length > 0) { sections['recent'] = { title: 'Recent', @@ -30,8 +40,8 @@ export class SearchSrv { }); } - private queryForRecentDashboards() { - const dashIds = _.take(impressionSrv.getDashboardOpened(), 30); + private queryForRecentDashboards(): Promise { + const dashIds: number[] = _.take(impressionSrv.getDashboardOpened(), 30); if (dashIds.length === 0) { return Promise.resolve([]); } @@ -45,7 +55,7 @@ export class SearchSrv { }); } - private toggleRecent(section) { + private toggleRecent(section: Section) { this.recentIsOpen = section.expanded = !section.expanded; store.set('search.sections.recent', this.recentIsOpen); @@ -59,13 +69,13 @@ export class SearchSrv { }); } - private toggleStarred(section) { + private toggleStarred(section: Section) { this.starredIsOpen = section.expanded = !section.expanded; store.set('search.sections.starred', this.starredIsOpen); return Promise.resolve(section); } - private getStarred(sections) { + private getStarred(sections: Sections) { if (!contextSrv.isSignedIn) { return Promise.resolve(); } @@ -84,7 +94,7 @@ export class SearchSrv { }); } - search(options) { + search(options: any) { const sections: any = {}; const promises = []; const query = _.clone(options); @@ -118,7 +128,7 @@ export class SearchSrv { }); } - private handleSearchResult(sections, results) { + private handleSearchResult(sections: Sections, results: Hit[]): any { if (results.length === 0) { return sections; } @@ -177,7 +187,7 @@ export class SearchSrv { } } - private toggleFolder(section) { + private toggleFolder(section: Section) { section.expanded = !section.expanded; section.icon = section.expanded ? 'fa fa-folder-open' : 'fa fa-folder'; diff --git a/public/app/core/specs/manage_dashboards.test.ts b/public/app/core/specs/manage_dashboards.test.ts index ef5e240fd36..5e94d26ea89 100644 --- a/public/app/core/specs/manage_dashboards.test.ts +++ b/public/app/core/specs/manage_dashboards.test.ts @@ -16,6 +16,7 @@ const mockSection = (overides?: object): Section => { items: [], checked: false, expanded: false, + removable: false, hideHeader: false, icon: '', score: 0, diff --git a/public/app/core/specs/search_srv.test.ts b/public/app/core/specs/search_srv.test.ts index 550d11bbf9e..6566224c613 100644 --- a/public/app/core/specs/search_srv.test.ts +++ b/public/app/core/specs/search_srv.test.ts @@ -1,8 +1,12 @@ +// @ts-ignore +import { IQService } from 'angular'; + import { SearchSrv } from 'app/core/services/search_srv'; import { BackendSrvMock } from 'test/mocks/backend_srv'; import impressionSrv from 'app/core/services/impression_srv'; import { contextSrv } from 'app/core/services/context_srv'; import { beforeEach } from 'test/lib/common'; +import { BackendSrv } from '../services/backend_srv'; jest.mock('app/core/store', () => { return { @@ -18,18 +22,18 @@ jest.mock('app/core/services/impression_srv', () => { }); describe('SearchSrv', () => { - let searchSrv, backendSrvMock; + let searchSrv: SearchSrv, backendSrvMock: BackendSrvMock; beforeEach(() => { backendSrvMock = new BackendSrvMock(); - searchSrv = new SearchSrv(backendSrvMock, Promise); + searchSrv = new SearchSrv(backendSrvMock as BackendSrv, (Promise as any) as IQService); contextSrv.isSignedIn = true; impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); }); describe('With recent dashboards', () => { - let results; + let results: any; beforeEach(() => { backendSrvMock.search = jest @@ -56,7 +60,7 @@ describe('SearchSrv', () => { }); describe('and 3 recent dashboards removed in backend', () => { - let results; + let results: any; beforeEach(() => { backendSrvMock.search = jest @@ -80,7 +84,7 @@ describe('SearchSrv', () => { }); describe('With starred dashboards', () => { - let results; + let results: any; beforeEach(() => { backendSrvMock.search = jest.fn().mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }])); @@ -97,7 +101,7 @@ describe('SearchSrv', () => { }); describe('With starred dashboards and recent', () => { - let results; + let results: any; beforeEach(() => { backendSrvMock.search = jest @@ -125,7 +129,7 @@ describe('SearchSrv', () => { }); describe('with no query string and dashboards with folders returned', () => { - let results; + let results: any; beforeEach(() => { backendSrvMock.search = jest @@ -173,12 +177,10 @@ describe('SearchSrv', () => { }); describe('with query string and dashboards with folders returned', () => { - let results; + let results: any; beforeEach(() => { - backendSrvMock.search = jest.fn(); - - backendSrvMock.search.mockReturnValue( + backendSrvMock.search = jest.fn().mockReturnValue( Promise.resolve([ { id: 2, @@ -249,8 +251,9 @@ describe('SearchSrv', () => { backendSrvMock.search = jest.fn(); backendSrvMock.search.mockReturnValue(Promise.resolve([])); - searchSrv.getRecentDashboards = () => { + searchSrv['getRecentDashboards'] = () => { getRecentDashboardsCalled = true; + return Promise.resolve(); }; return searchSrv.search({ skipRecent: true }).then(() => {}); @@ -269,8 +272,9 @@ describe('SearchSrv', () => { backendSrvMock.search.mockReturnValue(Promise.resolve([])); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); - searchSrv.getStarred = () => { + searchSrv['getStarred'] = () => { getStarredCalled = true; + return Promise.resolve(); }; return searchSrv.search({ skipStarred: true }).then(() => {}); From 40978ee08a088e14a913df3427c7317d1da8cc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 19 Mar 2019 15:10:00 +0100 Subject: [PATCH 186/194] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d58bb81a2..5468a2b41f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ * **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619) * **Heatmap**: legend shows wrong colors for small values [#14019](https://github.com/grafana/grafana/issues/14019) -# 6.0.2 (unreleased) +# 6.0.2 (2019-03-19) ### Bug Fixes * **Alerting**: Fixed issue with AlertList panel links resulting in panel not found errors. [#15975](https://github.com/grafana/grafana/pull/15975), [@torkelo](https://github.com/torkelo) From 9a4a8b0f857f502757641bba069718eecd063924 Mon Sep 17 00:00:00 2001 From: yalhyane Date: Tue, 19 Mar 2019 14:39:26 +0000 Subject: [PATCH 187/194] Update templating.md fix a typo in the example of `percentencode` option --- docs/sources/reference/templating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index b00e44943ef..2be2b6863a9 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -110,7 +110,7 @@ Formats single & multi valued variables for use in URL parameters. ```bash servers = ['foo()bar BAZ', 'test2'] -String to interpolate: '${servers:lucene}' +String to interpolate: '${servers:percentencode}' Interpolation result: 'foo%28%29bar%20BAZ%2Ctest2' ``` From 124bedbf24160de9b8baae4b707cddf9f43f8f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 19 Mar 2019 15:44:10 +0100 Subject: [PATCH 188/194] Update latest.json --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index e19a0f8550d..bc1ac9d90cc 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "6.0.1", - "testing": "6.0.1" + "stable": "6.0.2", + "testing": "6.0.2" } From 512f2dd02435083fc84d0b89dbc81f0435d5faad Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 19 Mar 2019 16:11:47 +0100 Subject: [PATCH 189/194] chore: Bump react and react-dom to 16.8.4 --- package.json | 10 +- packages/grafana-ui/package.json | 6 +- yarn.lock | 1986 +++++++++++++++--------------- 3 files changed, 1000 insertions(+), 1002 deletions(-) diff --git a/package.json b/package.json index c951e5241c1..afd5307035c 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "@types/jest": "^23.3.2", "@types/jquery": "^1.10.35", "@types/node": "^8.0.31", - "@types/react": "^16.7.6", - "@types/react-dom": "^16.0.9", + "@types/react": "^16.8.8", + "@types/react-dom": "^16.8.2", "@types/react-grid-layout": "^0.16.6", "@types/react-select": "^2.0.4", "@types/react-transition-group": "^2.0.15", @@ -191,8 +191,8 @@ "prismjs": "^1.6.0", "prop-types": "^15.6.2", "rc-cascader": "^0.14.0", - "react": "^16.6.3", - "react-dom": "^16.6.3", + "react": "^16.8.4", + "react-dom": "^16.8.4", "react-grid-layout": "0.16.6", "react-highlight-words": "0.11.0", "react-popper": "^1.3.0", @@ -219,7 +219,7 @@ }, "resolutions": { "caniuse-db": "1.0.30000772", - "**/@types/react": "16.7.6" + "**/@types/react": "16.8.8" }, "workspaces": { "packages": [ diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index aec32fd9282..bfe5d48cbf6 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -25,10 +25,10 @@ "lodash": "^4.17.10", "moment": "^2.22.2", "papaparse": "^4.6.3", - "react": "^16.6.3", + "react": "^16.8.4", "react-color": "^2.17.0", "react-custom-scrollbars": "^4.2.1", - "react-dom": "^16.6.3", + "react-dom": "^16.8.4", "react-highlight-words": "0.11.0", "react-popper": "^1.3.0", "react-transition-group": "^2.2.1", @@ -48,7 +48,7 @@ "@types/lodash": "^4.14.119", "@types/node": "^10.12.18", "@types/papaparse": "^4.5.9", - "@types/react": "^16.7.6", + "@types/react": "^16.8.8", "@types/react-custom-scrollbars": "^4.0.5", "@types/react-test-renderer": "^16.0.3", "@types/react-transition-group": "^2.0.15", diff --git a/yarn.lock b/yarn.lock index c75b8e5a57b..700d2df7544 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,33 +30,33 @@ source-map "^0.5.0" "@babel/core@^7.1.2", "@babel/core@^7.1.6": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.2.2.tgz#07adba6dde27bb5ad8d8672f15fde3e08184a687" - integrity sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b" + integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" + "@babel/generator" "^7.3.4" "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.2.2" + "@babel/parser" "^7.3.4" "@babel/template" "^7.2.2" - "@babel/traverse" "^7.2.2" - "@babel/types" "^7.2.2" + "@babel/traverse" "^7.3.4" + "@babel/types" "^7.3.4" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" - lodash "^4.17.10" + lodash "^4.17.11" resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.2.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.2.tgz#fff31a7b2f2f3dad23ef8e01be45b0d5c2fc0132" - integrity sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ== +"@babel/generator@^7.0.0", "@babel/generator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" + integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg== dependencies: - "@babel/types" "^7.3.2" + "@babel/types" "^7.3.4" jsesc "^2.5.1" - lodash "^4.17.10" + lodash "^4.17.11" source-map "^0.5.0" trim-right "^1.0.1" @@ -92,16 +92,17 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-create-class-features-plugin@^7.3.0": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.2.tgz#ba1685603eb1c9f2f51c9106d5180135c163fe73" - integrity sha512-tdW8+V8ceh2US4GsYdNVNoohq5uVwOf9k6krjwW4E1lINcHgttnWcNqgdoessn12dAy8QkbezlbQh2nXISNY+A== +"@babel/helper-create-class-features-plugin@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.4.tgz#092711a7a3ad8ea34de3e541644c2ce6af1f6f0c" + integrity sha512-uFpzw6L2omjibjxa8VGZsJUPL5wJH0zzGKpoz0ccBkzIa6C8kWNUbiBmQ0rgOKWlHJ6qzmfa6lTiGchiV8SC+g== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.2.3" + "@babel/helper-replace-supers" "^7.3.4" + "@babel/helper-split-export-declaration" "^7.0.0" "@babel/helper-define-map@^7.1.0": version "7.1.0" @@ -199,15 +200,15 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.0.0", "@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz#19970020cf22677d62b3a689561dbd9644d8c5e5" - integrity sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA== +"@babel/helper-replace-supers@^7.0.0", "@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.3.4.tgz#a795208e9b911a6eeb08e5891faacf06e7013e13" + integrity sha512-pvObL9WVf2ADs+ePg0jrqlhHoxRXlOa+SHRHzAXIz2xkYuOHfGl+fKxPMaS4Fq+uje8JQPobnertBBvyrWnQ1A== dependencies: "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.2.3" - "@babel/types" "^7.0.0" + "@babel/traverse" "^7.3.4" + "@babel/types" "^7.3.4" "@babel/helper-simple-access@^7.1.0": version "7.1.0" @@ -252,10 +253,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.1.3", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.2.tgz#95cdeddfc3992a6ca2a1315191c1679ca32c55cd" - integrity sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.1.3", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" + integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== "@babel/plugin-proposal-async-generator-functions@^7.1.0", "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -279,11 +280,11 @@ "@babel/plugin-syntax-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties@^7.2.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.0.tgz#272636bc0fa19a0bc46e601ec78136a173ea36cd" - integrity sha512-wNHxLkEKTQ2ay0tnsam2z7fGZUi+05ziDJflEt3AZTP3oXLKHJp9HqhfroB/vdMvt3sda9fAbq7FsG8QPDrZBg== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.4.tgz#410f5173b3dc45939f9ab30ca26684d72901405e" + integrity sha512-lUf8D3HLs4yYlAo8zjuneLvfxN7qfKv1Yzbj5vjqaqMJxgJA3Ipwp4VUJ+OrOdz53Wbww6ahwB8UhB2HQyLotA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.3.0" + "@babel/helper-create-class-features-plugin" "^7.3.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-decorators@7.1.2": @@ -312,10 +313,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.0.0" -"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.3.1": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz#6d1859882d4d778578e41f82cc5d7bf3d5daf6c1" - integrity sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA== +"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.4.tgz#47f73cf7f2a721aad5c0261205405c642e424654" + integrity sha512-j7VQmbbkA+qrzNqbKHrBsW3ddFnOeva6wzSe/zB7T+xaxGc+RCpwo44wCmRixAIGRoIpmVgvzFzNJqQcO3/9RA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" @@ -408,9 +409,9 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-typescript@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.2.0.tgz#55d240536bd314dcbbec70fd949c5cabaed1de29" - integrity sha512-WhKr6yu6yGpGcNMVgIBuI9MkredpVc7Y3YR4UzEZmDztHoL6wV56YBHLhWnjO1EvId1B32HrD3DRFc+zSoKI1g== + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz#a7cc3f66119a9f7ebe2de5383cce193473d65991" + integrity sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -421,10 +422,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-async-to-generator@^7.1.0", "@babel/plugin-transform-async-to-generator@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz#68b8a438663e88519e65b776f8938f3445b1a2ff" - integrity sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ== +"@babel/plugin-transform-async-to-generator@^7.1.0", "@babel/plugin-transform-async-to-generator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.3.4.tgz#4e45408d3c3da231c0e7b823f407a53a7eb3048c" + integrity sha512-Y7nCzv2fw/jEZ9f678MuKdMo99MFDJMT/PvD9LisrR5JDFcJH6vYeH6RnjVt3p5tceyGRvTtEN0VOlU+rgHZjA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -437,13 +438,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz#f17c49d91eedbcdf5dd50597d16f5f2f770132d4" - integrity sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q== +"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.3.4.tgz#5c22c339de234076eee96c8783b2fed61202c5c4" + integrity sha512-blRr2O8IOZLAOJklXLV4WhcEzpYafYQKSGT3+R26lWG41u/FODJuBggehtOwilVAcFu393v3OFj+HmaE6tVjhA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - lodash "^4.17.10" + lodash "^4.17.11" "@babel/plugin-transform-classes@7.1.0": version "7.1.0" @@ -459,17 +460,17 @@ "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" -"@babel/plugin-transform-classes@^7.1.0", "@babel/plugin-transform-classes@^7.2.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.2.tgz#6c90542f210ee975aa2aa8c8b5af7fa73a126953" - integrity sha512-gEZvgTy1VtcDOaQty1l10T3jQmJKlNVxLDCs+3rCVPr6nMkODLELxViq5X9l+rfxbie3XrfrMCYYY6eX3aOcOQ== +"@babel/plugin-transform-classes@^7.1.0", "@babel/plugin-transform-classes@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.3.4.tgz#dc173cb999c6c5297e0b5f2277fdaaec3739d0cc" + integrity sha512-J9fAvCFBkXEvBimgYxCjvaVDzL6thk0j0dBvCeZmIUDBwyt+nv6HfbImsSrWsYXfDNDivyANgJlFXDUWRTZBuA== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-define-map" "^7.1.0" "@babel/helper-function-name" "^7.1.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.1.0" + "@babel/helper-replace-supers" "^7.3.4" "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" @@ -527,9 +528,9 @@ "@babel/plugin-syntax-flow" "^7.0.0" "@babel/plugin-transform-flow-strip-types@^7.0.0": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.2.3.tgz#e3ac2a594948454e7431c7db33e1d02d51b5cd69" - integrity sha512-xnt7UIk9GYZRitqCnsVMjQK1O2eKZwFB3CvvHjf5SGx6K6vr/MScCKQDnf1DxRaj501e3pXjti+inbSXX2ZUoQ== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.3.4.tgz#00156236defb7dedddc2d3c9477dcc01a4494327" + integrity sha512-PmQC9R7DwpBFA+7ATKMyzViz3zCaMNouzZMPZN2K5PnbBbtL3AXFYTkDk+Hey5crQq2A90UG5Uthz0mel+XZrA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-flow" "^7.2.0" @@ -573,10 +574,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" -"@babel/plugin-transform-modules-systemjs@^7.0.0", "@babel/plugin-transform-modules-systemjs@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz#912bfe9e5ff982924c81d0937c92d24994bb9068" - integrity sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ== +"@babel/plugin-transform-modules-systemjs@^7.0.0", "@babel/plugin-transform-modules-systemjs@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.3.4.tgz#813b34cd9acb6ba70a84939f3680be0eb2e58861" + integrity sha512-VZ4+jlGOF36S7TjKs8g4ojp4MEI+ebCQZdswWb/T9I4X84j8OtFAyjXjt/M16iIm5RIZn0UMQgg/VgIwo/87vw== dependencies: "@babel/helper-hoist-variables" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -612,9 +613,9 @@ "@babel/helper-replace-supers" "^7.1.0" "@babel/plugin-transform-parameters@^7.1.0", "@babel/plugin-transform-parameters@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.2.0.tgz#0d5ad15dc805e2ea866df4dd6682bfe76d1408c2" - integrity sha512-kB9+hhUidIgUoBQ0MsxMewhzr8i60nMa2KgeJKQWYrqQpqcBYtnpR+JgkadZVZoaEZ/eKu9mclFaVwhRpLNSzA== + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.3.3.tgz#3a873e07114e1a5bee17d04815662c8317f10e30" + integrity sha512-IrIP25VvXWu/VlBWTpsjGptpomtIkYrN/3aDp4UKm7xK6UxZY88kcJ1UwETbzHAlwN21MnNfwlar0u8y3KpiXw== dependencies: "@babel/helper-call-delegate" "^7.1.0" "@babel/helper-get-function-arity" "^7.0.0" @@ -675,12 +676,12 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-jsx" "^7.2.0" -"@babel/plugin-transform-regenerator@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz#5b41686b4ed40bef874d7ed6a84bdd849c13e0c1" - integrity sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw== +"@babel/plugin-transform-regenerator@^7.0.0", "@babel/plugin-transform-regenerator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.3.4.tgz#1601655c362f5b38eead6a52631f5106b29fa46a" + integrity sha512-hvJg8EReQvXT6G9H2MvNPXkv9zK36Vxa1+csAVTpE1J3j0zlHplw76uudEbJxgvqZzAq9Yh45FLD4pk5mKRFQA== dependencies: - regenerator-transform "^0.13.3" + regenerator-transform "^0.13.4" "@babel/plugin-transform-runtime@7.1.0": version "7.1.0" @@ -729,7 +730,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-typescript@^7.1.0": +"@babel/plugin-transform-typescript@^7.1.0", "@babel/plugin-transform-typescript@^7.3.2": version "7.3.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.3.2.tgz#59a7227163e55738842f043d9e5bd7c040447d96" integrity sha512-Pvco0x0ZSCnexJnshMfaibQ5hnK8aUHSvjCQhC1JR8eeg+iBwt0AtCO7gWxJ358zZevuf9wPSO5rv+WJcbHPXQ== @@ -802,15 +803,15 @@ semver "^5.3.0" "@babel/preset-env@^7.1.0", "@babel/preset-env@^7.1.6", "@babel/preset-env@^7.2.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.1.tgz#389e8ca6b17ae67aaf9a2111665030be923515db" - integrity sha512-FHKrD6Dxf30e8xgHQO0zJZpUPfVZg+Xwgz5/RdSWCbza9QLNk4Qbp40ctRoqDxml3O8RMzB1DU55SXeDG6PqHQ== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1" + integrity sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-async-generator-functions" "^7.2.0" "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.3.1" + "@babel/plugin-proposal-object-rest-spread" "^7.3.4" "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" "@babel/plugin-syntax-async-generators" "^7.2.0" @@ -818,10 +819,10 @@ "@babel/plugin-syntax-object-rest-spread" "^7.2.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" "@babel/plugin-transform-arrow-functions" "^7.2.0" - "@babel/plugin-transform-async-to-generator" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.3.4" "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.2.0" - "@babel/plugin-transform-classes" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.3.4" + "@babel/plugin-transform-classes" "^7.3.4" "@babel/plugin-transform-computed-properties" "^7.2.0" "@babel/plugin-transform-destructuring" "^7.2.0" "@babel/plugin-transform-dotall-regex" "^7.2.0" @@ -832,13 +833,13 @@ "@babel/plugin-transform-literals" "^7.2.0" "@babel/plugin-transform-modules-amd" "^7.2.0" "@babel/plugin-transform-modules-commonjs" "^7.2.0" - "@babel/plugin-transform-modules-systemjs" "^7.2.0" + "@babel/plugin-transform-modules-systemjs" "^7.3.4" "@babel/plugin-transform-modules-umd" "^7.2.0" "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0" "@babel/plugin-transform-new-target" "^7.0.0" "@babel/plugin-transform-object-super" "^7.2.0" "@babel/plugin-transform-parameters" "^7.2.0" - "@babel/plugin-transform-regenerator" "^7.0.0" + "@babel/plugin-transform-regenerator" "^7.3.4" "@babel/plugin-transform-shorthand-properties" "^7.2.0" "@babel/plugin-transform-spread" "^7.2.0" "@babel/plugin-transform-sticky-regex" "^7.2.0" @@ -869,7 +870,7 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" -"@babel/preset-typescript@7.1.0", "@babel/preset-typescript@^7.1.0": +"@babel/preset-typescript@7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.1.0.tgz#49ad6e2084ff0bfb5f1f7fb3b5e76c434d442c7f" integrity sha512-LYveByuF9AOM8WrsNne5+N79k1YxjNB6gmpCQsnuSBAcV8QUeB+ZUxQzL7Rz7HksPbahymKkq2qBR+o36ggFZA== @@ -877,6 +878,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-typescript" "^7.1.0" +"@babel/preset-typescript@^7.1.0": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz#88669911053fa16b2b276ea2ede2ca603b3f307a" + integrity sha512-mzMVuIP4lqtn4du2ynEfdO0+RYcslwrZiJHXu4MGaC1ctJiW2fyaeDrtjJGs7R/KebZ1sgowcIoWf4uRpEfKEg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.3.2" + "@babel/runtime@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0.tgz#adeb78fedfc855aa05bc041640f3f6f98e85424c" @@ -885,9 +894,9 @@ regenerator-runtime "^0.12.0" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a" - integrity sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" + integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== dependencies: regenerator-runtime "^0.12.0" @@ -900,28 +909,28 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" - integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" + integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" + "@babel/generator" "^7.3.4" "@babel/helper-function-name" "^7.1.0" "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.2.3" - "@babel/types" "^7.2.2" + "@babel/parser" "^7.3.4" + "@babel/types" "^7.3.4" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.10" + lodash "^4.17.11" -"@babel/types@^7.0.0", "@babel/types@^7.1.6", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.2.tgz#424f5be4be633fff33fb83ab8d67e4a8290f5a2f" - integrity sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ== +"@babel/types@^7.0.0", "@babel/types@^7.1.6", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" + integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ== dependencies: esutils "^2.0.2" - lodash "^4.17.10" + lodash "^4.17.11" to-fast-properties "^2.0.0" "@emotion/babel-utils@^0.6.4": @@ -1040,20 +1049,6 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.1.3.tgz#b700d97385fa91affed60c71dfd51c67e9dad762" integrity sha512-QsYGKdhhuDFNq7bjm2r44y0mp5xW3uO3csuTPDWZc0OIiMQv+AIY5Cqwd4mJiC5N8estVl7qlvOx1hbtOuUWbw== -"@iamstarkov/listr-update-renderer@0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e" - integrity sha512-IJyxQWsYDEkf8C8QthBn5N8tIUR9V9je6j3sMIpAkonaadjbvxmRC6RAhpa3RKxndhNnU2M6iNbtJwd7usQYIA== - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^2.3.0" - strip-ansi "^3.0.1" - "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -1097,16 +1092,16 @@ integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== "@storybook/addon-actions@^4.1.7": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-4.1.11.tgz#8946ea78f050ae2d06a2f2231ec56d1831942e15" - integrity sha512-iVsxEPmOCuPMAaJhHbpxQhzEPzKnZad4GELNfKrwmmvv3mY+3UN/z208HguW4NHjhMJZVYSS3H/qic8CQS+pHw== + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-4.1.14.tgz#4bd962da767aa6a99867114894229c739f87d780" + integrity sha512-zq8MSSLXv+D+e/m3LdRrAMVrJP5xcETO4OtutJCdJeAkIjMvstnWncUEUHMltv/+bTBQgb77uoTFKVN4zrJL2g== dependencies: "@emotion/core" "^0.13.1" "@emotion/provider" "^0.11.2" "@emotion/styled" "^0.10.6" - "@storybook/addons" "4.1.11" - "@storybook/components" "4.1.11" - "@storybook/core-events" "4.1.11" + "@storybook/addons" "4.1.14" + "@storybook/components" "4.1.14" + "@storybook/core-events" "4.1.14" core-js "^2.5.7" deep-equal "^1.0.1" global "^4.3.2" @@ -1117,13 +1112,13 @@ uuid "^3.3.2" "@storybook/addon-info@^4.1.6": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/addon-info/-/addon-info-4.1.11.tgz#b2ea3a4fb4cad208f9d6075737b5bfe8636e28f9" - integrity sha512-eROXuXS5YgLeXsnkqjXqbZ8UFgNIwORDkn4UfD+Aej1//SWpGeNihOxQvx+pvs0NnsTR+/w4c1gbqa/Gr3f78w== + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/addon-info/-/addon-info-4.1.14.tgz#d0e155413881403e9dd0bcba04a05199141239a2" + integrity sha512-PcJfcZ0WnC+mr+AIn1LfacjruG6cLnCC98BdxpdTOMK+nncR/JffJ7ySqykFEnwRfLuSHGglIg+LwjUViQ4mwA== dependencies: - "@storybook/addons" "4.1.11" - "@storybook/client-logger" "4.1.11" - "@storybook/components" "4.1.11" + "@storybook/addons" "4.1.14" + "@storybook/client-logger" "4.1.14" + "@storybook/components" "4.1.14" core-js "^2.5.7" global "^4.3.2" marksy "^6.1.0" @@ -1134,14 +1129,14 @@ util-deprecate "^1.0.2" "@storybook/addon-knobs@^4.1.7": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-4.1.11.tgz#fd6c90d62a5bf5f94899746a95b02f1ef127cd81" - integrity sha512-UQzYZoo0WKHKHSayaEBLvyZNqlqCOKahXzT2r+hS3t6wRnHJSfPtEHD0xTYMJSkA5t+bhlIOFJy0tribd0sdPQ== + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-4.1.14.tgz#48bc37f58707cb00960a501f24f702c993f17ad8" + integrity sha512-ScMi3iZxdOYMzVv7jMUqfgyPBWYwkSW4OLrVFIr5/EPP9mAOwnlYz+tI/8zM6w7/cvdEPY2pCilKt2zhzEMQ9w== dependencies: "@emotion/styled" "^0.10.6" - "@storybook/addons" "4.1.11" - "@storybook/components" "4.1.11" - "@storybook/core-events" "4.1.11" + "@storybook/addons" "4.1.14" + "@storybook/components" "4.1.14" + "@storybook/core-events" "4.1.14" copy-to-clipboard "^3.0.8" core-js "^2.5.7" escape-html "^1.0.3" @@ -1153,39 +1148,39 @@ react-lifecycles-compat "^3.0.4" util-deprecate "^1.0.2" -"@storybook/addons@4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-4.1.11.tgz#a0d537bd10d123ecee6cb1f5f149b148ce250e57" - integrity sha512-n9oDs7GgJbiN5NYPkR3B3e5W0Tr6bIZvFfcJzgyP4dn50AUvS1IE1CEthezfn1L/nc2suw/8Oe30bOXOyTl/SQ== +"@storybook/addons@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-4.1.14.tgz#e976ac21b82043efb8fb2289063b370d94c29eae" + integrity sha512-nAieZLLeXzuUY8TIHshzVX8VUtUowsyohIKilZSzLIBFMpCEYlyPBilrE9ULqUEtVQWi5peAiKRZ1xJQxx/TIA== dependencies: - "@storybook/channels" "4.1.11" - "@storybook/components" "4.1.11" + "@storybook/channels" "4.1.14" + "@storybook/components" "4.1.14" global "^4.3.2" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-4.1.11.tgz#3320a5f3e05652466eff1c53843205c262a92dfb" - integrity sha512-/9p4I5CZWVl6mszY5AR5XPRdQ88LUaAt4iyhdxMIaqNRiVo3Rq4ptMXiw35eCr+sLQuG2KO2SiPIwaA4/FgQuw== +"@storybook/channel-postmessage@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-4.1.14.tgz#003dfbf8dd62f4f3f9d9b7766e72f57377a8589e" + integrity sha512-Kz1oOoJXoqZYWh7V4yykEAKsZwegY/VQzEIFQjlyCAvNyfCvN35AgAdrgLCU2Uwss5lds18SkdwxCOxmH2lnEA== dependencies: - "@storybook/channels" "4.1.11" + "@storybook/channels" "4.1.14" global "^4.3.2" json-stringify-safe "^5.0.1" -"@storybook/channels@4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-4.1.11.tgz#d161497fa3cd848cc9d518aa1c37052857e22e3c" - integrity sha512-zYusY8cno4keMozn2lDpBgyNSOueFh+hrPETioSB5Z8Kd3F5OjM7681vJC8QA67yOBEie2hHk0CVxRpuxziMwA== +"@storybook/channels@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-4.1.14.tgz#8af55d028d7f86f5a9abb9329df7e9d1121f404b" + integrity sha512-2BpKF7MXWfJeY9lRHrUseJ6JoZXshmo9B4np7TAex3NXOeRLsptQMCfQXgMgUMDQhABc0o9XBAruQOb9zMyd7w== -"@storybook/client-logger@4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-4.1.11.tgz#2b1e34e892045199592fdb01656e5dcdcd1999d7" - integrity sha512-Xxy6sY7Zd405o28wUAhlpqY2FbSZsTrsN3g/uo4Mqo4XD2f0Z4wIv1GOuM5DI2KlHpHGI+36YPO2VFx5Bq+yiQ== +"@storybook/client-logger@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-4.1.14.tgz#73a408280af594f66259f750bd058534f7a3a95f" + integrity sha512-8Vb4DaGvUsc/voPxOW2LAJZBK5ac8btvesGZZJO43HVWpmROLPrCcoKIUFLWgPmmXyHTkLuheTn9tcbok1BowQ== -"@storybook/components@4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-4.1.11.tgz#25458a4a4f2edd836b1e4b944cfcfcb4a3567036" - integrity sha512-KJA8Nr8MbXiibDLcndx1GRVmVDyBBL2Tbb1kfQfr58vDwz6qhYxempejY6W+voaEqohnFxrOtnnbqlCyf8peUQ== +"@storybook/components@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-4.1.14.tgz#54e33ab9bf09ce75f1b97e9212672921444a9fdc" + integrity sha512-UdSwxZutRUW8umaaPnris5M9pGz2220KXs/5g2URaw3rEL8eUaKLKJXSpFSOeXPhlCnV7B9R/qanG9GXGQKXmg== dependencies: "@emotion/core" "^0.13.1" "@emotion/provider" "^0.11.2" @@ -1198,27 +1193,27 @@ react-textarea-autosize "^7.0.4" render-fragment "^0.1.1" -"@storybook/core-events@4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-4.1.11.tgz#78cfb2b4014ca27909421cdebfa9c96533929a5a" - integrity sha512-rVb76xFLJkTFcBHL1oTdJW8O2N7q+Cc6Mo7v9u3TnM4WuRk08/GyzzO7sRvEg3Mvo59AOLu1uqYovRMo4tZEnQ== +"@storybook/core-events@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-4.1.14.tgz#919a577bab7d4c45ea6cb83f6cf8b35782a8f671" + integrity sha512-UBVTWZonTD5hHR7huWs15mYMKJONjQC6GBQy5AvKDpAIXtAlHe75mv5aZM5gRTP/ThtNE9V1HO4IF3C54dvUjg== -"@storybook/core@4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-4.1.11.tgz#f91cf77d4750edeb92717f6b2a2a4258b0a06c64" - integrity sha512-iUrtFCav7xJicCLhp4zdqbhaOXRWXrx4wMPSs0keBD2G7NQtSg/TQMUdx2VYFBl5thIFT1jt5dAm66y0Q2OCTQ== +"@storybook/core@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-4.1.14.tgz#488e6f8b4cbb9c1346e8c3755437e0269f790b74" + integrity sha512-864gvxZNaL3vjskyPkXqx88y+C51KenVY4g5AdX1MrerHHSzPzfSS9PPO+XT8rrJO6FQ5vRHyG9sxa0MkG6O3A== dependencies: "@babel/plugin-proposal-class-properties" "^7.2.0" "@babel/preset-env" "^7.2.0" "@emotion/core" "^0.13.1" "@emotion/provider" "^0.11.2" "@emotion/styled" "^0.10.6" - "@storybook/addons" "4.1.11" - "@storybook/channel-postmessage" "4.1.11" - "@storybook/client-logger" "4.1.11" - "@storybook/core-events" "4.1.11" - "@storybook/node-logger" "4.1.11" - "@storybook/ui" "4.1.11" + "@storybook/addons" "4.1.14" + "@storybook/channel-postmessage" "4.1.14" + "@storybook/client-logger" "4.1.14" + "@storybook/core-events" "4.1.14" + "@storybook/node-logger" "4.1.14" + "@storybook/ui" "4.1.14" airbnb-js-shims "^1 || ^2" autoprefixer "^9.3.1" babel-plugin-macros "^2.4.2" @@ -1282,10 +1277,10 @@ "@storybook/react-simple-di" "^1.2.1" babel-runtime "6.x.x" -"@storybook/node-logger@4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-4.1.11.tgz#8ea9779eb6260a02bf06c02eafbff5925b883f9f" - integrity sha512-rCXk1PUcakkV72oyTR+nOVDUGnkk1On8/sm9u3NtBEUuwsCtm4p+jh42Pp4jsTtWpG36AVABDtiN65VgCfS+9w== +"@storybook/node-logger@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-4.1.14.tgz#d97ba3f8bd727dc0a0266aaabc07e3111a58f0ca" + integrity sha512-eUWa+PnZ2t/j74PghtcjMINkPRLRWi8ekSbrbYsiwfErG6B6r/m89Ca6gvg/51o1e12rPz5B4cbCv6gtZEa7UA== dependencies: chalk "^2.4.1" core-js "^2.5.7" @@ -1330,16 +1325,16 @@ babel-runtime "^6.5.0" "@storybook/react@^4.1.4": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-4.1.11.tgz#fb3cda82fd3334a6653325ec281a2da284ac6895" - integrity sha512-NPNcfOlWmFBevza/+GXIK23h46HqdWKFmId19E0PtoWGoR5xOR56rnwagr2+yHngUb4AATUCzhzTwxfWGxSvBg== + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-4.1.14.tgz#fb1e68ffabaed258fa90cf328500803715b45bf6" + integrity sha512-sza+wvWymLv2e6aGEcVO4ka5J3gde298jRw3Qjf7DBHfKx0dBx1QCwNTCUn3Zw6NZvY3Li2gFHN1MFAwshsZtA== dependencies: "@babel/plugin-transform-react-constant-elements" "^7.2.0" "@babel/preset-flow" "^7.0.0" "@babel/preset-react" "^7.0.0" "@emotion/styled" "^0.10.6" - "@storybook/core" "4.1.11" - "@storybook/node-logger" "4.1.11" + "@storybook/core" "4.1.14" + "@storybook/node-logger" "4.1.14" "@svgr/webpack" "^4.0.3" babel-plugin-named-asset-import "^0.2.3" babel-plugin-react-docgen "^2.0.0" @@ -1355,16 +1350,16 @@ semver "^5.6.0" webpack "^4.23.1" -"@storybook/ui@4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-4.1.11.tgz#0c6fc34a8096028ef236a5196e7b91831702f2fb" - integrity sha512-bgIagh2Z4flGA7jv4JN++ThLwGq8CI8Wq+1/vhCiTxjTE3H1j9VNPdJfhNgxrQypcLRVHl5AKhf0mlSMyz0S1A== +"@storybook/ui@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-4.1.14.tgz#5dd50490fcede29d552ffe4e0870648a35f0c997" + integrity sha512-GV7MKjcFkfBmr7odrb/oJJ2VlDE1kKXjOo0Fk+3jYhdOd8JHv60Xj5z7Q91xqxPLwThNUZkCMUeWdbvyYRykFw== dependencies: "@emotion/core" "^0.13.1" "@emotion/provider" "^0.11.2" "@emotion/styled" "^0.10.6" - "@storybook/components" "4.1.11" - "@storybook/core-events" "4.1.11" + "@storybook/components" "4.1.14" + "@storybook/core-events" "4.1.14" "@storybook/mantra-core" "^1.7.2" "@storybook/podda" "^1.2.3" "@storybook/react-komposer" "^2.0.5" @@ -1527,9 +1522,11 @@ chalk "*" "@types/cheerio@*": - version "0.22.10" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.10.tgz#780d552467824be4a241b29510a7873a7432c4a6" - integrity sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg== + version "0.22.11" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.11.tgz#61c0facf9636d14ba5f77fc65ed8913aa845d717" + integrity sha512-x0X3kPbholdJZng9wDMhb2swvUi3UYRNAuWAmIPIWlfgAJZp//cql/qblE7181Mg7SjWVwq6ldCPCLn5AY/e7w== + dependencies: + "@types/node" "*" "@types/classnames@^2.2.6": version "2.2.7" @@ -1544,119 +1541,119 @@ commander "*" "@types/d3-array@*": - version "1.2.5" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.5.tgz#7f87eccfe53396d48700294a518c379be14c8254" - integrity sha512-kELkPCl/pCcelr5cXDoQyy3WOkLn8dVdYA+qmtQcuxX9gLoD4s12/CJf6Yxx4UvvuMKJHA8kUbdcH/3DY8SzNg== + version "1.2.6" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.6.tgz#bbc5d7e6e837353f5849c0ffad9bf6fbf9c775bb" + integrity sha512-/EcY/15X5tnwkMT2txpjiLUNJj5xHA2vGHOXI8NTYGhETK914RRLQLjNm6EpAI1D2IY5vh3CzuLODnqBAwKjPA== "@types/d3-axis@*": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.11.tgz#efd975f9fec14c2afd03828f3acec0ef97d37c3b" - integrity sha512-cuigApCyCwYJxaQPghj+BqaxzbdRdT/lpZBMtF7EuEIJ61NMQ8yvGnqFvHCIgJEmUu2Wb2wiZqy9kiHi3Ddftg== + version "1.0.12" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.12.tgz#8c124edfcc02f3b3a9cdaa2a28b8a20341401799" + integrity sha512-BZISgSD5M8TgURyNtcPAmUB9sk490CO1Thb6/gIn0WZTt3Y50IssX+2Z0vTccoqZksUDTep0b+o4ofXslvNbqg== dependencies: "@types/d3-selection" "*" "@types/d3-brush@*": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-1.0.9.tgz#c71070845946eeee4cf330e04123a3997e6476bf" - integrity sha512-mAx8IVc0luUHfk51pl0UN1vzybnAzLMUsvIwLt3fbsqqPkSXr+Pu1AxOPPeyNc27LhHJnfH/LCV7Jlv+Yzqu1A== + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-1.0.10.tgz#aa9b5545d816c29d19cff20118f236713af8e9fb" + integrity sha512-J8jREATIrfJaAfhJivqaEKPnJsRlwwrOPje+ABqZFgamADjll+q9zaDXnYyjiGPPsiJEU+Qq9jQi5rECxIOfhg== dependencies: "@types/d3-selection" "*" "@types/d3-chord@*": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.8.tgz#08c0fbb10281be0a5b3fdf48c9c081af02f79fb6" - integrity sha512-F0ftYOo7FenAIxsRjXLt8vbij0NLDuVcL+xaGY7R9jUmF2Mrpj1T5XukBI9Cad+Ei7YSxEWREIO+CYcaKCl2qQ== + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.9.tgz#ccc5de03ff079025491b7aa6b750670a140b45ae" + integrity sha512-UA6lI9CVW5cT5Ku/RV4hxoFn4mKySHm7HEgodtfRthAj1lt9rKZEPon58vyYfk+HIAm33DtJJgZwMXy2QgyPXw== "@types/d3-collection@*": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.7.tgz#829e1db477d6bbbcdc038cbc489f22798752d707" - integrity sha512-vR3BT0GwHc5y93Jv6bxn3zoxP/vGu+GdXu/r1ApjbP9dLk9I2g6NiV7iP/QMQSuFZd0It0n/qWrfXHxCWwHIkg== + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.8.tgz#aa9552c570a96e33c132e0fd20e331f64baa9dd5" + integrity sha512-y5lGlazdc0HNO0F3UUX2DPE7OmYvd9Kcym4hXwrJcNUkDaypR5pX+apuMikl9LfTxKItJsY9KYvzBulpCKyvuQ== "@types/d3-color@*": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.1.tgz#26141c3c554e320edd40726b793570a3ae57397e" - integrity sha512-xwb1tqvYNWllbHuhMFhiXk63Imf+QNq/dJdmbXmr2wQVnwGenCuj3/0IWJ9hdIFQIqzvhT7T37cvx93jtAsDbQ== + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.2.tgz#80cf7cfff7401587b8f89307ba36fe4a576bc7cf" + integrity sha512-6pBxzJ8ZP3dYEQ4YjQ+NVbQaOflfgXq/JbDiS99oLobM2o72uAST4q6yPxHv6FOTCRC/n35ktuo8pvw/S4M7sw== "@types/d3-dispatch@*": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-1.0.6.tgz#19b173f669cd2ab7dd3d862e8037aae1a98c7508" - integrity sha512-xyWJQMr832vqhu6fD/YqX+MSFBWnkxasNhcStvlhqygXxj0cKqPft0wuGoH5TIq5ADXgP83qeNVa4R7bEYN3uA== + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-1.0.7.tgz#6721aefbb9862ce78c20a87a1490c21f57c3ed7f" + integrity sha512-M+z84G7UKwK6hEPnGCSccOg8zJ3Nk2hgDQ9sCstHXgsFU0sMxlIZVKqKB5oxUDbALqQG6ucg0G9e8cmOSlishg== "@types/d3-drag@*": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.2.2.tgz#1cdd7716212a8cdef0a24c782c9d86c6aeb4a451" - integrity sha512-+UKFeaMVTfSQvMO0PTzOyLXSr7OZbF2Rx1iNVwo2XsyiOsd4MSuLyJKUwRmGn67044QpbNzr+VD6/8iBBLExWw== + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.2.3.tgz#d8ddccca28e939e9c689bea6f40a937e48c39051" + integrity sha512-rWB5SPvkYVxW3sqUxHOJUZwifD0KqvKwvt1bhNqcLpW6Azsd0BJgRNcyVW8GAferaAk5r8dzeZnf9zKlg9+xMQ== dependencies: "@types/d3-selection" "*" "@types/d3-dsv@*": - version "1.0.35" - resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.35.tgz#eeb884bfbaa6775daa41ebd2dc8c07a24505f311" - integrity sha512-QeH7cN9phcm68TDwpSGmzE71/JtGoKZ2rZJABNUMQ7nYIhHkm2UldqI1Cp2pjEo8ycSeutudjzq+Lfim/ZCadQ== + version "1.0.36" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.36.tgz#e91129d7c02b1b814838d001e921e8b9a67153d0" + integrity sha512-jbIWQ27QJcBNMZbQv0NSQMHnBDCmxghAxePxgyiPH1XPCRkOsTBei7jcdi3fDrUCGpCV3lKrSZFSlOkhUQVClA== "@types/d3-ease@*": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-1.0.7.tgz#93a301868be9e15061f3d44343b1ab3f8acb6f09" - integrity sha1-k6MBhovp4VBh89RDQ7GrP4rLbwk= + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-1.0.8.tgz#b440761fb85985d76259ec9c5bf01c4c56778ac2" + integrity sha512-VRf8czVWHSJPoUWxMunzpePK02//wHDAswknU8QWzcyrQn6pqe46bHRYi2smSpw5VjsT2CG8k/QeWIdWPS3Bmg== "@types/d3-force@*": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.2.0.tgz#430d572eb3922bc463726dd06580829ef36b6434" - integrity sha512-rfNJogFDPEO16RBqA4anZtiscYeMxreNg8zUKGnBi/1DnrZ42rG5RvOS/qXqBqLZJ3HY0ouAx8AZqgT3/hHbFA== + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.2.1.tgz#c28803ea36fe29788db69efa0ad6c2dc09544e83" + integrity sha512-jqK+I36uz4kTBjyk39meed5y31Ab+tXYN/x1dn3nZEus9yOHCLc+VrcIYLc/aSQ0Y7tMPRlIhLetulME76EiiA== "@types/d3-format@*": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.3.0.tgz#c5e115fac8e6861ce656fe9861892b22f6b0cfcb" - integrity sha512-ZiY4j3iJvAdOwzwW24WjlZbUNvqOsnPAMfPBmdXqxj3uKJbrzBlRrdGl5uC89pZpFs9Dc92E81KcwG2uEgkIZA== + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.3.1.tgz#35bf88264bd6bcda39251165bb827f67879c4384" + integrity sha512-KAWvReOKMDreaAwOjdfQMm0HjcUMlQG47GwqdVKgmm20vTd2pucj0a70c3gUSHrnsmo6H2AMrkBsZU2UhJLq8A== "@types/d3-geo@*": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.11.0.tgz#f7921c5c50d1a43df928846d8a7e47140455687f" - integrity sha512-/IbMHRG9cur+6hkWvBrRg3DnnUWtaSW8Bl6nu1OO1J8K25BxRYyLslyjIBbwlK0kV0haztlAR2LCIRuDc/U2LA== + version "1.11.1" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.11.1.tgz#e96ec91f16221d87507fec66b2cc889f52d2493e" + integrity sha512-Ox8WWOG3igDRoep/dNsGbOiSJYdUG3ew/6z0ETvHyAtXZVBjOE0S96zSSmzgl0gqQ3RdZjn2eeJOj9oRcMZPkQ== dependencies: "@types/geojson" "*" "@types/d3-hierarchy@*": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#b9a941bbfbd8f83a163a1667aae8a52f41a9edab" - integrity sha512-DKhqURrURt2c7MsF9sHiF2wrWf2+yZR4Q9oIG026t/ZY4VWoM0Yd7UonaR+rygyReWcFSEjKC/+5A27TgD8R8g== + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.6.tgz#4c017521900813ea524c9ecb8d7985ec26a9ad9a" + integrity sha512-vvSaIDf/Ov0o3KwMT+1M8+WbnnlRiGjlGD5uvk83a1mPCTd/E5x12bUJ/oP55+wUY/4Kb5kc67rVpVGJ2KUHxg== "@types/d3-interpolate@*": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.3.0.tgz#65b9627900bfdd82474875d9b23d574a4388af7c" - integrity sha512-Ng4ds7kPSvP/c3W3J5PPUQlgewif1tGBqCeh5lgY+UG82Y7H9zQ8c2gILsEFDLg7wRGOwnuKZ940Q/LSN14w9w== + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.3.1.tgz#1c280511f622de9b0b47d463fa55f9a4fd6f5fc8" + integrity sha512-z8Zmi08XVwe8e62vP6wcA+CNuRhpuUU5XPEfqpG0hRypDE5BWNthQHB1UNWWDB7ojCbGaN4qBdsWp5kWxhT1IQ== dependencies: "@types/d3-color" "*" "@types/d3-path@*": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.7.tgz#a0736fceed688a695f48265a82ff7a3369414b81" - integrity sha512-U8dFRG+8WhkLJr2sxZ9Cw/5WeRgBnNqMxGdA1+Z0+ZG6tK0s75OQ4OXnxeyfKuh6E4wQPY8OAKr1+iNDx01BEQ== + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79" + integrity sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA== "@types/d3-polygon@*": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-1.0.6.tgz#db25c630a2afb9191fe51ba61dd37baee9dd44c7" - integrity sha512-E6Kyodn9JThgLq20nxSbEce9ow5/ePgm9PX2EO6W1INIL4DayM7cFaiG10DStuamjYAd0X4rntW2q+GRjiIktw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-1.0.7.tgz#7b3947aa2d48287ff535230d3d396668ab17bfdf" + integrity sha512-Xuw0eSjQQKs8jTiNbntWH0S+Xp+JyhqxmQ0YAQ3rDu6c3kKMFfgsaGN7Jv5u3zG6yVX/AsLP/Xs/QRjmi9g43Q== "@types/d3-quadtree@*": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.6.tgz#45da9e603688ba90eedd3d40f6e504764e06e493" - integrity sha512-sphVuDdiSIaxLt9kQgebJW98pTktQ/xuN7Ysd8X68Rnjeg/q8+c36/ShlqU52qoKg9nob/JEHH1uQMdxURZidQ== + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.7.tgz#8e29464ff5b326f6612c1428d9362b4b35de2b70" + integrity sha512-0ajFawWicfjsaCLh6NzxOyVDYhQAmMFbsiI3MPGLInorauHFEh9/Cl6UHNf+kt/J1jfoxKY/ZJaKAoDpbvde5Q== "@types/d3-queue@*": - version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-queue/-/d3-queue-3.0.7.tgz#94dc7af693281ab78ccdf381a8c1f71ef16659c1" - integrity sha512-nBbDO69wu1TUWqtGYAePw40jSPcQSt5VwAf7403vYopVCs3Rtbt5f47j2wbuMY4Z2x543VbTIlDo5gwdpV5O+Q== + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-queue/-/d3-queue-3.0.8.tgz#fad6212f14f34a549fc67144e354f032fb25a447" + integrity sha512-1FWOiI/MYwS5Z1Sa9EvS1Xet3isiVIIX5ozD6iGnwHonGcqL+RcC1eThXN5VfDmAiYt9Me9EWNEv/9J9k9RIKQ== "@types/d3-random@*": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-1.1.1.tgz#38647ce2ff4ce7d0d56974334c1c4092513c8b9f" - integrity sha512-jUPeBq1XKK9/5XasTvy5QAUwFeMsjma2yt/nP02yC2Tijovx7i/W5776U/HZugxc5SSmtpx4Z3g9KFVon0QrjQ== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-1.1.2.tgz#6f77e8b7bb64ac393f92d33fe8f71038bc4f3cde" + integrity sha512-Jui+Zn28pQw/3EayPKaN4c/PqTvqNbIPjHkgIIFnxne1FdwNjfHtAIsZIBMKlquQNrrMjFzCrlF2gPs3xckqaA== "@types/d3-request@*": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-request/-/d3-request-1.0.4.tgz#b7bde4c63794ee6a47dc3d71d64ca91b9ac87349" - integrity sha512-6ZVaWdXNjXEp3A+PB/vMTIZDfmEiSay3oDyy7HpsTmnSAWSsqfXYTE9RxMmZs8MY0QMFbtous0LiUSrv5uOGXA== + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/d3-request/-/d3-request-1.0.5.tgz#a2717ab95cd1e504662f52802aff1476af38cce4" + integrity sha512-X+/c/qXp92o056C5Qbcp7jL27YRHpmIqOchHb/WB7NwFFqkBtAircqO7oKWv2GTtX4LyEqiDF9gqXsV+ldOlIg== dependencies: "@types/d3-dsv" "*" @@ -1668,48 +1665,48 @@ "@types/d3-time" "*" "@types/d3-selection@*": - version "1.3.4" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.3.4.tgz#2f9b71e25fb73bc95c3842dd52b7e6d523292896" - integrity sha512-WQ6Ivy7VuUlZ/Grqc8493ZxC+y/fpvZLy5+8ELvmCr2hll8eJPUqC05l6fgRRA7kjqlpbH7lbmvY6pRKf6yzxw== + version "1.4.1" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.1.tgz#fa1f8710a6b5d7cfe5c6caa61d161be7cae4a022" + integrity sha512-bv8IfFYo/xG6dxri9OwDnK3yCagYPeRIjTlrcdYJSx+FDWlCeBDepIHUpqROmhPtZ53jyna0aUajZRk0I3rXNA== "@types/d3-shape@*": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.0.tgz#ea195c543a1f6d3e250ceb07a7d208d80ee37b01" - integrity sha512-ERWJ8bNZjkzfWfPAlkN3XCqYOOsWTnqTX0jX2Bx+WLd2AfEl97WXr2igwssFc91MadrZLw7HNS/JTUZPQL5sZQ== + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.1.tgz#1b4f92b7efd7306fe2474dc6ee94c0f0ed2e6ab6" + integrity sha512-usqdvUvPJ7AJNwpd2drOzRKs1ELie53p2m2GnPKr076/ADM579jVTJ5dPsoZ5E/CMNWk8lvPWYQSvilpp6jjwg== dependencies: "@types/d3-path" "*" "@types/d3-time-format@*": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.1.0.tgz#011e0fb7937be34a9a8f580ae1e2f2f1336a8a22" - integrity sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA== + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.1.1.tgz#dd2c79ec4575f1355484ab6b10407824668eba42" + integrity sha512-tJSyXta8ZyJ52wDDHA96JEsvkbL6jl7wowGmuf45+fAkj5Y+SQOnz0N7/H68OWmPshPsAaWMQh+GAws44IzH3g== "@types/d3-time@*": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.9.tgz#c2cf05a3cd51f810b8d8a9bbca0c74030d4e535e" - integrity sha512-m+D4NbQdDlTVaO7QgXAnatR3IDxQYDMBtRhgSCi5rs9R1LPq1y7/2aqa1FJ2IWjFm1mOV63swDxonnCDlHgHMA== + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.10.tgz#d338c7feac93a98a32aac875d1100f92c7b61f4f" + integrity sha512-aKf62rRQafDQmSiv1NylKhIMmznsjRN+MnXRXTqHoqm0U/UZzVpdrtRnSIfdiLS616OuC1soYeX1dBg2n1u8Xw== "@types/d3-timer@*": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.8.tgz#a3441d9605367059e14ad8c3494132143cbc8d58" - integrity sha512-AKUgQ/nljUFcUO2P3gK24weVI5XwUTdJvjoh8gJ0yxT4aJ+d7t2Or3TB+k9dEYl14BAjoj32D0ky+YzQSVszfg== + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.9.tgz#aed1bde0cf18920d33f5d44839d73de393633fd3" + integrity sha512-WvfJ3LFxBbWjqRGz9n7GJt08RrTHPJDVsIwwoCMROlqF+iDacYiAFjf9oqnq0mXpb2juA2N/qjKP+MKdal3YNQ== "@types/d3-transition@*": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.3.tgz#efcf4941dae22135d595514ba488f4f370d396b0" - integrity sha512-1EukXNuVu/z2G1GZpZagzFJnie9C5zze17ox/vhTgGXNy46rYAm4UkhLLlUeeZ1ndq88k95SOeC8898RpKMLOQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.4.tgz#3c7a35ae9acfc59dfef1eb7308ebabf0fc0680de" + integrity sha512-/vsmKVUIXEyCcIXYAlw7bnYkIs9/J/nZbptRJFKUN3FdXq/dF6j9z9xXzerkyU6TDHLrMrwx9eGwdKyTIy/j9w== dependencies: "@types/d3-selection" "*" "@types/d3-voronoi@*": - version "1.1.8" - resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.8.tgz#a039cb8368bce4efc1a70aebe744d210851cf1a7" - integrity sha512-zqNhW7QsYQGlfOdrwPNPG3Wk64zUa4epKRurkJ/dVc6oeXrB+iTDt8sRZ0KZKOOXvvfa1dcdB0e45TZeLBiodQ== + version "1.1.9" + resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz#7bbc210818a3a5c5e0bafb051420df206617c9e5" + integrity sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ== "@types/d3-zoom@*": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.7.3.tgz#ed9421551328157f70edabc401d8c91c38d360d9" - integrity sha512-Tz7+z4+Id0MxERw/ozinC5QHJmGLARs9Mpi/7VVfiR+9AHcFGe9q+fjQa30/oPNY8WPuCh5p5uuXmBYAJ3y91Q== + version "1.7.4" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.7.4.tgz#9226ffd2bd3846ec0e4a4e2bff211612d3aafad5" + integrity sha512-5jnFo/itYhJeB2khO/lKe730kW/h2EbKMOvY0uNp3+7NdPm4w63DwPEMxifQZ7n902xGYK5DdU67FmToSoy4VA== dependencies: "@types/d3-interpolate" "*" "@types/d3-selection" "*" @@ -1751,9 +1748,9 @@ "@types/d3-zoom" "*" "@types/enzyme@^3.1.13": - version "3.1.17" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.17.tgz#41f553bfdbaa00880488feabb3ade47d5489db42" - integrity sha512-pZ+Blk1hODkprPZ9cxXd8njxdBnbLGWOKAmKk0QhpJvWzI4q4F20FHHUnkZXPXJt5WnK6SbbY5lfTKoz1M/CTw== + version "3.9.0" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.9.0.tgz#a81c91e2dfd2d70e67f013f2c0e5efed6df05489" + integrity sha512-o0C7ooyBtj9NKyMzn2BWN53W4J21KPhO+/v+qqQX28Pcz0Z1B3DjL9bq2ZR4TN70PVw8O7gkhuFtC7VN3tausg== dependencies: "@types/cheerio" "*" "@types/react" "*" @@ -1764,9 +1761,9 @@ integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== "@types/geojson@*": - version "7946.0.5" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.5.tgz#9aea839ea5af4b1bc079f1d9fa977d48665e02b0" - integrity sha512-rLlMXpd3rdlrp0+xsrda/hFfOpIxgqFcRpk005UKbHtcdFK+QXAjhBAPnvO58qF4O1LdDXrcaiJxMgstCIlcaw== + version "7946.0.6" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.6.tgz#416f388a06b227784a2d91a88a53f14de05cd54b" + integrity sha512-f6qai3iR62QuMPPdgyH+LyiXTL2n9Rf62UniJjV7KHrbiwzLTZUKsdq0mFSTxAHbO7JvwxwC4tH0m1UnweuLrA== "@types/inquirer@^0.0.43": version "0.0.43" @@ -1787,24 +1784,24 @@ integrity sha512-SVtqEcudm7yjkTwoRA1gC6CNMhGDdMx4Pg8BPdiqI7bXXdCn1BPmtxgeWYQOgDxrq53/5YTlhq5ULxBEAlWIBg== "@types/lodash@^4.14.119": - version "4.14.119" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39" - integrity sha512-Z3TNyBL8Vd/M9D9Ms2S3LmFq2sSMzahodD6rCS9V2N44HUMINb75jNkSuwAx7eo2ufqTdfOdtGQpNbieUjPQmw== + version "4.14.123" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.123.tgz#39be5d211478c8dd3bdae98ee75bb7efe4abfe4d" + integrity sha512-pQvPkc4Nltyx7G1Ww45OjVqUsJP4UsZm+GWJpigXgkikZqJgRm4c48g027o6tdgubWHwFRF15iFd+Y4Pmqv6+Q== -"@types/node@*": - version "11.9.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.0.tgz#35fea17653490dab82e1d5e69731abfdbf13160d" - integrity sha512-ry4DOrC+xenhQbzk1iIPzCZGhhPGEFv7ia7Iu6XXSLVluiJIe9FfG7Iu3mObH9mpxEXCWLCMU4JWbCCR9Oy1Zg== +"@types/node@*", "@types/node@^11.9.5": + version "11.11.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.3.tgz#7c6b0f8eaf16ae530795de2ad1b85d34bf2f5c58" + integrity sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg== "@types/node@^10.12.18": - version "10.12.25" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.25.tgz#0d01a7dd6127de60d861ece4a650963042abb538" - integrity sha512-IcvnGLGSQFDvC07Bz2I8SX+QKErDZbUdiQq7S2u3XyzTyJfUmT0sWJMbeQkMzpTAkO7/N7sZpW/arUM2jfKsbQ== + version "10.14.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.1.tgz#8701cd760acc20beba5ffe0b7a1b879f39cb8c41" + integrity sha512-Rymt08vh1GaW4vYB6QP61/5m/CFLGnFZP++bJpWbiNxceNa6RBipDmb413jvtSf/R1gg5a/jQVl2jY4XVRscEA== "@types/node@^8.0.31": - version "8.10.40" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.40.tgz#4314888d5cd537945d73e9ce165c04cc550144a4" - integrity sha512-RRSjdwz63kS4u7edIwJUn8NqKLLQ6LyqF/X4+4jp38MBT3Vwetewi2N4dgJEshLbDwNgOJXNYoOwzVZUSSLhkQ== + version "8.10.44" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.44.tgz#b00cf3595c6a3d75740af9768739a8125053a5a9" + integrity sha512-HY3SK7egERHGUfY8p6ztXIEQWcIPHouYhCGcLAPQin7gE2G/fALFz+epnMwcxKUS6aKqTVoAFdi+t1llQd3xcw== "@types/papaparse@^4.5.9": version "4.5.9" @@ -1814,19 +1811,19 @@ "@types/node" "*" "@types/prop-types@*": - version "15.5.8" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.8.tgz#8ae4e0ea205fe95c3901a5a1df7f66495e3a56ce" - integrity sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw== + version "15.7.0" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.0.tgz#4c48fed958d6dcf9487195a0ef6456d5f6e0163a" + integrity sha512-eItQyV43bj4rR3JPV0Skpl1SncRCdziTEK9/v8VwXmV6d/qOUO8/EuWeHBbCZcsfSHfzI5UyMJLCSXtxxznyZg== "@types/q@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18" - integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA== + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" + integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== "@types/react-color@^2.14.0": - version "2.14.0" - resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-2.14.0.tgz#e01f6069902819fe9f6544375829a485f894206f" - integrity sha512-5UfGjUsu7bXop7K064nNrIGgS7wPjGEY7Or9tAE6BLslDtfhRnAlqWo9N2BIntEZ/3KpXlRnt0MBuc+hZJTevw== + version "2.17.0" + resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-2.17.0.tgz#7f3c958bb43ebeedc7e04309576a235d5233ce9d" + integrity sha512-NQCLW437DXzaV/XvtoH3cBW75f0KQ9ZtFvvXnn7QEudLTR5zGxLdsEhPffrateSizsG2CTml4X+2/2TyEisotQ== dependencies: "@types/react" "*" @@ -1837,10 +1834,10 @@ dependencies: "@types/react" "*" -"@types/react-dom@*", "@types/react-dom@^16.0.9": - version "16.8.0" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.0.tgz#c565f43f9d2ec911f9e0b8f3b74e25e67879aa3f" - integrity sha512-Jp4ufcEEjVJEB0OHq2MCZcE1u3KYUKO6WnSuiU/tZeYeiZxUoQavfa/TZeiIT+1XoN6l0lQVNM30VINZFDeolQ== +"@types/react-dom@*", "@types/react-dom@^16.8.2": + version "16.8.2" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.2.tgz#9bd7d33f908b243ff0692846ef36c81d4941ad12" + integrity sha512-MX7n1wq3G/De15RGAAqnmidzhr2Y9O/ClxPxyqaNg96pGyeXUYPSvujgzEVpLo9oIP4Wn1UETl+rxTN02KEpBw== dependencies: "@types/react" "*" @@ -1852,25 +1849,25 @@ "@types/react" "*" "@types/react-select@^2.0.4": - version "2.0.11" - resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-2.0.11.tgz#9b2b1fdb12b67a5a617c5f572e15617636cc65af" - integrity sha512-kITn4R50eUJCi2YT3JFZS4z5M2SJJqqYiVUX1HyLSFWbHbF6J25ZPKCCXANQrsnQzSrac2XiNpR5oYBif6l93g== + version "2.0.15" + resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-2.0.15.tgz#51d607667f59a12e980abcc5bbf9636307293e44" + integrity sha512-lbtGCfZ82lKAU0KPoO6M81ZqoT3cOOLWTNkwgmPlBekNBt95ccWItAIKiGZnoO7+gzk413biIxetRSM2CoLL8w== dependencies: "@types/react" "*" "@types/react-dom" "*" "@types/react-transition-group" "*" "@types/react-test-renderer@^16.0.3": - version "16.8.0" - resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.0.tgz#dbed6549f97a7f114b6920bf553a5db7e61bf83c" - integrity sha512-m563EQSTVB2g6h+FDUH2cgfiRdjL1KHVyi643EQQSFIblMPrWwJh/adqTcMS/FhJHvhEboR4pmhrhEXyHDDsmQ== + version "16.8.1" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.1.tgz#96f3ce45a3a41c94eca532a99103dd3042c9d055" + integrity sha512-8gU69ELfJGxzVWVYj4MTtuHxz9nO+d175XeQ1XrXXxesUBsB4KK6OCfzVhEX6leZWWBDVtMJXp/rUjhClzL7gw== dependencies: "@types/react" "*" "@types/react-transition-group@*", "@types/react-transition-group@^2.0.15": - version "2.0.15" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.15.tgz#e5ee3fe558832e141cc6041bdd54caea7b787af8" - integrity sha512-S0QnNzbHoWXDbKBl/xk5dxA4FT+BNlBcI3hku991cl8Cz3ytOkUMcCRtzdX11eb86E131bSsQqy5WrPCdJYblw== + version "2.0.16" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.16.tgz#2dcb9e396ab385ee19c4af1c9caa469a14cd042f" + integrity sha512-FUJEx2BGJPU1qVQoWd9v7wpOwnCPTWhcE4iTaU5prry9SvwiI11lCXOci8Nz9cM/Fuf650l7Skg6nlVeCYjPFA== dependencies: "@types/react" "*" @@ -1882,10 +1879,10 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react@*", "@types/react@16.7.6", "@types/react@^16.7.6": - version "16.7.6" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040" - integrity sha512-QBUfzftr/8eg/q3ZRgf/GaDP6rTYc7ZNem+g4oZM38C9vXyV8AWRWaTQuW5yCoZTsfHrN7b3DeEiUnqH9SrnpA== +"@types/react@*", "@types/react@16.8.8", "@types/react@^16.7.6", "@types/react@^16.8.8": + version "16.8.8" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.8.tgz#4b60a469fd2469f7aa6eaa0f8cfbc51f6d76e662" + integrity sha512-xwEvyet96u7WnB96kqY0yY7qxx/pEpU51QeACkKFtrgjjXITQn0oO1iwPEraXVgh10ZFPix7gs1R4OJXF7P5sg== dependencies: "@types/prop-types" "*" csstype "^2.2.0" @@ -1999,30 +1996,30 @@ "@types/rx-lite-virtualtime" "*" "@types/storybook__addon-actions@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@types/storybook__addon-actions/-/storybook__addon-actions-3.4.1.tgz#8f90d76b023b58ee794170f2fe774a3fddda2c1d" - integrity sha512-An8pNb1/7QhkdOT8Ht5WjJsSxAh2mWti/J4eILwUHpXVZ1j3xlVaOzwTbg8twN4DjgOAggjEDOj6Bx8YOWh9Pg== + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/storybook__addon-actions/-/storybook__addon-actions-3.4.2.tgz#1d08689cc3259269ddb3479a2307c9d16944309e" + integrity sha512-CWxGz2pXav9PHcwrtXmkuH+xJL7sAu2AmIGEbkdT3Xs5jzBPZUEDEN//ZF7o6IOPP/tdXU37K1hrVMt9TDO0Bw== "@types/storybook__addon-info@^3.4.2": - version "3.4.3" - resolved "https://registry.yarnpkg.com/@types/storybook__addon-info/-/storybook__addon-info-3.4.3.tgz#c952150737830d665b8c95a9b652b18226dd8afa" - integrity sha512-j9lhGbdSV6ydZGJ24CShlMEnvYTiGYBM+sfT88XnLm3j/mr/VkBzU9fhGlDPdWk7/rDgqkrztDk/0E3AsxBNRg== + version "3.4.4" + resolved "https://registry.yarnpkg.com/@types/storybook__addon-info/-/storybook__addon-info-3.4.4.tgz#6ded3159f6e279746790566544333f83d06a2703" + integrity sha512-x3AIgYfaojkjxq9WxtQE/ZdQzptisiSzL4J5cwKvO8vIMyHPdzD4lMAOEs+ucP43TxmZldrdutqzzq20jbbZKQ== dependencies: "@types/react" "*" "@types/storybook__react" "*" "@types/storybook__addon-knobs@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/storybook__addon-knobs/-/storybook__addon-knobs-4.0.0.tgz#b218f0d84888833cc8b8d7a7b524175e8bb3030f" - integrity sha512-x3GNz8f0fQv7USvDuVXdZ4p/7nofFHyH6iB/qwR84Yp97xZxOzlQ0SY+6K14tVbdi9P7Qm5DZ2kZr0a+Io8qEQ== + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/storybook__addon-knobs/-/storybook__addon-knobs-4.0.4.tgz#66bd835a086a8d881013a09386da65270713c3a0" + integrity sha512-dN7sRS7pjLHVRY+Cnk7G94kfc7LZQAzEGnqOY0XG9ZdKse25tAKlAyidI7rjxwJ54v1CWswP8p+X4lBznV3cRw== dependencies: "@types/react" "*" "@types/storybook__react" "*" "@types/storybook__react@*", "@types/storybook__react@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/storybook__react/-/storybook__react-4.0.0.tgz#52cc452fbab599568d595075a90142ef4a1233f6" - integrity sha512-Iq3RX953fqZRwWN3jywm8pUx1/Atev+x/9tF7/2CNA+Ii55sGSJJRWMRthUKQXTa3zOexcvfksfVYdUaIZY91w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/storybook__react/-/storybook__react-4.0.1.tgz#b6320c9d027b8ee7ef1445fef8b4cba196d48ace" + integrity sha512-knkZErqv8Iy2QbebqBa5tsy2itIMKdO6bcQ7C19nmgTc+j1pnQhXCGcVyARzAQ1/NAuSYudSWQAKG+plgK7hyQ== dependencies: "@types/react" "*" "@types/webpack-env" "*" @@ -2064,9 +2061,9 @@ source-map "^0.6.1" "@types/unist@*", "@types/unist@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.2.tgz#5dc0a7f76809b7518c0df58689cd16a19bd751c6" - integrity sha512-iHI60IbyfQilNubmxsq4zqSjdynlmc2Q/QvH9kjzg9+CCYVVzq1O6tc7VBzSygIwnmOt07w80IG6HDQvjv3Liw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" + integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== "@types/vfile-message@*": version "1.0.1" @@ -2086,9 +2083,9 @@ "@types/vfile-message" "*" "@types/webpack-env@*": - version "1.13.7" - resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.7.tgz#137a4e57aa31ab57b1baf66f5dc3b6bf085e9944" - integrity sha512-rzi6fw7hhxPcCoNVsgysHFlKnhYYvVj7AJwdAO0HQNP5vg9sY0DoRRC1pfuCQm94cOa1sab82HGUtdFlWHIhBg== + version "1.13.9" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.9.tgz#a67287861c928ebf4159a908d1fb1a2a34d4097a" + integrity sha512-p8zp5xqkly3g4cCmo2mKOHI9+Z/kObmDj0BmjbDDJQlgDTiEGTbm17MEwTAusV6XceCy+bNw9q/ZHXHyKo3zkg== "@types/webpack@^3.0.5": version "3.8.17" @@ -2100,15 +2097,6 @@ "@types/uglify-js" "*" source-map "^0.6.0" -"@webassemblyjs/ast@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace" - integrity sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA== - dependencies: - "@webassemblyjs/helper-module-context" "1.7.11" - "@webassemblyjs/helper-wasm-bytecode" "1.7.11" - "@webassemblyjs/wast-parser" "1.7.11" - "@webassemblyjs/ast@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.6.tgz#3ef8c45b3e5e943a153a05281317474fef63e21e" @@ -2119,42 +2107,44 @@ "@webassemblyjs/wast-parser" "1.7.6" mamacro "^0.0.3" -"@webassemblyjs/floating-point-hex-parser@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz#a69f0af6502eb9a3c045555b1a6129d3d3f2e313" - integrity sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg== +"@webassemblyjs/ast@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" + integrity sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ== + dependencies: + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" "@webassemblyjs/floating-point-hex-parser@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.6.tgz#7cb37d51a05c3fe09b464ae7e711d1ab3837801f" integrity sha512-VBOZvaOyBSkPZdIt5VBMg3vPWxouuM13dPXGWI1cBh3oFLNcFJ8s9YA7S9l4mPI7+Q950QqOmqj06oa83hNWBA== -"@webassemblyjs/helper-api-error@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz#c7b6bb8105f84039511a2b39ce494f193818a32a" - integrity sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg== +"@webassemblyjs/floating-point-hex-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" + integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ== "@webassemblyjs/helper-api-error@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.6.tgz#99b7e30e66f550a2638299a109dda84a622070ef" integrity sha512-SCzhcQWHXfrfMSKcj8zHg1/kL9kb3aa5TN4plc/EREOs5Xop0ci5bdVBApbk2yfVi8aL+Ly4Qpp3/TRAUInjrg== -"@webassemblyjs/helper-buffer@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz#3122d48dcc6c9456ed982debe16c8f37101df39b" - integrity sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w== +"@webassemblyjs/helper-api-error@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" + integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA== "@webassemblyjs/helper-buffer@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.6.tgz#ba0648be12bbe560c25c997e175c2018df39ca3e" integrity sha512-1/gW5NaGsEOZ02fjnFiU8/OEEXU1uVbv2um0pQ9YVL3IHSkyk6xOwokzyqqO1qDZQUAllb+V8irtClPWntbVqw== -"@webassemblyjs/helper-code-frame@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz#cf8f106e746662a0da29bdef635fcd3d1248364b" - integrity sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw== - dependencies: - "@webassemblyjs/wast-printer" "1.7.11" +"@webassemblyjs/helper-buffer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" + integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q== "@webassemblyjs/helper-code-frame@1.7.6": version "1.7.6" @@ -2163,20 +2153,22 @@ dependencies: "@webassemblyjs/wast-printer" "1.7.6" -"@webassemblyjs/helper-fsm@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz#df38882a624080d03f7503f93e3f17ac5ac01181" - integrity sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A== +"@webassemblyjs/helper-code-frame@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" + integrity sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ== + dependencies: + "@webassemblyjs/wast-printer" "1.8.5" "@webassemblyjs/helper-fsm@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.6.tgz#ae1741c6f6121213c7a0b587fb964fac492d3e49" integrity sha512-HCS6KN3wgxUihGBW7WFzEC/o8Eyvk0d56uazusnxXthDPnkWiMv+kGi9xXswL2cvfYfeK5yiM17z2K5BVlwypw== -"@webassemblyjs/helper-module-context@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz#d874d722e51e62ac202476935d649c802fa0e209" - integrity sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg== +"@webassemblyjs/helper-fsm@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" + integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow== "@webassemblyjs/helper-module-context@1.7.6": version "1.7.6" @@ -2185,25 +2177,23 @@ dependencies: mamacro "^0.0.3" -"@webassemblyjs/helper-wasm-bytecode@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz#dd9a1e817f1c2eb105b4cf1013093cb9f3c9cb06" - integrity sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ== +"@webassemblyjs/helper-module-context@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" + integrity sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g== + dependencies: + "@webassemblyjs/ast" "1.8.5" + mamacro "^0.0.3" "@webassemblyjs/helper-wasm-bytecode@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.6.tgz#98e515eaee611aa6834eb5f6a7f8f5b29fefb6f1" integrity sha512-PzYFCb7RjjSdAOljyvLWVqd6adAOabJW+8yRT+NWhXuf1nNZWH+igFZCUK9k7Cx7CsBbzIfXjJc7u56zZgFj9Q== -"@webassemblyjs/helper-wasm-section@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz#9c9ac41ecf9fbcfffc96f6d2675e2de33811e68a" - integrity sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q== - dependencies: - "@webassemblyjs/ast" "1.7.11" - "@webassemblyjs/helper-buffer" "1.7.11" - "@webassemblyjs/helper-wasm-bytecode" "1.7.11" - "@webassemblyjs/wasm-gen" "1.7.11" +"@webassemblyjs/helper-wasm-bytecode@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" + integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ== "@webassemblyjs/helper-wasm-section@1.7.6": version "1.7.6" @@ -2215,12 +2205,15 @@ "@webassemblyjs/helper-wasm-bytecode" "1.7.6" "@webassemblyjs/wasm-gen" "1.7.6" -"@webassemblyjs/ieee754@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz#c95839eb63757a31880aaec7b6512d4191ac640b" - integrity sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ== +"@webassemblyjs/helper-wasm-section@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" + integrity sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA== dependencies: - "@xtuc/ieee754" "^1.2.0" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" "@webassemblyjs/ieee754@1.7.6": version "1.7.6" @@ -2229,12 +2222,12 @@ dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.11.tgz#d7267a1ee9c4594fd3f7e37298818ec65687db63" - integrity sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw== +"@webassemblyjs/ieee754@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" + integrity sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g== dependencies: - "@xtuc/long" "4.2.1" + "@xtuc/ieee754" "^1.2.0" "@webassemblyjs/leb128@1.7.6": version "1.7.6" @@ -2243,29 +2236,22 @@ dependencies: "@xtuc/long" "4.2.1" -"@webassemblyjs/utf8@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.11.tgz#06d7218ea9fdc94a6793aa92208160db3d26ee82" - integrity sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA== +"@webassemblyjs/leb128@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" + integrity sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A== + dependencies: + "@xtuc/long" "4.2.2" "@webassemblyjs/utf8@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.6.tgz#eb62c66f906af2be70de0302e29055d25188797d" integrity sha512-oId+tLxQ+AeDC34ELRYNSqJRaScB0TClUU6KQfpB8rNT6oelYlz8axsPhf6yPTg7PBJ/Z5WcXmUYiHEWgbbHJw== -"@webassemblyjs/wasm-edit@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz#8c74ca474d4f951d01dbae9bd70814ee22a82005" - integrity sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg== - dependencies: - "@webassemblyjs/ast" "1.7.11" - "@webassemblyjs/helper-buffer" "1.7.11" - "@webassemblyjs/helper-wasm-bytecode" "1.7.11" - "@webassemblyjs/helper-wasm-section" "1.7.11" - "@webassemblyjs/wasm-gen" "1.7.11" - "@webassemblyjs/wasm-opt" "1.7.11" - "@webassemblyjs/wasm-parser" "1.7.11" - "@webassemblyjs/wast-printer" "1.7.11" +"@webassemblyjs/utf8@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" + integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw== "@webassemblyjs/wasm-edit@1.7.6": version "1.7.6" @@ -2281,16 +2267,19 @@ "@webassemblyjs/wasm-parser" "1.7.6" "@webassemblyjs/wast-printer" "1.7.6" -"@webassemblyjs/wasm-gen@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz#9bbba942f22375686a6fb759afcd7ac9c45da1a8" - integrity sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA== +"@webassemblyjs/wasm-edit@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" + integrity sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q== dependencies: - "@webassemblyjs/ast" "1.7.11" - "@webassemblyjs/helper-wasm-bytecode" "1.7.11" - "@webassemblyjs/ieee754" "1.7.11" - "@webassemblyjs/leb128" "1.7.11" - "@webassemblyjs/utf8" "1.7.11" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/helper-wasm-section" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-opt" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + "@webassemblyjs/wast-printer" "1.8.5" "@webassemblyjs/wasm-gen@1.7.6": version "1.7.6" @@ -2303,15 +2292,16 @@ "@webassemblyjs/leb128" "1.7.6" "@webassemblyjs/utf8" "1.7.6" -"@webassemblyjs/wasm-opt@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz#b331e8e7cef8f8e2f007d42c3a36a0580a7d6ca7" - integrity sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg== +"@webassemblyjs/wasm-gen@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" + integrity sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg== dependencies: - "@webassemblyjs/ast" "1.7.11" - "@webassemblyjs/helper-buffer" "1.7.11" - "@webassemblyjs/wasm-gen" "1.7.11" - "@webassemblyjs/wasm-parser" "1.7.11" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" "@webassemblyjs/wasm-opt@1.7.6": version "1.7.6" @@ -2323,17 +2313,15 @@ "@webassemblyjs/wasm-gen" "1.7.6" "@webassemblyjs/wasm-parser" "1.7.6" -"@webassemblyjs/wasm-parser@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz#6e3d20fa6a3519f6b084ef9391ad58211efb0a1a" - integrity sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg== +"@webassemblyjs/wasm-opt@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" + integrity sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q== dependencies: - "@webassemblyjs/ast" "1.7.11" - "@webassemblyjs/helper-api-error" "1.7.11" - "@webassemblyjs/helper-wasm-bytecode" "1.7.11" - "@webassemblyjs/ieee754" "1.7.11" - "@webassemblyjs/leb128" "1.7.11" - "@webassemblyjs/utf8" "1.7.11" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" "@webassemblyjs/wasm-parser@1.7.6": version "1.7.6" @@ -2347,17 +2335,17 @@ "@webassemblyjs/leb128" "1.7.6" "@webassemblyjs/utf8" "1.7.6" -"@webassemblyjs/wast-parser@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz#25bd117562ca8c002720ff8116ef9072d9ca869c" - integrity sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ== +"@webassemblyjs/wasm-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" + integrity sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw== dependencies: - "@webassemblyjs/ast" "1.7.11" - "@webassemblyjs/floating-point-hex-parser" "1.7.11" - "@webassemblyjs/helper-api-error" "1.7.11" - "@webassemblyjs/helper-code-frame" "1.7.11" - "@webassemblyjs/helper-fsm" "1.7.11" - "@xtuc/long" "4.2.1" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" "@webassemblyjs/wast-parser@1.7.6": version "1.7.6" @@ -2372,14 +2360,17 @@ "@xtuc/long" "4.2.1" mamacro "^0.0.3" -"@webassemblyjs/wast-printer@1.7.11": - version "1.7.11" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz#c4245b6de242cb50a2cc950174fdbf65c78d7813" - integrity sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg== +"@webassemblyjs/wast-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" + integrity sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg== dependencies: - "@webassemblyjs/ast" "1.7.11" - "@webassemblyjs/wast-parser" "1.7.11" - "@xtuc/long" "4.2.1" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/floating-point-hex-parser" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-code-frame" "1.8.5" + "@webassemblyjs/helper-fsm" "1.8.5" + "@xtuc/long" "4.2.2" "@webassemblyjs/wast-printer@1.7.6": version "1.7.6" @@ -2390,6 +2381,15 @@ "@webassemblyjs/wast-parser" "1.7.6" "@xtuc/long" "4.2.1" +"@webassemblyjs/wast-printer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" + integrity sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" + "@xtuc/long" "4.2.2" + "@webpack-contrib/schema-utils@^1.0.0-beta.0": version "1.0.0-beta.0" resolved "https://registry.yarnpkg.com/@webpack-contrib/schema-utils/-/schema-utils-1.0.0-beta.0.tgz#bf9638c9464d177b48209e84209e23bee2eb4f65" @@ -2412,6 +2412,11 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.1.tgz#5c85d662f76fa1d34575766c5dcd6615abcd30d8" integrity sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g== +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + JSONStream@^1.3.2: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -2485,10 +2490,10 @@ acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0, acorn@^5.5.3, acorn@^5.6.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== -acorn@^6.0.1, acorn@^6.0.5: - version "6.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818" - integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw== +acorn@^6.0.1, acorn@^6.0.5, acorn@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" + integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== acorn@~2.6.4: version "2.6.4" @@ -2512,7 +2517,7 @@ address@1.0.3, address@^1.0.1: resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" integrity sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg== -agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: +agent-base@4, agent-base@^4.1.0, agent-base@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== @@ -2571,9 +2576,9 @@ ajv@^4.7.0: json-stable-stringify "^1.0.1" ajv@^6.1.0, ajv@^6.1.1, ajv@^6.5.5: - version "6.9.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.1.tgz#a4d3683d74abc5670e75f0b16520f70a20ea8dc1" - integrity sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA== + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" @@ -2651,9 +2656,9 @@ ansi-align@^3.0.0: string-width "^3.0.0" ansi-colors@^3.0.0: - version "3.2.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" - integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== ansi-escapes@^1.1.0: version "1.4.0" @@ -2680,10 +2685,10 @@ ansi-regex@^3.0.0, ansi-regex@~3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" - integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w== +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== ansi-styles@^2.2.1: version "2.2.1" @@ -3001,9 +3006,9 @@ astral-regex@^1.0.0: integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== async-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - integrity sha1-GdOGodntxufByF04iu28xW0zYC0= + version "1.0.2" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.2.tgz#8b8a7ca2a658f927e9f307d6d1a42f4199f0f735" + integrity sha512-6xrbvN0MOBKSJDdonmSSz2OwFSgxRaVtBDes26mj9KIGtDo+g9xosFRSC+i1gQh2oAN/tQ62AI/pGZGQjVOiRg== async-foreach@^0.1.3: version "0.1.3" @@ -3020,12 +3025,12 @@ async@^1.5.0, async@^1.5.2, async@~1.5.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.0.0, async@^2.1.4, async@^2.5.0, async@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== +async@^2.0.0, async@^2.1.4, async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" + integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== dependencies: - lodash "^4.17.10" + lodash "^4.17.11" async@~0.2.6: version "0.2.10" @@ -3060,12 +3065,12 @@ autoprefixer@^6.3.1, autoprefixer@^6.4.0: postcss-value-parser "^3.2.3" autoprefixer@^9.3.1: - version "9.4.7" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.7.tgz#f997994f9a810eae47b38fa6d8a119772051c4ff" - integrity sha512-qS5wW6aXHkm53Y4z73tFGsUhmZu4aMPV9iHXYlF0c/wxjknXNHuj/1cIQb+6YH692DbJGGWcckAXX+VxKvahMA== + version "9.5.0" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.5.0.tgz#7e51d0355c11596e6cf9a0afc9a44e86d1596c70" + integrity sha512-hMKcyHsZn5+qL6AUeP3c8OyuteZ4VaUlg+fWbyl8z7PqsKHF/Bf8/px3K6AT8aMzDkBo8Bc11245MM+itDBOxQ== dependencies: - browserslist "^4.4.1" - caniuse-lite "^1.0.30000932" + browserslist "^4.4.2" + caniuse-lite "^1.0.30000947" normalize-range "^0.1.2" num2fraction "^1.2.2" postcss "^7.0.14" @@ -4439,14 +4444,14 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" -browserslist@^4.1.0, browserslist@^4.3.4, browserslist@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.1.tgz#42e828954b6b29a7a53e352277be429478a69062" - integrity sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A== +browserslist@^4.1.0, browserslist@^4.3.4, browserslist@^4.4.2: + version "4.5.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.1.tgz#2226cada1947b33f4cfcf7b608dcb519b6128106" + integrity sha512-/pPw5IAUyqaQXGuD5vS8tcbudyPZ241jk1W5pQBsGDfcjNQt7p8qxZhgMNuygDShte1PibLFexecWUPgmVLfrg== dependencies: - caniuse-lite "^1.0.30000929" - electron-to-chromium "^1.3.103" - node-releases "^1.1.3" + caniuse-lite "^1.0.30000949" + electron-to-chromium "^1.3.116" + node-releases "^1.1.11" bs-logger@0.x: version "0.2.6" @@ -4702,9 +4707,9 @@ camelcase@^4.0.0, camelcase@^4.1.0: integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= camelcase@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" - integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + version "5.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45" + integrity sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ== caniuse-api@^1.5.2: version "1.6.1" @@ -4721,10 +4726,10 @@ caniuse-db@1.0.30000772, caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, can resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b" integrity sha1-UarokXaChureSj2DGep21qAbUSs= -caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000932: - version "1.0.30000936" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000936.tgz#5d33b118763988bf721b9b8ad436d0400e4a116b" - integrity sha512-orX4IdpbFhdNO7bTBhSbahp1EBpqzBc+qrvTRVUFfZgA4zta7TdM6PN5ZxkEUgDnz36m+PfWGcdX7AVfFWItJw== +caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000947, caniuse-lite@^1.0.30000949: + version "1.0.30000950" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000950.tgz#8c559d66e332b34e919d1086cc6d29c1948856ae" + integrity sha512-Cs+4U9T0okW2ftBsCIHuEYXXkki7mjXmjCh4c6PzYShk04qDEr76/iC7KwhLoWoY65wcra1XOsRD+S7BptEb5A== capture-exit@^1.2.0: version "1.2.0" @@ -4860,9 +4865,9 @@ child-process-promise@^2.2.1: promise-polyfill "^6.0.1" chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.1.tgz#adc39ad55a2adf26548bd2afa048f611091f9184" - integrity sha512-gfw3p2oQV2wEt+8VuMlNsPjCxDxvvgnm/kz+uATu805mWVF8IJN7uz9DN7iBz+RMJISmiVbCOBFs9qBGMjtPfQ== + version "2.1.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058" + integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg== dependencies: anymatch "^2.0.0" async-each "^1.0.1" @@ -4994,10 +4999,10 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-spinners@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" - integrity sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg== +cli-spinners@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.0.0.tgz#4b078756fc17a8f72043fdc9f1f14bf4fa87e2df" + integrity sha512-yiEBmhaKPPeBj7wWm4GEdtPZK940p9pl3EANIrnJ3JnvWyrPjcFcsEq6qRUuQ7fzB0+Y82ld3p6B34xo95foWw== cli-table2@~0.2.0: version "0.2.0" @@ -5159,14 +5164,7 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= -coa@~1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" - integrity sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0= - dependencies: - q "^1.1.2" - -coa@~2.0.1: +coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== @@ -5175,6 +5173,13 @@ coa@~2.0.1: chalk "^2.4.1" q "^1.1.2" +coa@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" + integrity sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0= + dependencies: + q "^1.1.2" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -5272,7 +5277,7 @@ comma-separated-tokens@^1.0.0: dependencies: trim "0.0.1" -commander@*, commander@2, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.19.0, commander@^2.8.1, commander@^2.9.0: +commander@*, commander@2, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.19.0, commander@^2.8.1, commander@^2.9.0, commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -5282,7 +5287,7 @@ commander@2.11.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ== -commander@2.17.x, commander@~2.17.1: +commander@2.17.x: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== @@ -5343,23 +5348,23 @@ compress-commons@^1.2.0: normalize-path "^2.0.0" readable-stream "^2.0.0" -compressible@~2.0.14: - version "2.0.15" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.15.tgz#857a9ab0a7e5a07d8d837ed43fe2defff64fe212" - integrity sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw== +compressible@~2.0.16: + version "2.0.16" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.16.tgz#a49bf9858f3821b64ce1be0296afc7380466a77f" + integrity sha512-JQfEOdnI7dASwCuSPWIeVYwc/zMsu/+tRhoUvEfXz2gxOA2DNjmG5vhtFdBlhWPPGo+RdT9S3tgc/uH5qgDiiA== dependencies: - mime-db ">= 1.36.0 < 2" + mime-db ">= 1.38.0 < 2" compression@^1.5.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db" - integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg== + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== dependencies: accepts "~1.3.5" bytes "3.0.0" - compressible "~2.0.14" + compressible "~2.0.16" debug "2.6.9" - on-headers "~1.0.1" + on-headers "~1.0.2" safe-buffer "5.1.2" vary "~1.1.2" @@ -5505,9 +5510,9 @@ core-js@^1.0.0: integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= core-js@^2.0.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0, core-js@^2.5.7: - version "2.6.4" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.4.tgz#b8897c062c4d769dd30a0ac5c73976c47f92ea0d" - integrity sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A== + version "2.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" + integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -5525,13 +5530,14 @@ cosmiconfig@^4.0.0: require-from-string "^2.0.1" cosmiconfig@^5.0.2, cosmiconfig@^5.0.5, cosmiconfig@^5.0.7: - version "5.0.7" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04" - integrity sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA== + version "5.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.1.0.tgz#6c5c35e97f37f985061cdf653f114784231185cf" + integrity sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q== dependencies: import-fresh "^2.0.0" is-directory "^0.3.1" js-yaml "^3.9.0" + lodash.get "^4.4.2" parse-json "^4.0.0" crc32-stream@^2.0.0: @@ -5726,7 +5732,7 @@ css-loader@^1.0.1: postcss-value-parser "^3.3.0" source-list-map "^2.0.0" -css-select-base-adapter@~0.1.0: +css-select-base-adapter@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== @@ -5782,9 +5788,9 @@ css-url-regex@^1.1.0: integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w= css-what@2.1, css-what@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d" - integrity sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== cssesc@^0.1.0: version "0.1.0" @@ -5834,7 +5840,7 @@ cssnano@^3.10.0: postcss-value-parser "^3.2.3" postcss-zindex "^2.0.1" -csso@^3.5.0: +csso@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg== @@ -5855,16 +5861,16 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": integrity sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A== cssstyle@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb" - integrity sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog== + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.2.1.tgz#3aceb2759eaf514ac1a21628d723d6043a819495" + integrity sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A== dependencies: cssom "0.3.x" csstype@^2.2.0, csstype@^2.5.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01" - integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow== + version "2.6.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.3.tgz#b701e5968245bf9b08d54ac83d00b624e622a9fa" + integrity sha512-rINUZXOkcBmoHWEyu7JdHu5JMzkGRoMX4ov9830WNgxf5UYxcBUO0QTKAqeJ5EZfSdlrcJYkC8WwfVW7JYi4yg== currently-unhandled@^0.4.1: version "0.4.1" @@ -6283,28 +6289,28 @@ debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3. dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0: +debug@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== dependencies: ms "2.0.0" -debug@^3.1.0, debug@^3.2.5: +debug@^3.1.0, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" -debuglog@^1.0.1: +debuglog@*, debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -6365,12 +6371,12 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -default-gateway@^2.6.0: - version "2.7.2" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.7.2.tgz#b7ef339e5e024b045467af403d50348db4642d0f" - integrity sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ== +default-gateway@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== dependencies: - execa "^0.10.0" + execa "^1.0.0" ip-regex "^2.1.0" default-require-extensions@^1.0.0: @@ -6617,11 +6623,11 @@ doctrine@^2.0.0: esutils "^2.0.2" dom-align@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.8.0.tgz#c0e89b5b674c6e836cd248c52c2992135f093654" - integrity sha512-B85D4ef2Gj5lw0rK0KM2+D5/pH7yqNxg2mB+E8uzFaolpm7RQmsxEfjyEuNiF8UBBkffumYDeKRzTzc3LePP+w== + version "1.8.2" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.8.2.tgz#fdcd36bce25ba8d34fe3582efd57ac767df490bd" + integrity sha512-17vInOylbB7H4qua7QRsmQT05FFTZemO8BhnOPgF9BPqjAPDyQr/9V8fmJbn05vQ31m2gu3EJSSYN2u94szUZg== -dom-converter@~0.2: +dom-converter@^0.2: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== @@ -6645,12 +6651,12 @@ dom-css@^2.0.0: "@babel/runtime" "^7.1.2" dom-serializer@0, dom-serializer@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" - integrity sha1-BzxpdUbOB4DOI75KKOKT5AvDDII= + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== dependencies: - domelementtype "~1.1.1" - entities "~1.1.1" + domelementtype "^1.3.0" + entities "^1.1.1" dom-walk@^0.1.0: version "0.1.1" @@ -6662,16 +6668,11 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.0: +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@~1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" - integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs= - domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -6679,13 +6680,6 @@ domexception@^1.0.1: dependencies: webidl-conversions "^4.0.2" -domhandler@2.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594" - integrity sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ= - dependencies: - domelementtype "1" - domhandler@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" @@ -6693,13 +6687,6 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domutils@1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" - integrity sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU= - dependencies: - domelementtype "1" - domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" @@ -6815,10 +6802,10 @@ ejs@^2.5.7, ejs@^2.5.9, ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== -electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.62: - version "1.3.113" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" - integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g== +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.116, electron-to-chromium@^1.3.62: + version "1.3.116" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.116.tgz#1dbfee6a592a0c14ade77dbdfe54fef86387d702" + integrity sha512-NKwKAXzur5vFCZYBHpdWjTMO8QptNLNP80nItkSIgUOapPAo9Uia+RvkCaZJtO7fhQaVElSvBPWEc2ku6cKsPA== elegant-spinner@^1.0.1: version "1.0.1" @@ -6918,27 +6905,27 @@ envinfo@^5.7.0: integrity sha512-pwdo0/G3CIkQ0y6PCXq4RdkvId2elvtPCJMG0konqlrfkWQbf1DWeH9K2b/cvu2YgGvPPTOnonZxXM1gikFu1w== enzyme-adapter-react-16@^1.5.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.9.1.tgz#6d49a3a31c3a0fccf527610f31b837e0f307128a" - integrity sha512-Egzogv1y77DUxdnq/CyHxLHaNxmSSKDDSDNNB/EiAXCZVFXdFibaNy2uUuRQ1n24T2m6KH/1Rw16XDRq+1yVEg== + version "1.11.2" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.11.2.tgz#8efeafb27e96873a5492fdef3f423693182eb9d4" + integrity sha512-2ruTTCPRb0lPuw/vKTXGVZVBZqh83MNDnakMhzxhpJcIbneEwNy2Cv0KvL97pl57/GOazJHflWNLjwWhex5AAA== dependencies: - enzyme-adapter-utils "^1.10.0" - function.prototype.name "^1.1.0" + enzyme-adapter-utils "^1.10.1" object.assign "^4.1.0" object.values "^1.1.0" - prop-types "^15.6.2" - react-is "^16.7.0" + prop-types "^15.7.2" + react-is "^16.8.4" react-test-renderer "^16.0.0-0" + semver "^5.6.0" -enzyme-adapter-utils@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.0.tgz#5836169f68b9e8733cb5b69cad5da2a49e34f550" - integrity sha512-VnIXJDYVTzKGbdW+lgK8MQmYHJquTQZiGzu/AseCZ7eHtOMAj4Rtvk8ZRopodkfPves0EXaHkXBDkVhPa3t0jA== +enzyme-adapter-utils@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.1.tgz#58264efa19a7befdbf964fb7981a108a5452ac96" + integrity sha512-oasinhhLoBuZsIkTe8mx0HiudtfErUtG0Ooe1FOplu/t4c9rOmyG5gtrBASK6u4whHIRWvv0cbZMElzNTR21SA== dependencies: function.prototype.name "^1.1.0" object.assign "^4.1.0" object.fromentries "^2.0.0" - prop-types "^15.6.2" + prop-types "^15.7.2" semver "^5.6.0" enzyme-to-json@^3.3.4: @@ -6949,17 +6936,19 @@ enzyme-to-json@^3.3.4: lodash "^4.17.4" enzyme@^3.6.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.8.0.tgz#646d2d5d0798cb98fdec39afcee8a53237b47ad5" - integrity sha512-bfsWo5nHyZm1O1vnIsbwdfhU989jk+squU9NKvB+Puwo5j6/Wg9pN5CO0YJelm98Dao3NPjkDZk+vvgwpMwYxw== + version "3.9.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.9.0.tgz#2b491f06ca966eb56b6510068c7894a7e0be3909" + integrity sha512-JqxI2BRFHbmiP7/UFqvsjxTirWoM1HfeaJrmVSZ9a1EADKkZgdPcAuISPMpoUiHlac9J4dYt81MC5BBIrbJGMg== dependencies: array.prototype.flat "^1.2.1" cheerio "^1.0.0-rc.2" function.prototype.name "^1.1.0" has "^1.0.3" + html-element-map "^1.0.0" is-boolean-object "^1.0.0" is-callable "^1.1.4" is-number-object "^1.0.3" + is-regex "^1.0.4" is-string "^1.0.4" is-subset "^0.1.1" lodash.escape "^4.0.1" @@ -7029,13 +7018,13 @@ es-to-primitive@^1.2.0: is-symbol "^1.0.2" es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.47" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.47.tgz#d24232e1380daad5449a817be19bde9729024a11" - integrity sha512-/1TItLfj+TTfWoeRcDn/0FbGV6SNo4R+On2GGVucPU/j3BWnXE2Co8h8CTo4Tu34gFJtnmwS9xiScKs4EjZhdw== + version "0.10.49" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.49.tgz#059a239de862c94494fec28f8150c977028c6c5e" + integrity sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg== dependencies: es6-iterator "~2.0.3" es6-symbol "~3.1.1" - next-tick "1" + next-tick "^1.0.0" es5-shim@^4.5.10: version "4.5.12" @@ -7069,9 +7058,9 @@ es6-promise@^3.0.2: integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM= es6-promise@^4.0.3: - version "4.2.5" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" - integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg== + version "4.2.6" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" + integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q== es6-promisify@^5.0.0: version "5.0.0" @@ -7092,9 +7081,9 @@ es6-set@~0.1.5: event-emitter "~0.3.5" es6-shim@^0.35.3: - version "0.35.4" - resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.4.tgz#8d5a4109756383d3f0323421089c423acf8378f1" - integrity sha512-oJidbXjN/VWXZJs41E9JEqWzcFbjt43JupimIoVX82Thzt5qy1CiYezdhRmWkj3KOuwJ106IG/ZZrcFC6fgIUQ== + version "0.35.5" + resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.5.tgz#46f59dc0a84a1c5029e8ff1166ca0a902077a9ab" + integrity sha512-E9kK/bjtCQRpN1K28Xh4BlmP8egvZBGJJ+9GtnzOwt7mdqtrjHFuVGr7QJfdjBIKqrlU5duPf3pCBoDrkjVYFg== es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: version "3.1.1" @@ -7133,9 +7122,9 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escodegen@^1.9.1: - version "1.11.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" - integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw== + version "1.11.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" + integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== dependencies: esprima "^3.1.3" estraverse "^4.2.0" @@ -7155,9 +7144,9 @@ escope@^3.6.0: estraverse "^4.1.1" eslint-scope@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" - integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA== + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -7253,6 +7242,11 @@ estree-walker@^0.5.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39" integrity sha512-XpCnW/AE10ws/kDAs37cngSkvgIR8aN3G0MS85m7dUpuK2EREo9VJ00uvw6Dg/hXEpfsE1I1TvJOJr+Z+TL+ig== +estree-walker@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.0.tgz#5d865327c44a618dde5699f763891ae31f257dae" + integrity sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw== + esutils@^2.0.0, esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -7320,19 +7314,6 @@ exec-sh@^0.2.0: dependencies: merge "^1.2.0" -execa@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" - integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== - dependencies: - cross-spawn "^6.0.0" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -7748,12 +7729,12 @@ find-cache-dir@^1.0.0: pkg-dir "^2.0.0" find-cache-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" - integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA== + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== dependencies: commondir "^1.0.1" - make-dir "^1.0.0" + make-dir "^2.0.0" pkg-dir "^3.0.0" find-npm-prefix@^1.0.2: @@ -7823,9 +7804,9 @@ flatten@^1.0.2: integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I= flow-parser@^0.*: - version "0.92.1" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.92.1.tgz#c81fb52b202c7b1195f253bb01ac6dd2f5c15dfc" - integrity sha512-l6rlAGgMTTRYPOj5XUzbCeZB2bsK2cimPcoQD06YtQC2BI2wu9AhMQH+FkKV2Kd1Aa9EMNxVyF05QzNaiYdObQ== + version "0.95.1" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.95.1.tgz#e1045d958b7e05f65e4fd3304f266f1fd0619698" + integrity sha512-HKkivrDpJduju6PfvnTdycPp5U8jemBDBwT/PlqiiW4TNOqmlSIbgniMcssl0h7vpoCUrQ/LX3CCIc/QSIDJWA== flush-write-stream@^1.0.0: version "1.1.1" @@ -7841,11 +7822,11 @@ fn-name@~2.0.1: integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc= follow-redirects@^1.0.0, follow-redirects@^1.2.5: - version "1.6.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.6.1.tgz#514973c44b5757368bad8bddfe52f81f015c94cb" - integrity sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ== + version "1.7.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" + integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== dependencies: - debug "=3.1.0" + debug "^3.2.6" for-in@^0.1.3: version "0.1.8" @@ -8053,9 +8034,9 @@ function.prototype.name@^1.1.0: is-callable "^1.1.3" fuse.js@^3.0.1, fuse.js@^3.3.0: - version "3.4.2" - resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.2.tgz#d7a638c436ecd7b9c4c0051478c09594eb956212" - integrity sha512-WVbrm+cAxPtyMqdtL7cYhR7aZJPhtOfjNClPya8GKMVukKDYs7pEnPINeRVX1C9WmWgU8MdYGYbUPAP2AJXdoQ== + version "3.4.4" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.4.tgz#f98f55fcb3b595cf6a3e629c5ffaf10982103e95" + integrity sha512-pyLQo/1oR5Ywf+a/tY8z4JygnIglmRxVUOiyFAbd11o9keUDpUJSMGRWJngcnkURj30kDHPmhoKY8ChJiz3EpQ== g-status@^2.0.2: version "2.0.2" @@ -8675,11 +8656,11 @@ handle-thing@^2.0.0: integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== handlebars@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a" - integrity sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w== + version "4.1.1" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.1.tgz#6e4e41c18ebe7719ae4d38e5aca3d32fa3dd23d3" + integrity sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA== dependencies: - async "^2.5.0" + neo-async "^2.6.0" optimist "^0.6.1" source-map "^0.6.1" optionalDependencies: @@ -8872,12 +8853,7 @@ hoist-non-react-statics@1.x.x, hoist-non-react-statics@^1.2.0: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" integrity sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs= -hoist-non-react-statics@^2.5.0: - version "2.5.5" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" - integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== - -hoist-non-react-statics@^3.1.0: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -8893,9 +8869,9 @@ home-or-tmp@^2.0.0: os-tmpdir "^1.0.1" homedir-polyfill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" - integrity sha1-TCu8inWJmP7r9e1oWA921GdotLw= + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== dependencies: parse-passwd "^1.0.0" @@ -8924,6 +8900,13 @@ html-comment-regex@^1.1.0: resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== +html-element-map@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.0.0.tgz#19a41940225153ecdfead74f8509154ff1cdc18b" + integrity sha512-/SP6aOiM5Ai9zALvCxDubIeez0LvG3qP7R9GcRDnJEP/HBmv0A8A9K0o8+HFudcFt46+i921ANjzKsjPjb7Enw== + dependencies: + array-filter "^1.0.0" + html-encoding-sniffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" @@ -9006,27 +8989,17 @@ html-webpack-plugin@^4.0.0-beta.2: tapable "^1.1.0" util.promisify "1.0.0" -htmlparser2@^3.9.1: - version "3.10.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464" - integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ== +htmlparser2@^3.3.0, htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== dependencies: - domelementtype "^1.3.0" + domelementtype "^1.3.1" domhandler "^2.3.0" domutils "^1.5.1" entities "^1.1.1" inherits "^2.0.1" - readable-stream "^3.0.6" - -htmlparser2@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" - integrity sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4= - dependencies: - domelementtype "1" - domhandler "2.1" - domutils "1.1" - readable-stream "1.0" + readable-stream "^3.1.1" http-cache-semantics@3.8.1, http-cache-semantics@^3.8.0, http-cache-semantics@^3.8.1: version "3.8.1" @@ -9061,17 +9034,17 @@ http-proxy-agent@^2.0.0, http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -http-proxy-middleware@~0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab" - integrity sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q== +http-proxy-middleware@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== dependencies: - http-proxy "^1.16.2" + http-proxy "^1.17.0" is-glob "^4.0.0" - lodash "^4.17.5" - micromatch "^3.1.9" + lodash "^4.17.11" + micromatch "^3.1.10" -http-proxy@^1.16.2: +http-proxy@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== @@ -9241,7 +9214,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@^0.1.4: +imurmurhash@*, imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -9386,13 +9359,13 @@ inquirer@^6.0.0, inquirer@^6.2.0, inquirer@^6.2.2: strip-ansi "^5.0.0" through "^2.3.6" -internal-ip@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27" - integrity sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q== +internal-ip@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.2.0.tgz#46e81b638d84c338e5c67e42b1a17db67d0814fa" + integrity sha512-ZY8Rk+hlvFeuMmG5uH1MXhhdeMntmIaxaInvAmzMq/SHV8rv4Kh+6GiQNNDQd0wZFrcO+FiTBo8lui/osKOyJw== dependencies: - default-gateway "^2.6.0" - ipaddr.js "^1.5.2" + default-gateway "^4.0.1" + ipaddr.js "^1.9.0" interpret@^1.0.0, interpret@^1.1.0: version "1.2.0" @@ -9439,7 +9412,7 @@ ipaddr.js@1.8.0: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= -ipaddr.js@^1.5.2: +ipaddr.js@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== @@ -10351,10 +10324,11 @@ jest-worker@^23.2.0: merge-stream "^1.0.1" jest-worker@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.0.0.tgz#3d3483b077bf04f412f47654a27bba7e947f8b6d" - integrity sha512-s64/OThpfQvoCeHG963MiEZOAAxu8kHsaL/rCMF7lpdzo7vgF0CtPml9hfguOMgykgH/eOm4jFP4ibfHLruytg== + version "24.4.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.4.0.tgz#fbc452b0120bb5c2a70cdc88fa132b48eeb11dd0" + integrity sha512-BH9X/klG9vxwoO99ZBUbZFfV8qO0XNZ5SIiCyYK2zOuJBl6YJVAeNIQjcoOVNu4HGEHeYEKsUWws8kSlSbZ9YQ== dependencies: + "@types/node" "*" merge-stream "^1.0.1" supports-color "^6.1.0" @@ -10392,9 +10366,9 @@ js-tokens@^3.0.2: integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= js-yaml@^3.12.0, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.9.0: - version "3.12.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" - integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA== + version "3.12.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc" + integrity sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -10795,11 +10769,10 @@ libnpx@^10.2.0: yargs "^11.0.0" lint-staged@^8.1.3: - version "8.1.3" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.1.3.tgz#bb069db5466c0fe16710216e633a84f2b362fa60" - integrity sha512-6TGkikL1B+6mIOuSNq2TV6oP21IhPMnV8q0cf9oYZ296ArTVNcbFh1l1pfVOHHbBIYLlziWNsQ2q45/ffmJ4AA== + version "8.1.5" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.1.5.tgz#372476fe1a58b8834eb562ed4c99126bd60bdd79" + integrity sha512-e5ZavfnSLcBJE1BTzRTqw6ly8OkqVyO3GL2M6teSmTBYQ/2BuueD5GIt2RPsP31u/vjKdexUyDCxSyK75q4BDA== dependencies: - "@iamstarkov/listr-update-renderer" "0.4.1" chalk "^2.3.1" commander "^2.14.1" cosmiconfig "^5.0.2" @@ -10812,7 +10785,8 @@ lint-staged@^8.1.3: is-glob "^4.0.0" is-windows "^1.0.2" listr "^0.14.2" - lodash "^4.17.5" + listr-update-renderer "^0.5.0" + lodash "^4.17.11" log-symbols "^2.2.0" micromatch "^3.1.8" npm-which "^3.0.1" @@ -10950,11 +10924,11 @@ locate-path@^3.0.0: path-exists "^3.0.0" lock-verify@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.0.2.tgz#148e4f85974915c9e3c34d694b7de9ecb18ee7a8" - integrity sha512-QNVwK0EGZBS4R3YQ7F1Ox8p41Po9VGl2QG/2GsuvTbkJZYSsPeWHKMbbH6iZMCHWSMww5nrJroZYnGzI4cePuw== + version "2.1.0" + resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.1.0.tgz#fff4c918b8db9497af0c5fa7f6d71555de3ceb47" + integrity sha512-vcLpxnGvrqisKvLQ2C2v0/u7LVly17ak2YSgoK4PrdsYBXQIax19vhKiLfvKNFx7FRrpTnitrpzF/uuCMuorIg== dependencies: - npm-package-arg "^5.1.2 || 6" + npm-package-arg "^6.1.0" semver "^5.4.1" lockfile@^1.0.4: @@ -10964,6 +10938,11 @@ lockfile@^1.0.4: dependencies: signal-exit "^3.0.2" +lodash._baseindexof@*: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" + integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= + lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -10972,12 +10951,29 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" +lodash._bindcallback@*: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= + +lodash._cacheindexof@*: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" + integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= + +lodash._createcache@*: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" + integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= + dependencies: + lodash._getnative "^3.0.0" + lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@^3.0.0: +lodash._getnative@*, lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -11022,6 +11018,11 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -11061,16 +11062,16 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.merge@^4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" - integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== - lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== +lodash.restparam@*: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= + lodash.some@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" @@ -11233,6 +11234,14 @@ make-dir@^1.0.0, make-dir@^1.1.0: dependencies: pify "^3.0.0" +make-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + make-error@1.x, make-error@^1.1.1, make-error@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" @@ -11419,12 +11428,12 @@ mem@^1.1.0: mimic-fn "^1.0.0" mem@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a" - integrity sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg== + version "4.2.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.2.0.tgz#5ee057680ed9cb8dad8a78d820f9a8897a102025" + integrity sha512-5fJxa68urlY0Ir8ijatKa3eRz5lwXnRCTvo9+TbTGAuTFJOwpGcY0X05moBd0nW45965Njt4CDI2GFQoG8DvqA== dependencies: map-age-cleaner "^0.1.1" - mimic-fn "^1.0.0" + mimic-fn "^2.0.0" p-is-promise "^2.0.0" memoize-one@^4.0.0: @@ -11437,7 +11446,7 @@ memoize-one@^5.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.0.tgz#d55007dffefb8de7546659a1722a5d42e128286e" integrity sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw== -memory-fs@^0.4.0, memory-fs@~0.4.1: +memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -11543,22 +11552,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.36.0 < 2": +"mime-db@>= 1.38.0 < 2", mime-db@~1.38.0: version "1.38.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== -mime-db@~1.37.0: - version "1.37.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" - integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== - mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19: - version "2.1.21" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" - integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== + version "2.1.22" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" + integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== dependencies: - mime-db "~1.37.0" + mime-db "~1.38.0" mime@1.4.1: version "1.4.1" @@ -11575,6 +11579,11 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +mimic-fn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.0.0.tgz#0913ff0b121db44ef5848242c38bbb35d44cabde" + integrity sha512-jbex9Yd/3lmICXwYT6gA/j2mNQGU48wCh/VzRd+/Y/PjYQtlg1gLMdZqvu9s/xH7qKvngxRObl56XZR609IMbA== + mimic-response@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -11768,9 +11777,9 @@ mousetrap-global-bind@^1.1.0: integrity sha1-zX3pIivQZG+i4BDVTISnTCaojt0= mousetrap@^1.6.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.2.tgz#caadd9cf886db0986fb2fee59a82f6bd37527587" - integrity sha512-jDjhi7wlHwdO6q6DS7YRmSHcuI+RVxadBkLt3KHrhd3C2b+w5pKefg3oj5beTcHZyVFA9Aksf+yEE1y5jxUjVA== + version "1.6.3" + resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a" + integrity sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA== move-concurrently@^1.0.1: version "1.0.1" @@ -11833,9 +11842,9 @@ mute-stream@~0.0.4: integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== nan@^2.10.0, nan@^2.6.2, nan@^2.9.2: - version "2.12.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" - integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== + version "2.13.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd" + integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA== nanomatch@^1.2.9: version "1.2.13" @@ -11891,7 +11900,7 @@ negotiator@0.6.1: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= -neo-async@^2.5.0: +neo-async@^2.5.0, neo-async@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835" integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA== @@ -11901,7 +11910,7 @@ nested-object-assign@^1.0.1: resolved "https://registry.yarnpkg.com/nested-object-assign/-/nested-object-assign-1.0.3.tgz#5aca69390d9affe5a612152b5f0843ae399ac597" integrity sha512-kgq1CuvLyUcbcIuTiCA93cQ2IJFSlRwXcN+hLcb2qLJwC2qrePHGZZa7IipyWqaWF6tQjdax2pQnVxdq19Zzwg== -next-tick@1: +next-tick@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= @@ -12106,10 +12115,10 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-releases@^1.0.0-alpha.11, node-releases@^1.1.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.7.tgz#b09a10394d0ed8f7778f72bb861dde68b146303b" - integrity sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA== +node-releases@^1.0.0-alpha.11, node-releases@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.11.tgz#9a0841a4b0d92b7d5141ed179e764f42ad22724a" + integrity sha512-8v1j5KfP+s5WOTa1spNUAOfreajQPN12JXbRR0oDE+YrJBQCXBnNqUDj27EKpPLOoSiU3tKi3xGPB+JaOdUEQQ== dependencies: semver "^5.3.0" @@ -12299,7 +12308,7 @@ npm-logical-tree@^1.2.1: resolved "https://registry.yarnpkg.com/npm-logical-tree/-/npm-logical-tree-1.2.1.tgz#44610141ca24664cad35d1e607176193fd8f5b88" integrity sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg== -"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^5.1.2 || 6", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: +"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.0.tgz#15ae1e2758a5027efb4c250554b85a737db7fcc1" integrity sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA== @@ -12310,9 +12319,9 @@ npm-logical-tree@^1.2.1: validate-npm-package-name "^3.0.0" npm-packlist@^1.1.10, npm-packlist@^1.1.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.3.0.tgz#7f01e8e44408341379ca98cfd756e7b29bd2626c" - integrity sha512-qPBc6CnxEzpOcc4bjoIBJbYdy0D/LFFPUdxvfwor4/w3vxeE0h6TiOVurCEPpQ6trjN77u/ShyfeJGsbAfB3dA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc" + integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" @@ -12542,9 +12551,9 @@ number-is-nan@^1.0.0: integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= nwsapi@^2.0.7: - version "2.1.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.0.tgz#781065940aed90d9bb01ca5d0ce0fcf81c32712f" - integrity sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg== + version "2.1.1" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.1.tgz#08d6d75e69fd791bdea31507ffafe8c843b67e9c" + integrity sha512-T5GaA1J/d34AC8mkrFD2O0DR17kwJ702ZOtJOsS8RpbsQZVOC2/xYFb1i/cw+xdM54JIlMuojjDOYct8GIWtwg== oauth-sign@~0.9.0: version "0.9.0" @@ -12672,10 +12681,10 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -on-headers@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" - integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0, once@~1.4.0: version "1.4.0" @@ -12706,13 +12715,20 @@ opener@~1.4.3: resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" integrity sha1-XG2ixdflgx6P+jlklQ+NZnSskLg= -opn@5.4.0, opn@^5.1.0, opn@^5.3.0, opn@^5.4.0: +opn@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== dependencies: is-wsl "^1.1.0" +opn@^5.1.0, opn@^5.3.0, opn@^5.4.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + optimist@^0.6.1, optimist@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -12742,13 +12758,13 @@ optionator@^0.8.1: wordwrap "~1.0.0" ora@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-3.1.0.tgz#dbedd8c03b5d017fb67083e87ee52f5ec89823ed" - integrity sha512-vRBPaNCclUi8pUxRF/G8+5qEQkc6EgzKK1G2ZNJUIGu088Un5qIxFXeDgymvPRM9nmrcUOGzQgS1Vmtz+NtlMw== + version "3.2.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-3.2.0.tgz#67e98a7e11f7f0ac95deaaaf11bb04de3d09e481" + integrity sha512-XHMZA5WieCbtg+tu0uPF8CjvwQdNzKCX6BVh3N6GFsEXH40mTk5dsw/ya1lBTUGJslcEFJFQ8cBhOgkkZXQtMA== dependencies: chalk "^2.4.2" cli-cursor "^2.1.0" - cli-spinners "^1.3.1" + cli-spinners "^2.0.0" log-symbols "^2.2.0" strip-ansi "^5.0.0" wcwidth "^1.0.1" @@ -12870,9 +12886,9 @@ p-limit@^1.1.0: p-try "^1.0.0" p-limit@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.1.0.tgz#1d5a0d20fb12707c758a655f6bbc4386b5930d68" - integrity sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g== + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== dependencies: p-try "^2.0.0" @@ -12925,9 +12941,9 @@ p-try@^1.0.0: integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= p-try@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" - integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.1.0.tgz#c1a0f1030e97de018bb2c718929d2af59463e505" + integrity sha512-H2RyIJ7+A3rjkwKC2l5GGtU4H1vkxKCAGsWasNVd0Set+6i4znxbWy6/j16YDPJDWxhsgZiKAstMEP8wCdSpjA== package-json@^4.0.0: version "4.0.1" @@ -13001,9 +13017,9 @@ pacote@^8.1.6: which "^1.3.0" pako@~1.0.5: - version "1.0.8" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.8.tgz#6844890aab9c635af868ad5fecc62e8acbba3ea4" - integrity sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA== + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== papaparse@^4.6.3: version "4.6.3" @@ -13027,9 +13043,9 @@ param-case@2.1.x, param-case@^2.1.0: no-case "^2.2.0" parse-asn1@^5.0.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.3.tgz#1600c6cc0727365d68b97f3aa78939e735a75204" - integrity sha512-VrPoetlz7B/FqjBLD2f5wBVZvsZVLnRUrxVLfRYhGXCODa/NWE4p3Wp+6+aV3ZPL3KM7/OZmxDIwwijD7yuucg== + version "5.1.4" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" + integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== dependencies: asn1.js "^4.0.0" browserify-aes "^1.0.0" @@ -13224,6 +13240,11 @@ pify@^3.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -13897,11 +13918,12 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: - version "15.7.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.1.tgz#2fa61e0a699d428b40320127733ee2931f05d9d1" - integrity sha512-f8Lku2z9kERjOCcnDOPm68EBJAO2K00Q5mSgPAUE/gJuBgsYLbVy6owSrtcHj90zt8PvW+z0qaIIgsIhHOa1Qw== +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== dependencies: + loose-envify "^1.4.0" object-assign "^4.1.1" react-is "^16.8.1" @@ -14055,9 +14077,9 @@ query-string@^5.0.1: strict-uri-encode "^1.0.0" query-string@^6.1.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.2.0.tgz#468edeb542b7e0538f9f9b1aeb26f034f19c86e1" - integrity sha512-5wupExkIt8RYL4h/FE+WTg3JHk62e6fFPWtAZA9J5IWK1PfTfKkMS93HBUHcFpeYi9KsY5pFbh+ldvEyaz5MyA== + version "6.4.0" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.4.0.tgz#1566c0cec3a2da2d82c222ed3f9e2a921dba5e6a" + integrity sha512-Werid2I41/tJTqOGPJ3cC3vwrIh/8ZupBQbp7BSsqXzr+pTin3aMJ/EZb8UEuk7ZO3VqQFvq2qck/ihc6wqIdw== dependencies: decode-uri-component "^0.2.0" strict-uri-encode "^2.0.0" @@ -14117,9 +14139,9 @@ randomatic@^3.0.0: math-random "^1.0.1" randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" - integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A== + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" @@ -14152,9 +14174,9 @@ raw-loader@^0.5.1: integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= rc-align@^2.4.0: - version "2.4.3" - resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.3.tgz#b9b3c2a6d68adae71a8e1d041cd5e3b2a655f99a" - integrity sha512-h5KgyB5IXYR7iKpYFcMr54cuQ2eozPCZ11kbXPG5+6CWvmyJ+c0R/yjndVndiNk2G3MKcTMbJNdDv5DIckLAxQ== + version "2.4.5" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.5.tgz#c941a586f59d1017f23a428f0b468663fb7102ab" + integrity sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw== dependencies: babel-runtime "^6.26.0" dom-align "^1.7.0" @@ -14312,28 +14334,28 @@ react-docgen@^3.0.0: node-dir "^0.1.10" recast "^0.16.0" -react-dom@^16.6.3, react-dom@^16.7.0: - version "16.8.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.1.tgz#ec860f98853d09d39bafd3a6f1e12389d283dbb4" - integrity sha512-N74IZUrPt6UiDjXaO7UbDDFXeUXnVhZzeRLy/6iqqN1ipfjrhR60Bp5NuBK+rv3GMdqdIuwIl22u1SYwf330bg== +react-dom@^16.6.3, react-dom@^16.7.0, react-dom@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.4.tgz#1061a8e01a2b3b0c8160037441c3bf00a0e3bc48" + integrity sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.1" + scheduler "^0.13.4" react-draggable@3.x, "react-draggable@^2.2.6 || ^3.0.3": - version "3.1.1" - resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.1.1.tgz#ed1db43e09b146805f6d158296e58eae4f768d36" - integrity sha512-tqIgDUm4XPSFbxelYpcsnayPU79P26ChnszDl5/RDFKfMuHnRxypS+OFfEyAEO1CtqaB3lrecQ2dyNIE2G0TlQ== + version "3.2.1" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.2.1.tgz#45d09a9a227988dc85674b23ab3c75b6e820dae5" + integrity sha512-r+3Bs9InID2lyIEbR8UIRVtpn4jgu1ArFEZgIy8vibJjijLSdNLX7rH9U68BBVD4RD9v44RXbaK4EHLyKXzNQw== dependencies: classnames "^2.2.5" prop-types "^15.6.0" react-error-overlay@^5.1.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.3.tgz#16fcbde75ed4dc6161dc6dc959b48e92c6ffa9ad" - integrity sha512-GoqeM3Xadie7XUApXOjkY3Qhs8RkwB/Za4WMedBGrOKH1eTuKGyoAECff7jiVonJchOx6KZ9i8ILO5XIoHB+Tg== + version "5.1.4" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.4.tgz#88dfb88857c18ceb3b9f95076f850d7121776991" + integrity sha512-fp+U98OMZcnduQ+NSEiQa4s/XMsbp+5KlydmkbESOw4P69iWZ68ZMFM5a2BuE0FgqPBKApJyRuYHR95jM8lAmg== react-fuzzy@^0.5.2: version "0.5.2" @@ -14365,15 +14387,15 @@ react-highlight-words@0.11.0: prop-types "^15.5.8" react-hot-loader@^4.3.6: - version "4.6.5" - resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.6.5.tgz#059619c8ac2aae9c6e8178ddc2eac535093cdd2e" - integrity sha512-ZPAJEWVd8KDdm6dcK0iWrnJiGHruLrcbkIpqn/wQmNjnROpsm2nzrWh23Yh3I/XAjB+35pMa/ZgariwGqwFD9A== + version "4.8.0" + resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.8.0.tgz#0b7c7dd9407415e23eb8246fdd28b0b839f54cb6" + integrity sha512-HY9F0vITYSVmXhAR6tPkMk240nxmoH8+0rca9iO2B82KVguiCiBJkieS0Wb4CeSIzLWecYx3iOcq8dcbnp0bxA== dependencies: fast-levenshtein "^2.0.6" global "^4.3.0" - hoist-non-react-statics "^2.5.0" + hoist-non-react-statics "^3.3.0" loader-utils "^1.1.0" - lodash.merge "^4.6.1" + lodash "^4.17.11" prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" shallowequal "^1.0.2" @@ -14400,10 +14422,10 @@ react-inspector@^2.3.0: is-dom "^1.0.9" prop-types "^15.6.1" -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: - version "16.8.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.1.tgz#a80141e246eb894824fb4f2901c0c50ef31d4cdb" - integrity sha512-ioMCzVDWvCvKD8eeT+iukyWrBGrA3DiFYkXfBsVYIRdaREZuBjENG+KjrikavCLasozqRWTwFUagU/O4vPpRMA== +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" + integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: version "3.0.4" @@ -14490,21 +14512,21 @@ react-style-proptype@^3.0.0: prop-types "^15.5.4" react-table@^6.8.6: - version "6.9.1" - resolved "https://registry.yarnpkg.com/react-table/-/react-table-6.9.1.tgz#50a1713a4de90089f14dcb8cf0f313992ad93a20" - integrity sha512-QWAwEX24kZPjimhrqRMcdJRi0LX+4MWJG7p9WmAPrfEoYG2HTR1odxa5jLPu7sKVkgsUp0PtZ2jxQ4aXabRJNA== + version "6.9.2" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-6.9.2.tgz#6a59adfeb8d5deced288241ed1c7847035b5ec5f" + integrity sha512-sTbNHU8Um0xRtmCd1js873HXnXaMWeBwZoiljuj0l1d44eaqjKyYPK/3HCBbJg1yeE2O5pQJ3Km0tlm9niNL9w== dependencies: classnames "^2.2.5" react-test-renderer@^16.0.0-0, react-test-renderer@^16.5.0, react-test-renderer@^16.7.0: - version "16.8.1" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.1.tgz#72845ad9269be526126e97853311982f781767be" - integrity sha512-Bd21TN3+YVl6GZwav6O0T6m5UwGfOj+2+xZH5VH93ToD6M5uclN/c+R1DGX49ueG413KZPUx7Kw3sOYz2aJgfg== + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.4.tgz#abee4c2c3bf967a8892a7b37f77370c5570d5329" + integrity sha512-jQ9Tf/ilIGSr55Cz23AZ/7H3ABEdo9oy2zF9nDHZyhLHDSLKuoILxw2ifpBfuuwQvj4LCoqdru9iZf7gwFH28A== dependencies: object-assign "^4.1.1" prop-types "^15.6.2" - react-is "^16.8.1" - scheduler "^0.13.1" + react-is "^16.8.4" + scheduler "^0.13.4" react-textarea-autosize@^7.0.4: version "7.1.0" @@ -14515,9 +14537,9 @@ react-textarea-autosize@^7.0.4: prop-types "^15.6.0" react-transition-group@^2.0.0, react-transition-group@^2.2.1: - version "2.5.3" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.3.tgz#26de363cab19e5c88ae5dbae105c706cf953bb92" - integrity sha512-2DGFck6h99kLNr8pOFk+z4Soq3iISydwOFeeEVPjTN6+Y01CmvbWmnN02VuTWyFdnRtIDPe+wy2q6Ui8snBPZg== + version "2.6.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.6.1.tgz#abf4a95e2f13fb9ba83a970a896fedbc5c4856a2" + integrity sha512-9DHwCy0aOYEe35frlEN68N9ut/THDQBLnVoQuKTvzF4/s3tk7lqkefCqxK2Nv96fOh6JXk6tQtliygk6tl3bQA== dependencies: dom-helpers "^3.3.1" loose-envify "^1.4.0" @@ -14549,15 +14571,15 @@ react-virtualized@^9.21.0: prop-types "^15.6.0" react-lifecycles-compat "^3.0.4" -react@^16.6.3, react@^16.7.0: - version "16.8.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.1.tgz#ae11831f6cb2a05d58603a976afc8a558e852c4a" - integrity sha512-wLw5CFGPdo7p/AgteFz7GblI2JPOos0+biSoxf1FPsGxWQZdN/pj6oToJs1crn61DL3Ln7mN86uZ4j74p31ELQ== +react@^16.6.3, react@^16.7.0, react@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.4.tgz#fdf7bd9ae53f03a9c4cd1a371432c206be1c4768" + integrity sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.1" + scheduler "^0.13.4" reactcss@^1.2.0: version "1.2.3" @@ -14608,9 +14630,9 @@ read-installed@~4.0.3: graceful-fs "^4.1.2" read-package-tree@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.2.1.tgz#6218b187d6fac82289ce4387bbbaf8eef536ad63" - integrity sha512-2CNoRoh95LxY47LvqrehIAfUVda2JbuFE/HaGYs42bNrGG+ojbw1h3zOcPcQ+1GQ3+rkzNndZn85u1XyZ3UsIA== + version "5.2.2" + resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.2.2.tgz#4b6a0ef2d943c1ea36a578214c9a7f6b7424f7a8" + integrity sha512-rW3XWUUkhdKmN2JKB4FL563YAgtINifso5KShykufR03nJ5loGFlkUMe1g/yxmqX073SoYYTsgXu7XdDinKZuA== dependencies: debuglog "^1.0.1" dezalgo "^1.0.0" @@ -14681,20 +14703,10 @@ read@1, read@~1.0.1, read@~1.0.7: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@1.0: - version "1.0.34" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" - integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readable-stream@^3.0.6: - version "3.1.1" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06" - integrity sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA== +readable-stream@^3.0.6, readable-stream@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d" + integrity sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -14710,7 +14722,7 @@ readable-stream@~1.1.10: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" integrity sha1-n6+jfShr5dksuuve4DDcm19AZ0c= @@ -14866,10 +14878,10 @@ redux@^4.0.0, redux@^4.0.1: loose-envify "^1.4.0" symbol-observable "^1.2.0" -regenerate-unicode-properties@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" - integrity sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw== +regenerate-unicode-properties@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662" + integrity sha512-SbA/iNrBUf6Pv2zU8Ekv1Qbhv92yxL4hiDa2siuxs4KKn4oOoMDHXjAf7+Nz9qinUQ46B1LcWEi/PhJfPWpZWQ== dependencies: regenerate "^1.4.0" @@ -14897,10 +14909,10 @@ regenerator-transform@^0.10.0: babel-types "^6.19.0" private "^0.1.6" -regenerator-transform@^0.13.3: - version "0.13.3" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" - integrity sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA== +regenerator-transform@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" + integrity sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A== dependencies: private "^0.1.6" @@ -14927,13 +14939,9 @@ regexp-replace-loader@^1.0.1: loader-utils "^1.0.2" regexp-tree@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.1.tgz#27b455f9b138ca2e84c090e9aff1ffe2a04d97fa" - integrity sha512-HwRjOquc9QOwKTgbxvZTcddS5mlNlwePMQ3NFL8broajMLD5CXDAqas8Y5yxJH5QtZp5iRor3YCILd5pz71Cgw== - dependencies: - cli-table3 "^0.5.0" - colors "^1.1.2" - yargs "^12.0.5" + version "0.1.5" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.5.tgz#7cd71fca17198d04b4176efd79713f2998009397" + integrity sha512-nUmxvfJyAODw+0B13hj8CFVAxhe7fDEAgJgaotBu3nnR+IgGgZq59YedJP5VYTlkEfqjuK6TuRpnymKdatLZfQ== regexp.prototype.flags@^1.2.0: version "1.2.0" @@ -14961,16 +14969,16 @@ regexpu-core@^2.0.0: regjsparser "^0.1.4" regexpu-core@^4.1.3, regexpu-core@^4.2.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.4.0.tgz#8d43e0d1266883969720345e70c275ee0aec0d32" - integrity sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA== + version "4.5.4" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae" + integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ== dependencies: regenerate "^1.4.0" - regenerate-unicode-properties "^7.0.0" + regenerate-unicode-properties "^8.0.2" regjsgen "^0.5.0" regjsparser "^0.6.0" unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.0.2" + unicode-match-property-value-ecmascript "^1.1.0" registry-auth-token@^3.0.1: version "3.3.2" @@ -15044,13 +15052,13 @@ render-fragment@^0.1.1: integrity sha512-+DnAcalJYR8GE5VRuQGGu78Q0GDe8EXnkuk4DF8gbAhIeS6LRt4j+aaggLLj4PtQVfXNC61McXvXI58WqmRleQ== renderkid@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.2.tgz#12d310f255360c07ad8fde253f6c9e9de372d2aa" - integrity sha512-FsygIxevi1jSiPY9h7vZmBFUbAOcbYm9UwyiLNdVsLRs/5We9Ob5NMPbGYUTWiLq5L+ezlVdE0A8bbME5CWTpg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" + integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== dependencies: css-select "^1.1.0" - dom-converter "~0.2" - htmlparser2 "~3.3.0" + dom-converter "^0.2" + htmlparser2 "^3.3.0" strip-ansi "^3.0.0" utila "^0.4.0" @@ -15088,21 +15096,21 @@ request-progress@^2.0.1: dependencies: throttleit "^1.0.0" -request-promise-core@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6" - integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY= +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== dependencies: - lodash "^4.13.1" + lodash "^4.17.11" request-promise-native@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" - integrity sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU= + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== dependencies: - request-promise-core "1.1.1" - stealthy-require "^1.1.0" - tough-cookie ">=2.3.3" + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" request@^2.74.0, request@^2.81.0, request@^2.85.0, request@^2.87.0, request@^2.88.0: version "2.88.0" @@ -15305,23 +15313,23 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: inherits "^2.0.1" rollup-plugin-commonjs@^9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.2.0.tgz#4604e25069e0c78a09e08faa95dc32dec27f7c89" - integrity sha512-0RM5U4Vd6iHjL6rLvr3lKBwnPsaVml+qxOGaaNUWN1lSq6S33KhITOfHmvxV3z2vy9Mk4t0g4rNlVaJJsNQPWA== + version "9.2.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.2.1.tgz#bb151ca8fa23600c7a03e25f9f0a45b1ee922dac" + integrity sha512-X0A/Cp/t+zbONFinBhiTZrfuUaVwRIp4xsbKq/2ohA2CDULa/7ONSJTelqxon+Vds2R2t2qJTqJQucKUC8GKkw== dependencies: estree-walker "^0.5.2" magic-string "^0.25.1" - resolve "^1.8.1" + resolve "^1.10.0" rollup-pluginutils "^2.3.3" rollup-plugin-node-resolve@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.0.0.tgz#9bc6b8205e9936cc0e26bba2415f1ecf1e64d9b2" - integrity sha512-7Ni+/M5RPSUBfUaP9alwYQiIKnKeXCOHiqBpKUl9kwp3jX5ZJtgXAait1cne6pGEVUUztPD6skIKH9Kq9sNtfw== + version "4.0.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.0.1.tgz#f95765d174e5daeef9ea6268566141f53aa9d422" + integrity sha512-fSS7YDuCe0gYqKsr5OvxMloeZYUSgN43Ypi1WeRZzQcWtHgFayV5tUSPYpxuaioIIWaBXl6NrVk0T2/sKwueLg== dependencies: builtin-modules "^3.0.0" is-module "^1.0.0" - resolve "^1.8.1" + resolve "^1.10.0" rollup-plugin-sourcemaps@^0.4.2: version "0.4.2" @@ -15342,9 +15350,9 @@ rollup-plugin-terser@^4.0.4: terser "^3.14.1" rollup-plugin-typescript2@^0.19.2: - version "0.19.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.19.2.tgz#87d9c799cd6e02efbedbba25af12753a1e92b6c2" - integrity sha512-DRG7SaYX0QzBIz6rII5nm1UkiceS95r8mJjujugybyIueNF3auvzGTHMK62O7As/0q5RHjXsOguWOUv+KJKLFA== + version "0.19.3" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.19.3.tgz#713063233461765f030a2baa2640905c2656164f" + integrity sha512-lsRqfBCZhMl/tq9AT5YnQvzQWzXtnx3EQYFcHD72gul7nyyoOrzx5yCEH20smpw58v6UkHHZz03FbdLEPoHWjA== dependencies: fs-extra "7.0.1" resolve "1.8.1" @@ -15361,7 +15369,7 @@ rollup-plugin-visualizer@^0.9.2: source-map "^0.7.3" typeface-oswald "0.0.54" -rollup-pluginutils@2.3.3, rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.3.3: +rollup-pluginutils@2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.3.3.tgz#3aad9b1eb3e7fe8262820818840bf091e5ae6794" integrity sha512-2XZwja7b6P5q4RZ5FhyX1+f46xi1Z3qBKigLRZ6VTZjwbN0K1IFGMlwm06Uu0Emcre2Z63l77nq/pzn+KxIEoA== @@ -15369,14 +15377,22 @@ rollup-pluginutils@2.3.3, rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.3.3: estree-walker "^0.5.2" micromatch "^2.3.11" +rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.3.3: + version "2.5.0" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.5.0.tgz#23be0f05ac3972ea7b08fc7870cb91fde5b23a09" + integrity sha512-9Muh1H+XB5f5ONmKMayUoTYR1EZwHbwJJ9oZLrKT5yuTf/RLIQ5mYIGsrERquVucJmjmaAW0Y7+6Qo1Ep+5w3Q== + dependencies: + estree-walker "^0.6.0" + micromatch "^3.1.10" + rollup@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.1.2.tgz#8d094b85683b810d0c05a16bd7618cf70d48eba7" - integrity sha512-OkdMxqMl8pWoQc5D8y1cIinYQPPLV8ZkfLgCzL6SytXeNA2P7UHynEQXI9tYxuAjAMsSyvRaWnyJDLHMxq0XAg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.6.0.tgz#4329f4634718197c678d18491724d50d8b7ee76c" + integrity sha512-qu9iWyuiOxAuBM8cAwLuqPclYdarIpayrkfQB7aTGTiyYPbvx+qVF33sIznfq4bxZCiytQux/FvZieUBAXivCw== dependencies: "@types/estree" "0.0.39" - "@types/node" "*" - acorn "^6.0.5" + "@types/node" "^11.9.5" + acorn "^6.1.1" rst-selector-parser@^2.2.3: version "2.2.3" @@ -15542,10 +15558,10 @@ sax@^1.2.4, sax@~1.2.1, sax@~1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.1.tgz#1a217df1bfaabaf4f1b92a9127d5d732d85a9591" - integrity sha512-VJKOkiKIN2/6NOoexuypwSrybx13MY7NSy9RNt8wPvZDMRT1CW6qlpF5jXRToXNHz3uWzbm2elNpZfXfGPqP9A== +scheduler@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298" + integrity sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -15880,9 +15896,9 @@ slash@^2.0.0: integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== slate-base64-serializer@^0.2.36: - version "0.2.95" - resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.95.tgz#018ccf749f0f0f62fc2d5f4722fd770d359cf76b" - integrity sha512-WK8roQUQBM7lHXNS6HYNmMSJ5tJmuoLeZkHJEHWCEl+1op1m5sC2onzBfpIRNP8AijlZ3m+lGlxfLO+3VtBMxw== + version "0.2.97" + resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.97.tgz#232220da1320cdc918369834940f1c997b1e80f9" + integrity sha512-f8TnU8rPz8qzKsGaJJwEzPoR5JQ55kB03nO/aIeXRVg4PPY/Rp9VaEcBP6vmeimH51gf3vpe8S5RUITbW9pXHw== dependencies: isomorphic-base64 "^1.0.2" @@ -16064,12 +16080,12 @@ socks-proxy-agent@^3.0.1: socks "^1.1.10" socks-proxy-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz#5936bf8b707a993079c6f37db2091821bffa6473" - integrity sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw== + version "4.0.2" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" + integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg== dependencies: - agent-base "~4.2.0" - socks "~2.2.0" + agent-base "~4.2.1" + socks "~2.3.2" socks@^1.1.10: version "1.1.10" @@ -16079,10 +16095,10 @@ socks@^1.1.10: ip "^1.1.4" smart-buffer "^1.0.13" -socks@~2.2.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.3.tgz#7399ce11e19b2a997153c983a9ccb6306721f2dc" - integrity sha512-+2r83WaRT3PXYoO/1z+RDEBE7Z2f9YcdQnJ0K/ncXXbV5gJ6wYfNAebYFYiiUjM6E4JyXnPY8cimwyvFYHVUUA== +socks@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.2.tgz#ade388e9e6d87fdb11649c15746c578922a5883e" + integrity sha512-pCpjxQgOByDHLlNqlnh/mNSAxIUkyBBuwwhTcV+enZGbDaClPvHdvm6uvOwZfFJkam7cGhBNbb4JxiP8UZkRvQ== dependencies: ip "^1.1.5" smart-buffer "4.0.2" @@ -16142,10 +16158,10 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" -source-map-support@^0.5.3, source-map-support@^0.5.6, source-map-support@~0.5.9: - version "0.5.10" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" - integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== +source-map-support@^0.5.3, source-map-support@^0.5.6, source-map-support@~0.5.10: + version "0.5.11" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.11.tgz#efac2ce0800355d026326a0ca23e162aeac9a4e2" + integrity sha512-//sajEx/fGL3iw6fltKMdPvy8kL3kJ2O3iuYlRoT3k9Kb4BjOoZ+BZzaNHeuaruSt+Kf3Zk9tnfAQg9/AJqUVQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -16296,7 +16312,7 @@ ssri@^6.0.0, ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" -stable@~0.1.3, stable@~0.1.5, stable@~0.1.6: +stable@^0.1.8, stable@~0.1.3, stable@~0.1.5: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== @@ -16341,7 +16357,7 @@ stdout-stream@^1.4.0: dependencies: readable-stream "^2.0.1" -stealthy-require@^1.1.0: +stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= @@ -16437,13 +16453,13 @@ string-width@^1.0.1, string-width@^1.0.2: strip-ansi "^4.0.0" string-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.0.0.tgz#5a1690a57cc78211fffd9bf24bbe24d090604eb1" - integrity sha512-rr8CUxBbvOZDUvc5lNIJ+OC1nPVpz+Siw9VBtUjB9b6jZehZLFt0JMCZzShFHIsI8cbhm0EsNIfWJMFV3cu3Ew== + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== dependencies: emoji-regex "^7.0.1" is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.0.0" + strip-ansi "^5.1.0" string.prototype.matchall@^3.0.0: version "3.0.1" @@ -16544,12 +16560,12 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" -strip-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" - integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow== +strip-ansi@^5.0.0, strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: - ansi-regex "^4.0.0" + ansi-regex "^4.1.0" strip-ansi@~0.1.0: version "0.1.1" @@ -16650,7 +16666,7 @@ supports-color@^4.5.0: dependencies: has-flag "^2.0.0" -supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0: +supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -16686,22 +16702,22 @@ svgo@^0.7.0: whet.extend "~0.9.9" svgo@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985" - integrity sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g== + version "1.2.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.2.0.tgz#305a8fc0f4f9710828c65039bb93d5793225ffc3" + integrity sha512-xBfxJxfk4UeVN8asec9jNxHiv3UAMv/ujwBWGYvQhhMb2u3YTGKkiybPcLFDLq7GLLWE9wa73e0/m8L5nTzQbw== dependencies: - coa "~2.0.1" - colors "~1.1.2" + chalk "^2.4.1" + coa "^2.0.2" css-select "^2.0.0" - css-select-base-adapter "~0.1.0" + css-select-base-adapter "^0.1.1" css-tree "1.0.0-alpha.28" css-url-regex "^1.1.0" - csso "^3.5.0" + csso "^3.5.1" js-yaml "^3.12.0" mkdirp "~0.5.1" - object.values "^1.0.4" + object.values "^1.1.0" sax "~1.2.4" - stable "~0.1.6" + stable "^0.1.8" unquote "~1.1.1" util.promisify "~1.0.0" @@ -16828,9 +16844,9 @@ term-size@^1.2.0: execa "^0.7.0" terser-webpack-plugin@^1.1.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz#9bff3a891ad614855a7dde0d707f7db5a927e3d9" - integrity sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg== + version "1.2.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8" + integrity sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA== dependencies: cacache "^11.0.2" find-cache-dir "^2.0.0" @@ -16842,13 +16858,13 @@ terser-webpack-plugin@^1.1.0: worker-farm "^1.5.2" terser@^3.14.1, terser@^3.16.1: - version "3.16.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493" - integrity sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow== + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" + integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ== dependencies: - commander "~2.17.1" + commander "^2.19.0" source-map "~0.6.1" - source-map-support "~0.5.9" + source-map-support "~0.5.10" test-exclude@^4.2.1: version "4.2.3" @@ -17050,16 +17066,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@>=2.3.3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" - integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== - dependencies: - ip-regex "^2.1.0" - psl "^1.1.28" - punycode "^2.1.1" - -tough-cookie@^2.3.4: +tough-cookie@^2.3.3, tough-cookie@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -17156,9 +17163,9 @@ ts-loader@^5.1.0: semver "^5.0.1" ts-node@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.2.tgz#9ecdf8d782a0ca4c80d1d641cbb236af4ac1b756" - integrity sha512-MosTrinKmaAcWgO8tqMjMJB22h+sp3Rd1i4fdoWY4mhBDekOwIAKI/bzmRi7IcbCmjquccYg2gcF6NBkLgr0Tw== + version "8.0.3" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.3.tgz#aa60b836a24dafd8bf21b54766841a232fdbc641" + integrity sha512-2qayBA4vdtVRuDo11DEFSsD/SFsBXQBRZZhbRGSIkmYmVkWjULn/GGMdG10KVqkaGndljfaTD8dKjWgcejO8YA== dependencies: arg "^4.1.0" diff "^3.1.0" @@ -17190,9 +17197,9 @@ tslint-react@^3.6.0: tsutils "^2.13.1" tslint@^5.8.0: - version "5.12.1" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.12.1.tgz#8cec9d454cf8a1de9b0a26d7bdbad6de362e52c1" - integrity sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw== + version "5.14.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.14.0.tgz#be62637135ac244fc9b37ed6ea5252c9eba1616e" + integrity sha512-IUla/ieHVnB8Le7LdQFRGlVJid2T/gaJe5VkjzRVSRR6pA2ODYrnfR1hmxi+5+au9l50jBwpbBL34txgv4NnTQ== dependencies: babel-code-frame "^6.22.0" builtin-modules "^1.1.1" @@ -17202,12 +17209,13 @@ tslint@^5.8.0: glob "^7.1.1" js-yaml "^3.7.0" minimatch "^3.0.4" + mkdirp "^0.5.1" resolve "^1.3.2" semver "^5.3.0" tslib "^1.8.0" - tsutils "^2.27.2" + tsutils "^2.29.0" -tsutils@^2.13.1, tsutils@^2.27.2: +tsutils@^2.13.1, tsutils@^2.29.0: version "2.29.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== @@ -17272,9 +17280,9 @@ typeface-oswald@0.0.54: integrity sha512-U1WMNp4qfy4/3khIfHMVAIKnNu941MXUfs3+H9R8PFgnoz42Hh9pboSFztWr86zut0eXC8byalmVhfkiKON/8Q== typescript@^3.0.3, typescript@^3.2.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3.tgz#f1657fc7daa27e1a8930758ace9ae8da31403221" - integrity sha512-Y21Xqe54TBVp+VDSNbuDYdGw0BpoR/Q6wo/+35M8PAU0vipahnyduJWirxxdxjsAkS7hue53x2zp8gz7F05u0A== + version "3.3.3333" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6" + integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw== ua-parser-js@^0.7.18: version "0.7.19" @@ -17300,11 +17308,11 @@ uglify-js@2.6.x: yargs "~3.10.0" uglify-js@3.4.x, uglify-js@^3.1.4: - version "3.4.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" - integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q== + version "3.4.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" + integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== dependencies: - commander "~2.17.1" + commander "~2.19.0" source-map "~0.6.1" uglify-to-browserify@~1.0.0: @@ -17376,15 +17384,15 @@ unicode-match-property-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript "^1.0.4" unicode-property-aliases-ecmascript "^1.0.4" -unicode-match-property-value-ecmascript@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz#9f1dc76926d6ccf452310564fd834ace059663d4" - integrity sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ== +unicode-match-property-value-ecmascript@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" + integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g== unicode-property-aliases-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" - integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg== + version "1.0.5" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" + integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw== unified@^7.0.2: version "7.1.0" @@ -17496,9 +17504,9 @@ unzip-response@^2.0.1: integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= upath@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" - integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw== + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== update-notifier@^2.3.0, update-notifier@^2.5.0: version "2.5.0" @@ -17914,43 +17922,33 @@ webpack-core@^0.6.5: source-list-map "~0.1.7" source-map "~0.4.1" -webpack-dev-middleware@3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz#1132fecc9026fd90f0ecedac5cbff75d1fb45890" - integrity sha512-Q9Iyc0X9dP9bAsYskAVJ/hmIZZQwf/3Sy4xCAZgL5cUkjZmUZLt4l5HpbST/Pdgjn3u6pE7u5OdGd1apgzRujA== +webpack-dev-middleware@^3.4.0, webpack-dev-middleware@^3.5.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.1.tgz#91f2531218a633a99189f7de36045a331a4b9cd4" + integrity sha512-XQmemun8QJexMEvNFbD2BIg4eSKrmSIMrTfnl2nql2Sc6OGAYFyb8rwuYrCjl/IiEYYuyTEiimMscu7EXji/Dw== dependencies: - memory-fs "~0.4.1" - mime "^2.3.1" - range-parser "^1.0.3" - webpack-log "^2.0.0" - -webpack-dev-middleware@^3.4.0: - version "3.5.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.5.2.tgz#d768b6194f3fe8d72d51feded49de359e8d96ffb" - integrity sha512-nPmshdt1ckcwWjI0Ubrdp8KroeuprW6xFKYqk0u3MflNMBXvHPnMATsC7/L/enwav2zvLCfj/Usr47qnF3KQyA== - dependencies: - memory-fs "~0.4.1" + memory-fs "^0.4.1" mime "^2.3.1" range-parser "^1.0.3" webpack-log "^2.0.0" webpack-dev-server@^3.1.0: - version "3.1.14" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz#60fb229b997fc5a0a1fc6237421030180959d469" - integrity sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ== + version "3.2.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.2.1.tgz#1b45ce3ecfc55b6ebe5e36dab2777c02bc508c4e" + integrity sha512-sjuE4mnmx6JOh9kvSbPYw3u/6uxCLHNWfhWaIPwcXWsvWOPN+nc5baq4i9jui3oOBRXGonK9+OI0jVkaz6/rCw== dependencies: ansi-html "0.0.7" bonjour "^3.5.0" chokidar "^2.0.0" compression "^1.5.2" connect-history-api-fallback "^1.3.0" - debug "^3.1.0" + debug "^4.1.1" del "^3.0.0" express "^4.16.2" html-entities "^1.2.0" - http-proxy-middleware "~0.18.0" + http-proxy-middleware "^0.19.1" import-local "^2.0.0" - internal-ip "^3.0.1" + internal-ip "^4.2.0" ip "^1.1.5" killable "^1.0.0" loglevel "^1.4.1" @@ -17964,9 +17962,9 @@ webpack-dev-server@^3.1.0: sockjs-client "1.3.0" spdy "^4.0.0" strip-ansi "^3.0.0" - supports-color "^5.1.0" + supports-color "^6.1.0" url "^0.11.0" - webpack-dev-middleware "3.4.0" + webpack-dev-middleware "^3.5.1" webpack-log "^2.0.0" yargs "12.0.2" @@ -18044,14 +18042,14 @@ webpack@4.19.1: webpack-sources "^1.2.0" webpack@^4.23.1: - version "4.29.3" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.3.tgz#e0b406a7b4201ed5e4fb4f84fd7359f9a7db4647" - integrity sha512-xPJvFeB+8tUflXFq+OgdpiSnsCD5EANyv56co5q8q8+YtEasn5Sj3kzY44mta+csCIEB0vneSxnuaHkOL2h94A== + version "4.29.6" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.6.tgz#66bf0ec8beee4d469f8b598d3988ff9d8d90e955" + integrity sha512-MwBwpiE1BQpMDkbnUUaW6K8RFZjljJHArC6tWQJoFm0oQtfoSebtg4Y7/QHnJ/SddtjYLHaKGX64CFjG5rehJw== dependencies: - "@webassemblyjs/ast" "1.7.11" - "@webassemblyjs/helper-module-context" "1.7.11" - "@webassemblyjs/wasm-edit" "1.7.11" - "@webassemblyjs/wasm-parser" "1.7.11" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" acorn "^6.0.5" acorn-dynamic-import "^4.0.0" ajv "^6.1.0" @@ -18385,7 +18383,7 @@ yargs@^11.0.0, yargs@^11.1.0: y18n "^3.2.1" yargs-parser "^9.0.2" -yargs@^12.0.1, yargs@^12.0.5: +yargs@^12.0.1: version "12.0.5" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== From 1178115d50ba85b61fa03172dc26768904a8ded4 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 19 Mar 2019 16:15:19 +0100 Subject: [PATCH 190/194] fix: ts issue on SelectOption test --- packages/grafana-ui/src/components/Select/SelectOption.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/Select/SelectOption.test.tsx b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx index 0a5a8864d64..d91bc9ee6af 100644 --- a/packages/grafana-ui/src/components/Select/SelectOption.test.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx @@ -25,6 +25,7 @@ const model: OptionProps = { key: '', onClick: jest.fn(), onMouseOver: jest.fn(), + onMouseMove: jest.fn(), tabIndex: 1, }, label: 'Option label', From 12d452ce92d5fe8c0f6854c29f8802eaa0ace68f Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 19 Mar 2019 16:31:26 +0100 Subject: [PATCH 191/194] Snapshot update --- .../components/Select/__snapshots__/SelectOption.test.tsx.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap b/packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap index c52be902edd..95c15ab013f 100644 --- a/packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap +++ b/packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap @@ -4,6 +4,7 @@ exports[`SelectOption renders correctly 1`] = `
From 9205b82f19d44ef79806d136444617626c46e393 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 19 Mar 2019 16:38:14 +0100 Subject: [PATCH 192/194] Remove leftover from first iteration --- public/app/plugins/panel/graph/thresholds_form.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts index c67a030188d..08acd887559 100644 --- a/public/app/plugins/panel/graph/thresholds_form.ts +++ b/public/app/plugins/panel/graph/thresholds_form.ts @@ -31,8 +31,6 @@ export class ThresholdFormCtrl { fill: true, line: true, yaxis: 'left', - fillColor: 'rgba(234,112, 112, 0.12)', - lineColor: 'rgba(237, 46, 24, 0.60)', }); this.panelCtrl.render(); } From d845aacbdc8bff3e5daa7efddc7817bbb261e126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 19 Mar 2019 17:44:58 +0100 Subject: [PATCH 193/194] refactor: merged types and updated references --- .../SharedPreferences/SharedPreferences.tsx | 19 ++++++++++++-- public/app/core/services/backend_srv.ts | 25 ++----------------- public/app/core/services/search_srv.ts | 5 ++-- public/app/types/search.ts | 17 ++++++++++--- 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 171e0e8109e..a39eefcec4b 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; import { FormLabel, Select } from '@grafana/ui'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; -import { DashboardSearchHit } from 'app/types'; +import { DashboardSearchHit, DashboardSearchHitType } from 'app/types'; export interface Props { resourceUri: string; @@ -41,6 +41,21 @@ export class SharedPreferences extends PureComponent { async componentDidMount() { const prefs = await this.backendSrv.get(`/api/${this.props.resourceUri}/preferences`); const dashboards = await this.backendSrv.search({ starred: true }); + const defaultDashboardHit: DashboardSearchHit = { + id: 0, + title: 'Default', + tags: [], + type: '' as DashboardSearchHitType, + uid: '', + uri: '', + url: '', + folderId: 0, + folderTitle: '', + folderUid: '', + folderUrl: '', + isStarred: false, + slug: '', + }; if (prefs.homeDashboardId > 0 && !dashboards.find(d => d.id === prefs.homeDashboardId)) { const missing = await this.backendSrv.search({ dashboardIds: [prefs.homeDashboardId] }); @@ -53,7 +68,7 @@ export class SharedPreferences extends PureComponent { homeDashboardId: prefs.homeDashboardId, theme: prefs.theme, timezone: prefs.timezone, - dashboards: [{ id: 0, title: 'Default', tags: [], type: '', uid: '', uri: '', url: '' }, ...dashboards], + dashboards: [defaultDashboardHit, ...dashboards], }); } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 53ab3ab6ce7..0d7d098dcea 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -3,28 +3,7 @@ import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import config from 'app/core/config'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; - -export enum HitType { - DashHitDB = 'dash-db', - DashHitHome = 'dash-home', - DashHitFolder = 'dash-folder', -} - -export interface Hit { - id: number; - uid: string; - title: string; - uri: string; - url: string; - slug: string; - type: HitType; - tags: string[]; - isStarred: boolean; - folderId: number; - folderUid: string; - folderTitle: string; - folderUrl: string; -} +import { DashboardSearchHit } from 'app/types/search'; export class BackendSrv { private inFlightRequests = {}; @@ -259,7 +238,7 @@ export class BackendSrv { return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 }); } - search(query): Promise { + search(query): Promise { return this.get('/api/search', query); } diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 4a605d3fc50..068fe3ffbc3 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -7,8 +7,9 @@ import coreModule from 'app/core/core_module'; import impressionSrv from 'app/core/services/impression_srv'; import store from 'app/core/store'; import { contextSrv } from 'app/core/services/context_srv'; -import { BackendSrv, Hit } from './backend_srv'; +import { BackendSrv } from './backend_srv'; import { Section } from '../components/manage_dashboards/manage_dashboards'; +import { DashboardSearchHit } from 'app/types/search'; interface Sections { [key: string]: Partial
; @@ -128,7 +129,7 @@ export class SearchSrv { }); } - private handleSearchResult(sections: Sections, results: Hit[]): any { + private handleSearchResult(sections: Sections, results: DashboardSearchHit[]): any { if (results.length === 0) { return sections; } diff --git a/public/app/types/search.ts b/public/app/types/search.ts index e5e17288de1..e15797f41a3 100644 --- a/public/app/types/search.ts +++ b/public/app/types/search.ts @@ -1,9 +1,20 @@ +export enum DashboardSearchHitType { + DashHitDB = 'dash-db', + DashHitHome = 'dash-home', + DashHitFolder = 'dash-folder', +} export interface DashboardSearchHit { id: number; - tags: string[]; - title: string; - type: string; uid: string; + title: string; uri: string; url: string; + slug: string; + type: DashboardSearchHitType; + tags: string[]; + isStarred: boolean; + folderId: number; + folderUid: string; + folderTitle: string; + folderUrl: string; } From 42c87141a5d692701ed5372221e7bf875f99ff2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Mar 2019 18:24:09 +0100 Subject: [PATCH 194/194] Minor progress on fixing no-implicit any issues --- package.json | 5 +- .../app/core/components/Animations/FadeIn.tsx | 6 +- .../core/components/Animations/SlideDown.tsx | 16 +++-- .../CopyToClipboard/CopyToClipboard.tsx | 4 +- .../EmptyListCTA/EmptyListCTA.test.tsx | 4 +- .../JSONFormatter/JSONFormatter.tsx | 1 - .../components/form_dropdown/form_dropdown.ts | 35 ++++++----- .../components/json_explorer/json_explorer.ts | 2 +- .../layout_selector/layout_selector.ts | 8 +-- public/app/features/api-keys/ApiKeysPage.tsx | 2 +- .../DashboardPermissions.tsx | 2 +- .../features/dashboard/state/PanelModel.ts | 12 ++-- .../features/folders/FolderPermissions.tsx | 2 +- public/app/features/teams/TeamGroupSync.tsx | 2 +- public/app/features/teams/TeamMembers.tsx | 2 +- .../panel/singlestat2/SingleStatPanel.tsx | 1 - public/app/types/{ => jquery}/jquery.d.ts | 0 tsconfig.json | 2 +- yarn.lock | 61 +++++-------------- 19 files changed, 75 insertions(+), 92 deletions(-) rename public/app/types/{ => jquery}/jquery.d.ts (100%) diff --git a/package.json b/package.json index afd5307035c..56b1803b35a 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/react-select": "^2.0.4", "@types/react-transition-group": "^2.0.15", "@types/react-virtualized": "^9.18.12", + "@types/clipboard": "^2.0.1", "angular-mocks": "1.6.6", "autoprefixer": "^6.4.0", "axios": "^0.17.1", @@ -107,7 +108,7 @@ "systemjs-plugin-css": "^0.1.36", "ts-jest": "^23.10.4", "ts-loader": "^5.1.0", - "ts-node": "^8.0.2", + "ts-node": "8.0.2", "tslib": "^1.9.3", "tslint": "^5.8.0", "tslint-loader": "^3.5.3", @@ -176,7 +177,7 @@ "baron": "^3.0.3", "brace": "^0.10.0", "classnames": "^2.2.6", - "clipboard": "^1.7.1", + "clipboard": "^2.0.4", "d3": "^4.11.0", "d3-scale-chromatic": "^1.3.0", "eventemitter3": "^2.0.3", diff --git a/public/app/core/components/Animations/FadeIn.tsx b/public/app/core/components/Animations/FadeIn.tsx index d667b54261e..a782a418b3c 100644 --- a/public/app/core/components/Animations/FadeIn.tsx +++ b/public/app/core/components/Animations/FadeIn.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { FC, CSSProperties } from 'react'; import Transition, { ExitHandler } from 'react-transition-group/Transition'; interface Props { @@ -10,12 +10,12 @@ interface Props { } export const FadeIn: FC = props => { - const defaultStyle = { + const defaultStyle: CSSProperties = { transition: `opacity ${props.duration}ms linear`, opacity: 0, }; - const transitionStyles = { + const transitionStyles: { [str: string]: CSSProperties } = { exited: { opacity: 0, display: 'none' }, entering: { opacity: 0 }, entered: { opacity: 1 }, diff --git a/public/app/core/components/Animations/SlideDown.tsx b/public/app/core/components/Animations/SlideDown.tsx index 6e8995298dc..c5cbf03b241 100644 --- a/public/app/core/components/Animations/SlideDown.tsx +++ b/public/app/core/components/Animations/SlideDown.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { CSSProperties, FC } from 'react'; import Transition from 'react-transition-group/Transition'; interface Style { @@ -16,11 +16,18 @@ export const defaultStyle: Style = { overflow: 'hidden', }; -export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => { +export interface Props { + children: React.ReactNode; + in: boolean; + maxHeight?: number; + style?: CSSProperties; +} + +export const SlideDown: FC = ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => { // There are 4 main states a Transition can be in: // ENTERING, ENTERED, EXITING, EXITED - // https://reactcommunity.org/react-transition-group/ - const transitionStyles = { + // https://reactcommunity.or[g/react-transition-group/ + const transitionStyles: { [str: string]: CSSProperties } = { exited: { maxHeight: 0 }, entering: { maxHeight: maxHeight }, entered: { maxHeight: 'unset', overflow: 'visible' }, @@ -34,6 +41,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = de style={{ ...style, ...transitionStyles[state], + inProp, }} > {children} diff --git a/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx b/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx index ea63de58b47..bd9b51e6d4f 100644 --- a/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx +++ b/public/app/core/components/CopyToClipboard/CopyToClipboard.tsx @@ -11,10 +11,10 @@ interface Props { } export class CopyToClipboard extends PureComponent { - clipboardjs: any; + clipboardjs: ClipboardJS; myRef: any; - constructor(props) { + constructor(props: Props) { super(props); this.myRef = React.createRef(); } diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx index 21700bb4d03..ac07b671c31 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; import EmptyListCTA from './EmptyListCTA'; const model = { @@ -16,7 +16,7 @@ const model = { describe('EmptyListCTA', () => { it('renders correctly', () => { - const tree = renderer.create().toJSON(); + const tree = shallow(); expect(tree).toMatchSnapshot(); }); }); diff --git a/public/app/core/components/JSONFormatter/JSONFormatter.tsx b/public/app/core/components/JSONFormatter/JSONFormatter.tsx index 73c055de94b..66f17444d1f 100644 --- a/public/app/core/components/JSONFormatter/JSONFormatter.tsx +++ b/public/app/core/components/JSONFormatter/JSONFormatter.tsx @@ -1,5 +1,4 @@ import React, { PureComponent, createRef } from 'react'; -// import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js'; import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now interface Props { diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 81d4b336443..b0c3e48b835 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -1,7 +1,8 @@ import _ from 'lodash'; import coreModule from '../../core_module'; +import { ISCEService, IQService } from 'angular'; -function typeaheadMatcher(this: any, item) { +function typeaheadMatcher(this: any, item: string) { let str = this.query; if (str === '') { return true; @@ -16,8 +17,8 @@ function typeaheadMatcher(this: any, item) { } export class FormDropdownCtrl { - inputElement: any; - linkElement: any; + inputElement: JQLite; + linkElement: JQLite; model: any; display: any; text: any; @@ -37,7 +38,13 @@ export class FormDropdownCtrl { debounce: number; /** @ngInject */ - constructor(private $scope, $element, private $sce, private templateSrv, private $q) { + constructor( + private $scope: any, + $element: JQLite, + private $sce: ISCEService, + private templateSrv: any, + private $q: IQService + ) { this.inputElement = $element.find('input').first(); this.linkElement = $element.find('a').first(); this.linkMode = true; @@ -99,7 +106,7 @@ export class FormDropdownCtrl { } } - getOptionsInternal(query) { + getOptionsInternal(query: string) { const result = this.getOptions({ $query: query }); if (this.isPromiseLike(result)) { return result; @@ -107,7 +114,7 @@ export class FormDropdownCtrl { return this.$q.when(result); } - isPromiseLike(obj) { + isPromiseLike(obj: any) { return obj && typeof obj.then === 'function'; } @@ -117,7 +124,7 @@ export class FormDropdownCtrl { } else { // if we have text use it if (this.lookupText) { - this.getOptionsInternal('').then(options => { + this.getOptionsInternal('').then((options: any) => { const item = _.find(options, { value: this.model }); this.updateDisplay(item ? item.text : this.model); }); @@ -127,12 +134,12 @@ export class FormDropdownCtrl { } } - typeaheadSource(query, callback) { - this.getOptionsInternal(query).then(options => { + typeaheadSource(query: string, callback: (res: any) => void) { + this.getOptionsInternal(query).then((options: any) => { this.optionCache = options; // extract texts - const optionTexts = _.map(options, op => { + const optionTexts = _.map(options, (op: any) => { return _.escape(op.text); }); @@ -147,7 +154,7 @@ export class FormDropdownCtrl { }); } - typeaheadUpdater(text) { + typeaheadUpdater(text: string) { if (text === this.text) { clearTimeout(this.cancelBlur); this.inputElement.focus(); @@ -159,7 +166,7 @@ export class FormDropdownCtrl { return text; } - switchToLink(fromClick) { + switchToLink(fromClick: boolean) { if (this.linkMode && !fromClick) { return; } @@ -178,7 +185,7 @@ export class FormDropdownCtrl { this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200); } - updateValue(text) { + updateValue(text: string) { text = _.unescape(text); if (text === '' || this.text === text) { @@ -214,7 +221,7 @@ export class FormDropdownCtrl { }); } - updateDisplay(text) { + updateDisplay(text: string) { this.text = text; this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text)); } diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index e4c92f662b1..f17f0f7ad7f 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -232,7 +232,7 @@ export class JsonExplorer { // some pretty handling of number arrays if (this.isNumberArray()) { - this.json.forEach((val, index) => { + this.json.forEach((val: any, index: number) => { if (index > 0) { arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ',')); } diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts index b3f3cdc14d1..a72a2fd5b4a 100644 --- a/public/app/core/components/layout_selector/layout_selector.ts +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -16,7 +16,7 @@ export class LayoutSelectorCtrl { mode: string; /** @ngInject */ - constructor(private $rootScope) { + constructor(private $rootScope: any) { this.mode = store.get('grafana.list.layout.mode') || 'grid'; } @@ -46,18 +46,18 @@ export function layoutSelector() { } /** @ngInject */ -export function layoutMode($rootScope) { +export function layoutMode($rootScope: any) { return { restrict: 'A', scope: {}, - link: (scope, elem) => { + link: (scope: any, elem: any) => { const layout = store.get('grafana.list.layout.mode') || 'grid'; let className = 'card-list-layout-' + layout; elem.addClass(className); $rootScope.onAppEvent( 'layout-mode-changed', - (evt, newLayout) => { + (evt: any, newLayout: any) => { elem.removeClass(className); className = 'card-list-layout-' + newLayout; elem.addClass(className); diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 4be5012a985..538f21d0d34 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -7,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel'; import { getApiKeys, getApiKeysCount } from './state/selectors'; import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; import Page from 'app/core/components/Page/Page'; -import SlideDown from 'app/core/components/Animations/SlideDown'; +import { SlideDown } from 'app/core/components/Animations/SlideDown'; import ApiKeysAddedModal from './ApiKeysAddedModal'; import config from 'app/core/config'; import appEvents from 'app/core/app_events'; diff --git a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx index f358281e018..7c610c87d9f 100644 --- a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { Tooltip } from '@grafana/ui'; -import SlideDown from 'app/core/components/Animations/SlideDown'; +import { SlideDown } from 'app/core/components/Animations/SlideDown'; import { StoreState, FolderInfo } from 'app/types'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; import { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 62a630c0408..68f017adb9e 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -111,12 +111,12 @@ export class PanelModel { cachedPluginOptions?: any; legend?: { show: boolean }; - constructor(model) { + constructor(model: any) { this.events = new Emitter(); // copy properties from persisted model for (const property in model) { - this[property] = model[property]; + (this as any)[property] = model[property]; } // defaults @@ -150,7 +150,7 @@ export class PanelModel { } } - getOptions(panelDefaults) { + getOptions(panelDefaults: any) { return _.defaultsDeep(this.options || {}, panelDefaults); } @@ -227,7 +227,7 @@ export class PanelModel { } return { ...acc, - [property]: this[property], + [property]: (this as any)[property], }; }, {}); } @@ -236,7 +236,7 @@ export class PanelModel { const prevOptions = this.cachedPluginOptions[pluginId] || {}; Object.keys(prevOptions).map(property => { - this[property] = prevOptions[property]; + (this as any)[property] = prevOptions[property]; }); } @@ -252,7 +252,7 @@ export class PanelModel { continue; } - delete this[key]; + delete (this as any)[key]; } this.cachedPluginOptions[oldPluginId] = oldOptions; diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index a3aae423b75..705bb8f7848 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import Page from 'app/core/components/Page/Page'; import { Tooltip } from '@grafana/ui'; -import SlideDown from 'app/core/components/Animations/SlideDown'; +import { SlideDown } from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index 363fc2f2ccc..5f47c4b906d 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; -import SlideDown from 'app/core/components/Animations/SlideDown'; +import { SlideDown } from 'app/core/components/Animations/SlideDown'; import { Tooltip } from '@grafana/ui'; import { TeamGroup } from '../../types'; import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions'; diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index bee431c3f9f..ab06f465097 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; -import SlideDown from 'app/core/components/Animations/SlideDown'; +import { SlideDown } from 'app/core/components/Animations/SlideDown'; import { UserPicker } from 'app/core/components/Select/UserPicker'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { TeamMember, User } from 'app/types'; diff --git a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx index 12f3be64a38..df162f31400 100644 --- a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx +++ b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx @@ -17,7 +17,6 @@ export const getSingleStatValues = (props: PanelProps): D decimals: valueOptions.decimals, mappings: valueMappings, thresholds: options.thresholds, - prefix: replaceVariables(valueOptions.prefix), suffix: replaceVariables(valueOptions.suffix), theme: config.theme, diff --git a/public/app/types/jquery.d.ts b/public/app/types/jquery/jquery.d.ts similarity index 100% rename from public/app/types/jquery.d.ts rename to public/app/types/jquery/jquery.d.ts diff --git a/tsconfig.json b/tsconfig.json index f223c027af6..68a68abc58c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,7 @@ "noUnusedLocals": true, "baseUrl": "public", "pretty": true, - "typeRoots": ["node_modules/@types", "types"], + "typeRoots": ["node_modules/@types", "public/app/types"], "paths": { "app": ["app"], "sass": ["sass"] diff --git a/yarn.lock b/yarn.lock index 700d2df7544..31e99135fbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1533,6 +1533,11 @@ resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.7.tgz#fb68cc9be8487e6ea5b13700e759bfbab7e0fefd" integrity sha512-rzOhiQ55WzAiFgXRtitP/ZUT8iVNyllEpylJ5zHzR4vArUvMB39GTk+Zon/uAM0JxEFAWnwsxC2gH8s+tZ3Myg== +"@types/clipboard@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/clipboard/-/clipboard-2.0.1.tgz#75a74086c293d75b12bc93ff13bc7797fef05a40" + integrity sha512-gJJX9Jjdt3bIAePQRRjYWG20dIhAgEqonguyHxXuqALxsoDsDLimihqrSg8fXgVTJ4KZCzkfglKtwsh/8dLfbA== + "@types/commander@^2.12.2": version "2.12.2" resolved "https://registry.yarnpkg.com/@types/commander/-/commander-2.12.2.tgz#183041a23842d4281478fa5d23c5ca78e6fd08ae" @@ -1879,7 +1884,7 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react@*", "@types/react@16.8.8", "@types/react@^16.7.6", "@types/react@^16.8.8": +"@types/react@*", "@types/react@16.8.8", "@types/react@^16.8.8": version "16.8.8" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.8.tgz#4b60a469fd2469f7aa6eaa0f8cfbc51f6d76e662" integrity sha512-xwEvyet96u7WnB96kqY0yY7qxx/pEpU51QeACkKFtrgjjXITQn0oO1iwPEraXVgh10ZFPix7gs1R4OJXF7P5sg== @@ -5044,16 +5049,7 @@ cli-width@^2.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= -clipboard@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b" - integrity sha1-Ng1taUbpmnof7zleQrqStem1oWs= - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - -clipboard@^2.0.0: +clipboard@^2.0.0, clipboard@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d" integrity sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ== @@ -6310,7 +6306,7 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -9214,7 +9210,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -10938,11 +10934,6 @@ lockfile@^1.0.4: dependencies: signal-exit "^3.0.2" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -10951,29 +10942,12 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -11067,11 +11041,6 @@ lodash.mergewith@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - lodash.some@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" @@ -14722,7 +14691,7 @@ readable-stream@~1.1.10: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" integrity sha1-n6+jfShr5dksuuve4DDcm19AZ0c= @@ -17162,10 +17131,10 @@ ts-loader@^5.1.0: micromatch "^3.1.4" semver "^5.0.1" -ts-node@^8.0.2: - version "8.0.3" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.3.tgz#aa60b836a24dafd8bf21b54766841a232fdbc641" - integrity sha512-2qayBA4vdtVRuDo11DEFSsD/SFsBXQBRZZhbRGSIkmYmVkWjULn/GGMdG10KVqkaGndljfaTD8dKjWgcejO8YA== +ts-node@8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.2.tgz#9ecdf8d782a0ca4c80d1d641cbb236af4ac1b756" + integrity sha512-MosTrinKmaAcWgO8tqMjMJB22h+sp3Rd1i4fdoWY4mhBDekOwIAKI/bzmRi7IcbCmjquccYg2gcF6NBkLgr0Tw== dependencies: arg "^4.1.0" diff "^3.1.0"