From 1693f083cc8e35efaa9daf9bc8398c9d2c70b525 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 11 Feb 2019 16:57:49 +0100 Subject: [PATCH 01/12] Move deduplication calculation from Logs component to redux selector --- package.json | 2 + public/app/core/utils/reselect.ts | 5 +++ public/app/features/explore/Logs.tsx | 16 +++---- public/app/features/explore/LogsContainer.tsx | 42 ++++++++++++++++++- .../app/features/explore/state/actionTypes.ts | 13 +++++- public/app/features/explore/state/reducers.ts | 11 +++++ public/app/types/explore.ts | 7 +++- yarn.lock | 12 ++++++ 8 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 public/app/core/utils/reselect.ts diff --git a/package.json b/package.json index fae51a1d856..22cfe33a4d0 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "dependencies": { "@babel/polyfill": "^7.0.0", "@torkelo/react-select": "2.1.1", + "@types/reselect": "^2.2.0", "angular": "1.6.6", "angular-bindonce": "0.3.1", "angular-native-dragdrop": "1.2.2", @@ -187,6 +188,7 @@ "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", "remarkable": "^1.7.1", + "reselect": "^4.0.0", "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^6.3.3", "slate": "^0.33.4", diff --git a/public/app/core/utils/reselect.ts b/public/app/core/utils/reselect.ts new file mode 100644 index 00000000000..7c8fc7727b0 --- /dev/null +++ b/public/app/core/utils/reselect.ts @@ -0,0 +1,5 @@ +import { memoize } from 'lodash'; +import { createSelectorCreator } from 'reselect'; + +const hashFn = (...args) => args.reduce((acc, val) => acc + '-' + JSON.stringify(val), ''); +export const createLodashMemoizedSelector = createSelectorCreator(memoize, hashFn); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index f41555b9121..0e3b3f3558e 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -9,8 +9,6 @@ import { LogsDedupDescription, LogsDedupStrategy, LogsModel, - dedupLogRows, - filterLogLevels, LogLevel, LogsMetaKind, } from 'app/core/logs_model'; @@ -51,6 +49,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { interface Props { data?: LogsModel; + dedupedData?: LogsModel; width: number; exploreId: string; highlighterExpressions: string[]; @@ -59,16 +58,17 @@ interface Props { scanning?: boolean; scanRange?: RawTimeRange; dedupStrategy: LogsDedupStrategy; + hiddenLogLevels: Set; onChangeTime?: (range: RawTimeRange) => void; onClickLabel?: (label: string, value: string) => void; onStartScanning?: () => void; onStopScanning?: () => void; onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void; + onToggleLogLevel: (hiddenLogLevels: Set) => void; } interface State { deferLogs: boolean; - hiddenLogLevels: Set; renderAll: boolean; showLabels: boolean | null; // Tristate: null means auto showLocalTime: boolean; @@ -81,7 +81,6 @@ export default class Logs extends PureComponent { state = { deferLogs: true, - hiddenLogLevels: new Set(), renderAll: false, showLabels: null, showLocalTime: true, @@ -142,7 +141,7 @@ export default class Logs extends PureComponent { onToggleLogLevel = (rawLevel: string, hiddenRawLevels: Set) => { const hiddenLogLevels: Set = new Set(Array.from(hiddenRawLevels).map(level => LogLevel[level])); - this.setState({ hiddenLogLevels }); + this.props.onToggleLogLevel(hiddenLogLevels); }; onClickScan = (event: React.SyntheticEvent) => { @@ -166,21 +165,18 @@ export default class Logs extends PureComponent { scanning, scanRange, width, + dedupedData, } = this.props; if (!data) { return null; } - const { deferLogs, hiddenLogLevels, renderAll, showLocalTime, showUtc, } = this.state; + const { deferLogs, renderAll, showLocalTime, showUtc } = this.state; let { showLabels } = this.state; const { dedupStrategy } = this.props; const hasData = data && data.rows && data.rows.length > 0; const showDuplicates = dedupStrategy !== LogsDedupStrategy.none; - - // Filtering - const filteredData = filterLogLevels(data, hiddenLogLevels); - const dedupedData = dedupLogRows(filteredData, dedupStrategy); const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0); const meta = [...data.meta]; diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 190c1c43b5a..9fd06afae9b 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -4,18 +4,21 @@ import { connect } from 'react-redux'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { ExploreId, ExploreItemState } from 'app/types/explore'; -import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model'; +import { LogsModel, LogsDedupStrategy, LogLevel, filterLogLevels, dedupLogRows } from 'app/core/logs_model'; import { StoreState } from 'app/types'; import { toggleLogs, changeDedupStrategy } from './state/actions'; import Logs from './Logs'; import Panel from './Panel'; +import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes'; +import { createLodashMemoizedSelector } from 'app/core/utils/reselect'; interface LogsContainerProps { exploreId: ExploreId; loading: boolean; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; + dedupedResult?: LogsModel; onChangeTime: (range: TimeRange) => void; onClickLabel: (key: string, value: string) => void; onStartScanning: () => void; @@ -25,8 +28,10 @@ interface LogsContainerProps { scanRange?: RawTimeRange; showingLogs: boolean; toggleLogs: typeof toggleLogs; + toggleLogLevelAction: typeof toggleLogLevelAction; changeDedupStrategy: typeof changeDedupStrategy; dedupStrategy: LogsDedupStrategy; + hiddenLogLevels: Set; width: number; } @@ -39,12 +44,21 @@ export class LogsContainer extends PureComponent { this.props.changeDedupStrategy(this.props.exploreId, dedupStrategy); }; + hangleToggleLogLevel = (hiddenLogLevels: Set) => { + const { exploreId } = this.props; + this.props.toggleLogLevelAction({ + exploreId, + hiddenLogLevels, + }); + }; + render() { const { exploreId, loading, logsHighlighterExpressions, logsResult, + dedupedResult, onChangeTime, onClickLabel, onStartScanning, @@ -54,6 +68,7 @@ export class LogsContainer extends PureComponent { scanning, scanRange, width, + hiddenLogLevels, } = this.props; return ( @@ -61,6 +76,7 @@ export class LogsContainer extends PureComponent { { onStartScanning={onStartScanning} onStopScanning={onStopScanning} onDedupStrategyChange={this.handleDedupStrategyChange} + onToggleLogLevel={this.hangleToggleLogLevel} range={range} scanning={scanning} scanRange={scanRange} width={width} + hiddenLogLevels={hiddenLogLevels} /> ); @@ -90,12 +108,29 @@ const selectItemUIState = (itemState: ExploreItemState) => { dedupStrategy, }; }; + +const logsSelector = (state: ExploreItemState) => state.logsResult; +const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels; +const dedupStrategySelector = (state: ExploreItemState) => state.dedupStrategy; +const deduplicatedLogsSelector = createLodashMemoizedSelector( + logsSelector, hiddenLogLevelsSelector, dedupStrategySelector, + (logs, hiddenLogLevels, dedupStrategy) => { + if (!logs) { + return null; + } + const filteredData = filterLogLevels(logs, new Set(hiddenLogLevels)); + return dedupLogRows(filteredData, dedupStrategy); + } +); + function mapStateToProps(state: StoreState, { exploreId }) { const explore = state.explore; const item: ExploreItemState = explore[exploreId]; const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, range } = item; const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); - const {showingLogs, dedupStrategy} = selectItemUIState(item); + const { showingLogs, dedupStrategy } = selectItemUIState(item); + const hiddenLogLevels = new Set(item.hiddenLogLevels); + const dedupedResult = deduplicatedLogsSelector(item); return { loading, @@ -106,12 +141,15 @@ function mapStateToProps(state: StoreState, { exploreId }) { showingLogs, range, dedupStrategy, + hiddenLogLevels, + dedupedResult, }; } const mapDispatchToProps = { toggleLogs, changeDedupStrategy, + toggleLogLevelAction, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index d54a8754c3d..c54eef97a43 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -18,6 +18,7 @@ import { ExploreUIState, } from 'app/types/explore'; import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory'; +import { LogLevel } from 'app/core/logs_model'; /** Higher order actions * @@ -201,6 +202,11 @@ export interface UpdateDatasourceInstancePayload { datasourceInstance: DataSourceApi; } +export interface ToggleLogLevelPayload { + exploreId: ExploreId; + hiddenLogLevels: Set; +} + export interface QueriesImportedPayload { exploreId: ExploreId; queries: DataQuery[]; @@ -397,6 +403,10 @@ export const updateDatasourceInstanceAction = actionCreatorFactory( + 'explore/TOGGLE_LOG_LEVEL' +).create(); + /** * Resets state for explore. */ @@ -436,4 +446,5 @@ export type Action = | ActionOf | ActionOf | ActionOf - | ActionOf; + | ActionOf + | ActionOf; diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 255591ee6e3..db3e9a95858 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -38,6 +38,7 @@ import { toggleTableAction, queriesImportedAction, updateUIStateAction, + toggleLogLevelAction, } from './actionTypes'; export const DEFAULT_RANGE = { @@ -467,6 +468,16 @@ export const itemReducer = reducerFactory({} as ExploreItemSta }; }, }) + .addMapper({ + filter: toggleLogLevelAction, + mapper: (state, action): ExploreItemState => { + const { hiddenLogLevels } = action.payload; + return { + ...state, + hiddenLogLevels: Array.from(hiddenLogLevels) + }; + }, + }) .create(); /** diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 066ca226157..7a6af04b2ee 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -11,7 +11,7 @@ import { } from '@grafana/ui'; import { Emitter } from 'app/core/core'; -import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model'; +import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model'; import TableModel from 'app/core/table_model'; export interface CompletionItem { @@ -242,6 +242,11 @@ export interface ExploreItemState { * Current logs deduplication strategy */ dedupStrategy?: LogsDedupStrategy; + + /** + * Currently hidden log series + */ + hiddenLogLevels?: LogLevel[]; } export interface ExploreUIState { diff --git a/yarn.lock b/yarn.lock index df2e1cea37e..3c86bdf810f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1819,6 +1819,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/reselect@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/reselect/-/reselect-2.2.0.tgz#c667206cfdc38190e1d379babe08865b2288575f" + integrity sha1-xmcgbP3DgZDh03m6vgiGWyKIV18= + dependencies: + reselect "*" + "@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" @@ -14894,6 +14901,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@*, reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" From edd9576f1586178501b1d75726944d1df70fcc1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 12 Feb 2019 08:04:30 +0100 Subject: [PATCH 02/12] Fixed elastic5 docker compose block --- devenv/docker/blocks/elastic5/docker-compose.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/devenv/docker/blocks/elastic5/docker-compose.yaml b/devenv/docker/blocks/elastic5/docker-compose.yaml index 33a7d9855b0..3a2ef39faba 100644 --- a/devenv/docker/blocks/elastic5/docker-compose.yaml +++ b/devenv/docker/blocks/elastic5/docker-compose.yaml @@ -1,6 +1,3 @@ -# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine -version: '2' -services: elasticsearch5: image: elasticsearch:5 command: elasticsearch From 3b9105e1bebb24e518548faf258576e0b30f70a8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 12 Feb 2019 08:45:21 +0100 Subject: [PATCH 03/12] enable testing provsioned datasources closes #12164 --- .../datasources/settings/ButtonRow.test.tsx | 1 + .../datasources/settings/ButtonRow.tsx | 8 +- .../settings/DataSourceSettingsPage.tsx | 89 ++++++++++--------- .../__snapshots__/ButtonRow.test.tsx.snap | 7 ++ .../DataSourceSettingsPage.test.tsx.snap | 4 + 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/public/app/features/datasources/settings/ButtonRow.test.tsx b/public/app/features/datasources/settings/ButtonRow.test.tsx index 0acab8941ff..84b16d829d5 100644 --- a/public/app/features/datasources/settings/ButtonRow.test.tsx +++ b/public/app/features/datasources/settings/ButtonRow.test.tsx @@ -7,6 +7,7 @@ const setup = (propOverrides?: object) => { isReadOnly: true, onSubmit: jest.fn(), onDelete: jest.fn(), + onTest: jest.fn(), }; Object.assign(props, propOverrides); diff --git a/public/app/features/datasources/settings/ButtonRow.tsx b/public/app/features/datasources/settings/ButtonRow.tsx index 6b85e21405c..3e8ac060010 100644 --- a/public/app/features/datasources/settings/ButtonRow.tsx +++ b/public/app/features/datasources/settings/ButtonRow.tsx @@ -4,14 +4,20 @@ export interface Props { isReadOnly: boolean; onDelete: () => void; onSubmit: (event) => void; + onTest: (event) => void; } -const ButtonRow: FC = ({ isReadOnly, onDelete, onSubmit }) => { +const ButtonRow: FC = ({ isReadOnly, onDelete, onSubmit, onTest }) => { return (
+ {isReadOnly && ( + + )} diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index ff840390cf5..fe1121ed73e 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -72,6 +72,12 @@ export class DataSourceSettingsPage extends PureComponent { this.testDataSource(); }; + onTest = async (evt: React.FormEvent) => { + evt.preventDefault(); + + this.testDataSource(); + }; + onDelete = () => { appEvents.emit('confirm-modal', { title: 'Delete', @@ -180,52 +186,55 @@ export class DataSourceSettingsPage extends PureComponent { return ( - {this.hasDataSource &&
-
-
- {this.isReadOnly() && this.renderIsReadOnlyMessage()} - {this.shouldRenderInfoBox() &&
{this.getInfoText()}
} + {this.hasDataSource && ( +
+
+ + {this.isReadOnly() && this.renderIsReadOnlyMessage()} + {this.shouldRenderInfoBox() &&
{this.getInfoText()}
} - setIsDefault(state)} - onNameChange={name => setDataSourceName(name)} - /> - - {dataSourceMeta.module && ( - setIsDefault(state)} + onNameChange={name => setDataSourceName(name)} /> - )} -
- {testingMessage && ( -
-
- {testingStatus === 'error' ? ( - - ) : ( - - )} -
-
-
{testingMessage}
-
-
+ {dataSourceMeta.module && ( + )} -
- this.onSubmit(event)} - isReadOnly={this.isReadOnly()} - onDelete={this.onDelete} - /> - +
+ {testingMessage && ( +
+
+ {testingStatus === 'error' ? ( + + ) : ( + + )} +
+
+
{testingMessage}
+
+
+ )} +
+ + this.onSubmit(event)} + isReadOnly={this.isReadOnly()} + onDelete={this.onDelete} + onTest={event => this.onTest(event)} + /> + +
-
} + )} ); diff --git a/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap index bd190f60b03..d4ec7eeea1e 100644 --- a/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap +++ b/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap @@ -12,6 +12,13 @@ exports[`Render should render component 1`] = ` > Save & Test +
@@ -202,6 +203,7 @@ exports[`Render should render beta info text 1`] = ` isReadOnly={false} onDelete={[Function]} onSubmit={[Function]} + onTest={[Function]} />
@@ -302,6 +304,7 @@ exports[`Render should render component 1`] = ` isReadOnly={false} onDelete={[Function]} onSubmit={[Function]} + onTest={[Function]} /> @@ -407,6 +410,7 @@ exports[`Render should render is ready only message 1`] = ` isReadOnly={true} onDelete={[Function]} onSubmit={[Function]} + onTest={[Function]} /> From da80286f97f1075ff59fd9b82601a4f2c40230be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 12 Feb 2019 11:10:31 +0100 Subject: [PATCH 04/12] Fixes #15372 with number input and parseFloat --- .../ThresholdsEditor/ThresholdsEditor.test.tsx | 6 +++--- .../ThresholdsEditor/ThresholdsEditor.tsx | 17 ++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 845ff5f6bf4..2b6af67df22 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ChangeEvent } from 'react'; import { shallow } from 'enzyme'; import { ThresholdsEditor, Props } from './ThresholdsEditor'; @@ -118,7 +118,7 @@ describe('change threshold value', () => { ]; const instance = setup({ thresholds }); - const mockEvent = { target: { value: 12 } }; + const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent; instance.onChangeThresholdValue(mockEvent, thresholds[0]); @@ -137,7 +137,7 @@ describe('change threshold value', () => { thresholds, }; - const mockEvent = { target: { value: 78 } }; + const mockEvent = ({ target: { value: '78' } } as any) as ChangeEvent; instance.onChangeThresholdValue(mockEvent, thresholds[1]); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index b2a2e07c58d..f4db23d6656 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, ChangeEvent } from 'react'; import { Threshold } from '../../types'; import { ColorPicker } from '../ColorPicker/ColorPicker'; import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; @@ -94,14 +94,15 @@ export class ThresholdsEditor extends PureComponent { ); }; - onChangeThresholdValue = (event: any, threshold: Threshold) => { + onChangeThresholdValue = (event: ChangeEvent, threshold: Threshold) => { if (threshold.index === 0) { return; } const { thresholds } = this.state; - const parsedValue = parseInt(event.target.value, 10); - const value = isNaN(parsedValue) ? null : parsedValue; + const cleanValue = event.target.value.replace(/,/g, '.'); + const parsedValue = parseFloat(cleanValue); + const value = isNaN(parsedValue) ? '' : parsedValue; const newThresholds = thresholds.map(t => { if (t === threshold && t.index !== 0) { @@ -164,16 +165,14 @@ export class ThresholdsEditor extends PureComponent {
{threshold.color && (
- this.onChangeThresholdColor(threshold, color)} - /> + this.onChangeThresholdColor(threshold, color)} />
)}
this.onChangeThresholdValue(event, threshold)} value={value} onBlur={this.onBlur} From 1310d356fc3cfc189fc4f4d26f8b441b12c1ac2a Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 12 Feb 2019 12:33:54 +0100 Subject: [PATCH 05/12] removes unused session code --- pkg/middleware/recovery_test.go | 2 -- pkg/services/session/session.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index e041d42e56b..6736d699a39 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" macaron "gopkg.in/macaron.v1" @@ -66,7 +65,6 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) { sc.userAuthTokenService = newFakeUserAuthTokenService() sc.m.Use(GetContextHandler(sc.userAuthTokenService)) // mock out gc goroutine - session.StartSessionGC = func() {} sc.m.Use(OrgRedirect()) sc.m.Use(AddDefaultResponseHeaders()) diff --git a/pkg/services/session/session.go b/pkg/services/session/session.go index 2e60b8a25d7..3481c99ce58 100644 --- a/pkg/services/session/session.go +++ b/pkg/services/session/session.go @@ -19,7 +19,7 @@ const ( var sessionManager *ms.Manager var sessionOptions *ms.Options -var StartSessionGC func() +var StartSessionGC func() = func() {} var GetSessionCount func() int var sessionLogger = log.New("session") var sessionConnMaxLifetime int64 From dd19ec3b22c63e2500a2e2190cdaf6de8ea18954 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 12 Feb 2019 12:36:46 +0100 Subject: [PATCH 06/12] Move explore selectors to a separate file --- public/app/features/explore/LogsContainer.tsx | 31 ++----------------- .../app/features/explore/state/selectors.ts | 30 ++++++++++++++++++ 2 files changed, 33 insertions(+), 28 deletions(-) create mode 100644 public/app/features/explore/state/selectors.ts diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 9fd06afae9b..f91b14175cb 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -4,14 +4,14 @@ import { connect } from 'react-redux'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { ExploreId, ExploreItemState } from 'app/types/explore'; -import { LogsModel, LogsDedupStrategy, LogLevel, filterLogLevels, dedupLogRows } from 'app/core/logs_model'; +import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model'; import { StoreState } from 'app/types'; import { toggleLogs, changeDedupStrategy } from './state/actions'; import Logs from './Logs'; import Panel from './Panel'; import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes'; -import { createLodashMemoizedSelector } from 'app/core/utils/reselect'; +import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/features/explore/state/selectors'; interface LogsContainerProps { exploreId: ExploreId; @@ -98,37 +98,12 @@ export class LogsContainer extends PureComponent { } } -const selectItemUIState = (itemState: ExploreItemState) => { - const { showingGraph, showingLogs, showingTable, showingStartPage, dedupStrategy } = itemState; - return { - showingGraph, - showingLogs, - showingTable, - showingStartPage, - dedupStrategy, - }; -}; - -const logsSelector = (state: ExploreItemState) => state.logsResult; -const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels; -const dedupStrategySelector = (state: ExploreItemState) => state.dedupStrategy; -const deduplicatedLogsSelector = createLodashMemoizedSelector( - logsSelector, hiddenLogLevelsSelector, dedupStrategySelector, - (logs, hiddenLogLevels, dedupStrategy) => { - if (!logs) { - return null; - } - const filteredData = filterLogLevels(logs, new Set(hiddenLogLevels)); - return dedupLogRows(filteredData, dedupStrategy); - } -); - function mapStateToProps(state: StoreState, { exploreId }) { const explore = state.explore; const item: ExploreItemState = explore[exploreId]; const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, range } = item; const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); - const { showingLogs, dedupStrategy } = selectItemUIState(item); + const { showingLogs, dedupStrategy } = exploreItemUIStateSelector(item); const hiddenLogLevels = new Set(item.hiddenLogLevels); const dedupedResult = deduplicatedLogsSelector(item); diff --git a/public/app/features/explore/state/selectors.ts b/public/app/features/explore/state/selectors.ts new file mode 100644 index 00000000000..fff52651646 --- /dev/null +++ b/public/app/features/explore/state/selectors.ts @@ -0,0 +1,30 @@ +import { createLodashMemoizedSelector } from 'app/core/utils/reselect'; +import { ExploreItemState } from 'app/types'; +import { filterLogLevels, dedupLogRows } from 'app/core/logs_model'; + +export const exploreItemUIStateSelector = (itemState: ExploreItemState) => { + const { showingGraph, showingLogs, showingTable, showingStartPage, dedupStrategy } = itemState; + return { + showingGraph, + showingLogs, + showingTable, + showingStartPage, + dedupStrategy, + }; +}; + +const logsSelector = (state: ExploreItemState) => state.logsResult; +const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels; +const dedupStrategySelector = (state: ExploreItemState) => state.dedupStrategy; +export const deduplicatedLogsSelector = createLodashMemoizedSelector( + logsSelector, + hiddenLogLevelsSelector, + dedupStrategySelector, + (logs, hiddenLogLevels, dedupStrategy) => { + if (!logs) { + return null; + } + const filteredData = filterLogLevels(logs, new Set(hiddenLogLevels)); + return dedupLogRows(filteredData, dedupStrategy); + } +); From 49e0572611e4b640daf14383f9cf6158964e861a Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 30 Jan 2019 16:42:44 +0100 Subject: [PATCH 07/12] fix: Error tooltip should have white text on red background. Not red text on red background --- packages/grafana-ui/src/components/Tooltip/_Tooltip.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss b/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss index 503b977bbf0..a33724b3460 100644 --- a/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss +++ b/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss @@ -31,7 +31,7 @@ $popper-margin-from-ref: 5px; // Themes &.popper__background--error { - @include popper-theme($tooltipBackgroundError, $tooltipBackgroundError); + @include popper-theme($tooltipBackgroundError, $white); } &.popper__background--info { From 2d4e1a80bc968f562aa7b3be9b42266aa028bc51 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 4 Feb 2019 14:50:10 +0100 Subject: [PATCH 08/12] chore: wip: Replace brace with ace-builds to get latest version of ace --- package.json | 2 +- .../components/code_editor/code_editor.ts | 28 +++++++++---------- yarn.lock | 17 ++++------- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index f004ee07732..f26cadbe4bf 100644 --- a/package.json +++ b/package.json @@ -151,13 +151,13 @@ "dependencies": { "@babel/polyfill": "^7.0.0", "@torkelo/react-select": "2.1.1", + "ace-builds": "^1.4.2", "angular": "1.6.6", "angular-bindonce": "0.3.1", "angular-native-dragdrop": "1.2.2", "angular-route": "1.6.6", "angular-sanitize": "1.6.6", "baron": "^3.0.3", - "brace": "^0.10.0", "classnames": "^2.2.6", "clipboard": "^1.7.1", "d3": "^4.11.0", diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index ca9023e8657..004edb780b8 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -30,20 +30,20 @@ import coreModule from 'app/core/core_module'; import config from 'app/core/config'; -import ace from 'brace'; +import * as ace from 'ace-builds/src-noconflict/ace'; import './theme-grafana-dark'; -import 'brace/ext/language_tools'; -import 'brace/theme/textmate'; -import 'brace/mode/text'; -import 'brace/snippets/text'; -import 'brace/mode/sql'; -import 'brace/snippets/sql'; -import 'brace/mode/sqlserver'; -import 'brace/snippets/sqlserver'; -import 'brace/mode/markdown'; -import 'brace/snippets/markdown'; -import 'brace/mode/json'; -import 'brace/snippets/json'; +import 'ace-builds/src-noconflict/ext-language_tools'; +import 'ace-builds/src-noconflict/theme-textmate'; +import 'ace-builds/src-noconflict/mode-text'; +import 'ace-builds/src-noconflict/snippets/text'; +import 'ace-builds/src-noconflict/mode-sql'; +import 'ace-builds/src-noconflict/snippets/sql'; +import 'ace-builds/src-noconflict/mode-sqlserver'; +import 'ace-builds/src-noconflict/snippets/sqlserver'; +import 'ace-builds/src-noconflict/mode-markdown'; +import 'ace-builds/src-noconflict/snippets/markdown'; +import 'ace-builds/src-noconflict/mode-json'; +import 'ace-builds/src-noconflict/snippets/json'; const DEFAULT_THEME_DARK = 'ace/theme/grafana-dark'; const DEFAULT_THEME_LIGHT = 'ace/theme/textmate'; @@ -143,7 +143,7 @@ function link(scope, elem, attrs) { }); function setLangMode(lang) { - ace.acequire('ace/ext/language_tools'); + // ace.acequire('ace/ext/language_tools'); // TODO: Do we need this? codeEditor.setOptions({ enableBasicAutocompletion: true, enableLiveAutocompletion: true, diff --git a/yarn.lock b/yarn.lock index 2fb4a5d3ee2..ac6c8c9ce78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2265,6 +2265,11 @@ accepts@~1.3.4, accepts@~1.3.5: mime-types "~2.1.18" negotiator "0.6.1" +ace-builds@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.2.tgz#6afc2e43a7b5effdc44d8407436112852568e80d" + integrity sha512-M1JtZctO2Zg+1qeGUFZXtYKsyaRptqQtqpVzlj80I0NzGW9MF3um0DBuizIvQlrPYUlTdm+wcOPZpZoerkxQdA== + acorn-dynamic-import@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" @@ -4125,13 +4130,6 @@ brace-expansion@^1.0.0, brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/brace/-/brace-0.10.0.tgz#edef4eb9b0928ba1ee5f717ffc157749a6dd5d76" - integrity sha1-7e9OubCSi6HuX3F//BV3SabdXXY= - dependencies: - w3c-blob "0.0.1" - braces@^1.8.2: version "1.8.5" resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" @@ -17283,11 +17281,6 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" -w3c-blob@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/w3c-blob/-/w3c-blob-0.0.1.tgz#b0cd352a1a50f515563420ffd5861f950f1d85b8" - integrity sha1-sM01KhpQ9RVWNCD/1YYflQ8dhbg= - w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" From 2a655cb38a57022a95a890e3be9d8eaff42756c3 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 12 Feb 2019 12:35:06 +0100 Subject: [PATCH 09/12] Revert "chore: wip: Replace brace with ace-builds to get latest version of ace" This reverts commit c98b86fd6b58ac5f77c197d7551751e62d53bedd. --- package.json | 2 +- .../components/code_editor/code_editor.ts | 28 +++++++++---------- yarn.lock | 17 +++++++---- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index f26cadbe4bf..f004ee07732 100644 --- a/package.json +++ b/package.json @@ -151,13 +151,13 @@ "dependencies": { "@babel/polyfill": "^7.0.0", "@torkelo/react-select": "2.1.1", - "ace-builds": "^1.4.2", "angular": "1.6.6", "angular-bindonce": "0.3.1", "angular-native-dragdrop": "1.2.2", "angular-route": "1.6.6", "angular-sanitize": "1.6.6", "baron": "^3.0.3", + "brace": "^0.10.0", "classnames": "^2.2.6", "clipboard": "^1.7.1", "d3": "^4.11.0", diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 004edb780b8..ca9023e8657 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -30,20 +30,20 @@ import coreModule from 'app/core/core_module'; import config from 'app/core/config'; -import * as ace from 'ace-builds/src-noconflict/ace'; +import ace from 'brace'; import './theme-grafana-dark'; -import 'ace-builds/src-noconflict/ext-language_tools'; -import 'ace-builds/src-noconflict/theme-textmate'; -import 'ace-builds/src-noconflict/mode-text'; -import 'ace-builds/src-noconflict/snippets/text'; -import 'ace-builds/src-noconflict/mode-sql'; -import 'ace-builds/src-noconflict/snippets/sql'; -import 'ace-builds/src-noconflict/mode-sqlserver'; -import 'ace-builds/src-noconflict/snippets/sqlserver'; -import 'ace-builds/src-noconflict/mode-markdown'; -import 'ace-builds/src-noconflict/snippets/markdown'; -import 'ace-builds/src-noconflict/mode-json'; -import 'ace-builds/src-noconflict/snippets/json'; +import 'brace/ext/language_tools'; +import 'brace/theme/textmate'; +import 'brace/mode/text'; +import 'brace/snippets/text'; +import 'brace/mode/sql'; +import 'brace/snippets/sql'; +import 'brace/mode/sqlserver'; +import 'brace/snippets/sqlserver'; +import 'brace/mode/markdown'; +import 'brace/snippets/markdown'; +import 'brace/mode/json'; +import 'brace/snippets/json'; const DEFAULT_THEME_DARK = 'ace/theme/grafana-dark'; const DEFAULT_THEME_LIGHT = 'ace/theme/textmate'; @@ -143,7 +143,7 @@ function link(scope, elem, attrs) { }); function setLangMode(lang) { - // ace.acequire('ace/ext/language_tools'); // TODO: Do we need this? + ace.acequire('ace/ext/language_tools'); codeEditor.setOptions({ enableBasicAutocompletion: true, enableLiveAutocompletion: true, diff --git a/yarn.lock b/yarn.lock index ac6c8c9ce78..2fb4a5d3ee2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2265,11 +2265,6 @@ accepts@~1.3.4, accepts@~1.3.5: mime-types "~2.1.18" negotiator "0.6.1" -ace-builds@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.2.tgz#6afc2e43a7b5effdc44d8407436112852568e80d" - integrity sha512-M1JtZctO2Zg+1qeGUFZXtYKsyaRptqQtqpVzlj80I0NzGW9MF3um0DBuizIvQlrPYUlTdm+wcOPZpZoerkxQdA== - acorn-dynamic-import@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" @@ -4130,6 +4125,13 @@ brace-expansion@^1.0.0, brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/brace/-/brace-0.10.0.tgz#edef4eb9b0928ba1ee5f717ffc157749a6dd5d76" + integrity sha1-7e9OubCSi6HuX3F//BV3SabdXXY= + dependencies: + w3c-blob "0.0.1" + braces@^1.8.2: version "1.8.5" resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" @@ -17281,6 +17283,11 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" +w3c-blob@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/w3c-blob/-/w3c-blob-0.0.1.tgz#b0cd352a1a50f515563420ffd5861f950f1d85b8" + integrity sha1-sM01KhpQ9RVWNCD/1YYflQ8dhbg= + w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" From 335042b2d0343155bb735533c4481f69c2d59c45 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 12 Feb 2019 14:29:27 +0100 Subject: [PATCH 10/12] fix: No need to have edit permissions to be able to "Save as" a dashboard --- .../dashboard/components/DashboardSettings/SettingsCtrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts index e5cfac97d5f..fc3b98b4848 100755 --- a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts +++ b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts @@ -38,7 +38,7 @@ export class SettingsCtrl { }); }); - this.canSaveAs = this.dashboard.meta.canEdit && contextSrv.hasEditPermissionInFolders; + this.canSaveAs = contextSrv.hasEditPermissionInFolders; this.canSave = this.dashboard.meta.canSave; this.canDelete = this.dashboard.meta.canSave; From 2c4cb03cd309de05b3712e955ea54e3eced7c9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 12 Feb 2019 12:49:40 +0100 Subject: [PATCH 11/12] Fixed issues with double page body and husky pre-commit hook --- package.json | 3 +- .../datasources/NewDataSourcePage.tsx | 56 +- .../datasources/settings/ButtonRow.tsx | 8 +- .../settings/DataSourceSettingsPage.tsx | 80 +- .../__snapshots__/ButtonRow.test.tsx.snap | 8 - .../DataSourceSettingsPage.test.tsx.snap | 700 +++++++++--------- .../features/folders/FolderSettingsPage.tsx | 50 +- .../FolderSettingsPage.test.tsx.snap | 192 +++-- public/app/features/teams/TeamPages.tsx | 4 +- yarn.lock | 69 +- 10 files changed, 583 insertions(+), 587 deletions(-) diff --git a/package.json b/package.json index f004ee07732..b8e45a52321 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "html-loader": "^0.5.1", "html-webpack-harddisk-plugin": "^0.2.0", "html-webpack-plugin": "^3.2.0", - "husky": "^0.14.3", + "husky": "^1.3.1", "jest": "^23.6.0", "jest-date-mock": "^1.0.6", "lint-staged": "^8.1.3", @@ -120,7 +120,6 @@ "typecheck": "tsc --noEmit", "jest": "jest --notify --watch", "api-tests": "jest --notify --watch --config=tests/api/jest.js", - "precommit": "grunt precommit", "storybook": "cd packages/grafana-ui && yarn storybook" }, "husky": { diff --git a/public/app/features/datasources/NewDataSourcePage.tsx b/public/app/features/datasources/NewDataSourcePage.tsx index 1d926048b8c..f512bdfe3c9 100644 --- a/public/app/features/datasources/NewDataSourcePage.tsx +++ b/public/app/features/datasources/NewDataSourcePage.tsx @@ -35,34 +35,32 @@ class NewDataSourcePage extends PureComponent { return ( -
-

Choose data source type

-
- -
-
- {dataSourceTypes.map((plugin, index) => { - return ( -
this.onDataSourceTypeClicked(plugin)} - className="add-data-source-grid-item" - key={`${plugin.id}-${index}`} - > - - {plugin.name} -
- ); - })} -
+

Choose data source type

+
+ +
+
+ {dataSourceTypes.map((plugin, index) => { + return ( +
this.onDataSourceTypeClicked(plugin)} + className="add-data-source-grid-item" + key={`${plugin.id}-${index}`} + > + + {plugin.name} +
+ ); + })}
@@ -74,7 +72,7 @@ function mapStateToProps(state: StoreState) { return { navModel: getNavModel(state.navIndex, 'datasources'), dataSourceTypes: getDataSourceTypes(state.dataSources), - isLoading: state.dataSources.isLoadingDataSources + isLoading: state.dataSources.isLoadingDataSources, }; } diff --git a/public/app/features/datasources/settings/ButtonRow.tsx b/public/app/features/datasources/settings/ButtonRow.tsx index 36fd6f0283a..9f633ee6bcf 100644 --- a/public/app/features/datasources/settings/ButtonRow.tsx +++ b/public/app/features/datasources/settings/ButtonRow.tsx @@ -10,9 +10,11 @@ export interface Props { const ButtonRow: FC = ({ isReadOnly, onDelete, onSubmit, onTest }) => { return (
- + {!isReadOnly && ( + + )} {isReadOnly && ( - -
- -
+
+
+
+ + +
+
+ + +
+
diff --git a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap index e51e5c0e180..cd6cdf4a032 100644 --- a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap +++ b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap @@ -7,62 +7,58 @@ exports[`Render should enable save button 1`] = ` -
-

+
+
- Folder Settings -

-
- -
- - + +
+
+
-
+ - -
- -
+ + Delete + +
+
@@ -75,62 +71,58 @@ exports[`Render should render component 1`] = ` -
-

+
+
- Folder Settings -

-
- -
- - + +
+
+
-
+ - -
- -
+ + Delete + +
+
diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 7a38197ff71..235936aa8f5 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -49,9 +49,9 @@ export class TeamPages extends PureComponent { async fetchTeam() { const { loadTeam, teamId } = this.props; - this.setState({isLoading: true}); + this.setState({ isLoading: true }); const team = await loadTeam(teamId); - this.setState({isLoading: false}); + this.setState({ isLoading: false }); return team; } diff --git a/yarn.lock b/yarn.lock index 2fb4a5d3ee2..60a72ec86ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4712,6 +4712,11 @@ ci-info@^1.5.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + cidr-regex@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1" @@ -7922,6 +7927,11 @@ get-stdin@^4.0.1: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= +get-stdin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" + integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== + get-stream@3.0.0, get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -8886,14 +8896,21 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -husky@^0.14.3: - version "0.14.3" - resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3" - integrity sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA== +husky@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/husky/-/husky-1.3.1.tgz#26823e399300388ca2afff11cfa8a86b0033fae0" + integrity sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg== dependencies: - is-ci "^1.0.10" - normalize-path "^1.0.0" - strip-indent "^2.0.0" + cosmiconfig "^5.0.7" + execa "^1.0.0" + find-up "^3.0.0" + get-stdin "^6.0.0" + is-ci "^2.0.0" + pkg-dir "^3.0.0" + please-upgrade-node "^3.1.1" + read-pkg "^4.0.1" + run-node "^1.0.0" + slash "^2.0.0" iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" @@ -9279,6 +9296,13 @@ is-ci@^1.0.10: dependencies: ci-info "^1.5.0" +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + is-cidr@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-1.0.0.tgz#fb5aacf659255310359da32cae03e40c6a1c2afc" @@ -11925,11 +11949,6 @@ normalize-path@2.0.1: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" integrity sha1-R4hqwWYnYNQmG32XnSQXCdPOP3o= -normalize-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" - integrity sha1-MtDkcvkf80VwHBWoMRAY07CpA3k= - normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -12948,7 +12967,7 @@ pkg-up@^1.0.0: dependencies: find-up "^1.0.0" -please-upgrade-node@^3.0.2: +please-upgrade-node@^3.0.2, please-upgrade-node@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz#ed320051dfcc5024fae696712c8288993595e8ac" integrity sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ== @@ -14336,6 +14355,15 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" +read-pkg@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-4.0.1.tgz#963625378f3e1c4d48c85872b5a6ec7d5d093237" + integrity sha1-ljYlN48+HE1IyFhytabsfV0JMjc= + dependencies: + normalize-package-data "^2.3.2" + parse-json "^4.0.0" + pify "^3.0.0" + read@1, read@~1.0.1, read@~1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" @@ -14990,6 +15018,11 @@ run-async@^2.0.0, run-async@^2.2.0: dependencies: is-promise "^2.1.0" +run-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e" + integrity sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A== + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -15448,6 +15481,11 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slate-base64-serializer@^0.2.36: version "0.2.94" resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.94.tgz#b908c3af481b9a0ead78f313653414c4b2b4b2d5" @@ -16147,11 +16185,6 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= - strip-json-comments@~1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" From e163aadfe4af86f1ef7c0da6b73ce40294ecccfb Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 11 Feb 2019 21:12:01 +0100 Subject: [PATCH 12/12] use authtoken for session quota restrictions closes #15360 --- pkg/api/api.go | 6 ++-- pkg/api/dashboard.go | 9 +++--- pkg/api/dashboard_test.go | 6 +++- pkg/api/http_server.go | 2 ++ pkg/login/ext_user.go | 24 ++++++++++---- pkg/login/ldap_test.go | 5 ++- pkg/middleware/middleware_test.go | 8 +++++ pkg/middleware/quota.go | 24 ++++++++------ pkg/middleware/quota_test.go | 47 ++++++++++++++++++---------- pkg/models/user_token.go | 1 + pkg/services/auth/auth_token.go | 24 ++++++++++---- pkg/services/auth/auth_token_test.go | 12 +++++++ pkg/services/quota/quota.go | 23 ++++++++++++-- 13 files changed, 139 insertions(+), 52 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 0685ef3814d..6da127fb550 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -16,7 +16,7 @@ func (hs *HTTPServer) registerRoutes() { reqOrgAdmin := middleware.ReqOrgAdmin redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL() - quota := middleware.Quota + quota := middleware.Quota(hs.QuotaService) bind := binding.Bind r := hs.RouteRegister @@ -286,7 +286,7 @@ func (hs *HTTPServer) registerRoutes() { dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff)) - dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(PostDashboard)) + dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(hs.PostDashboard)) dashboardRoute.Get("/home", Wrap(GetHomeDashboard)) dashboardRoute.Get("/tags", GetDashboardTags) dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard)) @@ -294,7 +294,7 @@ func (hs *HTTPServer) registerRoutes() { dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) { dashIdRoute.Get("/versions", Wrap(GetDashboardVersions)) dashIdRoute.Get("/versions/:id", Wrap(GetDashboardVersion)) - dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(RestoreDashboardVersion)) + dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(hs.RestoreDashboardVersion)) dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 2789b0bf51e..20d717ef8fa 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -18,7 +18,6 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/guardian" - "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -208,14 +207,14 @@ func DeleteDashboardByUID(c *m.ReqContext) Response { }) } -func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response { +func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response { cmd.OrgId = c.OrgId cmd.UserId = c.UserId dash := cmd.GetDashboardModel() if dash.Id == 0 && dash.Uid == "" { - limitReached, err := quota.QuotaReached(c, "dashboard") + limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard") if err != nil { return Error(500, "failed to get quota", err) } @@ -463,7 +462,7 @@ func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOption } // RestoreDashboardVersion restores a dashboard to the given version. -func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response { +func (hs *HTTPServer) RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response { dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "") if rsp != nil { return rsp @@ -490,7 +489,7 @@ func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersio saveCmd.Dashboard.Set("uid", dash.Uid) saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) - return PostDashboard(c, saveCmd) + return hs.PostDashboard(c, saveCmd) } func GetDashboardTags(c *m.ReqContext) { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 8ee40920cbc..44d5cd32430 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -881,12 +881,16 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() + hs := HTTPServer{ + Bus: bus.GetBus(), + } + sc := setupScenarioContext(url) sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId} - return PostDashboard(c, cmd) + return hs.PostDashboard(c, cmd) }) origNewDashboardService := dashboards.NewService diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index cadf6896bf4..2a430147b55 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/hooks" + "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" @@ -55,6 +56,7 @@ type HTTPServer struct { CacheService *cache.CacheService `inject:""` DatasourceCache datasources.CacheService `inject:""` AuthTokenService models.UserTokenService `inject:""` + QuotaService *quota.QuotaService `inject:""` } func (hs *HTTPServer) Init() error { diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go index 42fb37ff9d0..f217f9fe33c 100644 --- a/pkg/login/ext_user.go +++ b/pkg/login/ext_user.go @@ -4,18 +4,30 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/quota" ) func init() { - bus.AddHandler("auth", UpsertUser) + registry.RegisterService(&LoginService{}) } var ( logger = log.New("login.ext_user") ) -func UpsertUser(cmd *m.UpsertUserCommand) error { +type LoginService struct { + Bus bus.Bus `inject:""` + QuotaService *quota.QuotaService `inject:""` +} + +func (ls *LoginService) Init() error { + ls.Bus.AddHandler(ls.UpsertUser) + + return nil +} + +func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error { extUser := cmd.ExternalUser userQuery := &m.GetUserByAuthInfoQuery{ @@ -37,7 +49,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error { return ErrInvalidCredentials } - limitReached, err := quota.QuotaReached(cmd.ReqContext, "user") + limitReached, err := ls.QuotaService.QuotaReached(cmd.ReqContext, "user") if err != nil { log.Warn("Error getting user quota. error: %v", err) return ErrGettingUserQuota @@ -57,7 +69,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error { AuthModule: extUser.AuthModule, AuthId: extUser.AuthId, } - if err := bus.Dispatch(cmd2); err != nil { + if err := ls.Bus.Dispatch(cmd2); err != nil { return err } } @@ -78,12 +90,12 @@ func UpsertUser(cmd *m.UpsertUserCommand) error { // Sync isGrafanaAdmin permission if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin { - if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil { + if err := ls.Bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil { return err } } - err = bus.Dispatch(&m.SyncTeamsCommand{ + err = ls.Bus.Dispatch(&m.SyncTeamsCommand{ User: cmd.Result, ExternalUser: extUser, }) diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index c02fa02e030..ef20feb1373 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -395,8 +395,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) { defer bus.ClearBusHandlers() sc := &scenarioContext{} + loginService := &LoginService{ + Bus: bus.GetBus(), + } - bus.AddHandler("test", UpsertUser) + bus.AddHandler("test", loginService.UpsertUser) bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.SyncTeamsCommand) error { return nil diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 8545c3856c9..1fbd303bebd 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -682,6 +682,7 @@ type fakeUserAuthTokenService struct { 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 { @@ -704,6 +705,9 @@ func newFakeUserAuthTokenService() *fakeUserAuthTokenService { revokeTokenProvider: func(token *m.UserToken) error { return nil }, + activeAuthTokenCount: func() (int64, error) { + return 10, nil + }, } } @@ -722,3 +726,7 @@ func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP, 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/quota.go b/pkg/middleware/quota.go index 43efca43485..51f906e2c92 100644 --- a/pkg/middleware/quota.go +++ b/pkg/middleware/quota.go @@ -9,16 +9,20 @@ import ( "github.com/grafana/grafana/pkg/services/quota" ) -func Quota(target string) macaron.Handler { - return func(c *m.ReqContext) { - limitReached, err := quota.QuotaReached(c, target) - if err != nil { - c.JsonApiErr(500, "failed to get quota", err) - return - } - if limitReached { - c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil) - return +// Quota returns a function that returns a function used to call quotaservice based on target name +func Quota(quotaService *quota.QuotaService) func(target string) macaron.Handler { + //https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky + return func(target string) macaron.Handler { + return func(c *m.ReqContext) { + limitReached, err := quotaService.QuotaReached(c, target) + if err != nil { + c.JsonApiErr(500, "failed to get quota", err) + return + } + if limitReached { + c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil) + return + } } } } diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index e2a6ef63377..52b696cf037 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -3,9 +3,10 @@ package middleware import ( "testing" + "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" ) @@ -13,10 +14,6 @@ import ( func TestMiddlewareQuota(t *testing.T) { Convey("Given the grafana quota middleware", t, func() { - session.GetSessionCount = func() int { - return 4 - } - setting.AnonymousEnabled = false setting.Quota = setting.QuotaSettings{ Enabled: true, @@ -39,6 +36,12 @@ func TestMiddlewareQuota(t *testing.T) { }, } + fakeAuthTokenService := newFakeUserAuthTokenService() + qs := "a.QuotaService{ + AuthTokenService: fakeAuthTokenService, + } + QuotaFn := Quota(qs) + middlewareScenario("with user not logged in", func(sc *scenarioContext) { bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error { query.Result = &m.GlobalQuotaDTO{ @@ -48,26 +51,30 @@ func TestMiddlewareQuota(t *testing.T) { } return nil }) + Convey("global quota not reached", func() { - sc.m.Get("/user", Quota("user"), sc.defaultHandler) + sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler) sc.fakeReq("GET", "/user").exec() So(sc.resp.Code, ShouldEqual, 200) }) + Convey("global quota reached", func() { setting.Quota.Global.User = 4 - sc.m.Get("/user", Quota("user"), sc.defaultHandler) + sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler) sc.fakeReq("GET", "/user").exec() So(sc.resp.Code, ShouldEqual, 403) }) + Convey("global session quota not reached", func() { setting.Quota.Global.Session = 10 - sc.m.Get("/user", Quota("session"), sc.defaultHandler) + sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler) sc.fakeReq("GET", "/user").exec() So(sc.resp.Code, ShouldEqual, 200) }) + Convey("global session quota reached", func() { setting.Quota.Global.Session = 1 - sc.m.Get("/user", Quota("session"), sc.defaultHandler) + sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler) sc.fakeReq("GET", "/user").exec() So(sc.resp.Code, ShouldEqual, 403) }) @@ -95,6 +102,7 @@ func TestMiddlewareQuota(t *testing.T) { } return nil }) + bus.AddHandler("userQuota", func(query *m.GetUserQuotaByTargetQuery) error { query.Result = &m.UserQuotaDTO{ Target: query.Target, @@ -103,6 +111,7 @@ func TestMiddlewareQuota(t *testing.T) { } return nil }) + bus.AddHandler("orgQuota", func(query *m.GetOrgQuotaByTargetQuery) error { query.Result = &m.OrgQuotaDTO{ Target: query.Target, @@ -111,45 +120,49 @@ func TestMiddlewareQuota(t *testing.T) { } return nil }) + Convey("global datasource quota reached", func() { setting.Quota.Global.DataSource = 4 - sc.m.Get("/ds", Quota("data_source"), sc.defaultHandler) + sc.m.Get("/ds", QuotaFn("data_source"), sc.defaultHandler) sc.fakeReq("GET", "/ds").exec() So(sc.resp.Code, ShouldEqual, 403) }) + Convey("user Org quota not reached", func() { setting.Quota.User.Org = 5 - sc.m.Get("/org", Quota("org"), sc.defaultHandler) + sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler) sc.fakeReq("GET", "/org").exec() So(sc.resp.Code, ShouldEqual, 200) }) + Convey("user Org quota reached", func() { setting.Quota.User.Org = 4 - sc.m.Get("/org", Quota("org"), sc.defaultHandler) + sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler) sc.fakeReq("GET", "/org").exec() So(sc.resp.Code, ShouldEqual, 403) }) + Convey("org dashboard quota not reached", func() { setting.Quota.Org.Dashboard = 10 - sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler) + sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler) sc.fakeReq("GET", "/dashboard").exec() So(sc.resp.Code, ShouldEqual, 200) }) + Convey("org dashboard quota reached", func() { setting.Quota.Org.Dashboard = 4 - sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler) + sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler) sc.fakeReq("GET", "/dashboard").exec() So(sc.resp.Code, ShouldEqual, 403) }) + Convey("org dashboard quota reached but quotas disabled", func() { setting.Quota.Org.Dashboard = 4 setting.Quota.Enabled = false - sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler) + sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler) sc.fakeReq("GET", "/dashboard").exec() So(sc.resp.Code, ShouldEqual, 200) }) - }) - }) } diff --git a/pkg/models/user_token.go b/pkg/models/user_token.go index c8084cf1eba..388bc2dd4a2 100644 --- a/pkg/models/user_token.go +++ b/pkg/models/user_token.go @@ -29,4 +29,5 @@ type UserTokenService interface { LookupToken(unhashedToken string) (*UserToken, error) TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error) RevokeToken(token *UserToken) error + ActiveTokenCount() (int64, error) } diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index ef5dccd779f..648575d54cd 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -35,6 +35,13 @@ func (s *UserAuthTokenService) Init() error { return nil } +func (s *UserAuthTokenService) ActiveTokenCount() (int64, error) { + var model userAuthToken + count, err := s.SQLStore.NewSession().Where(`created_at > ? AND rotated_at > ?`, s.createdAfterParam(), s.rotatedAfterParam()).Count(&model) + + return count, err +} + func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) { clientIP = util.ParseIPAddress(clientIP) token, err := util.RandomHex(16) @@ -79,13 +86,8 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserTo s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) } - tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour - tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour - createdAfter := getTime().Add(-tokenMaxLifetime).Unix() - rotatedAfter := getTime().Add(-tokenMaxInactiveLifetime).Unix() - var model userAuthToken - exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, createdAfter, rotatedAfter).Get(&model) + exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, s.createdAfterParam(), s.rotatedAfterParam()).Get(&model) if err != nil { return nil, err } @@ -219,6 +221,16 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error { return nil } +func (s *UserAuthTokenService) createdAfterParam() int64 { + tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour + return getTime().Add(-tokenMaxLifetime).Unix() +} + +func (s *UserAuthTokenService) rotatedAfterParam() int64 { + tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour + return getTime().Add(-tokenMaxInactiveLifetime).Unix() +} + func hashToken(token string) string { hashBytes := sha256.Sum256([]byte(token + setting.SecretKey)) return hex.EncodeToString(hashBytes[:]) diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 26dcbc5c868..49e7acc3a5b 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -31,6 +31,12 @@ func TestUserAuthToken(t *testing.T) { So(userToken, ShouldNotBeNil) So(userToken.AuthTokenSeen, ShouldBeFalse) + Convey("Can count active tokens", func() { + count, err := userAuthTokenService.ActiveTokenCount() + So(err, ShouldBeNil) + So(count, ShouldEqual, 1) + }) + Convey("When lookup unhashed token should return user auth token", func() { userToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) So(err, ShouldBeNil) @@ -114,6 +120,12 @@ func TestUserAuthToken(t *testing.T) { notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) So(err, ShouldEqual, models.ErrUserTokenNotFound) So(notGood, ShouldBeNil) + + Convey("should not find active token when expired", func() { + count, err := userAuthTokenService.ActiveTokenCount() + So(err, ShouldBeNil) + So(count, ShouldEqual, 0) + }) }) Convey("when rotated_at is 5 days ago and created_at is 29 days and 23:59:59 ago should not find token", func() { diff --git a/pkg/services/quota/quota.go b/pkg/services/quota/quota.go index 2ec399437e6..ff2528e31e8 100644 --- a/pkg/services/quota/quota.go +++ b/pkg/services/quota/quota.go @@ -3,11 +3,23 @@ package quota import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/session" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" ) -func QuotaReached(c *m.ReqContext, target string) (bool, error) { +func init() { + registry.RegisterService(&QuotaService{}) +} + +type QuotaService struct { + AuthTokenService m.UserTokenService `inject:""` +} + +func (qs *QuotaService) Init() error { + return nil +} + +func (qs *QuotaService) QuotaReached(c *m.ReqContext, target string) (bool, error) { if !setting.Quota.Enabled { return false, nil } @@ -30,7 +42,12 @@ func QuotaReached(c *m.ReqContext, target string) (bool, error) { return true, nil } if target == "session" { - usedSessions := session.GetSessionCount() + + usedSessions, err := qs.AuthTokenService.ActiveTokenCount() + if err != nil { + return false, err + } + if int64(usedSessions) > scope.DefaultLimit { c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit) return true, nil