From 99bb6ebd2b0670ee7470dbbe04e8c1ef672438a1 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Fri, 8 Apr 2022 12:18:52 -0700 Subject: [PATCH 001/121] Search: Update tag filter options dynamically (#47165) --- .../src/components/Select/SelectBase.tsx | 2 + .../grafana-ui/src/components/Select/types.ts | 1 + .../core/components/TagFilter/TagFilter.tsx | 130 ++++++++++++------ .../playlist/PlaylistEditPage.test.tsx | 6 + .../features/playlist/PlaylistForm.test.tsx | 6 + .../playlist/PlaylistNewPage.test.tsx | 6 + .../components/DashboardSearch.test.tsx | 5 +- .../components/SearchResultsFilter.test.tsx | 1 + .../app/features/search/page/SearchPage.tsx | 3 +- 9 files changed, 113 insertions(+), 47 deletions(-) diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index bd313b231e7..b74dc5258d0 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -128,6 +128,7 @@ export function SelectBase({ onInputChange, onKeyDown, onOpenMenu, + onFocus, openMenuOnFocus = false, options = [], placeholder = 'Choose', @@ -235,6 +236,7 @@ export function SelectBase({ onKeyDown, onMenuClose: onCloseMenu, onMenuOpen: onOpenMenu, + onFocus, formatOptionLabel, openMenuOnFocus, options, diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index 4b93366dc86..12606c768c0 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -61,6 +61,7 @@ export interface SelectCommonProps { onInputChange?: (value: string, actionMeta: InputActionMeta) => void; onKeyDown?: (event: React.KeyboardEvent) => void; onOpenMenu?: () => void; + onFocus?: () => void; openMenuOnFocus?: boolean; options?: Array>; placeholder?: string; diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index c69fd5ac889..c419f6a8db7 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -1,11 +1,9 @@ -// Libraries -import React, { FC } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/css'; import { components } from 'react-select'; -import debounce from 'debounce-promise'; -import { stylesFactory, useTheme, Icon, AsyncMultiSelect } from '@grafana/ui'; -import { escapeStringForRegex, GrafanaTheme } from '@grafana/data'; -// Components +import { Icon, MultiSelect, useStyles2 } from '@grafana/ui'; +import { escapeStringForRegex, GrafanaTheme2 } from '@grafana/data'; + import { TagOption } from './TagOption'; import { TagBadge } from './TagBadge'; @@ -14,6 +12,12 @@ export interface TermCount { count: number; } +interface TagSelectOption { + value: string; + label: string; + count: number; +} + export interface Props { allowCustomValue?: boolean; formatCreateLabel?: (input: string) => string; @@ -45,29 +49,70 @@ export const TagFilter: FC = ({ tags, width, }) => { - const theme = useTheme(); - const styles = getStyles(theme); + const styles = useStyles2(getStyles); - const onLoadOptions = async (query: string) => { + const currentlySelectedTags = tags.map((tag) => ({ value: tag, label: tag, count: 0 })); + const [options, setOptions] = useState(currentlySelectedTags); + const [isLoading, setIsLoading] = useState(false); + const [previousTags, setPreviousTags] = useState(tags); + + // Necessary to force re-render to keep tag options up to date / relevant + const selectKey = useMemo(() => tags.join(), [tags]); + + const onLoadOptions = useCallback(async () => { const options = await tagOptions(); - return options.map((option) => ({ - value: option.term, - label: option.term, - count: option.count, - })); - }; + return options.map((option) => { + if (tags.includes(option.term)) { + return { + value: option.term, + label: option.term, + count: 0, + }; + } else { + return { + value: option.term, + label: option.term, + count: option.count, + }; + } + }); + }, [tagOptions, tags]); - const debouncedLoadOptions = debounce(onLoadOptions, 300); + const onFocus = useCallback(async () => { + setIsLoading(true); + const results = await onLoadOptions(); + setOptions(results); + setIsLoading(false); + }, [onLoadOptions]); + + useEffect(() => { + // Load options when tag is selected externally + if (tags.length > 0 && options.length === 0) { + onFocus(); + } + }, [onFocus, options.length, tags.length]); + + useEffect(() => { + // Update selected tags to not include (counts) when selected externally + if (tags !== previousTags) { + setPreviousTags(tags); + onFocus(); + } + }, [onFocus, previousTags, tags]); const onTagChange = (newTags: any[]) => { // On remove with 1 item returns null, so we need to make sure it's an empty array in that case // https://github.com/JedWatson/react-select/issues/3632 + newTags.forEach((tag) => (tag.count = 0)); + onChange((newTags || []).map((tag) => tag.value)); }; - const value = tags.map((tag) => ({ value: tag, label: tag, count: 0 })); - const selectOptions = { + key: selectKey, + onFocus, + isLoading, + options, allowCreateWhileLoading: true, allowCustomValue, formatCreateLabel, @@ -77,12 +122,11 @@ export const TagFilter: FC = ({ getOptionValue: (i: any) => i.value, inputId, isMulti: true, - loadOptions: debouncedLoadOptions, loadingMessage: 'Loading...', noOptionsMessage: 'No tags found', onChange: onTagChange, placeholder, - value, + value: currentlySelectedTags, width, components: { Option: TagOption, @@ -109,37 +153,35 @@ export const TagFilter: FC = ({ Clear tags )} - } aria-label="Tag filter" /> + } aria-label="Tag filter" /> ); }; TagFilter.displayName = 'TagFilter'; -const getStyles = stylesFactory((theme: GrafanaTheme) => { - return { - tagFilter: css` - position: relative; - min-width: 180px; - flex-grow: 1; +const getStyles = (theme: GrafanaTheme2) => ({ + tagFilter: css` + position: relative; + min-width: 180px; + flex-grow: 1; - .label-tag { - margin-left: 6px; - cursor: pointer; - } - `, - clear: css` - text-decoration: underline; - font-size: 12px; - position: absolute; - top: -22px; - right: 0; + .label-tag { + margin-left: 6px; cursor: pointer; - color: ${theme.colors.textWeak}; + } + `, + clear: css` + text-decoration: underline; + font-size: 12px; + position: absolute; + top: -22px; + right: 0; + cursor: pointer; + color: ${theme.colors.text.secondary}; - &:hover { - color: ${theme.colors.textStrong}; - } - `, - }; + &:hover { + color: ${theme.colors.text.primary}; + } + `, }); diff --git a/public/app/features/playlist/PlaylistEditPage.test.tsx b/public/app/features/playlist/PlaylistEditPage.test.tsx index 0d8941313fb..2766c0fe6c4 100644 --- a/public/app/features/playlist/PlaylistEditPage.test.tsx +++ b/public/app/features/playlist/PlaylistEditPage.test.tsx @@ -12,6 +12,12 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => backendSrv, })); +jest.mock('../../core/components/TagFilter/TagFilter', () => ({ + TagFilter: () => { + return <>mocked-tag-filter; + }, +})); + async function getTestContext({ name, interval, items }: Partial = {}) { jest.clearAllMocks(); const playlist = { name, items, interval } as unknown as Playlist; diff --git a/public/app/features/playlist/PlaylistForm.test.tsx b/public/app/features/playlist/PlaylistForm.test.tsx index 8a5a774579e..b401838b698 100644 --- a/public/app/features/playlist/PlaylistForm.test.tsx +++ b/public/app/features/playlist/PlaylistForm.test.tsx @@ -6,6 +6,12 @@ import userEvent from '@testing-library/user-event'; import { Playlist } from './types'; import { PlaylistForm } from './PlaylistForm'; +jest.mock('../../core/components/TagFilter/TagFilter', () => ({ + TagFilter: () => { + return <>mocked-tag-filter; + }, +})); + function getTestContext({ name, interval, items }: Partial = {}) { const onSubmitMock = jest.fn(); const playlist = { name, items, interval } as unknown as Playlist; diff --git a/public/app/features/playlist/PlaylistNewPage.test.tsx b/public/app/features/playlist/PlaylistNewPage.test.tsx index f3469adba79..487bd152607 100644 --- a/public/app/features/playlist/PlaylistNewPage.test.tsx +++ b/public/app/features/playlist/PlaylistNewPage.test.tsx @@ -19,6 +19,12 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => backendSrv, })); +jest.mock('../../core/components/TagFilter/TagFilter', () => ({ + TagFilter: () => { + return <>mocked-tag-filter; + }, +})); + function getTestContext({ name, interval, items }: Partial = {}) { jest.clearAllMocks(); const playlist = { name, items, interval } as unknown as Playlist; diff --git a/public/app/features/search/components/DashboardSearch.test.tsx b/public/app/features/search/components/DashboardSearch.test.tsx index 029ab069a51..8004154fc21 100644 --- a/public/app/features/search/components/DashboardSearch.test.tsx +++ b/public/app/features/search/components/DashboardSearch.test.tsx @@ -112,10 +112,11 @@ describe('DashboardSearch', () => { await waitFor(() => screen.getByLabelText('Tag filter')); const tagComponent = screen.getByLabelText('Tag filter'); - await selectOptionInTest(tagComponent, 'tag1'); - expect(tagComponent).toBeInTheDocument(); + tagComponent.focus(); + await waitFor(() => selectOptionInTest(tagComponent, 'tag1')); + await waitFor(() => expect(mockSearch).toHaveBeenCalledWith({ query: '', diff --git a/public/app/features/search/components/SearchResultsFilter.test.tsx b/public/app/features/search/components/SearchResultsFilter.test.tsx index ff587ac1ec7..e88ce063da2 100644 --- a/public/app/features/search/components/SearchResultsFilter.test.tsx +++ b/public/app/features/search/components/SearchResultsFilter.test.tsx @@ -83,6 +83,7 @@ describe('SearchResultsFilter', () => { query: { ...searchQuery, tag: [] }, }); const tagComponent = await screen.findByLabelText('Tag filter'); + await tagComponent.focus(); await selectOptionInTest(tagComponent, 'tag1'); expect(mockFilterByTags).toHaveBeenCalledTimes(1); diff --git a/public/app/features/search/page/SearchPage.tsx b/public/app/features/search/page/SearchPage.tsx index a9e8ced3802..923f677bc43 100644 --- a/public/app/features/search/page/SearchPage.tsx +++ b/public/app/features/search/page/SearchPage.tsx @@ -63,7 +63,8 @@ export default function SearchPage() { {results.loading && } {results.value?.body && (
-
+ +
{query.datasource && ( + + + ( + + + remove(index)} + > + Remove + + + ); + })} +
+ )} + + )} @@ -376,5 +390,8 @@ const getStyles = (theme: GrafanaTheme2) => { margin-left: ${theme.spacing(1.5)}; } `, + noMatchersWarning: css` + padding: ${theme.spacing(1)} ${theme.spacing(2)}; + `, }; }; diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx index c25f812d57a..8719728235e 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx @@ -90,7 +90,13 @@ export const AmRoutesTable: FC = ({ id: 'matchingCriteria', label: 'Matching labels', // eslint-disable-next-line react/display-name - renderCell: (item) => , + renderCell: (item) => { + return item.data.object_matchers.length ? ( + + ) : ( + Matches all alert instances + ); + }, size: 10, }, { From 42431e6ad452142e3e61fc75a9fc3826de98c52f Mon Sep 17 00:00:00 2001 From: Garrett Guillotte <100453168+gguillotte-grafana@users.noreply.github.com> Date: Tue, 12 Apr 2022 00:22:28 -0700 Subject: [PATCH 019/121] Explore/Logs: Improve and add descriptions of Explore's Escape newlines feature (#46709) * Explore/Logs: Clarify phrasing of newline escape fix tooltip Rewrite the tooltip for the smart feature in PR #31352 that replaces incorrectly escaped newlines in log lines. - Clarify the functionality of the feature to emphasize its interactivity. - Remove language suggesting that the feature is experimental, when we intended to suggest reviewing the results manually for correctness. * Docs: Document escape newlines feature Describe and provide steps for using the "Escape newlines" feature in Explore added in PR #31352. * Rewrite topic lead to clarify conditional behavior * Describe reversion of replacements as a standalone task Reverting the replacements is a separate action, so it should have its own task. This also describes the option's transformation and tightens the task language. * Remove Grafana 7 version qualifier * Clarify escape sequence detection lede on task Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * Clarify "Remove escaping" state change Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * Clarify confidence of tooltip content Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> --- docs/sources/explore/logs-integration.md | 11 +++++++++++ public/app/features/explore/LogsMetaRow.tsx | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/sources/explore/logs-integration.md b/docs/sources/explore/logs-integration.md index 3e19c75196f..824faec1c20 100644 --- a/docs/sources/explore/logs-integration.md +++ b/docs/sources/explore/logs-integration.md @@ -97,6 +97,17 @@ You can change the order of received logs from the default descending order (new Each log row has an extendable area with its labels and detected fields, for more robust interaction. For all labels we have added the ability to filter for (positive filter) and filter out (negative filter) selected labels. Each field or label also has a stats icon to display ad-hoc statistics in relation to all displayed logs. +### Escaping newlines + +Explore automatically detects some incorrectly escaped sequences in log lines, such as newlines (`\n`, `\r`) or tabs (`\t`). When it detects such sequences, Explore provides an "Escape newlines" option. + +To automatically fix incorrectly escaped sequences that Explore has detected: + +1. Click "Escape newlines" to replace the sequences. +2. Manually review the replacements to confirm their correctness. + +Explore replaces these sequences. When it does so, the option will change from "Escape newlines" to "Remove escaping". Evaluate the changes as the parsing may not be accurate based on the input received. You can revert the replacements by clicking "Remove escaping". + #### Derived fields links By using Derived fields, you can turn any part of a log message into an internal or external link. The created link is visible as a button next to the Detected field in the Log details view. diff --git a/public/app/features/explore/LogsMetaRow.tsx b/public/app/features/explore/LogsMetaRow.tsx index 7a62da7a8ad..c7841420117 100644 --- a/public/app/features/explore/LogsMetaRow.tsx +++ b/public/app/features/explore/LogsMetaRow.tsx @@ -71,7 +71,7 @@ export const LogsMetaRow: React.FC = React.memo( label: 'Your logs might have incorrectly escaped content', value: ( - diff --git a/public/app/features/explore/AddToDashboard/addToDashboard.test.ts b/public/app/features/explore/AddToDashboard/addToDashboard.test.ts new file mode 100644 index 00000000000..b81c7c9ca48 --- /dev/null +++ b/public/app/features/explore/AddToDashboard/addToDashboard.test.ts @@ -0,0 +1,146 @@ +import { DataQuery, MutableDataFrame } from '@grafana/data'; +import { ExplorePanelData } from 'app/types'; +import { createEmptyQueryResponse } from '../state/utils'; +import { setDashboardInLocalStorage } from './addToDashboard'; +import * as api from 'app/features/dashboard/state/initDashboard'; +import { backendSrv } from 'app/core/services/backend_srv'; + +describe('addPanelToDashboard', () => { + let spy: jest.SpyInstance; + beforeAll(() => { + spy = jest.spyOn(api, 'setDashboardToFetchFromLocalStorage'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('Correct datasource ref is used', async () => { + await setDashboardInLocalStorage({ + queries: [], + queryResponse: createEmptyQueryResponse(), + datasource: { type: 'loki', uid: 'someUid' }, + }); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + dashboard: expect.objectContaining({ + panels: expect.arrayContaining([expect.objectContaining({ datasource: { type: 'loki', uid: 'someUid' } })]), + }), + }) + ); + }); + + it('All queries are correctly passed through', async () => { + const queries: DataQuery[] = [{ refId: 'A' }, { refId: 'B', hide: true }]; + + await setDashboardInLocalStorage({ + queries, + queryResponse: createEmptyQueryResponse(), + }); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + dashboard: expect.objectContaining({ + panels: expect.arrayContaining([expect.objectContaining({ targets: expect.arrayContaining(queries) })]), + }), + }) + ); + }); + + it('Previous panels should not be removed', async () => { + const queries: DataQuery[] = [{ refId: 'A' }]; + const existingPanel = { prop: 'this should be kept' }; + jest.spyOn(backendSrv, 'getDashboardByUid').mockResolvedValue({ + dashboard: { + templating: { list: [] }, + title: 'Previous panels should not be removed', + uid: 'someUid', + panels: [existingPanel], + }, + meta: {}, + }); + + await setDashboardInLocalStorage({ + queries, + queryResponse: createEmptyQueryResponse(), + dashboardUid: 'someUid', + datasource: { type: '' }, + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + dashboard: expect.objectContaining({ + panels: expect.arrayContaining([ + expect.objectContaining({ targets: expect.arrayContaining(queries) }), + existingPanel, + ]), + }), + }) + ); + }); + + describe('Setting visualization type', () => { + describe('Defaults to table', () => { + const cases: Array<[string, DataQuery[], ExplorePanelData]> = [ + ['If response is empty', [{ refId: 'A' }], createEmptyQueryResponse()], + ['If no query is active', [{ refId: 'A', hide: true }], createEmptyQueryResponse()], + [ + 'If no query is active, even when there is a response from a previous execution', + [{ refId: 'A', hide: true }], + { ...createEmptyQueryResponse(), logsFrames: [new MutableDataFrame({ refId: 'A', fields: [] })] }, + ], + [ + // trace view is not supported in dashboards, we expect to fallback to table panel + 'If there are trace frames', + [{ refId: 'A' }], + { ...createEmptyQueryResponse(), traceFrames: [new MutableDataFrame({ refId: 'A', fields: [] })] }, + ], + ]; + + it.each(cases)('%s', async (_, queries, queryResponse) => { + await setDashboardInLocalStorage({ queries, queryResponse }); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + dashboard: expect.objectContaining({ + panels: expect.arrayContaining([expect.objectContaining({ type: 'table' })]), + }), + }) + ); + }); + }); + + describe('Correctly set visualization based on response', () => { + type TestArgs = { + framesType: string; + expectedPanel: string; + }; + // Note: traceFrames test is "duplicated" in "Defaults to table" tests. + // This is intentional as a way to enforce explicit tests for that case whenever in the future we'll + // add support for creating traceview panels + it.each` + framesType | expectedPanel + ${'logsFrames'} | ${'logs'} + ${'graphFrames'} | ${'timeseries'} + ${'nodeGraphFrames'} | ${'nodeGraph'} + ${'traceFrames'} | ${'table'} + `( + 'Sets visualization to $expectedPanel if there are $frameType frames', + async ({ framesType, expectedPanel }: TestArgs) => { + const queries = [{ refId: 'A' }]; + const queryResponse: ExplorePanelData = { + ...createEmptyQueryResponse(), + [framesType]: [new MutableDataFrame({ refId: 'A', fields: [] })], + }; + + await setDashboardInLocalStorage({ queries, queryResponse }); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + dashboard: expect.objectContaining({ + panels: expect.arrayContaining([expect.objectContaining({ type: expectedPanel })]), + }), + }) + ); + } + ); + }); + }); +}); diff --git a/public/app/features/explore/AddToDashboard/addToDashboard.ts b/public/app/features/explore/AddToDashboard/addToDashboard.ts index e27e041cf7d..6128f221de8 100644 --- a/public/app/features/explore/AddToDashboard/addToDashboard.ts +++ b/public/app/features/explore/AddToDashboard/addToDashboard.ts @@ -1,37 +1,81 @@ -import { DataQuery, DataSourceRef } from '@grafana/data'; -import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { DataFrame, DataQuery, DataSourceRef } from '@grafana/data'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { + getNewDashboardModelData, + setDashboardToFetchFromLocalStorage, +} from 'app/features/dashboard/state/initDashboard'; +import { DashboardDTO, ExplorePanelData } from 'app/types'; -export interface SaveToNewDashboardDTO { - dashboardName: string; - folderId: number; +export enum AddToDashboardError { + FETCH_DASHBOARD = 'fetch-dashboard', + SET_DASHBOARD_LS = 'set-dashboard-ls-error', } -interface SaveOptions { + +interface AddPanelToDashboardOptions { queries: DataQuery[]; - panel: string; - datasource: DataSourceRef; + queryResponse: ExplorePanelData; + datasource?: DataSourceRef; + dashboardUid?: string; } -const createDashboard = ( - dashboardName: string, - folderId: number, - queries: DataQuery[], - datasource: DataSourceRef, - panel: string -) => { - const dashboard = getDashboardSrv().create({ title: dashboardName }, { folderId }); +function createDashboard(): DashboardDTO { + const dto = getNewDashboardModelData(); - dashboard.addPanel({ targets: queries, type: panel, title: 'New Panel', datasource }); + // getNewDashboardModelData adds by default the "add-panel" panel. We don't want that. + dto.dashboard.panels = []; - return getDashboardSrv().saveDashboard({ dashboard, folderId }, { showErrorAlert: false, showSuccessAlert: false }); -}; + return dto; +} -export const addToDashboard = async (data: SaveToNewDashboardDTO, options: SaveOptions): Promise => { - const res = await createDashboard( - data.dashboardName, - data.folderId, - options.queries, - options.datasource, - options.panel - ); - return res.data.url; -}; +export async function setDashboardInLocalStorage(options: AddPanelToDashboardOptions) { + const panelType = getPanelType(options.queries, options.queryResponse); + const panel = { + targets: options.queries, + type: panelType, + title: 'New Panel', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + datasource: options.datasource, + }; + + let dto: DashboardDTO; + + if (options.dashboardUid) { + try { + dto = await backendSrv.getDashboardByUid(options.dashboardUid); + } catch (e) { + throw AddToDashboardError.FETCH_DASHBOARD; + } + } else { + dto = createDashboard(); + } + + dto.dashboard.panels = [panel, ...(dto.dashboard.panels ?? [])]; + + try { + setDashboardToFetchFromLocalStorage(dto); + } catch { + throw AddToDashboardError.SET_DASHBOARD_LS; + } +} + +const isVisible = (query: DataQuery) => !query.hide; +const hasRefId = (refId: DataFrame['refId']) => (frame: DataFrame) => frame.refId === refId; + +function getPanelType(queries: DataQuery[], queryResponse: ExplorePanelData) { + for (const { refId } of queries.filter(isVisible)) { + // traceview is not supported in dashboards, skipping it for now. + const hasQueryRefId = hasRefId(refId); + if (queryResponse.graphFrames.some(hasQueryRefId)) { + return 'timeseries'; + } + if (queryResponse.logsFrames.some(hasQueryRefId)) { + return 'logs'; + } + if (queryResponse.nodeGraphFrames.some(hasQueryRefId)) { + return 'nodeGraph'; + } + } + + // falling back to table + return 'table'; +} diff --git a/public/app/features/explore/AddToDashboard/index.test.tsx b/public/app/features/explore/AddToDashboard/index.test.tsx index 415980c1249..62db02ba6b4 100644 --- a/public/app/features/explore/AddToDashboard/index.test.tsx +++ b/public/app/features/explore/AddToDashboard/index.test.tsx @@ -1,31 +1,25 @@ -import React from 'react'; -import { act, render, screen, waitForElementToBeRemoved } from '@testing-library/react'; -import { ExploreId, ExplorePanelData, ExploreState } from 'app/types'; -import { Provider } from 'react-redux'; -import { configureStore } from 'app/store/configureStore'; +import React, { ReactNode } from 'react'; +import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { DataQuery, MutableDataFrame } from '@grafana/data'; -import { createEmptyQueryResponse } from '../state/utils'; -import { locationService } from '@grafana/runtime'; -import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types'; -import * as api from './addToDashboard'; -import * as dashboardApi from 'app/features/manage-dashboards/state/actions'; +import { configureStore } from 'app/store/configureStore'; +import { ExploreId, ExploreState } from 'app/types'; +import { Provider } from 'react-redux'; import { AddToDashboard } from '.'; +import * as api from './addToDashboard'; +import { locationService, setEchoSrv } from '@grafana/runtime'; +import * as initDashboard from 'app/features/dashboard/state/initDashboard'; +import { DataQuery } from '@grafana/data'; +import { createEmptyQueryResponse } from '../state/utils'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { DashboardSearchItemType } from 'app/features/search/types'; +import { Echo } from 'app/core/services/echo/Echo'; -const setup = ( - children: JSX.Element, - queries: DataQuery[] = [], - queryResponse: ExplorePanelData = createEmptyQueryResponse() -) => { +const setup = (children: ReactNode, queries: DataQuery[] = [{ refId: 'A' }]) => { const store = configureStore({ explore: { left: { queries, - queryResponse, - datasourceInstance: { - type: 'loki', - uid: 'someuid', - }, + queryResponse: createEmptyQueryResponse(), }, } as ExploreState, }); @@ -33,269 +27,336 @@ const setup = ( return render({children}); }; -const createFolder = (title: string, id: number): DashboardSearchHit => ({ - title, - id, - isStarred: false, - type: DashboardSearchItemType.DashFolder, - items: [], - url: '', - uri: '', - tags: [], -}); - const openModal = async () => { userEvent.click(screen.getByRole('button', { name: /add to dashboard/i })); expect(await screen.findByRole('dialog', { name: 'Add panel to dashboard' })).toBeInTheDocument(); }; -describe('Add to Dashboard Button', () => { - const searchFoldersResponse = Promise.resolve([createFolder('Folder 1', 1), createFolder('Folder 2', 2)]); - const redirectURL = '/some/redirect/url'; - let addToDashboardMock: jest.SpyInstance< - ReturnType, - Parameters - >; - - const waitForSearchFolderResponse = async () => { - return act(async () => { - await searchFoldersResponse; - }); - }; - - beforeEach(() => { - jest.spyOn(dashboardApi, 'searchFolders').mockReturnValue(searchFoldersResponse); - addToDashboardMock = jest.spyOn(api, 'addToDashboard').mockResolvedValue('/some/redirect/url'); +describe('AddToDashboardButton', () => { + beforeAll(() => { + setEchoSrv(new Echo()); }); - afterEach(() => { - jest.restoreAllMocks(); - }); + it('Is disabled if explore pane has no queries', async () => { + setup(, []); - it('Opens and closes the modal correctly', async () => { - setup(, [{ refId: 'A' }]); + const button = await screen.findByRole('button', { name: /add to dashboard/i }); + expect(button).toBeDisabled(); - await openModal(); - - userEvent.click(screen.getByRole('button', { name: /cancel/i })); + userEvent.click(button); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); - describe('navigation', () => { - it('Navigates to dashboard when clicking on "Save and go to dashboard"', async () => { - locationService.push = jest.fn(); + describe('Success path', () => { + const addToDashboardResponse = Promise.resolve(); - setup(, [{ refId: 'A' }]); + const waitForAddToDashboardResponse = async () => { + return act(async () => { + await addToDashboardResponse; + }); + }; - await openModal(); - - userEvent.click(screen.getByRole('button', { name: /save and go to dashboard/i })); - - await waitForSearchFolderResponse(); - - expect(locationService.push).toHaveBeenCalledWith(redirectURL); + beforeEach(() => { + jest.spyOn(api, 'setDashboardInLocalStorage').mockReturnValue(addToDashboardResponse); }); - it('Does NOT navigate to dashboard when clicking on "Save and keep exploring"', async () => { - locationService.push = jest.fn(); + afterEach(() => { + jest.restoreAllMocks(); + }); - setup(, [{ refId: 'A' }]); + it('Opens and closes the modal correctly', async () => { + setup(); await openModal(); - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); - - await waitForSearchFolderResponse(); + userEvent.click(screen.getByRole('button', { name: /cancel/i })); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); - expect(locationService.push).not.toHaveBeenCalled(); + describe('navigation', () => { + it('Navigates to dashboard when clicking on "Open"', async () => { + // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. + const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); + const pushSpy = jest.spyOn(locationService, 'push'); + + setup(); + + await openModal(); + + userEvent.click(screen.getByRole('button', { name: /open$/i })); + + await waitForAddToDashboardResponse(); + + expect(screen.queryByRole('dialog', { name: 'Add panel to dashboard' })).not.toBeInTheDocument(); + + expect(pushSpy).toHaveBeenCalled(); + expect(openSpy).not.toHaveBeenCalled(); + }); + + it('Navigates to dashboard in a new tab when clicking on "Open in a new tab"', async () => { + // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. + const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); + const pushSpy = jest.spyOn(locationService, 'push'); + + setup(); + + await openModal(); + + userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); + + await waitForAddToDashboardResponse(); + + expect(openSpy).toHaveBeenCalledWith(expect.anything(), '_blank'); + expect(pushSpy).not.toHaveBeenCalled(); + }); + }); + + describe('Save to new dashboard', () => { + describe('Navigate to correct dashboard when saving', () => { + it('Opens the new dashboard in a new tab', async () => { + // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. + const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); + + setup(); + + await openModal(); + + userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); + + await waitForAddToDashboardResponse(); + + expect(openSpy).toHaveBeenCalledWith('dashboard/new', '_blank'); + }); + + it('Navigates to the new dashboard', async () => { + const pushSpy = jest.spyOn(locationService, 'push'); + + setup(); + + await openModal(); + + userEvent.click(screen.getByRole('button', { name: /open$/i })); + + await waitForAddToDashboardResponse(); + + expect(screen.queryByRole('dialog', { name: 'Add panel to dashboard' })).not.toBeInTheDocument(); + + expect(pushSpy).toHaveBeenCalledWith('dashboard/new'); + }); + }); + }); + + describe('Save to existing dashboard', () => { + it('Renders the dashboard picker when switching to "Existing Dashboard"', async () => { + setup(); + + await openModal(); + + expect(screen.queryByRole('combobox', { name: /dashboard/ })).not.toBeInTheDocument(); + + userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); + expect(screen.getByRole('combobox', { name: /dashboard/ })).toBeInTheDocument(); + }); + + it('Does not submit if no dashboard is selected', async () => { + locationService.push = jest.fn(); + + setup(); + + await openModal(); + + userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); + + userEvent.click(screen.getByRole('button', { name: /open$/i })); + await waitForAddToDashboardResponse(); + + expect(locationService.push).not.toHaveBeenCalled(); + }); + + describe('Navigate to correct dashboard when saving', () => { + it('Opens the selected dashboard in a new tab', async () => { + // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. + const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); + + jest.spyOn(backendSrv, 'getDashboardByUid').mockResolvedValue({ + dashboard: { templating: { list: [] }, title: 'Dashboard Title', uid: 'someUid' }, + meta: {}, + }); + jest.spyOn(backendSrv, 'search').mockResolvedValue([ + { + id: 1, + uid: 'someUid', + isStarred: false, + items: [], + title: 'Dashboard Title', + tags: [], + type: DashboardSearchItemType.DashDB, + uri: 'someUri', + url: 'someUrl', + }, + ]); + + setup(); + + await openModal(); + + userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); + + userEvent.click(screen.getByRole('combobox', { name: /dashboard/i })); + + await waitFor(async () => { + await screen.findByLabelText('Select option'); + }); + userEvent.click(screen.getByLabelText('Select option')); + + userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); + + await waitFor(async () => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + expect(openSpy).toBeCalledWith('d/someUid', '_blank'); + }); + + it('Navigates to the selected dashboard', async () => { + const pushSpy = jest.spyOn(locationService, 'push'); + + jest.spyOn(backendSrv, 'getDashboardByUid').mockResolvedValue({ + dashboard: { templating: { list: [] }, title: 'Dashboard Title', uid: 'someUid' }, + meta: {}, + }); + jest.spyOn(backendSrv, 'search').mockResolvedValue([ + { + id: 1, + uid: 'someUid', + isStarred: false, + items: [], + title: 'Dashboard Title', + tags: [], + type: DashboardSearchItemType.DashDB, + uri: 'someUri', + url: 'someUrl', + }, + ]); + + setup(); + + await openModal(); + + userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); + + userEvent.click(screen.getByRole('combobox', { name: /dashboard/i })); + + await waitFor(async () => { + await screen.findByLabelText('Select option'); + }); + userEvent.click(screen.getByLabelText('Select option')); + + userEvent.click(screen.getByRole('button', { name: /open$/i })); + + await waitFor(async () => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + expect(pushSpy).toBeCalledWith('d/someUid'); + }); + }); }); }); - it('Correct datasource ref is useed', async () => { - setup(, [{ refId: 'A' }]); + describe('Error handling', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); - await openModal(); + it('Shows an error if opening a new tab fails', async () => { + jest.spyOn(global, 'open').mockReturnValue(null); + const removeDashboardSpy = jest.spyOn(initDashboard, 'removeDashboardToFetchFromLocalStorage'); - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); + setup(); - await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' })); + await openModal(); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - expect(addToDashboardMock).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - datasource: { - type: 'loki', - uid: 'someuid', + userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); + + await waitFor(async () => { + expect(await screen.findByRole('alert')).toBeInTheDocument(); + }); + + expect(removeDashboardSpy).toHaveBeenCalled(); + }); + + it('Shows an error if saving to localStorage fails', async () => { + jest.spyOn(initDashboard, 'setDashboardToFetchFromLocalStorage').mockImplementation(() => { + throw 'SOME ERROR'; + }); + + setup(); + + await openModal(); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + + userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); + + await waitFor(async () => { + expect(await screen.findByRole('alert')).toBeInTheDocument(); + }); + }); + + it('Shows an error if fetching dashboard fails', async () => { + jest.spyOn(backendSrv, 'getDashboardByUid').mockRejectedValue('SOME ERROR'); + jest.spyOn(backendSrv, 'search').mockResolvedValue([ + { + id: 1, + uid: 'someUid', + isStarred: false, + items: [], + title: 'Dashboard Title', + tags: [], + type: DashboardSearchItemType.DashDB, + uri: 'someUri', + url: 'someUrl', }, - }) - ); - }); + ]); - it('All queries are correctly passed through', async () => { - const queries: DataQuery[] = [{ refId: 'A' }, { refId: 'B', hide: true }]; - setup(, queries); + setup(); - await openModal(); + await openModal(); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); + userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); - await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' })); + userEvent.click(screen.getByRole('combobox', { name: /dashboard/i })); - expect(addToDashboardMock).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - queries: queries, - }) - ); - }); + await waitFor(async () => { + await screen.findByLabelText('Select option'); + }); + userEvent.click(screen.getByLabelText('Select option')); - it('Defaults to table if no response is available', async () => { - const queries: DataQuery[] = [{ refId: 'A' }]; - setup(, queries, createEmptyQueryResponse()); + userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); - await openModal(); - - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); - - await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' })); - - expect(addToDashboardMock).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - panel: 'table', - }) - ); - }); - - it('Defaults to table if no query is active', async () => { - const queries: DataQuery[] = [{ refId: 'A', hide: true }]; - setup(, queries); - - await openModal(); - - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); - - await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' })); - - expect(addToDashboardMock).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - panel: 'table', - }) - ); - }); - - it('Filters out hidden queries when selecting visualization', async () => { - const queries: DataQuery[] = [{ refId: 'A', hide: true }, { refId: 'B' }]; - setup(, queries, { - ...createEmptyQueryResponse(), - graphFrames: [new MutableDataFrame({ refId: 'B', fields: [] })], - logsFrames: [new MutableDataFrame({ refId: 'A', fields: [] })], + await waitFor(async () => { + expect(await screen.findByRole('alert')).toBeInTheDocument(); + }); }); - await openModal(); + it('Shows an error if an unknown error happens', async () => { + jest.spyOn(api, 'setDashboardInLocalStorage').mockRejectedValue('SOME ERROR'); - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); + setup(); - await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' })); + await openModal(); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - // Query A comes before B, but it's hidden. visualization will be picked according to frames generated by B - expect(addToDashboardMock).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - queries: queries, - panel: 'timeseries', - }) - ); - }); + userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); - it('Sets visualization to logs if there are log frames', async () => { - const queries: DataQuery[] = [{ refId: 'A' }]; - setup(, queries, { - ...createEmptyQueryResponse(), - logsFrames: [new MutableDataFrame({ refId: 'A', fields: [] })], + await waitFor(async () => { + expect(await screen.findByRole('alert')).toBeInTheDocument(); + }); }); - - await openModal(); - - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); - - await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' })); - - // Query A comes before B, but it's hidden. visualization will be picked according to frames generated by B - expect(addToDashboardMock).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - panel: 'logs', - }) - ); - }); - - it('Sets visualization to timeseries if there are graph frames', async () => { - const queries: DataQuery[] = [{ refId: 'A' }]; - setup(, queries, { - ...createEmptyQueryResponse(), - graphFrames: [new MutableDataFrame({ refId: 'A', fields: [] })], - }); - - await openModal(); - - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); - - await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' })); - - expect(addToDashboardMock).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - panel: 'timeseries', - }) - ); - }); - - it('Sets visualization to nodeGraph if there are node graph frames', async () => { - const queries: DataQuery[] = [{ refId: 'A' }]; - setup(, queries, { - ...createEmptyQueryResponse(), - nodeGraphFrames: [new MutableDataFrame({ refId: 'A', fields: [] })], - }); - - await openModal(); - - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); - - await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' })); - - expect(addToDashboardMock).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - panel: 'nodeGraph', - }) - ); - }); - - // trace view is not supported in dashboards, defaulting to table - it('Sets visualization to table if there are trace frames', async () => { - const queries: DataQuery[] = [{ refId: 'A' }]; - setup(, queries, { - ...createEmptyQueryResponse(), - traceFrames: [new MutableDataFrame({ refId: 'A', fields: [] })], - }); - - await openModal(); - - userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i })); - - await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' })); - - expect(addToDashboardMock).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - panel: 'table', - }) - ); }); }); diff --git a/public/app/features/explore/AddToDashboard/index.tsx b/public/app/features/explore/AddToDashboard/index.tsx index 6cc0f31303f..50d19c3e643 100644 --- a/public/app/features/explore/AddToDashboard/index.tsx +++ b/public/app/features/explore/AddToDashboard/index.tsx @@ -1,41 +1,9 @@ import React, { useState } from 'react'; -import { DataFrame, DataQuery } from '@grafana/data'; -import { ExploreId, StoreState } from 'app/types'; -import { useSelector, useDispatch } from 'react-redux'; -import { getExploreItemSelector } from '../state/selectors'; -import { addToDashboard, SaveToNewDashboardDTO } from './addToDashboard'; -import { locationService } from '@grafana/runtime'; -import { notifyApp } from 'app/core/actions'; -import { createSuccessNotification } from 'app/core/copy/appNotification'; +import { ExploreId } from 'app/types'; +import { AddToDashboardModal } from './AddToDashboardModal'; import { ToolbarButton } from '@grafana/ui'; -import { AddToDashboardModal, ErrorResponse } from './AddToDashboardModal'; - -const isVisible = (query: DataQuery) => !query.hide; -const hasRefId = (refId: DataFrame['refId']) => (frame: DataFrame) => frame.refId === refId; - -const getMainPanel = ( - queries: DataQuery[], - graphFrames?: DataFrame[], - logsFrames?: DataFrame[], - nodeGraphFrames?: DataFrame[] -) => { - for (const { refId } of queries.filter(isVisible)) { - // traceview is not supported in dashboards, skipping it for now. - const hasQueryRefId = hasRefId(refId); - if (graphFrames?.some(hasQueryRefId)) { - return 'timeseries'; - } - if (logsFrames?.some(hasQueryRefId)) { - return 'logs'; - } - if (nodeGraphFrames?.some(hasQueryRefId)) { - return 'nodeGraph'; - } - } - - // falling back to table - return 'table'; -}; +import { useSelector } from 'react-redux'; +import { getExploreItemSelector } from '../state/selectors'; interface Props { exploreId: ExploreId; @@ -43,41 +11,8 @@ interface Props { export const AddToDashboard = ({ exploreId }: Props) => { const [isOpen, setIsOpen] = useState(false); - const dispatch = useDispatch(); const selectExploreItem = getExploreItemSelector(exploreId); - - const { queries, panel, datasource } = useSelector((state: StoreState) => { - const exploreItem = selectExploreItem(state); - const queries = exploreItem?.queries || []; - const datasource = exploreItem?.datasourceInstance; - const { graphFrames, logsFrames, nodeGraphFrames } = exploreItem?.queryResponse || {}; - - return { - queries, - datasource: { type: datasource?.type, uid: datasource?.uid }, - panel: getMainPanel(queries, graphFrames, logsFrames, nodeGraphFrames), - }; - }); - - const handleSave = async (data: SaveToNewDashboardDTO, redirect: boolean): Promise => { - try { - const redirectURL = await addToDashboard(data, { - queries, - datasource, - panel, - }); - - if (redirect) { - locationService.push(redirectURL); - } else { - dispatch(notifyApp(createSuccessNotification(`Panel saved to ${data.dashboardName}`))); - setIsOpen(false); - } - return; - } catch (e) { - return { message: e.data?.message, status: e.data?.status ?? 'unknown-error' }; - } - }; + const explorePaneHasQueries = !!useSelector(selectExploreItem)?.queries?.length; return ( <> @@ -85,12 +20,12 @@ export const AddToDashboard = ({ exploreId }: Props) => { icon="apps" onClick={() => setIsOpen(true)} aria-label="Add to dashboard" - disabled={queries.length === 0} + disabled={!explorePaneHasQueries} > Add to dashboard - {isOpen && setIsOpen(false)} onSave={handleSave} />} + {isOpen && setIsOpen(false)} exploreId={exploreId} />} ); }; diff --git a/public/app/features/search/components/SearchCard.tsx b/public/app/features/search/components/SearchCard.tsx index 06bdb6c5072..12a2d3e55a8 100644 --- a/public/app/features/search/components/SearchCard.tsx +++ b/public/app/features/search/components/SearchCard.tsx @@ -24,7 +24,7 @@ export function getThumbnailURL(uid: string, isLight?: boolean) { export function SearchCard({ editable, item, onTagSelected, onToggleChecked }: Props) { const [hasImage, setHasImage] = useState(true); - const [lastUpdated, setLastUpdated] = useState(); + const [lastUpdated, setLastUpdated] = useState(null); const [showExpandedView, setShowExpandedView] = useState(false); const timeout = useRef(null); @@ -64,7 +64,11 @@ export function SearchCard({ editable, item, onTagSelected, onToggleChecked }: P if (item.uid && !lastUpdated) { const dashboard = await backendSrv.getDashboardByUid(item.uid); const { updated } = dashboard.meta; - setLastUpdated(new Date(updated).toLocaleString()); + if (updated) { + setLastUpdated(new Date(updated).toLocaleString()); + } else { + setLastUpdated(null); + } } }; diff --git a/public/app/features/search/components/SearchCardExpanded.tsx b/public/app/features/search/components/SearchCardExpanded.tsx index f83b7d807b6..350e2b54347 100644 --- a/public/app/features/search/components/SearchCardExpanded.tsx +++ b/public/app/features/search/components/SearchCardExpanded.tsx @@ -11,7 +11,7 @@ export interface Props { imageHeight: number; imageWidth: number; item: DashboardSectionItem; - lastUpdated?: string; + lastUpdated?: string | null; } export function SearchCardExpanded({ className, imageHeight, imageWidth, item, lastUpdated }: Props) { @@ -48,10 +48,12 @@ export function SearchCardExpanded({ className, imageHeight, imageWidth, item, l {folderTitle} -
-
Last updated
- {lastUpdated ?
{lastUpdated}
: } -
+ {lastUpdated !== null && ( +
+
Last updated
+ {lastUpdated ?
{lastUpdated}
: } +
+ )}
diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index aec415207be..614c6061663 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,6 +1,7 @@ import { DashboardAcl } from './acl'; import { DataQuery } from '@grafana/data'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; +import { VariableModel } from 'app/features/variables/types'; export interface DashboardDTO { redirectUri?: string; @@ -51,6 +52,11 @@ export interface AnnotationsPermissions { export interface DashboardDataDTO { title: string; + uid: string; + templating: { + list: VariableModel[]; + }; + panels?: any[]; } export enum DashboardRoutes { From 3b2ca399e27248ce0c681f9f507716fff340ec9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 12 Apr 2022 13:52:27 +0200 Subject: [PATCH 032/121] Chore: update latest.json (#47627) --- latest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest.json b/latest.json index f1c9e890e90..9a36ea00bc2 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "8.4.5", + "stable": "8.4.6", "testing": "8.5.0-beta1" } From 98cbecc4a5cc05bd1e51e66ddc0014251b0dd7fe Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 12 Apr 2022 13:52:55 +0200 Subject: [PATCH 033/121] Allow queries import when changing data source type (#47435) * Enable queries import when changing datasource * Supporting empty imports * Review applied --- .../features/query/components/QueryGroup.tsx | 5 +- .../query/state/updateQueries.test.ts | 348 +++++++++++++++--- .../app/features/query/state/updateQueries.ts | 39 +- 3 files changed, 326 insertions(+), 66 deletions(-) diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index 438d0b4c207..a28f71cc65d 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -111,7 +111,10 @@ export class QueryGroup extends PureComponent { onChangeDataSource = async (newSettings: DataSourceInstanceSettings) => { const { dsSettings } = this.state; - const queries = updateQueries(newSettings, this.state.queries, dsSettings); + const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined; + const nextDS = await getDataSourceSrv().get(newSettings.uid); + + const queries = await updateQueries(nextDS, this.state.queries, currentDS); const dataSource = await this.dataSourceSrv.get(newSettings.name); this.onChange({ diff --git a/public/app/features/query/state/updateQueries.test.ts b/public/app/features/query/state/updateQueries.test.ts index 13dc5b4ad56..f4f8a2de817 100644 --- a/public/app/features/query/state/updateQueries.test.ts +++ b/public/app/features/query/state/updateQueries.test.ts @@ -1,20 +1,54 @@ +import { + DataQuery, + DataSourceApi, + DataSourceWithQueryExportSupport, + DataSourceWithQueryImportSupport, +} from '@grafana/data'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { updateQueries } from './updateQueries'; +const oldUidDS = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, +} as DataSourceApi; + +const mixedDS = { + uid: 'mixed', + meta: { + id: 'mixed', + mixed: true, + }, +} as DataSourceApi; + +const newUidDS = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, +} as DataSourceApi; + +const newUidSameTypeDS = { + uid: 'new-uid-same-type', + type: 'old-type', + meta: { + id: 'old-type', + }, +} as DataSourceApi; + describe('updateQueries', () => { - it('Should update all queries except expression query when changing data source with same type', () => { - const updated = updateQueries( - { - uid: 'new-uid', - type: 'same-type', - meta: {}, - } as any, + it('Should update all queries except expression query when changing data source with same type', async () => { + const updated = await updateQueries( + newUidSameTypeDS, [ { refId: 'A', datasource: { uid: 'old-uid', - type: 'same-type', + type: 'old-type', }, }, { @@ -22,23 +56,16 @@ describe('updateQueries', () => { datasource: ExpressionDatasourceRef, }, ], - { - uid: 'old-uid', - type: 'same-type', - } as any + oldUidDS ); - expect(updated[0].datasource).toEqual({ type: 'same-type', uid: 'new-uid' }); + expect(updated[0].datasource).toEqual({ type: 'old-type', uid: 'new-uid-same-type' }); expect(updated[1].datasource).toEqual(ExpressionDatasourceRef); }); - it('Should clear queries when changing type', () => { - const updated = updateQueries( - { - uid: 'new-uid', - type: 'new-type', - meta: {}, - } as any, + it('Should clear queries when changing type', async () => { + const updated = await updateQueries( + newUidDS, [ { refId: 'A', @@ -55,25 +82,16 @@ describe('updateQueries', () => { }, }, ], - { - uid: 'old-uid', - type: 'old-type', - } as any + oldUidDS ); expect(updated.length).toEqual(1); expect(updated[0].datasource).toEqual({ type: 'new-type', uid: 'new-uid' }); }); - it('Should preserve query data source when changing to mixed', () => { - const updated = updateQueries( - { - uid: 'mixed', - type: 'mixed', - meta: { - mixed: true, - }, - } as any, + it('Should preserve query data source when changing to mixed', async () => { + const updated = await updateQueries( + mixedDS, [ { refId: 'A', @@ -90,25 +108,16 @@ describe('updateQueries', () => { }, }, ], - { - uid: 'old-uid', - type: 'old-type', - } as any + oldUidDS ); expect(updated[0].datasource).toEqual({ type: 'old-type', uid: 'old-uid' }); expect(updated[1].datasource).toEqual({ type: 'other-type', uid: 'other-uid' }); }); - it('should change nothing mixed updated to mixed', () => { - const updated = updateQueries( - { - uid: 'mixed', - type: 'mixed', - meta: { - mixed: true, - }, - } as any, + it('should change nothing mixed updated to mixed', async () => { + const updated = await updateQueries( + mixedDS, [ { refId: 'A', @@ -125,16 +134,249 @@ describe('updateQueries', () => { }, }, ], - { - uid: 'mixed', - type: 'mixed', - meta: { - mixed: true, - }, - } as any + mixedDS ); expect(updated[0].datasource).toEqual({ type: 'old-type', uid: 'old-uid' }); expect(updated[1].datasource).toEqual({ type: 'other-type', uid: 'other-uid' }); }); }); + +describe('updateQueries with import', () => { + describe('abstract queries support', () => { + it('should migrate abstract queries', async () => { + const exportSpy = jest.fn(); + const importSpy = jest.fn(); + + const newUidDSWithAbstract = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, + importFromAbstractQueries: (queries) => { + importSpy(queries); + const importedQueries = queries.map((q) => ({ ...q, imported: true })); + return Promise.resolve(importedQueries); + }, + } as DataSourceWithQueryImportSupport; + + const oldUidDSWithAbstract = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, + exportToAbstractQueries: (queries) => { + exportSpy(queries); + const exportedQueries = queries.map((q) => ({ ...q, exported: true })); + return Promise.resolve(exportedQueries); + }, + } as DataSourceWithQueryExportSupport; + + const queries = [ + { + refId: 'A', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + { + refId: 'B', + datasource: { + uid: 'other-uid', + type: 'other-type', + }, + }, + ]; + + const updated = await updateQueries(newUidDSWithAbstract as any, queries, oldUidDSWithAbstract as any); + + expect(exportSpy).toBeCalledWith(queries); + expect(importSpy).toBeCalledWith(queries.map((q) => ({ ...q, exported: true }))); + + expect(updated).toMatchInlineSnapshot(` + Array [ + Object { + "datasource": Object { + "type": "new-type", + "uid": "new-uid", + }, + "exported": true, + "imported": true, + "refId": "A", + }, + Object { + "datasource": Object { + "type": "new-type", + "uid": "new-uid", + }, + "exported": true, + "imported": true, + "refId": "B", + }, + ] + `); + }); + + it('should clear queries when no queries were imported', async () => { + const newUidDSWithAbstract = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, + importFromAbstractQueries: () => { + return Promise.resolve([]); + }, + } as DataSourceWithQueryImportSupport; + + const oldUidDSWithAbstract = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, + exportToAbstractQueries: (queries) => { + const exportedQueries = queries.map((q) => ({ ...q, exported: true })); + return Promise.resolve(exportedQueries); + }, + } as DataSourceWithQueryExportSupport; + + const queries = [ + { + refId: 'A', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + { + refId: 'B', + datasource: { + uid: 'other-uid', + type: 'other-type', + }, + }, + ]; + + const updated = await updateQueries(newUidDSWithAbstract as any, queries, oldUidDSWithAbstract as any); + + expect(updated.length).toEqual(1); + expect(updated[0].datasource).toEqual({ type: 'new-type', uid: 'new-uid' }); + }); + }); + + describe('importQueries support', () => { + it('should import queries when abstract queries are not supported by datasources', async () => { + const importSpy = jest.fn(); + + const newUidDSWithImport = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, + importQueries: (queries, origin) => { + importSpy(queries, origin); + const importedQueries = queries.map((q) => ({ ...q, imported: true })); + return Promise.resolve(importedQueries); + }, + } as DataSourceApi; + + const oldUidDS = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, + } as DataSourceApi; + + const queries = [ + { + refId: 'A', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + { + refId: 'B', + datasource: { + uid: 'other-uid', + type: 'other-type', + }, + }, + ]; + + const updated = await updateQueries(newUidDSWithImport, queries, oldUidDS); + + expect(importSpy).toBeCalledWith(queries, { uid: 'old-uid', type: 'old-type', meta: { id: 'old-type' } }); + + expect(updated).toMatchInlineSnapshot(` + Array [ + Object { + "datasource": Object { + "type": "new-type", + "uid": "new-uid", + }, + "imported": true, + "refId": "A", + }, + Object { + "datasource": Object { + "type": "new-type", + "uid": "new-uid", + }, + "imported": true, + "refId": "B", + }, + ] + `); + }); + + it('should clear queries when no queries were imported', async () => { + const newUidDSWithImport = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, + importQueries: (queries, origin) => { + return Promise.resolve([] as DataQuery[]); + }, + } as DataSourceApi; + + const oldUidDS = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, + } as DataSourceApi; + + const queries = [ + { + refId: 'A', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + { + refId: 'B', + datasource: { + uid: 'other-uid', + type: 'other-type', + }, + }, + ]; + + const updated = await updateQueries(newUidDSWithImport, queries, oldUidDS); + + expect(updated.length).toEqual(1); + expect(updated[0].datasource).toEqual({ type: 'new-type', uid: 'new-uid' }); + }); + }); +}); diff --git a/public/app/features/query/state/updateQueries.ts b/public/app/features/query/state/updateQueries.ts index d6998173599..3eb1a1b5aa7 100644 --- a/public/app/features/query/state/updateQueries.ts +++ b/public/app/features/query/state/updateQueries.ts @@ -1,27 +1,42 @@ -import { DataQuery, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data'; +import { DataQuery, DataSourceApi, hasQueryExportSupport, hasQueryImportSupport } from '@grafana/data'; import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend'; -export function updateQueries( - newSettings: DataSourceInstanceSettings, +export async function updateQueries( + nextDS: DataSourceApi, queries: DataQuery[], - dsSettings?: DataSourceInstanceSettings -): DataQuery[] { - const datasource = getDataSourceRef(newSettings); + currentDS?: DataSourceApi +): Promise { + let nextQueries = queries; + const datasource = { type: nextDS.type, uid: nextDS.uid }; // we are changing data source type - if (dsSettings?.type !== newSettings.type) { + if (currentDS?.meta.id !== nextDS.meta.id) { // If changing to mixed do nothing - if (newSettings.meta.mixed) { + if (nextDS.meta.mixed) { return queries; - } else { - // Changing to another datasource type clear queries + } + // when both data sources support abstract queries + else if (hasQueryExportSupport(currentDS) && hasQueryImportSupport(nextDS)) { + const abstractQueries = await currentDS.exportToAbstractQueries(queries); + nextQueries = await nextDS.importFromAbstractQueries(abstractQueries); + } + // when datasource supports query import + else if (currentDS && nextDS.importQueries) { + nextQueries = await nextDS.importQueries(queries, currentDS); + } + // Otherwise clear queries + else { return [{ refId: 'A', datasource }]; } } + if (nextQueries.length === 0) { + return [{ refId: 'A', datasource }]; + } + // Set data source on all queries except expression queries - return queries.map((query) => { - if (!isExpressionReference(query.datasource) && !newSettings.meta.mixed) { + return nextQueries.map((query) => { + if (!isExpressionReference(query.datasource) && !nextDS.meta.mixed) { query.datasource = datasource; } return query; From e979690011783c7264b6c9050b71671f987c16a6 Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Tue, 12 Apr 2022 14:15:16 +0200 Subject: [PATCH 034/121] Linter: fix linter for golangci-lint version v1.45.0 (#47623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update golangci-lint to v1.45.2 Version 1.45.0 added support for Go 1.18: https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md#v1450 Signed-off-by: Marcelo E. Magallon * Linter: fix some lints issue for golangci-lint version 1.45.2 Co-authored-by: Marcelo E. Magallon Co-authored-by: Gábor Farkas --- pkg/api/alerting.go | 4 ++-- pkg/services/contexthandler/contexthandler.go | 3 ++- pkg/services/searchV2/extract/dashboard_test.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 487d4ac9905..86a57e8e9a6 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -553,7 +553,7 @@ func (hs *HTTPServer) PauseAlert(c *models.ReqContext) response.Response { return response.Error(500, "", err) } - var resp models.AlertStateType = models.AlertStateUnknown + resp := models.AlertStateUnknown pausedState := "un-paused" if cmd.Paused { resp = models.AlertStatePaused @@ -579,7 +579,7 @@ func (hs *HTTPServer) PauseAllAlerts(c *models.ReqContext) response.Response { return response.Error(500, "Failed to pause alerts", err) } - var resp models.AlertStateType = models.AlertStatePending + resp := models.AlertStatePending pausedState := "un paused" if updateCmd.Paused { resp = models.AlertStatePaused diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 407b89fa326..de9752c1fca 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -29,7 +29,8 @@ import ( const ( InvalidUsernamePassword = "invalid username or password" - InvalidAPIKey = "invalid API key" + /* #nosec */ + InvalidAPIKey = "invalid API key" ) const ServiceName = "ContextHandler" diff --git a/pkg/services/searchV2/extract/dashboard_test.go b/pkg/services/searchV2/extract/dashboard_test.go index 34270bec827..0bc3a17090b 100644 --- a/pkg/services/searchV2/extract/dashboard_test.go +++ b/pkg/services/searchV2/extract/dashboard_test.go @@ -49,7 +49,7 @@ func TestReadDashboard(t *testing.T) { require.NoError(t, err) update := false - savedPath := "testdata/" + input + "-info.json" + savedPath := filepath.Join("testdata/", input+"-info.json") saved, err := os.ReadFile(savedPath) if err != nil { update = true From c449aadc1b15c04765baf499cbb045ea1e9024a8 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 12 Apr 2022 14:55:57 +0200 Subject: [PATCH 035/121] Prometheus: Fix executing query onBlur in Explore (#47561) --- public/app/core/config.ts | 2 +- .../components/PromQueryEditorByApp.test.tsx | 69 +++++++++++++++---- .../components/PromQueryCodeEditor.tsx | 11 ++- .../shared/QueryEditorModeToggle.tsx | 6 +- 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 3ff14355734..fcd399c9d30 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -18,5 +18,5 @@ export const updateConfig = (update: Partial) => { }; }; -// The `enable_alpha` flag is no exposed directly, this is equivolant +// The `enable_alpha` flag is not exposed directly, this is equivalent export const hasAlphaPanels = Boolean(config.panels?.debug?.state === PluginState.alpha); diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx index fef0655132a..ef78b652ea9 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx @@ -1,24 +1,41 @@ import React from 'react'; import { render, RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { PromQueryEditorByApp } from './PromQueryEditorByApp'; import { CoreApp } from '@grafana/data'; import { noop } from 'lodash'; import { PrometheusDatasource } from '../datasource'; import { testIds as alertingTestIds } from './PromQueryEditorForAlerting'; import { testIds as regularTestIds } from './PromQueryEditor'; -import { testIds as exploreTestIds } from './PromExploreQueryEditor'; // the monaco-based editor uses lazy-loading and that does not work // well with this test, and we do not need the monaco-related // functionality in this test anyway, so we mock it out. -jest.mock('./monaco-query-field/MonacoQueryFieldWrapper', () => { - const fakeQueryField = () =>
prometheus query field
; +jest.mock('./monaco-query-field/MonacoQueryFieldLazy', () => { + const fakeQueryField = (props: any) => { + return ; + }; return { - MonacoQueryFieldWrapper: fakeQueryField, + MonacoQueryFieldLazy: fakeQueryField, }; }); -function setup(app: CoreApp): RenderResult { +jest.mock('@grafana/runtime', () => { + const runtime = jest.requireActual('@grafana/runtime'); + return { + __esModule: true, + ...runtime, + config: { + ...runtime.config, + featureToggles: { + ...runtime.config.featureToggles, + promQueryBuilder: true, + }, + }, + }; +}); + +function setup(app: CoreApp): RenderResult & { onRunQuery: jest.Mock } { const dataSource = { createQuery: jest.fn((q) => q), getInitHints: () => [], @@ -30,16 +47,22 @@ function setup(app: CoreApp): RenderResult { metrics: [], }, } as unknown as PrometheusDatasource; + const onRunQuery = jest.fn(); - return render( + const renderOutput = render( ); + + return { + ...renderOutput, + onRunQuery, + }; } describe('PromQueryEditorByApp', () => { @@ -50,24 +73,44 @@ describe('PromQueryEditorByApp', () => { expect(queryByTestId(regularTestIds.editor)).toBeNull(); }); - it('should render regular query editor for unkown apps', () => { + it('should render editor selector for unkown apps', () => { const { getByTestId, queryByTestId } = setup(CoreApp.Unknown); - expect(getByTestId(regularTestIds.editor)).toBeInTheDocument(); + expect(getByTestId('QueryEditorModeToggle')).toBeInTheDocument(); expect(queryByTestId(alertingTestIds.editor)).toBeNull(); }); - it('should render regular query editor for explore', () => { + it('should render editor selector for explore', () => { const { getByTestId, queryByTestId } = setup(CoreApp.Explore); - expect(getByTestId(exploreTestIds.editor)).toBeInTheDocument(); + expect(getByTestId('QueryEditorModeToggle')).toBeInTheDocument(); expect(queryByTestId(alertingTestIds.editor)).toBeNull(); }); - it('should render regular query editor for dashboard', () => { + it('should render editor selector for dashboard', () => { const { getByTestId, queryByTestId } = setup(CoreApp.Dashboard); - expect(getByTestId(regularTestIds.editor)).toBeInTheDocument(); + expect(getByTestId('QueryEditorModeToggle')).toBeInTheDocument(); expect(queryByTestId(alertingTestIds.editor)).toBeNull(); }); + + it('should not run query onBlur in explore', () => { + const { getByTestId, onRunQuery } = setup(CoreApp.Explore); + + const input = getByTestId('dummy-code-input'); + expect(input).toBeInTheDocument(); + userEvent.type(input, 'metric'); + input.blur(); + expect(onRunQuery).not.toHaveBeenCalled(); + }); + + it('should run query onBlur in dashboard', () => { + const { getByTestId, onRunQuery } = setup(CoreApp.Dashboard); + + const input = getByTestId('dummy-code-input'); + expect(input).toBeInTheDocument(); + userEvent.type(input, 'metric'); + input.blur(); + expect(onRunQuery).toHaveBeenCalled(); + }); }); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx index 6d76ce7d793..7ab11a6c11e 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx @@ -6,7 +6,15 @@ import { useStyles2 } from '@grafana/ui'; import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -export function PromQueryCodeEditor({ query, datasource, range, onRunQuery, onChange, data }: PromQueryEditorProps) { +export function PromQueryCodeEditor({ + query, + datasource, + range, + onRunQuery, + onChange, + data, + app, +}: PromQueryEditorProps) { const styles = useStyles2(getStyles); return ( @@ -20,6 +28,7 @@ export function PromQueryCodeEditor({ query, datasource, range, onRunQuery, onCh history={[]} data={data} data-testid={testIds.editor} + app={app} />
); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle.tsx index ae44a01e4da..61a86aab19f 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle.tsx @@ -29,5 +29,9 @@ const editorModes = [ ]; export function QueryEditorModeToggle({ mode, onChange }: Props) { - return ; + return ( +
+ +
+ ); } From f10047b7085fd9ae76929d57b2f7c5547e0bf5a2 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Tue, 12 Apr 2022 14:12:03 +0100 Subject: [PATCH 036/121] Dashboard: Refactor panel cleanup (#47323) --- .betterer.results | 2 +- .../components/PanelEditor/PanelEditor.tsx | 1 - .../PanelEditor/state/actions.test.ts | 7 +++-- .../components/PanelEditor/state/actions.ts | 18 +++++++------ .../dashboard/dashgrid/DashboardGrid.test.tsx | 3 ++- .../dashboard/dashgrid/DashboardGrid.tsx | 17 ++++++++++-- .../dashboard/dashgrid/DashboardPanel.tsx | 13 +--------- public/app/features/dashboard/utils/panel.ts | 3 +++ public/app/features/panel/state/actions.ts | 26 ++++++++++++++++++- public/app/features/panel/state/reducers.ts | 13 +++++++--- 10 files changed, 71 insertions(+), 32 deletions(-) diff --git a/.betterer.results b/.betterer.results index 57cc3ad8dce..b57253c101f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -203,7 +203,7 @@ exports[`no enzyme tests`] = { "public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:809006195": [ [1, 35, 13, "RegExp match", "2409514259"] ], - "public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx:1798654441": [ + "public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx:3682134210": [ [1, 35, 13, "RegExp match", "2409514259"] ], "public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.test.tsx:3293585799": [ diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 9f61c323bf6..51d91021993 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -256,7 +256,6 @@ export class PanelEditorUnconnected extends PureComponent { lazy={false} width={panelSize.width} height={panelSize.height} - skipStateCleanUp={true} /> diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts index a41ece9b4de..1e864f160ac 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts @@ -1,7 +1,7 @@ import { thunkTester } from '../../../../../../test/core/thunk/thunkTester'; import { closeEditor, initialState, PanelEditorState } from './reducers'; import { exitPanelEditor, initPanelEditor, skipPanelUpdate } from './actions'; -import { cleanUpPanelState, panelModelAndPluginReady } from 'app/features/panel/state/reducers'; +import { panelModelAndPluginReady, removePanel } from 'app/features/panel/state/reducers'; import { DashboardModel, PanelModel } from '../../../state'; import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks'; @@ -48,6 +48,7 @@ describe('panelEditor actions', () => { }; const dispatchedActions = await thunkTester({ + panels: {}, panelEditor: state, dashboard: { getModel: () => dashboard, @@ -57,7 +58,7 @@ describe('panelEditor actions', () => { .whenThunkIsDispatched(); expect(dispatchedActions.length).toBe(2); - expect(dispatchedActions[0].type).toBe(cleanUpPanelState.type); + expect(dispatchedActions[0].type).toBe(removePanel.type); expect(dispatchedActions[1].type).toBe(closeEditor.type); expect(sourcePanel.getOptions()).toEqual({ prop: true }); expect(sourcePanel.id).toEqual(12); @@ -84,6 +85,7 @@ describe('panelEditor actions', () => { const dispatchedActions = await thunkTester({ panelEditor: state, + panels: {}, dashboard: { getModel: () => dashboard, }, @@ -119,6 +121,7 @@ describe('panelEditor actions', () => { const dispatchedActions = await thunkTester({ panelEditor: state, + panels: {}, dashboard: { getModel: () => dashboard, }, diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.ts index f604c29128f..7093dd47cdc 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.ts @@ -1,5 +1,8 @@ -import { DashboardModel, PanelModel } from '../../../state'; +import { pick } from 'lodash'; import { ThunkResult } from 'app/types'; +import store from 'app/core/store'; +import { panelModelAndPluginReady } from 'app/features/panel/state/reducers'; +import { cleanUpPanelState, initPanelState } from 'app/features/panel/state/actions'; import { closeEditor, PANEL_EDITOR_UI_STATE_STORAGE_KEY, @@ -8,10 +11,7 @@ import { setPanelEditorUIState, updateEditorInitState, } from './reducers'; -import { cleanUpPanelState, panelModelAndPluginReady } from 'app/features/panel/state/reducers'; -import store from 'app/core/store'; -import { pick } from 'lodash'; -import { initPanelState } from 'app/features/panel/state/actions'; +import { DashboardModel, PanelModel } from '../../../state'; export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult { return async (dispatch) => { @@ -63,9 +63,10 @@ export function updateDuplicateLibraryPanels( panel.configRev++; if (pluginChanged) { + const cleanUpKey = panel.key; panel.generateNewKey(); - dispatch(panelModelAndPluginReady({ key: panel.key, plugin: panel.plugin! })); + dispatch(panelModelAndPluginReady({ key: panel.key, plugin: panel.plugin!, cleanUpKey })); } // Resend last query result on source panel query runner @@ -125,9 +126,10 @@ export function exitPanelEditor(): ThunkResult { if (panelTypeChanged) { // Loaded plugin is not included in the persisted properties so is not handled by restoreModel sourcePanel.plugin = panel.plugin; + const cleanUpKey = sourcePanel.key; sourcePanel.generateNewKey(); - await dispatch(panelModelAndPluginReady({ key: sourcePanel.key, plugin: panel.plugin! })); + await dispatch(panelModelAndPluginReady({ key: sourcePanel.key, plugin: panel.plugin!, cleanUpKey })); } // Resend last query result on source panel query runner @@ -138,7 +140,7 @@ export function exitPanelEditor(): ThunkResult { }, 20); } - dispatch(cleanUpPanelState({ key: panel.key })); + dispatch(cleanUpPanelState(panel.key)); dispatch(closeEditor()); }; } diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx index 891a0f0a1f6..e12caba291c 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { DashboardGrid, Props } from './DashboardGrid'; +import { DashboardGridUnconnected as DashboardGrid, Props } from './DashboardGrid'; import { DashboardModel } from '../state'; jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => { @@ -67,6 +67,7 @@ function dashboardGridScenario(description: string, scenarioFn: (ctx: ScenarioCo editPanel: null, viewPanel: null, dashboard: getTestDashboard(), + cleanAndRemoveMany: jest.fn, }, setProps: (props: Partial) => { Object.assign(ctx.props, props); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 34a5eeb6926..29d6578dc9a 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -13,11 +13,13 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core import { DashboardPanel } from './DashboardPanel'; import { DashboardModel, PanelModel } from '../state'; import { Subscription } from 'rxjs'; +import { connect, ConnectedProps } from 'react-redux'; import { DashboardPanelsChangedEvent } from 'app/types/events'; +import { cleanAndRemoveMany } from 'app/features/panel/state/actions'; import { GridPos } from '../state/PanelModel'; import { config } from '@grafana/runtime'; -export interface Props { +export interface OwnProps { dashboard: DashboardModel; editPanel: PanelModel | null; viewPanel: PanelModel | null; @@ -27,7 +29,15 @@ export interface State { isLayoutInitialized: boolean; } -export class DashboardGrid extends PureComponent { +const mapDispatchToProps = { + cleanAndRemoveMany, +}; + +const connector = connect(null, mapDispatchToProps); + +export type Props = OwnProps & ConnectedProps; + +export class DashboardGridUnconnected extends PureComponent { private panelMap: { [key: string]: PanelModel } = {}; private eventSubs = new Subscription(); private windowHeight = 1200; @@ -51,6 +61,7 @@ export class DashboardGrid extends PureComponent { componentWillUnmount() { this.eventSubs.unsubscribe(); + this.props.cleanAndRemoveMany(Object.keys(this.panelMap)); } buildLayout() { @@ -315,3 +326,5 @@ function translateGridHeightToScreenHeight(gridHeight: number): number { } GrafanaGridItem.displayName = 'GridItemWithDimensions'; + +export const DashboardGrid = connector(DashboardGridUnconnected); diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 668ed6e8d42..0401ecf9397 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -5,7 +5,7 @@ import { PanelChromeAngular } from './PanelChromeAngular'; import { DashboardModel, PanelModel } from '../state'; import { StoreState } from 'app/types'; import { PanelPlugin } from '@grafana/data'; -import { cleanUpPanelState, setPanelInstanceState } from '../../panel/state/reducers'; +import { setPanelInstanceState } from '../../panel/state/reducers'; import { initPanelState } from '../../panel/state/actions'; import { LazyLoader } from './LazyLoader'; @@ -17,7 +17,6 @@ export interface OwnProps { isViewing: boolean; width: number; height: number; - skipStateCleanUp?: boolean; lazy?: boolean; } @@ -35,7 +34,6 @@ const mapStateToProps = (state: StoreState, props: OwnProps) => { const mapDispatchToProps = { initPanelState, - cleanUpPanelState, setPanelInstanceState, }; @@ -48,8 +46,6 @@ export class DashboardPanelUnconnected extends PureComponent { lazy: true, }; - specialPanels: { [key: string]: Function } = {}; - componentDidMount() { this.props.panel.isInView = !this.props.lazy; if (!this.props.plugin) { @@ -57,13 +53,6 @@ export class DashboardPanelUnconnected extends PureComponent { } } - componentWillUnmount() { - // Most of the time an unmount should result in cleanup but in PanelEdit it should not - if (!this.props.skipStateCleanUp) { - this.props.cleanUpPanelState({ key: this.props.stateKey }); - } - } - onInstanceStateChange = (value: any) => { this.props.setPanelInstanceState({ key: this.props.stateKey, value }); }; diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 2c68b00cd5b..d92497a43f0 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -1,5 +1,6 @@ // Store import store from 'app/core/store'; +import { dispatch } from 'app/store/store'; // Models import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; @@ -21,6 +22,7 @@ import { ShareModal } from 'app/features/dashboard/components/ShareModal'; import { ShowConfirmModalEvent, ShowModalReactEvent } from '../../../types/events'; import { AddLibraryPanelModal } from 'app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal'; import { UnlinkModal } from 'app/features/library-panels/components/UnlinkModal/UnlinkModal'; +import { cleanUpPanelState } from 'app/features/panel/state/actions'; export const removePanel = (dashboard: DashboardModel, panel: PanelModel, ask: boolean) => { // confirm deletion @@ -46,6 +48,7 @@ export const removePanel = (dashboard: DashboardModel, panel: PanelModel, ask: b } dashboard.removePanel(panel); + dispatch(cleanUpPanelState(panel.key)); }; export const duplicatePanel = (dashboard: DashboardModel, panel: PanelModel) => { diff --git a/public/app/features/panel/state/actions.ts b/public/app/features/panel/state/actions.ts index f3c9d51fa9e..0864c763632 100644 --- a/public/app/features/panel/state/actions.ts +++ b/public/app/features/panel/state/actions.ts @@ -2,7 +2,13 @@ import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPlugi import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { loadPanelPlugin } from 'app/features/plugins/admin/state/actions'; import { ThunkResult } from 'app/types'; -import { changePanelKey, panelModelAndPluginReady } from './reducers'; +import { + changePanelKey, + cleanUpAngularComponent, + panelModelAndPluginReady, + removePanel, + removePanels, +} from './reducers'; import { LibraryElementDTO } from 'app/features/library-panels/types'; import { toPanelModelLibraryPanel } from 'app/features/library-panels/utils'; import { PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events'; @@ -31,6 +37,24 @@ export function initPanelState(panel: PanelModel): ThunkResult { }; } +export function cleanUpPanelState(panelKey: string): ThunkResult { + return (dispatch, getStore) => { + const store = getStore().panels; + cleanUpAngularComponent(store[panelKey]); + dispatch(removePanel({ key: panelKey })); + }; +} + +export function cleanAndRemoveMany(panelKeys: string[]): ThunkResult { + return (dispatch, getStore) => { + const store = getStore().panels; + for (const key of panelKeys) { + cleanUpAngularComponent(store[key]); + } + dispatch(removePanels({ keys: panelKeys })); + }; +} + export interface ChangePanelPluginAndOptionsArgs { panel: PanelModel; pluginId: string; diff --git a/public/app/features/panel/state/reducers.ts b/public/app/features/panel/state/reducers.ts index 4fe2f8dce70..b809a223ba4 100644 --- a/public/app/features/panel/state/reducers.ts +++ b/public/app/features/panel/state/reducers.ts @@ -30,10 +30,14 @@ const panelsSlice = createSlice({ state[action.payload.newKey] = state[action.payload.oldKey]; delete state[action.payload.oldKey]; }, - cleanUpPanelState: (state, action: PayloadAction<{ key: string }>) => { - cleanUpAngularComponent(state[action.payload.key]); + removePanel: (state, action: PayloadAction<{ key: string }>) => { delete state[action.payload.key]; }, + removePanels: (state, action: PayloadAction<{ keys: string[] }>) => { + for (const key of action.payload.keys) { + delete state[key]; + } + }, setPanelInstanceState: (state, action: PayloadAction) => { state[action.payload.key].instanceState = action.payload.value; }, @@ -45,7 +49,7 @@ const panelsSlice = createSlice({ }, }); -function cleanUpAngularComponent(panelState?: Draft) { +export function cleanUpAngularComponent(panelState?: Draft) { if (panelState?.angularComponent) { panelState.angularComponent.destroy(); } @@ -72,8 +76,9 @@ export const { panelModelAndPluginReady, setPanelAngularComponent, setPanelInstanceState, - cleanUpPanelState, changePanelKey, + removePanel, + removePanels, } = panelsSlice.actions; export const panelsReducer = panelsSlice.reducer; From 85de0d88c7ffa5f6a0f317e763920ab5a04cd95d Mon Sep 17 00:00:00 2001 From: Maria Alexandra <239999+axelavargas@users.noreply.github.com> Date: Tue, 12 Apr 2022 14:51:40 +0100 Subject: [PATCH 037/121] Navigation: Show only + icons in overlay menu for new NavBar (#47347) * Nav: Show overlay icons based on allowed list * user essentials mob! :trident: * Navigation: clean up and use new backend prop to show plus icons and improve visual styling * Nav: Fix top padding * refactor to not use showIconInNavbar in NavBarMenuItem * remove a missed bit * refactor icon into const Co-authored-by: Ashley Harrison --- packages/grafana-data/src/types/navModel.ts | 1 + pkg/api/dtos/index.go | 33 ++++++++++--------- pkg/api/index.go | 6 ++-- .../components/NavBar/Next/NavBarItem.tsx | 3 +- .../components/NavBar/Next/NavBarMenuItem.tsx | 8 ++--- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts index a9cfee1a4e1..e706dde235f 100644 --- a/packages/grafana-data/src/types/navModel.ts +++ b/packages/grafana-data/src/types/navModel.ts @@ -30,6 +30,7 @@ export interface NavModelItem extends NavLinkDTO { highlightId?: string; tabSuffix?: ComponentType<{ className?: string }>; hideFromNavbar?: boolean; + showIconInNavbar?: boolean; } export enum NavSection { diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index 141a7b2f026..caba326a2a8 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -56,22 +56,23 @@ const ( ) type NavLink struct { - Id string `json:"id,omitempty"` - Text string `json:"text"` - Description string `json:"description,omitempty"` - Section string `json:"section,omitempty"` - SubTitle string `json:"subTitle,omitempty"` - Icon string `json:"icon,omitempty"` - Img string `json:"img,omitempty"` - Url string `json:"url,omitempty"` - Target string `json:"target,omitempty"` - SortWeight int64 `json:"sortWeight,omitempty"` - Divider bool `json:"divider,omitempty"` - HideFromMenu bool `json:"hideFromMenu,omitempty"` - HideFromTabs bool `json:"hideFromTabs,omitempty"` - Children []*NavLink `json:"children,omitempty"` - HighlightText string `json:"highlightText,omitempty"` - HighlightID string `json:"highlightId,omitempty"` + Id string `json:"id,omitempty"` + Text string `json:"text"` + Description string `json:"description,omitempty"` + Section string `json:"section,omitempty"` + SubTitle string `json:"subTitle,omitempty"` + Icon string `json:"icon,omitempty"` + Img string `json:"img,omitempty"` + Url string `json:"url,omitempty"` + Target string `json:"target,omitempty"` + SortWeight int64 `json:"sortWeight,omitempty"` + Divider bool `json:"divider,omitempty"` + HideFromMenu bool `json:"hideFromMenu,omitempty"` + HideFromTabs bool `json:"hideFromTabs,omitempty"` + ShowIconInNavbar bool `json:"showIconInNavbar,omitempty"` + Children []*NavLink `json:"children,omitempty"` + HighlightText string `json:"highlightText,omitempty"` + HighlightID string `json:"highlightId,omitempty"` } // NavIDCfg is the id for org configuration navigation node diff --git a/pkg/api/index.go b/pkg/api/index.go index d4e0bc8ce3d..3167a3012e0 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -460,17 +460,17 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true, }) dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{ - Text: "New dashboard", Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboard/new", HideFromTabs: true, Id: "new-dashboard", + Text: "New dashboard", Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboard/new", HideFromTabs: true, Id: "new-dashboard", ShowIconInNavbar: true, }) if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR { dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{ Text: "New folder", SubTitle: "Create a new folder to organize your dashboards", Id: "new-folder", - Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboards/folder/new", HideFromTabs: true, + Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboards/folder/new", HideFromTabs: true, ShowIconInNavbar: true, }) } dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{ Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "plus", - Url: hs.Cfg.AppSubURL + "/dashboard/import", HideFromTabs: true, + Url: hs.Cfg.AppSubURL + "/dashboard/import", HideFromTabs: true, ShowIconInNavbar: true, }) } return dashboardChildNavs diff --git a/public/app/core/components/NavBar/Next/NavBarItem.tsx b/public/app/core/components/NavBar/Next/NavBarItem.tsx index fb4731ce773..844686b6e00 100644 --- a/public/app/core/components/NavBar/Next/NavBarItem.tsx +++ b/public/app/core/components/NavBar/Next/NavBarItem.tsx @@ -102,12 +102,13 @@ const NavBarItem = ({ const translationKey = item.id && menuItemTranslations[item.id]; const itemText = translationKey ? i18n._(translationKey) : item.text; const isSection = item.menuItemType === NavMenuItemType.Section; + const icon = item.showIconInNavbar && !isSection ? (item.icon as IconName) : undefined; return ( + {icon && } {text} {target === '_blank' && ( @@ -76,7 +76,7 @@ export function NavBarMenuItem({ NavBarMenuItem.displayName = 'NavBarMenuItem'; -const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive']) => ({ +const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], hasIcon: boolean) => ({ linkContent: css({ display: 'grid', placeItems: 'center', @@ -97,7 +97,7 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive']) => ({ fontSize: 'inherit', height: '100%', overflowWrap: 'anywhere', - padding: '5px 12px 5px 10px', + padding: !hasIcon ? `${theme.spacing(0.5, 2)}` : '5px 12px 5px 10px', textAlign: 'left', '&:hover, &:focus-visible': { backgroundColor: theme.colors.action.hover, From 95009995e4f024ef5e3a21e5830e692b550bb9c9 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 12 Apr 2022 16:08:50 +0200 Subject: [PATCH 038/121] Prometheus: Move count_values from function to aggregation (#47260) * Move count_values from function to aggregation * Fix typos * Fix loki operations * Fix error that change the aggregation variant on blur * Fix loki ops --- .../loki/querybuilder/operations.ts | 28 ++- .../prometheus/querybuilder/aggregations.ts | 183 ++---------------- .../prometheus/querybuilder/operations.ts | 6 - .../shared/operationUtils.test.ts | 127 ++++++++++++ .../querybuilder/shared/operationUtils.ts | 176 +++++++++++++++++ 5 files changed, 340 insertions(+), 180 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.test.ts diff --git a/public/app/plugins/datasource/loki/querybuilder/operations.ts b/public/app/plugins/datasource/loki/querybuilder/operations.ts index d44f3e9ba7a..25c1cac13f2 100644 --- a/public/app/plugins/datasource/loki/querybuilder/operations.ts +++ b/public/app/plugins/datasource/loki/querybuilder/operations.ts @@ -1,5 +1,8 @@ -import { createAggregationOperation } from '../../prometheus/querybuilder/aggregations'; -import { getPromAndLokiOperationDisplayName } from '../../prometheus/querybuilder/shared/operationUtils'; +import { + createAggregationOperation, + createAggregationOperationWithParam, + getPromAndLokiOperationDisplayName, +} from '../../prometheus/querybuilder/shared/operationUtils'; import { QueryBuilderOperation, QueryBuilderOperationDef, @@ -16,8 +19,6 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { LokiOperationId.Min, LokiOperationId.Max, LokiOperationId.Avg, - LokiOperationId.TopK, - LokiOperationId.BottomK, LokiOperationId.Stddev, LokiOperationId.Stdvar, LokiOperationId.Count, @@ -28,6 +29,20 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { }) ); + const aggregationsWithParam = [LokiOperationId.TopK, LokiOperationId.BottomK].flatMap((opId) => { + return createAggregationOperationWithParam( + opId, + { + params: [{ name: 'K-value', type: 'number' }], + defaultParams: [5], + }, + { + addOperationHandler: addLokiOperation, + orderRank: LokiOperationOrder.Last, + } + ); + }); + const list: QueryBuilderOperationDef[] = [ createRangeOperation(LokiOperationId.Rate), createRangeOperation(LokiOperationId.CountOverTime), @@ -44,6 +59,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { createRangeOperation(LokiOperationId.StddevOverTime), createRangeOperation(LokiOperationId.QuantileOverTime), ...aggregations, + ...aggregationsWithParam, { id: LokiOperationId.Json, name: 'Json', @@ -145,7 +161,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { renderer: (model, def, innerExpr) => `${innerExpr} | line_format \`${model.params[0]}\``, addOperationHandler: addLokiOperation, explainHandler: () => - `This will replace log line using a specified template. The template can refer to stream labels and extracted labels. + `This will replace log line using a specified template. The template can refer to stream labels and extracted labels. Example: \`{{.status_code}} - {{.message}}\` @@ -166,7 +182,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { renderer: (model, def, innerExpr) => `${innerExpr} | label_format ${model.params[1]}=\`${model.params[0]}\``, addOperationHandler: addLokiOperation, explainHandler: () => - `This will change name of label to desired new label. In the example below, label "error_level" will be renamed to "level". + `This will change name of label to desired new label. In the example below, label "error_level" will be renamed to "level". Example: error_level=\`level\` diff --git a/public/app/plugins/datasource/prometheus/querybuilder/aggregations.ts b/public/app/plugins/datasource/prometheus/querybuilder/aggregations.ts index 6ecf67fadf8..ea3e2c4f837 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/aggregations.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/aggregations.ts @@ -1,18 +1,11 @@ -import pluralize from 'pluralize'; -import { LabelParamEditor } from './components/LabelParamEditor'; import { addOperationWithRangeVector } from './operations'; import { - defaultAddOperationHandler, - functionRendererLeft, + createAggregationOperation, + createAggregationOperationWithParam, getPromAndLokiOperationDisplayName, getRangeVectorParamDef, } from './shared/operationUtils'; -import { - QueryBuilderOperation, - QueryBuilderOperationDef, - QueryBuilderOperationParamDef, - QueryWithOperations, -} from './shared/types'; +import { QueryBuilderOperation, QueryBuilderOperationDef } from './shared/types'; import { PromVisualQueryOperationCategory, PromOperationId } from './types'; export function getAggregationOperations(): QueryBuilderOperationDef[] { @@ -22,8 +15,18 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] { ...createAggregationOperation(PromOperationId.Min), ...createAggregationOperation(PromOperationId.Max), ...createAggregationOperation(PromOperationId.Count), - ...createAggregationOperation(PromOperationId.TopK), - ...createAggregationOperation(PromOperationId.BottomK), + ...createAggregationOperationWithParam(PromOperationId.TopK, { + params: [{ name: 'K-value', type: 'number' }], + defaultParams: [5], + }), + ...createAggregationOperationWithParam(PromOperationId.BottomK, { + params: [{ name: 'K-value', type: 'number' }], + defaultParams: [5], + }), + ...createAggregationOperationWithParam(PromOperationId.CountValues, { + params: [{ name: 'Identifier', type: 'string' }], + defaultParams: ['count'], + }), createAggregationOverTime(PromOperationId.SumOverTime), createAggregationOverTime(PromOperationId.AvgOverTime), createAggregationOverTime(PromOperationId.MinOverTime), @@ -36,162 +39,6 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] { ]; } -/** - * This function is shared between Prometheus and Loki variants - */ -export function createAggregationOperation( - name: string, - overrides: Partial = {} -): QueryBuilderOperationDef[] { - const operations: QueryBuilderOperationDef[] = [ - { - id: name, - name: getPromAndLokiOperationDisplayName(name), - params: [ - { - name: 'By label', - type: 'string', - restParam: true, - optional: true, - }, - ], - defaultParams: [], - alternativesKey: 'plain aggregations', - category: PromVisualQueryOperationCategory.Aggregations, - renderer: functionRendererLeft, - paramChangedHandler: getOnLabelAdddedHandler(`__${name}_by`), - explainHandler: getAggregationExplainer(name, ''), - addOperationHandler: defaultAddOperationHandler, - ...overrides, - }, - { - id: `__${name}_by`, - name: `${getPromAndLokiOperationDisplayName(name)} by`, - params: [ - { - name: 'Label', - type: 'string', - restParam: true, - optional: true, - editor: LabelParamEditor, - }, - ], - defaultParams: [''], - alternativesKey: 'aggregations by', - category: PromVisualQueryOperationCategory.Aggregations, - renderer: getAggregationByRenderer(name), - paramChangedHandler: getLastLabelRemovedHandler(name), - explainHandler: getAggregationExplainer(name, 'by'), - addOperationHandler: defaultAddOperationHandler, - hideFromList: true, - ...overrides, - }, - { - id: `__${name}_without`, - name: `${getPromAndLokiOperationDisplayName(name)} without`, - params: [ - { - name: 'Label', - type: 'string', - restParam: true, - optional: true, - editor: LabelParamEditor, - }, - ], - defaultParams: [''], - alternativesKey: 'aggregations by', - category: PromVisualQueryOperationCategory.Aggregations, - renderer: getAggregationWithoutRenderer(name), - paramChangedHandler: getLastLabelRemovedHandler(name), - explainHandler: getAggregationExplainer(name, 'without'), - addOperationHandler: defaultAddOperationHandler, - hideFromList: true, - ...overrides, - }, - ]; - - // Handle some special aggregations that have parameters - if (name === 'topk' || name === 'bottomk') { - const param: QueryBuilderOperationParamDef = { - name: 'K-value', - type: 'number', - }; - operations[0].params.unshift(param); - operations[1].params.unshift(param); - operations[0].defaultParams = [5]; - operations[1].defaultParams = [5, '']; - operations[1].renderer = getAggregationByRendererWithParameter(name); - } - - return operations; -} - -function getAggregationByRenderer(aggregation: string) { - return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) { - return `${aggregation} by(${model.params.join(', ')}) (${innerExpr})`; - }; -} - -function getAggregationWithoutRenderer(aggregation: string) { - return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) { - return `${aggregation} without(${model.params.join(', ')}) (${innerExpr})`; - }; -} - -/** - * Very simple poc implementation, needs to be modified to support all aggregation operators - */ -function getAggregationExplainer(aggregationName: string, mode: 'by' | 'without' | '') { - return function aggregationExplainer(model: QueryBuilderOperation) { - const labels = model.params.map((label) => `\`${label}\``).join(' and '); - const labelWord = pluralize('label', model.params.length); - - switch (mode) { - case 'by': - return `Calculates ${aggregationName} over dimensions while preserving ${labelWord} ${labels}.`; - case 'without': - return `Calculates ${aggregationName} over the dimensions ${labels}. All other labels are preserved.`; - default: - return `Calculates ${aggregationName} over the dimensions.`; - } - }; -} - -function getAggregationByRendererWithParameter(aggregation: string) { - return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) { - const firstParam = model.params[0]; - const restParams = model.params.slice(1); - return `${aggregation} by(${restParams.join(', ')}) (${firstParam}, ${innerExpr})`; - }; -} - -/** - * This function will transform operations without labels to their plan aggregation operation - */ -function getLastLabelRemovedHandler(changeToOperartionId: string) { - return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) { - // If definition has more params then is defined there are no optional rest params anymore - // We then transform this operation into a different one - if (op.params.length < def.params.length) { - return { - ...op, - id: changeToOperartionId, - }; - } - - return op; - }; -} - -export function getOnLabelAdddedHandler(changeToOperartionId: string) { - return function onParamChanged(index: number, op: QueryBuilderOperation) { - return { - ...op, - id: changeToOperartionId, - }; - }; -} - function createAggregationOverTime(name: string): QueryBuilderOperationDef { return { id: name, diff --git a/public/app/plugins/datasource/prometheus/querybuilder/operations.ts b/public/app/plugins/datasource/prometheus/querybuilder/operations.ts index 85871adae54..77d207b38d5 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/operations.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/operations.ts @@ -155,12 +155,6 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { id: PromOperationId.Cosh, category: PromVisualQueryOperationCategory.Trigonometric, }), - createFunction({ - id: PromOperationId.CountValues, - params: [{ name: 'Identifier', type: 'string' }], - defaultParams: ['count'], - renderer: functionRendererLeft, - }), createFunction({ id: PromOperationId.DayOfMonth, category: PromVisualQueryOperationCategory.Time, diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.test.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.test.ts new file mode 100644 index 00000000000..3b6f532237e --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.test.ts @@ -0,0 +1,127 @@ +import { createAggregationOperation, createAggregationOperationWithParam } from './operationUtils'; + +describe('createAggregationOperation', () => { + it('returns correct aggregation definitions with overrides', () => { + expect(createAggregationOperation('test_aggregation', { category: 'test_category' })).toMatchObject([ + { + addOperationHandler: {}, + alternativesKey: 'plain aggregations', + category: 'test_category', + defaultParams: [], + explainHandler: {}, + id: 'test_aggregation', + name: 'Test aggregation', + paramChangedHandler: {}, + params: [ + { + name: 'By label', + optional: true, + restParam: true, + type: 'string', + }, + ], + renderer: {}, + }, + { + alternativesKey: 'aggregations by', + category: 'test_category', + defaultParams: [''], + explainHandler: {}, + hideFromList: true, + id: '__test_aggregation_by', + name: 'Test aggregation by', + paramChangedHandler: {}, + params: [ + { + editor: {}, + name: 'Label', + optional: true, + restParam: true, + type: 'string', + }, + ], + renderer: {}, + }, + { + alternativesKey: 'aggregations by', + category: 'test_category', + defaultParams: [''], + explainHandler: {}, + hideFromList: true, + id: '__test_aggregation_without', + name: 'Test aggregation without', + paramChangedHandler: {}, + params: [ + { + name: 'Label', + optional: true, + restParam: true, + type: 'string', + }, + ], + renderer: {}, + }, + ]); + }); +}); + +describe('createAggregationOperationWithParams', () => { + it('returns correct aggregation definitions with overrides and params', () => { + expect( + createAggregationOperationWithParam( + 'test_aggregation', + { + params: [{ name: 'K-value', type: 'number' }], + defaultParams: [5], + }, + { category: 'test_category' } + ) + ).toMatchObject([ + { + addOperationHandler: {}, + alternativesKey: 'plain aggregations', + category: 'test_category', + defaultParams: [5], + explainHandler: {}, + id: 'test_aggregation', + name: 'Test aggregation', + paramChangedHandler: {}, + params: [ + { name: 'K-value', type: 'number' }, + { name: 'By label', optional: true, restParam: true, type: 'string' }, + ], + renderer: {}, + }, + { + alternativesKey: 'aggregations by', + category: 'test_category', + defaultParams: [5, ''], + explainHandler: {}, + hideFromList: true, + id: '__test_aggregation_by', + name: 'Test aggregation by', + paramChangedHandler: {}, + params: [ + { name: 'K-value', type: 'number' }, + { editor: {}, name: 'Label', optional: true, restParam: true, type: 'string' }, + ], + renderer: {}, + }, + { + alternativesKey: 'aggregations by', + category: 'test_category', + defaultParams: [5, ''], + explainHandler: {}, + hideFromList: true, + id: '__test_aggregation_without', + name: 'Test aggregation without', + paramChangedHandler: {}, + params: [ + { name: 'K-value', type: 'number' }, + { name: 'Label', optional: true, restParam: true, type: 'string' }, + ], + renderer: {}, + }, + ]); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.ts index d49c2954be7..e685255f42a 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.ts @@ -3,9 +3,13 @@ import { QueryBuilderOperation, QueryBuilderOperationDef, QueryBuilderOperationParamDef, + QueryBuilderOperationParamValue, QueryWithOperations, } from './types'; import { SelectableValue } from '@grafana/data/src'; +import { LabelParamEditor } from '../components/LabelParamEditor'; +import { PromVisualQueryOperationCategory } from '../types'; +import pluralize from 'pluralize'; export function functionRendererLeft(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) { const params = renderParams(model, def, innerExpr); @@ -144,3 +148,175 @@ export function getRangeVectorParamDef(withRateInterval = false): QueryBuilderOp return param; } + +/** + * This function is shared between Prometheus and Loki variants + */ +export function createAggregationOperation( + name: string, + overrides: Partial = {} +): QueryBuilderOperationDef[] { + const operations: QueryBuilderOperationDef[] = [ + { + id: name, + name: getPromAndLokiOperationDisplayName(name), + params: [ + { + name: 'By label', + type: 'string', + restParam: true, + optional: true, + }, + ], + defaultParams: [], + alternativesKey: 'plain aggregations', + category: PromVisualQueryOperationCategory.Aggregations, + renderer: functionRendererLeft, + paramChangedHandler: getOnLabelAddedHandler(`__${name}_by`), + explainHandler: getAggregationExplainer(name, ''), + addOperationHandler: defaultAddOperationHandler, + ...overrides, + }, + { + id: `__${name}_by`, + name: `${getPromAndLokiOperationDisplayName(name)} by`, + params: [ + { + name: 'Label', + type: 'string', + restParam: true, + optional: true, + editor: LabelParamEditor, + }, + ], + defaultParams: [''], + alternativesKey: 'aggregations by', + category: PromVisualQueryOperationCategory.Aggregations, + renderer: getAggregationByRenderer(name), + paramChangedHandler: getLastLabelRemovedHandler(name), + explainHandler: getAggregationExplainer(name, 'by'), + addOperationHandler: defaultAddOperationHandler, + hideFromList: true, + ...overrides, + }, + { + id: `__${name}_without`, + name: `${getPromAndLokiOperationDisplayName(name)} without`, + params: [ + { + name: 'Label', + type: 'string', + restParam: true, + optional: true, + editor: LabelParamEditor, + }, + ], + defaultParams: [''], + alternativesKey: 'aggregations by', + category: PromVisualQueryOperationCategory.Aggregations, + renderer: getAggregationWithoutRenderer(name), + paramChangedHandler: getLastLabelRemovedHandler(name), + explainHandler: getAggregationExplainer(name, 'without'), + addOperationHandler: defaultAddOperationHandler, + hideFromList: true, + ...overrides, + }, + ]; + + return operations; +} + +export function createAggregationOperationWithParam( + name: string, + paramsDef: { params: QueryBuilderOperationParamDef[]; defaultParams: QueryBuilderOperationParamValue[] }, + overrides: Partial = {} +): QueryBuilderOperationDef[] { + const operations = createAggregationOperation(name, overrides); + operations[0].params.unshift(...paramsDef.params); + operations[1].params.unshift(...paramsDef.params); + operations[2].params.unshift(...paramsDef.params); + operations[0].defaultParams = paramsDef.defaultParams; + operations[1].defaultParams = [...paramsDef.defaultParams, '']; + operations[2].defaultParams = [...paramsDef.defaultParams, '']; + operations[1].renderer = getAggregationByRendererWithParameter(name); + operations[2].renderer = getAggregationByRendererWithParameter(name); + return operations; +} + +function getAggregationByRenderer(aggregation: string) { + return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) { + return `${aggregation} by(${model.params.join(', ')}) (${innerExpr})`; + }; +} + +function getAggregationWithoutRenderer(aggregation: string) { + return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) { + return `${aggregation} without(${model.params.join(', ')}) (${innerExpr})`; + }; +} + +/** + * Very simple poc implementation, needs to be modified to support all aggregation operators + */ +function getAggregationExplainer(aggregationName: string, mode: 'by' | 'without' | '') { + return function aggregationExplainer(model: QueryBuilderOperation) { + const labels = model.params.map((label) => `\`${label}\``).join(' and '); + const labelWord = pluralize('label', model.params.length); + + switch (mode) { + case 'by': + return `Calculates ${aggregationName} over dimensions while preserving ${labelWord} ${labels}.`; + case 'without': + return `Calculates ${aggregationName} over the dimensions ${labels}. All other labels are preserved.`; + default: + return `Calculates ${aggregationName} over the dimensions.`; + } + }; +} + +function getAggregationByRendererWithParameter(aggregation: string) { + return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) { + function mapType(p: QueryBuilderOperationParamValue) { + if (typeof p === 'string') { + return `\"${p}\"`; + } + return p; + } + const params = model.params.slice(0, -1); + const restParams = model.params.slice(1); + return `${aggregation} by(${restParams.join(', ')}) (${params.map(mapType).join(', ')}, ${innerExpr})`; + }; +} + +/** + * This function will transform operations without labels to their plan aggregation operation + */ +function getLastLabelRemovedHandler(changeToOperationId: string) { + return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) { + // If definition has more params then is defined there are no optional rest params anymore. + // We then transform this operation into a different one + if (op.params.length < def.params.length) { + return { + ...op, + id: changeToOperationId, + }; + } + + return op; + }; +} + +function getOnLabelAddedHandler(changeToOperationId: string) { + return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) { + // Check if we actually have the label param. As it's optional the aggregation can have one less, which is the + // case of just simple aggregation without label. When user adds the label it now has the same number of params + // as it's definition, and now we can change it to it's `_by` variant. + if (op.params.length === def.params.length) { + return { + ...op, + id: changeToOperationId, + }; + } + return op; + }; +} From 6592ac537a94915d4411d58ea76d895ee5734014 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Tue, 12 Apr 2022 07:20:32 -0700 Subject: [PATCH 039/121] AzureMonitor: Skip App Insights check for v9 (#47624) --- .../datasource/grafana-azure-monitor-datasource/datasource.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index d91ed228338..42997fb09f8 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -17,6 +17,7 @@ import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource'; import AzureResourceGraphDatasource from './azure_resource_graph/azure_resource_graph_datasource'; import AppInsightsDatasource from './components/deprecated/app_insights/app_insights_datasource'; import InsightsAnalyticsDatasource from './components/deprecated/insights_analytics/insights_analytics_datasource'; +import { gtGrafana9 } from './components/deprecated/utils'; import { getAzureCloud } from './credentials'; import ResourcePickerData from './resourcePicker/resourcePickerData'; import { @@ -174,7 +175,7 @@ export default class Datasource extends DataSourceApi Date: Tue, 12 Apr 2022 16:22:28 +0200 Subject: [PATCH 040/121] Change to pointer receiver in mock call (#47575) --- pkg/tsdb/cloudwatch/metric_find_query_test.go | 6 +++--- pkg/tsdb/cloudwatch/time_series_query_test.go | 2 +- pkg/tsdb/cloudwatch/utils_test.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/tsdb/cloudwatch/metric_find_query_test.go b/pkg/tsdb/cloudwatch/metric_find_query_test.go index 223a0869c8d..520d52d60a7 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query_test.go +++ b/pkg/tsdb/cloudwatch/metric_find_query_test.go @@ -32,7 +32,7 @@ func TestQuery_Metrics(t *testing.T) { var cwClient fakeCWClient NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { - return cwClient + return &cwClient } t.Run("Custom metrics", func(t *testing.T) { @@ -403,7 +403,7 @@ func TestQuery_GetDimensionKeys(t *testing.T) { var client fakeCWClient NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { - return client + return &client } metrics := []*cloudwatch.Metric{ @@ -527,7 +527,7 @@ func TestQuery_ListMetricsPagination(t *testing.T) { var client fakeCWClient NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { - return client + return &client } metrics := []*cloudwatch.Metric{ diff --git a/pkg/tsdb/cloudwatch/time_series_query_test.go b/pkg/tsdb/cloudwatch/time_series_query_test.go index 7c55c4d2459..72c12e6dc17 100644 --- a/pkg/tsdb/cloudwatch/time_series_query_test.go +++ b/pkg/tsdb/cloudwatch/time_series_query_test.go @@ -30,7 +30,7 @@ func TestTimeSeriesQuery(t *testing.T) { var cwClient fakeCWClient NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { - return cwClient + return &cwClient } t.Run("Custom metrics", func(t *testing.T) { diff --git a/pkg/tsdb/cloudwatch/utils_test.go b/pkg/tsdb/cloudwatch/utils_test.go index e6bf1156535..9f1213ed435 100644 --- a/pkg/tsdb/cloudwatch/utils_test.go +++ b/pkg/tsdb/cloudwatch/utils_test.go @@ -67,11 +67,11 @@ type fakeCWClient struct { MetricsPerPage int } -func (c fakeCWClient) GetMetricDataWithContext(aws.Context, *cloudwatch.GetMetricDataInput, ...request.Option) (*cloudwatch.GetMetricDataOutput, error) { +func (c *fakeCWClient) GetMetricDataWithContext(aws.Context, *cloudwatch.GetMetricDataInput, ...request.Option) (*cloudwatch.GetMetricDataOutput, error) { return &c.GetMetricDataOutput, nil } -func (c fakeCWClient) ListMetricsPages(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error { +func (c *fakeCWClient) ListMetricsPages(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error { if c.MetricsPerPage == 0 { c.MetricsPerPage = 1000 } From f80a0d2a9b0ac25649246711309954f560045f58 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 12 Apr 2022 07:24:46 -0700 Subject: [PATCH 041/121] PanelQueryRunner: use refId from results if the `key` value was not set in the packet (#47598) --- .../features/query/state/runRequest.test.ts | 36 ++++++++++++++++++- public/app/features/query/state/runRequest.ts | 4 ++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/public/app/features/query/state/runRequest.test.ts b/public/app/features/query/state/runRequest.test.ts index 6742d37d0bf..8a40ef7fd08 100644 --- a/public/app/features/query/state/runRequest.test.ts +++ b/public/app/features/query/state/runRequest.test.ts @@ -156,7 +156,7 @@ describe('runRequest', () => { }); }); - runRequestScenario('After tree responses, 2 with different keys', (ctx) => { + runRequestScenario('After three responses, 2 with different keys', (ctx) => { ctx.setup(() => { ctx.start(); ctx.emitPacket({ @@ -186,6 +186,40 @@ describe('runRequest', () => { }); }); + runRequestScenario('When the key is defined in refId', (ctx) => { + ctx.setup(() => { + ctx.start(); + ctx.emitPacket({ + data: [{ name: 'DataX-1', refId: 'X' } as DataFrame], + }); + ctx.emitPacket({ + data: [{ name: 'DataY-1', refId: 'Y' } as DataFrame], + }); + ctx.emitPacket({ + data: [{ name: 'DataY-2', refId: 'Y' } as DataFrame], + }); + }); + + it('should emit 3 separate results', () => { + expect(ctx.results.length).toBe(3); + }); + + it('should keep data for X and Y', () => { + expect(ctx.results[2].series).toMatchInlineSnapshot(` + Array [ + Object { + "name": "DataX-1", + "refId": "X", + }, + Object { + "name": "DataY-2", + "refId": "Y", + }, + ] + `); + }); + }); + runRequestScenario('After response with state Streaming', (ctx) => { ctx.setup(() => { ctx.start(); diff --git a/public/app/features/query/state/runRequest.ts b/public/app/features/query/state/runRequest.ts index 370df008e9b..76a7442ab3f 100644 --- a/public/app/features/query/state/runRequest.ts +++ b/public/app/features/query/state/runRequest.ts @@ -43,7 +43,9 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ ...state.packets, }; - packets[packet.key || 'A'] = packet; + // updates to the same key will replace previous values + const key = packet.key ?? packet.data?.[0]?.refId ?? 'A'; + packets[key] = packet; let loadingState = packet.state || LoadingState.Done; let error: DataQueryError | undefined = undefined; From 3e030d137fa4bde3333d761736c5fd9a9c34c61e Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Tue, 12 Apr 2022 17:36:49 +0300 Subject: [PATCH 042/121] CI: Remove unused downstream build pipelines (#47646) * Remove unused downstream build pipelines * Prettify --- scripts/drone/pipelines/main.star | 91 ++++++++++--------------------- 1 file changed, 29 insertions(+), 62 deletions(-) diff --git a/scripts/drone/pipelines/main.star b/scripts/drone/pipelines/main.star index 91562878bab..fb3f671cdcb 100644 --- a/scripts/drone/pipelines/main.star +++ b/scripts/drone/pipelines/main.star @@ -216,67 +216,34 @@ def main_pipelines(edition): services.append(ldap_service()) integration_test_steps.append(benchmark_ldap_step()) - pipelines = [ - docs_pipelines(edition, ver_mode, trigger), - pipeline( - name='main-test', edition=edition, trigger=trigger, services=[], - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + test_steps, - volumes=[], - ), - pipeline( - name='main-build-e2e-publish', edition=edition, trigger=trigger, services=[], - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + build_steps, - volumes=volumes, - ), - pipeline( - name='main-integration-tests', edition=edition, trigger=trigger, services=services, - steps=[download_grabpl_step()] + integration_test_steps, - volumes=volumes, - ), - pipeline( - name='windows-main', edition=edition, trigger=dict(trigger, repo = ['grafana/grafana']), - steps=initialize_step(edition, platform='windows', ver_mode=ver_mode) + windows_steps, - depends_on=['main-test', 'main-build-e2e-publish', 'main-integration-tests'], platform='windows', - ), notify_pipeline( - name='notify-drone-changes', slack_channel='slack-webhooks-test', trigger=drone_change_trigger, template=drone_change_template, secret='drone-changes-webhook', - ), - ] - if edition != 'enterprise': - pipelines.append(pipeline( - name='publish-main', edition=edition, trigger=dict(trigger, repo = ['grafana/grafana']), - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode, install_deps=False) + store_steps, - depends_on=['main-test', 'main-build-e2e-publish', 'main-integration-tests', 'windows-main',], - )) - - pipelines.append(notify_pipeline( - name='notify-main', slack_channel='grafana-ci-notifications', trigger=dict(trigger, status = ['failure']), - depends_on=['main-test', 'main-build-e2e-publish', 'main-integration-tests', 'windows-main', 'publish-main'], template=failure_template, secret='slack_webhook' - )) - else: - # Add downstream enterprise pipelines triggerable from OSS builds - trigger = { - 'event': ['custom',], - } - test_steps, build_steps, integration_test_steps, windows_steps, store_steps = get_steps(edition=edition, is_downstream=True) - pipelines.append(pipeline( - name='build-main-downstream', edition=edition, trigger=trigger, services=services, - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode, is_downstream=True) + test_steps + build_steps + integration_test_steps, - volumes=volumes, - )) - pipelines.append(pipeline( - name='windows-main-downstream', edition=edition, trigger=trigger, - steps=[download_grabpl_step()] + initialize_step(edition, platform='windows', ver_mode=ver_mode, is_downstream=True) + windows_steps, - platform='windows', depends_on=['build-main-downstream'], - )) - pipelines.append(pipeline( - name='publish-main-downstream', edition=edition, trigger=trigger, - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode, is_downstream=True, install_deps=False) + store_steps, - depends_on=['build-main-downstream', 'windows-main-downstream'], - )) - - pipelines.append(notify_pipeline( - name='notify-main-downstream', slack_channel='grafana-enterprise-ci-notifications', trigger=dict(trigger, status = ['failure']), - depends_on=['build-main-downstream', 'windows-main-downstream', 'publish-main-downstream'], template=failure_template, secret='slack_webhook', - )) + pipelines = [docs_pipelines(edition, ver_mode, trigger), pipeline( + name='main-test', edition=edition, trigger=trigger, services=[], + steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + test_steps, + volumes=[], + ), pipeline( + name='main-build-e2e-publish', edition=edition, trigger=trigger, services=[], + steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + build_steps, + volumes=volumes, + ), pipeline( + name='main-integration-tests', edition=edition, trigger=trigger, services=services, + steps=[download_grabpl_step()] + integration_test_steps, + volumes=volumes, + ), pipeline( + name='windows-main', edition=edition, trigger=dict(trigger, repo=['grafana/grafana']), + steps=initialize_step(edition, platform='windows', ver_mode=ver_mode) + windows_steps, + depends_on=['main-test', 'main-build-e2e-publish', 'main-integration-tests'], platform='windows', + ), notify_pipeline( + name='notify-drone-changes', slack_channel='slack-webhooks-test', trigger=drone_change_trigger, + template=drone_change_template, secret='drone-changes-webhook', + ), pipeline( + name='publish-main', edition=edition, trigger=dict(trigger, repo=['grafana/grafana']), + steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode, + install_deps=False) + store_steps, + depends_on=['main-test', 'main-build-e2e-publish', 'main-integration-tests', 'windows-main', ], + ), notify_pipeline( + name='notify-main', slack_channel='grafana-ci-notifications', trigger=dict(trigger, status=['failure']), + depends_on=['main-test', 'main-build-e2e-publish', 'main-integration-tests', 'windows-main', 'publish-main'], + template=failure_template, secret='slack_webhook' + )] return pipelines From 35f52aee55bbde72fa56bff4e463974725b1fdb2 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Tue, 12 Apr 2022 07:42:33 -0700 Subject: [PATCH 043/121] AzureMonitor: Fix Kusto editor schema (#47576) --- package.json | 2 +- .../azure_log_analytics/response_parser.ts | 23 ++++++++++++++++++- yarn.lock | 12 +++++----- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 37f76582ca8..de012ddeaa7 100644 --- a/package.json +++ b/package.json @@ -257,7 +257,7 @@ "@grafana/slate-react": "0.22.10-grafana", "@grafana/ui": "workspace:*", "@jaegertracing/jaeger-ui-components": "workspace:*", - "@kusto/monaco-kusto": "4.1.6", + "@kusto/monaco-kusto": "5.0.0", "@lezer/common": "0.15.12", "@lezer/lr": "0.15.8", "@lingui/core": "3.13.2", diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts index cdf82360a01..412eb8f1e7a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts @@ -1,5 +1,6 @@ -import { concat, find, flattenDeep, forEach, get, map } from 'lodash'; import { AnnotationEvent, dateTime, TimeSeries } from '@grafana/data'; +import { concat, find, flattenDeep, forEach, get, map } from 'lodash'; + import { AzureLogsTableData, AzureLogsVariable } from '../types'; import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata'; @@ -221,6 +222,26 @@ export function transformMetadataToKustoSchema(sourceSchema: AzureLogAnalyticsMe minorVersion: 0, }; + // TODO: We should define macros here as functions so they are interpreted as valid + // But we cannot do so for the issues listed here: https://github.com/Azure/monaco-kusto/issues/189 + // For example: + // database.functions.push( + // { + // name: '$__timeFilter', + // body: '', + // inputParameters: [ + // { + // name: 'timeColumn', + // type: 'System.String', + // cslType: 'string', + // cslDefaultValue: 'Timestamp', + // }, + // ], + // docString: 'Filter by a time column', + // functionKind: 'Unknown', + // }, + // ); + return { clusterType: 'Engine', cluster: { diff --git a/yarn.lock b/yarn.lock index 568fe87e119..edf380093a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5180,15 +5180,15 @@ __metadata: languageName: node linkType: hard -"@kusto/monaco-kusto@npm:4.1.6": - version: 4.1.6 - resolution: "@kusto/monaco-kusto@npm:4.1.6" +"@kusto/monaco-kusto@npm:5.0.0": + version: 5.0.0 + resolution: "@kusto/monaco-kusto@npm:5.0.0" dependencies: "@kusto/language-service": 2.0.0-beta.0 "@kusto/language-service-next": 0.0.54 peerDependencies: - monaco-editor: ^0.24.0 - checksum: c01440cc8a8511e3afdf6c63e99e23cac976237f423d083384e3ee0605935fba5b69cc20627d45bc56bac82ce42ec6f613282d24b12b3dc0a3456ab5ff631238 + monaco-editor: 0.33.0 + checksum: 961bcae8a84562ad0d1aa199bdacbe5adfffafb01e98a2cf927883331d45ad8fcdf7385f9bde00eed16f74a33e0dea41f9eb80127e2e5a341ea3b8a2aa9fb2d8 languageName: node linkType: hard @@ -20427,7 +20427,7 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@grafana/ui": "workspace:*" "@jaegertracing/jaeger-ui-components": "workspace:*" - "@kusto/monaco-kusto": 4.1.6 + "@kusto/monaco-kusto": 5.0.0 "@lezer/common": 0.15.12 "@lezer/lr": 0.15.8 "@lingui/cli": 3.13.2 From 884e06ec45c95e4b803763b8547c01bb5b370235 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Tue, 12 Apr 2022 10:46:22 -0400 Subject: [PATCH 044/121] chore: go mod tidy (#47648) --- go.mod | 2 +- go.sum | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3e34f3f6461..ee9a6a8221e 100644 --- a/go.mod +++ b/go.mod @@ -104,7 +104,7 @@ require ( go.opentelemetry.io/otel/trace v1.2.0 golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 golang.org/x/exp v0.0.0-20210220032938-85be41e4509f - golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 + golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 diff --git a/go.sum b/go.sum index 17e671ba1c9..45e79610803 100644 --- a/go.sum +++ b/go.sum @@ -868,7 +868,6 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= -github.com/getkin/kin-openapi v0.91.0 h1:mOSAljTAQONM0YVtI3+LvIQaa0zPwa3SH6UuiyEnbYQ= github.com/getkin/kin-openapi v0.91.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getkin/kin-openapi v0.94.0 h1:bAxg2vxgnHHHoeefVdmGbR+oxtJlcv5HsJJa3qmAHuo= github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= @@ -1368,7 +1367,6 @@ github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.m github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= github.com/grafana/grafana-plugin-sdk-go v0.125.0/go.mod h1:9YiJ5GUxIsIEUC0qR9+BJVP5M7mCSP6uc6Ne62YKkgc= -github.com/grafana/grafana-plugin-sdk-go v0.129.0 h1:apmA8x59QvW5Wov+FhAfM0IiNGjMi8V2Ou7xyMk1leE= github.com/grafana/grafana-plugin-sdk-go v0.129.0/go.mod h1:4edtosZepfQF9jkQwRywJsNSJzXTHmzbmcVcAl8MEQc= github.com/grafana/grafana-plugin-sdk-go v0.131.0 h1:8M+Qfch4WNi3PPpRhWtmcLFTCq8zlIjnxrc8iRigAY0= github.com/grafana/grafana-plugin-sdk-go v0.131.0/go.mod h1:jmrxelOJKrIK0yrsIzcotS8pbqPZozbmJgGy7k3hK1k= From 7f5e8bb00febed194262f0c3dff493090751222e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Tue, 12 Apr 2022 16:48:37 +0200 Subject: [PATCH 045/121] Temporarily skip a test that is intermittently timing out and causing build failures. (#47637) --- public/app/features/explore/spec/queryHistory.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index 87e9f8a2bd4..976fbb7bcd8 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -69,7 +69,10 @@ describe('Explore: Query History', () => { await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}']); }); - it('updates the state in both Explore panes', async () => { + /** + * TODO: #47635 check why this test times out + */ + it.skip('updates the state in both Explore panes', async () => { const urlParams = { left: serializeStateToUrlParam({ datasource: 'loki', From 712b239d5ae685db7e8f9d01d5c8aecca611f0b3 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Tue, 12 Apr 2022 16:58:09 +0200 Subject: [PATCH 046/121] Storage: remove pointer on write path (#47580) * remove pointer to contents in UpsertFileCommand * add command to UpsertFileCommand * fix client after API refactor --- pkg/infra/filestorage/api.go | 9 +- pkg/infra/filestorage/cdk_blob_filestorage.go | 4 +- pkg/infra/filestorage/db_filestorage.go | 4 +- pkg/infra/filestorage/fs_integration_test.go | 84 +++++++++---------- pkg/services/store/storage_disk.go | 2 +- 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/pkg/infra/filestorage/api.go b/pkg/infra/filestorage/api.go index 3a4a5c285ba..93515f2c30e 100644 --- a/pkg/infra/filestorage/api.go +++ b/pkg/infra/filestorage/api.go @@ -51,9 +51,12 @@ type Paging struct { } type UpsertFileCommand struct { - Path string - MimeType string - Contents *[]byte + Path string + MimeType string + + // Contents of an existing file won't be modified if cmd.Contents is nil + Contents []byte + // Properties of an existing file won't be modified if cmd.Properties is nil Properties map[string]string } diff --git a/pkg/infra/filestorage/cdk_blob_filestorage.go b/pkg/infra/filestorage/cdk_blob_filestorage.go index 27a5f4aac03..53f165fac9a 100644 --- a/pkg/infra/filestorage/cdk_blob_filestorage.go +++ b/pkg/infra/filestorage/cdk_blob_filestorage.go @@ -98,7 +98,7 @@ func (c cdkBlobStorage) Upsert(ctx context.Context, command *UpsertFileCommand) if command.Contents == nil { contents = make([]byte, 0) } else { - contents = *command.Contents + contents = command.Contents } metadata = make(map[string]string) @@ -115,7 +115,7 @@ func (c cdkBlobStorage) Upsert(ctx context.Context, command *UpsertFileCommand) contents = existing.Contents if command.Contents != nil { - contents = *command.Contents + contents = command.Contents } if command.Properties != nil { diff --git a/pkg/infra/filestorage/db_filestorage.go b/pkg/infra/filestorage/db_filestorage.go index 06cfba9c1f7..ae4db90b713 100644 --- a/pkg/infra/filestorage/db_filestorage.go +++ b/pkg/infra/filestorage/db_filestorage.go @@ -141,7 +141,7 @@ func (s dbFileStorage) Upsert(ctx context.Context, cmd *UpsertFileCommand) error if exists { existing.Updated = now if cmd.Contents != nil { - contents := *cmd.Contents + contents := cmd.Contents existing.Contents = contents existing.MimeType = cmd.MimeType existing.Size = int64(len(contents)) @@ -154,7 +154,7 @@ func (s dbFileStorage) Upsert(ctx context.Context, cmd *UpsertFileCommand) error } else { contentsToInsert := make([]byte, 0) if cmd.Contents != nil { - contentsToInsert = *cmd.Contents + contentsToInsert = cmd.Contents } file := &file{ diff --git a/pkg/infra/filestorage/fs_integration_test.go b/pkg/infra/filestorage/fs_integration_test.go index 5b9268497ff..1a756545a2c 100644 --- a/pkg/infra/filestorage/fs_integration_test.go +++ b/pkg/infra/filestorage/fs_integration_test.go @@ -152,7 +152,7 @@ func runTests(createCases func() []fsTestCase, t *testing.T) { func TestFsStorage(t *testing.T) { //skipTest := true - emptyFileBytes := make([]byte, 0) + emptyContents := make([]byte, 0) pngImage, _ := base64.StdEncoding.DecodeString(pngImageBase64) pngImageSize := int64(len(pngImage)) @@ -164,21 +164,21 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/folder2/file.jpg", - Contents: &[]byte{}, + Contents: emptyContents, Properties: map[string]string{"prop1": "val1", "prop2": "val"}, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/file-inner.jpg", - Contents: &[]byte{}, + Contents: emptyContents, Properties: map[string]string{"prop1": "val1", "prop2": "val"}, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/file-inner2.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, queryListFiles{ @@ -261,25 +261,25 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/ab/a.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/ab/a/a.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/ac/a.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/aba/a.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, queryListFiles{ @@ -307,14 +307,14 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/folder2/file.jpg", - Contents: &[]byte{}, + Contents: emptyContents, Properties: map[string]string{"prop1": "val1", "prop2": "val"}, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/file-inner.jpg", - Contents: &[]byte{}, + Contents: emptyContents, Properties: map[string]string{"prop1": "val1"}, }, }, @@ -370,13 +370,13 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/folder2/file.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/file-inner.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, queryListFiles{ @@ -411,19 +411,19 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/a", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/b", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder2/c", - Contents: &[]byte{}, + Contents: emptyContents, }, }, queryListFiles{ @@ -533,25 +533,25 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/folder2/file.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/file-inner.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folderX/folderZ/file.txt", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folderA/folderB/file.txt", - Contents: &[]byte{}, + Contents: emptyContents, }, }, queryListFolders{ @@ -585,25 +585,25 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/folder2/file.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder1/file-inner.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folderX/folderZ/file.txt", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folderA/folderB/file.txt", - Contents: &[]byte{}, + Contents: emptyContents, }, }, queryListFolders{ @@ -673,7 +673,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder/a.png", - Contents: &pngImage, + Contents: pngImage, Properties: map[string]string{"prop1": "val1", "prop2": "val"}, }, }, @@ -698,7 +698,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/Folder/A.png", - Contents: &emptyFileBytes, + Contents: emptyContents, }, }, queryGet{ @@ -718,7 +718,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/a.png", - Contents: &pngImage, + Contents: pngImage, Properties: map[string]string{"a": "av", "b": "bv"}, }, }, @@ -754,7 +754,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/aB.png", - Contents: &emptyFileBytes, + Contents: emptyContents, Properties: map[string]string{"a": "av", "b": "bv"}, }, }, @@ -791,7 +791,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/FILE.png", - Contents: &emptyFileBytes, + Contents: emptyContents, Properties: map[string]string{"a": "av", "b": "bv"}, }, }, @@ -803,13 +803,13 @@ func TestFsStorage(t *testing.T) { fName("FILE.png"), fProperties(map[string]string{"a": "av", "b": "bv"}), fSize(0), - fContents(emptyFileBytes), + fContents(emptyContents), ), }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/file.png", - Contents: &pngImage, + Contents: pngImage, }, }, queryGet{ @@ -832,7 +832,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/FILE.png", - Contents: &emptyFileBytes, + Contents: emptyContents, Properties: map[string]string{"a": "av", "b": "bv"}, }, }, @@ -855,7 +855,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/file.png", - Contents: &emptyFileBytes, + Contents: emptyContents, Properties: map[string]string{"a": "av", "b": "bv"}, }, }, @@ -888,7 +888,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/aB/cD/eF/file.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, queryListFolders{ @@ -1007,7 +1007,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/a/b/c/file.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, queryListFolders{ @@ -1119,7 +1119,7 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/folder/dashboards/myNewFolder/file.jpg", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdDeleteFolder{ @@ -1164,43 +1164,43 @@ func TestFsStorage(t *testing.T) { cmdUpsert{ cmd: UpsertFileCommand{ Path: "/s3/folder/dashboard.json", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/s3/folder/nested/dashboard.json", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/gitA/dashboard1.json", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/gitA/dashboard2.json", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/gitB/nested/dashboard.json", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/gitB/nested2/dashboard2.json", - Contents: &[]byte{}, + Contents: emptyContents, }, }, cmdUpsert{ cmd: UpsertFileCommand{ Path: "/gitC/nestedC/dashboardC.json", - Contents: &[]byte{}, + Contents: emptyContents, }, }, queryListFiles{ diff --git a/pkg/services/store/storage_disk.go b/pkg/services/store/storage_disk.go index e7dde055629..143a80b5e28 100644 --- a/pkg/services/store/storage_disk.go +++ b/pkg/services/store/storage_disk.go @@ -82,7 +82,7 @@ func (s *rootStorageDisk) Write(ctx context.Context, cmd *WriteValueRequest) (*W } err := s.store.Upsert(ctx, &filestorage.UpsertFileCommand{ Path: path, - Contents: &byteAray, + Contents: byteAray, }) if err != nil { return nil, err From a15c2021fef75f07e569790133244910b3566f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Tue, 12 Apr 2022 18:23:19 +0200 Subject: [PATCH 047/121] Loki: improve backend-compatibility (#47579) * loki: annotation queries: make them ready for the backend-mode * removed unused code --- .../app/plugins/datasource/loki/datasource.ts | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 69d9b771cc5..41a71d3709f 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -8,6 +8,7 @@ import Prism from 'prismjs'; import { AnnotationEvent, AnnotationQueryRequest, + CoreApp, DataFrame, DataFrameView, DataQueryError, @@ -31,6 +32,7 @@ import { QueryResultMeta, ScopedVars, TimeRange, + rangeUtil, } from '@grafana/data'; import { BackendSrvRequest, FetchError, getBackendSrv, config, DataSourceWithBackend } from '@grafana/runtime'; import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; @@ -78,6 +80,21 @@ const DEFAULT_QUERY_PARAMS: Partial = { query: '', }; +function makeRequest(query: LokiQuery, range: TimeRange, app: CoreApp, requestId: string): DataQueryRequest { + const intervalInfo = rangeUtil.calculateInterval(range, 1); + return { + targets: [query], + requestId, + interval: intervalInfo.interval, + intervalMs: intervalInfo.intervalMs, + range: range, + scopedVars: {}, + timezone: 'UTC', + app, + startTime: Date.now(), + }; +} + export class LokiDatasource extends DataSourceWithBackend implements @@ -652,32 +669,25 @@ export class LokiDatasource } async annotationQuery(options: any): Promise { - const { - expr, - maxLines, - instant, - stepInterval, - tagKeys = '', - titleFormat = '', - textFormat = '', - } = options.annotation; + const { expr, maxLines, instant, tagKeys = '', titleFormat = '', textFormat = '' } = options.annotation; if (!expr) { return []; } - const interpolatedExpr = this.templateSrv.replace(expr, {}, this.interpolateQueryExpr); - const query = { - refId: `annotation-${options.annotation.name}`, - expr: interpolatedExpr, + const id = `annotation-${options.annotation.name}`; + + const query: LokiQuery = { + refId: id, + expr, maxLines, instant, - stepInterval, queryType: instant ? LokiQueryType.Instant : LokiQueryType.Range, }; - const { data } = instant - ? await lastValueFrom(this.runInstantQuery(query, options as any)) - : await lastValueFrom(this.runRangeQuery(query, options as any)); + + const request = makeRequest(query, options.range, CoreApp.Dashboard, id); + + const { data } = await lastValueFrom(this.query(request)); const annotations: AnnotationEvent[] = []; const splitKeys: string[] = tagKeys.split(',').filter((v: string) => v !== ''); From d0abe1bb3d0d84f173795b362176a06a0eca290d Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 12 Apr 2022 17:30:50 +0100 Subject: [PATCH 048/121] Annotation FGAC checks for comments (#47468) * typo * remove unwanted change * remove unwanted change --- pkg/api/dashboard_test.go | 2 +- .../comments/commentmodel/permissions.go | 19 +++++++++++++------ pkg/services/comments/service.go | 5 +++-- pkg/services/live/live.go | 5 +++-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e439391b64a..dc2a22629ff 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -96,7 +96,7 @@ func newTestLive(t *testing.T, store *sqlstore.SQLStore) *live.GrafanaLive { nil, &usagestats.UsageStatsMock{T: t}, nil, - features, nil) + features, nil, accesscontrolmock.New()) require.NoError(t, err) return gLive } diff --git a/pkg/services/comments/commentmodel/permissions.go b/pkg/services/comments/commentmodel/permissions.go index e603e306c24..b1bcbee82d1 100644 --- a/pkg/services/comments/commentmodel/permissions.go +++ b/pkg/services/comments/commentmodel/permissions.go @@ -4,21 +4,22 @@ import ( "context" "strconv" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/sqlstore" ) type PermissionChecker struct { - sqlStore *sqlstore.SQLStore - features featuremgmt.FeatureToggles + sqlStore *sqlstore.SQLStore + features featuremgmt.FeatureToggles + accessControl accesscontrol.AccessControl } -func NewPermissionChecker(sqlStore *sqlstore.SQLStore, features featuremgmt.FeatureToggles) *PermissionChecker { - return &PermissionChecker{sqlStore: sqlStore, features: features} +func NewPermissionChecker(sqlStore *sqlstore.SQLStore, features featuremgmt.FeatureToggles, accessControl accesscontrol.AccessControl) *PermissionChecker { + return &PermissionChecker{sqlStore: sqlStore, features: features, accessControl: accessControl} } func (c *PermissionChecker) getDashboardByUid(ctx context.Context, orgID int64, uid string) (*models.Dashboard, error) { @@ -104,6 +105,12 @@ func (c *PermissionChecker) CheckWritePermissions(ctx context.Context, orgId int if !c.features.IsEnabled(featuremgmt.FlagAnnotationComments) { return false, nil } + if c.features.IsEnabled(featuremgmt.FlagAccesscontrol) { + evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsWrite, accesscontrol.ScopeAnnotationsTypeDashboard) + if canEdit, err := c.accessControl.Evaluate(ctx, signedInUser, evaluator); err != nil || !canEdit { + return canEdit, err + } + } repo := annotations.GetRepository() annotationID, err := strconv.ParseInt(objectID, 10, 64) if err != nil { diff --git a/pkg/services/comments/service.go b/pkg/services/comments/service.go index c2797fdaa87..a899af106ef 100644 --- a/pkg/services/comments/service.go +++ b/pkg/services/comments/service.go @@ -3,6 +3,7 @@ package comments import ( "context" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/comments/commentmodel" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/live" @@ -18,7 +19,7 @@ type Service struct { permissions *commentmodel.PermissionChecker } -func ProvideService(cfg *setting.Cfg, store *sqlstore.SQLStore, live *live.GrafanaLive, features featuremgmt.FeatureToggles) *Service { +func ProvideService(cfg *setting.Cfg, store *sqlstore.SQLStore, live *live.GrafanaLive, features featuremgmt.FeatureToggles, accessControl accesscontrol.AccessControl) *Service { s := &Service{ cfg: cfg, live: live, @@ -26,7 +27,7 @@ func ProvideService(cfg *setting.Cfg, store *sqlstore.SQLStore, live *live.Grafa storage: &sqlStorage{ sql: store, }, - permissions: commentmodel.NewPermissionChecker(store, features), + permissions: commentmodel.NewPermissionChecker(store, features, accessControl), } return s } diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index d03b6453076..085e3b40786 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/comments/commentmodel" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -70,7 +71,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r pluginStore plugins.Store, cacheService *localcache.CacheService, dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service, usageStatsService usagestats.Service, queryDataService *query.Service, toggles featuremgmt.FeatureToggles, - bus bus.Bus) (*GrafanaLive, error) { + bus bus.Bus, accessControl accesscontrol.AccessControl) (*GrafanaLive, error) { g := &GrafanaLive{ Cfg: cfg, Features: toggles, @@ -242,7 +243,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r g.GrafanaScope.Dashboards = dash g.GrafanaScope.Features["dashboard"] = dash g.GrafanaScope.Features["broadcast"] = features.NewBroadcastRunner(g.storage) - g.GrafanaScope.Features["comment"] = features.NewCommentHandler(commentmodel.NewPermissionChecker(g.SQLStore, g.Features)) + g.GrafanaScope.Features["comment"] = features.NewCommentHandler(commentmodel.NewPermissionChecker(g.SQLStore, g.Features, accessControl)) g.surveyCaller = survey.NewCaller(managedStreamRunner, node) err = g.surveyCaller.SetupHandlers() From 5cb5141c723ddc2d73109b89b1a9bb9b44fda498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Tue, 12 Apr 2022 18:55:39 +0200 Subject: [PATCH 049/121] Query History: Move local storage specific settings to persistence layer (#47500) * Load Rich History when the container is opened * Store rich history for each pane separately * Do not update currently opened query history when an item is added It's impossible to figure out if the item should be added or not, because filters are applied in the backend. We don't want to replicate that filtering logic in frontend. One way to make it work could be by refreshing both panes. * Test starring and deleting query history items when both panes are open * Remove e2e dependency on ExploreId * Fix unit test * Assert exact queries * Simplify test * Fix e2e tests * Fix toolbar a11y * Reload the history after an item is added * Fix unit test * Remove references to Explore from generic PageToolbar component * Update test name * Fix test assertion * Add issue item to TODO * Improve test assertion * Simplify test setup * Move query history settings to persistence layer * Fix test import * Fix unit test * Fix unit test * Test local storage settings API * Code formatting * Fix linting errors * Add an integration test * Add missing aria role * Fix a11y issues * Fix a11y issues * Use divs instead of ul/li Otherwis,e pa11y-ci reports the error below claiming there are no children with role=tab: Certain ARIA roles must contain particular children (https://dequeuniversity.com/rules/axe/4.3/aria-required-children?application=axeAPI) (#reactRoot > div > main > div:nth-child(3) > div > div:nth-child(1) > div > div:nth-child(1) > div > div > nav > div:nth-child(2) > ul)
  • * Clean up settings tab * Remove redundant aria label * Remove redundant container * Clean up test assertions --- .betterer.results | 2 +- .../grafana-ui/src/components/Tabs/Tab.tsx | 6 +- .../src/components/Tabs/TabsBar.tsx | 4 +- .../history/RichHistoryLocalStorage.test.ts | 14 +++ .../core/history/RichHistoryLocalStorage.ts | 17 +++ public/app/core/history/RichHistoryStorage.ts | 4 + public/app/core/utils/richHistory.ts | 10 +- public/app/core/utils/richHistoryTypes.ts | 17 +++ .../explore/RichHistory/RichHistory.test.tsx | 16 +++ .../explore/RichHistory/RichHistory.tsx | 113 +++++++----------- .../RichHistory/RichHistoryContainer.test.tsx | 34 ++++-- .../RichHistory/RichHistoryContainer.tsx | 43 +++++-- .../RichHistory/RichHistoryQueriesTab.tsx | 18 +-- ...st.tsx => RichHistorySettingsTab.test.tsx} | 4 +- ...ettings.tsx => RichHistorySettingsTab.tsx} | 37 +++--- .../RichHistoryStarredTab.test.tsx | 4 +- .../RichHistory/RichHistoryStarredTab.tsx | 18 +-- .../features/explore/spec/helper/assert.ts | 16 +++ .../explore/spec/helper/interactions.ts | 24 ++++ .../explore/spec/queryHistory.test.tsx | 42 ++++++- .../app/features/explore/state/explorePane.ts | 10 +- public/app/features/explore/state/history.ts | 69 ++++++++++- public/app/features/explore/state/main.ts | 15 +++ public/app/types/explore.ts | 7 ++ 24 files changed, 402 insertions(+), 142 deletions(-) rename public/app/features/explore/RichHistory/{RichHistorySettings.test.tsx => RichHistorySettingsTab.test.tsx} (87%) rename public/app/features/explore/RichHistory/{RichHistorySettings.tsx => RichHistorySettingsTab.tsx} (74%) diff --git a/.betterer.results b/.betterer.results index b57253c101f..e0cea5bc339 100644 --- a/.betterer.results +++ b/.betterer.results @@ -230,7 +230,7 @@ exports[`no enzyme tests`] = { "public/app/features/explore/LiveLogs.test.tsx:1667605379": [ [2, 17, 13, "RegExp match", "2409514259"] ], - "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3948011811": [ + "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:492930613": [ [1, 17, 13, "RegExp match", "2409514259"] ], "public/app/features/explore/RunButton.test.tsx:138299098": [ diff --git a/packages/grafana-ui/src/components/Tabs/Tab.tsx b/packages/grafana-ui/src/components/Tabs/Tab.tsx index 562eb2669ee..d45f33cc436 100644 --- a/packages/grafana-ui/src/components/Tabs/Tab.tsx +++ b/packages/grafana-ui/src/components/Tabs/Tab.tsx @@ -38,18 +38,20 @@ export const Tab = React.forwardRef( const linkClass = cx(tabsStyles.link, active ? tabsStyles.activeStyle : tabsStyles.notActive); return ( -
  • +
  • + ); } ); diff --git a/packages/grafana-ui/src/components/Tabs/TabsBar.tsx b/packages/grafana-ui/src/components/Tabs/TabsBar.tsx index b5eb2a07206..13fccb9223f 100644 --- a/packages/grafana-ui/src/components/Tabs/TabsBar.tsx +++ b/packages/grafana-ui/src/components/Tabs/TabsBar.tsx @@ -32,7 +32,9 @@ export const TabsBar = React.forwardRef(({ children, clas return (
    -
      {children}
    +
    + {children} +
    ); }); diff --git a/public/app/core/history/RichHistoryLocalStorage.test.ts b/public/app/core/history/RichHistoryLocalStorage.test.ts index 1bbc7f7b30f..747b6720935 100644 --- a/public/app/core/history/RichHistoryLocalStorage.test.ts +++ b/public/app/core/history/RichHistoryLocalStorage.test.ts @@ -5,6 +5,7 @@ import { DataQuery } from '@grafana/data'; import { afterEach, beforeEach } from '../../../test/lib/common'; import { RichHistoryStorageWarning } from './RichHistoryStorage'; import { backendSrv } from '../services/backend_srv'; +import { RichHistorySettings } from '../utils/richHistoryTypes'; const key = 'grafana.explore.richHistory'; @@ -99,6 +100,19 @@ describe('RichHistoryLocalStorage', () => { expect(await storage.getRichHistory()).toEqual([]); expect(store.getObject(key)).toEqual([]); }); + + it('should save and read settings', async () => { + const settings: RichHistorySettings = { + retentionPeriod: 2, + starredTabAsFirstTab: true, + activeDatasourceOnly: true, + lastUsedDatasourceFilters: [{ value: 'foobar' }], + }; + await storage.updateSettings(settings); + const storageSettings = storage.getSettings(); + + expect(settings).toMatchObject(storageSettings); + }); }); describe('retention policy and max limits', () => { diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index b4d05c0c931..ca98b0c6a45 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -5,6 +5,7 @@ import { DataQuery } from '@grafana/data'; import { find, isEqual, omit } from 'lodash'; import { createRetentionPeriodBoundary, RICH_HISTORY_SETTING_KEYS } from './richHistoryLocalStorageUtils'; import { fromDTO, toDTO } from './localStorageConverter'; +import { RichHistorySettings } from '../utils/richHistoryTypes'; export const RICH_HISTORY_KEY = 'grafana.explore.richHistory'; export const MAX_HISTORY_ITEMS = 10000; @@ -101,6 +102,22 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage { async updateComment(id: string, comment: string) { return updateRichHistory(id, (richHistoryDTO) => (richHistoryDTO.comment = comment)); } + + async getSettings() { + return { + activeDatasourceOnly: store.getObject(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, false), + retentionPeriod: store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7), + starredTabAsFirstTab: store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false), + lastUsedDatasourceFilters: store.getObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, []), + }; + } + + async updateSettings(settings: RichHistorySettings) { + store.set(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, settings.activeDatasourceOnly); + store.set(RICH_HISTORY_SETTING_KEYS.retentionPeriod, settings.retentionPeriod); + store.set(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, settings.starredTabAsFirstTab); + store.setObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, settings.lastUsedDatasourceFilters); + } } function updateRichHistory( diff --git a/public/app/core/history/RichHistoryStorage.ts b/public/app/core/history/RichHistoryStorage.ts index 4242e13658f..5e833c73f44 100644 --- a/public/app/core/history/RichHistoryStorage.ts +++ b/public/app/core/history/RichHistoryStorage.ts @@ -1,4 +1,5 @@ import { RichHistoryQuery } from '../../types'; +import { RichHistorySettings } from '../utils/richHistoryTypes'; /** * Errors are used when the operation on Rich History was not successful. @@ -44,4 +45,7 @@ export default interface RichHistoryStorage { deleteRichHistory(id: string): Promise; updateStarred(id: string, starred: boolean): Promise; updateComment(id: string, comment: string | undefined): Promise; + + getSettings(): Promise; + updateSettings(settings: RichHistorySettings): Promise; } diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index feebda044e3..5e08a4fa046 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -23,7 +23,7 @@ import { filterQueriesByTime, sortQueries, } from 'app/core/history/richHistoryLocalStorageUtils'; -import { SortOrder } from './richHistoryTypes'; +import { RichHistorySettings, SortOrder } from './richHistoryTypes'; export { SortOrder }; @@ -86,6 +86,14 @@ export async function getRichHistory(): Promise { return await getRichHistoryStorage().getRichHistory(); } +export async function updateRichHistorySettings(settings: RichHistorySettings): Promise { + await getRichHistoryStorage().updateSettings(settings); +} + +export async function getRichHistorySettings(): Promise { + return await getRichHistoryStorage().getSettings(); +} + export async function deleteAllFromRichHistory(): Promise { return getRichHistoryStorage().deleteAll(); } diff --git a/public/app/core/utils/richHistoryTypes.ts b/public/app/core/utils/richHistoryTypes.ts index 2322258de13..0f2030069ac 100644 --- a/public/app/core/utils/richHistoryTypes.ts +++ b/public/app/core/utils/richHistoryTypes.ts @@ -1,6 +1,23 @@ +import { SelectableValue } from '@grafana/data'; + export enum SortOrder { Descending = 'Descending', Ascending = 'Ascending', DatasourceAZ = 'Datasource A-Z', DatasourceZA = 'Datasource Z-A', } + +export interface RichHistorySettings { + retentionPeriod: number; + starredTabAsFirstTab: boolean; + activeDatasourceOnly: boolean; + lastUsedDatasourceFilters: SelectableValue[]; +} + +export type RichHistorySearchFilters = { + search: string; + sortOrder: SortOrder; + datasourceFilters: SelectableValue[]; + from: number; + to: number; +}; diff --git a/public/app/features/explore/RichHistory/RichHistory.test.tsx b/public/app/features/explore/RichHistory/RichHistory.test.tsx index b49c35b06e2..b588e6f3362 100644 --- a/public/app/features/explore/RichHistory/RichHistory.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.test.tsx @@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react'; import { GrafanaTheme } from '@grafana/data'; import { ExploreId } from '../../../types/explore'; import { RichHistory, RichHistoryProps, Tabs } from './RichHistory'; +import { SortOrder } from '../../../core/utils/richHistoryTypes'; jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() })); @@ -16,6 +17,21 @@ const setup = (propOverrides?: Partial) => { firstTab: Tabs.RichHistory, deleteRichHistory: jest.fn(), onClose: jest.fn(), + richHistorySearchFilters: { + search: '', + sortOrder: SortOrder.Descending, + datasourceFilters: [], + from: 0, + to: 7, + }, + richHistorySettings: { + retentionPeriod: 0, + starredTabAsFirstTab: false, + activeDatasourceOnly: true, + lastUsedDatasourceFilters: [], + }, + updateHistorySearchFilters: jest.fn(), + updateHistorySettings: jest.fn(), }; Object.assign(props, propOverrides); diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index 410ad7f7ea6..893a17ca796 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -2,8 +2,6 @@ import React, { PureComponent } from 'react'; //Services & Utils import { SortOrder } from 'app/core/utils/richHistory'; -import { RICH_HISTORY_SETTING_KEYS } from 'app/core/history/richHistoryLocalStorageUtils'; -import store from 'app/core/store'; import { Themeable, withTheme, TabbedContainer, TabConfig } from '@grafana/ui'; //Types @@ -11,9 +9,10 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore'; import { SelectableValue } from '@grafana/data'; //Components -import { RichHistorySettings } from './RichHistorySettings'; +import { RichHistorySettingsTab } from './RichHistorySettingsTab'; import { RichHistoryQueriesTab } from './RichHistoryQueriesTab'; import { RichHistoryStarredTab } from './RichHistoryStarredTab'; +import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; export enum Tabs { RichHistory = 'Query history', @@ -30,100 +29,74 @@ export const sortOrderOptions = [ export interface RichHistoryProps extends Themeable { richHistory: RichHistoryQuery[]; + richHistorySettings: RichHistorySettings; + richHistorySearchFilters: RichHistorySearchFilters; + updateHistorySettings: (settings: RichHistorySettings) => void; + updateHistorySearchFilters: (exploreId: ExploreId, filters: RichHistorySearchFilters) => void; + deleteRichHistory: () => void; activeDatasourceInstance?: string; firstTab: Tabs; exploreId: ExploreId; height: number; - deleteRichHistory: () => void; onClose: () => void; } -interface RichHistoryState { - sortOrder: SortOrder; - retentionPeriod: number; - starredTabAsFirstTab: boolean; - activeDatasourceOnly: boolean; - datasourceFilters: SelectableValue[]; -} +class UnThemedRichHistory extends PureComponent { + updateSettings = (settingsToUpdate: Partial) => { + this.props.updateHistorySettings({ ...this.props.richHistorySettings, ...settingsToUpdate }); + }; -class UnThemedRichHistory extends PureComponent { - constructor(props: RichHistoryProps) { - super(props); - this.state = { - sortOrder: SortOrder.Descending, - datasourceFilters: store.getObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, []), - retentionPeriod: store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7), - starredTabAsFirstTab: store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false), - activeDatasourceOnly: store.getBool(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, true), - }; - } + updateFilters = (filtersToUpdate: Partial) => { + this.props.updateHistorySearchFilters(this.props.exploreId, { + ...this.props.richHistorySearchFilters, + ...filtersToUpdate, + }); + }; onChangeRetentionPeriod = (retentionPeriod: SelectableValue) => { if (retentionPeriod.value !== undefined) { - this.setState({ - retentionPeriod: retentionPeriod.value, - }); - store.set(RICH_HISTORY_SETTING_KEYS.retentionPeriod, retentionPeriod.value); + this.updateSettings({ retentionPeriod: retentionPeriod.value }); } }; - toggleStarredTabAsFirstTab = () => { - const starredTabAsFirstTab = !this.state.starredTabAsFirstTab; - this.setState({ - starredTabAsFirstTab, - }); - store.set(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, starredTabAsFirstTab); - }; + toggleStarredTabAsFirstTab = () => + this.updateSettings({ starredTabAsFirstTab: !this.props.richHistorySettings.starredTabAsFirstTab }); - toggleActiveDatasourceOnly = () => { - const activeDatasourceOnly = !this.state.activeDatasourceOnly; - this.setState({ - activeDatasourceOnly, - }); - store.set(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, activeDatasourceOnly); - }; + toggleActiveDatasourceOnly = () => + this.updateSettings({ activeDatasourceOnly: !this.props.richHistorySettings.activeDatasourceOnly }); - onSelectDatasourceFilters = (value: SelectableValue[]) => { - try { - store.setObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, value); - } catch (error) { - console.error(error); - } - /* Set data source filters to state even though they were not successfully saved in - * localStorage to allow interaction and filtering. - **/ - this.setState({ datasourceFilters: value }); - }; + onSelectDatasourceFilters = (datasourceFilters: SelectableValue[]) => this.updateFilters({ datasourceFilters }); - onChangeSortOrder = (sortOrder: SortOrder) => this.setState({ sortOrder }); + onChangeSortOrder = (sortOrder: SortOrder) => this.updateFilters({ sortOrder }); /* If user selects activeDatasourceOnly === true, set datasource filter to currently active datasource. * Filtering based on datasource won't be available. Otherwise set to null, as filtering will be * available for user. */ - updateFilters() { - this.state.activeDatasourceOnly && this.props.activeDatasourceInstance - ? this.onSelectDatasourceFilters([ - { label: this.props.activeDatasourceInstance, value: this.props.activeDatasourceInstance }, - ]) - : this.onSelectDatasourceFilters(this.state.datasourceFilters); + initFilters() { + if (this.props.richHistorySettings.activeDatasourceOnly && this.props.activeDatasourceInstance) { + this.onSelectDatasourceFilters([ + { label: this.props.activeDatasourceInstance, value: this.props.activeDatasourceInstance }, + ]); + } } componentDidMount() { - this.updateFilters(); + this.initFilters(); } - componentDidUpdate(prevProps: RichHistoryProps, prevState: RichHistoryState) { - if ( - this.props.activeDatasourceInstance !== prevProps.activeDatasourceInstance || - this.state.activeDatasourceOnly !== prevState.activeDatasourceOnly - ) { - this.updateFilters(); + /** + * Updating filters on didMount and didUpdate because we don't know when activeDatasourceInstance is ready + */ + componentDidUpdate(prevProps: RichHistoryProps) { + if (this.props.activeDatasourceInstance !== prevProps.activeDatasourceInstance) { + this.initFilters(); } } render() { - const { datasourceFilters, sortOrder, activeDatasourceOnly, retentionPeriod } = this.state; + const { activeDatasourceOnly, retentionPeriod } = this.props.richHistorySettings; + const { datasourceFilters, sortOrder } = this.props.richHistorySearchFilters; const { richHistory, height, exploreId, deleteRichHistory, onClose, firstTab } = this.props; const QueriesTab: TabConfig = { @@ -166,10 +139,10 @@ class UnThemedRichHistory extends PureComponent ({ getExploreDatasources: jest.fn() })); @@ -15,8 +16,23 @@ const setup = (propOverrides?: Partial) => { richHistory: [], firstTab: Tabs.RichHistory, deleteRichHistory: jest.fn(), - loadRichHistory: jest.fn(), + initRichHistory: jest.fn(), + updateHistorySearchFilters: jest.fn(), + updateHistorySettings: jest.fn(), onClose: jest.fn(), + richHistorySearchFilters: { + search: '', + sortOrder: SortOrder.Descending, + datasourceFilters: [], + from: 0, + to: 7, + }, + richHistorySettings: { + retentionPeriod: 0, + starredTabAsFirstTab: false, + activeDatasourceOnly: true, + lastUsedDatasourceFilters: [], + }, }; Object.assign(props, propOverrides); @@ -25,6 +41,10 @@ const setup = (propOverrides?: Partial) => { }; describe('RichHistoryContainer', () => { + it('should show loading message when settings and filters are not ready', () => { + const { container } = setup({ richHistorySearchFilters: undefined, richHistorySettings: undefined }); + expect(container).toHaveTextContent('Loading...'); + }); it('should render component with correct width', () => { const { container } = setup(); expect(container.firstElementChild!.getAttribute('style')).toContain('width: 531.5px'); @@ -34,14 +54,14 @@ describe('RichHistoryContainer', () => { expect(container.firstElementChild!.getAttribute('style')).toContain('height: 400px'); }); it('should re-request rich history every time the component is mounted', () => { - const loadRichHistory = jest.fn(); - const { unmount } = setup({ loadRichHistory }); - expect(loadRichHistory).toBeCalledTimes(1); + const initRichHistory = jest.fn(); + const { unmount } = setup({ initRichHistory }); + expect(initRichHistory).toBeCalledTimes(1); unmount(); - expect(loadRichHistory).toBeCalledTimes(1); + expect(initRichHistory).toBeCalledTimes(1); - setup({ loadRichHistory }); - expect(loadRichHistory).toBeCalledTimes(2); + setup({ initRichHistory }); + expect(initRichHistory).toBeCalledTimes(2); }); }); diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index aa13771e438..93f03298c10 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -2,10 +2,6 @@ import React, { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -// Services & Utils -import store from 'app/core/store'; -import { RICH_HISTORY_SETTING_KEYS } from 'app/core/history/richHistoryLocalStorageUtils'; - // Types import { ExploreItemState, StoreState } from 'app/types'; import { ExploreId } from 'app/types/explore'; @@ -14,27 +10,36 @@ import { ExploreId } from 'app/types/explore'; import { RichHistory, Tabs } from './RichHistory'; //Actions -import { deleteRichHistory, loadRichHistory } from '../state/history'; +import { + deleteRichHistory, + initRichHistory, + updateHistorySettings, + updateHistorySearchFilters, +} from '../state/history'; import { ExploreDrawer } from '../ExploreDrawer'; function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) { const explore = state.explore; // @ts-ignore const item: ExploreItemState = explore[exploreId]; + const richHistorySearchFilters = item.richHistorySearchFilters; + const richHistorySettings = explore.richHistorySettings; const { datasourceInstance } = item; - const firstTab = store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false) - ? Tabs.Starred - : Tabs.RichHistory; + const firstTab = richHistorySettings?.starredTabAsFirstTab ? Tabs.Starred : Tabs.RichHistory; const { richHistory } = item; return { richHistory, firstTab, activeDatasourceInstance: datasourceInstance?.name, + richHistorySettings, + richHistorySearchFilters, }; } const mapDispatchToProps = { - loadRichHistory, + initRichHistory, + updateHistorySettings, + updateHistorySearchFilters, deleteRichHistory, }; @@ -57,13 +62,21 @@ export function RichHistoryContainer(props: Props) { activeDatasourceInstance, exploreId, deleteRichHistory, - loadRichHistory, + initRichHistory, + richHistorySettings, + updateHistorySettings, + richHistorySearchFilters, + updateHistorySearchFilters, onClose, } = props; useEffect(() => { - loadRichHistory(exploreId); - }, [loadRichHistory, exploreId]); + initRichHistory(exploreId); + }, [initRichHistory, exploreId]); + + if (!richHistorySettings || !richHistorySearchFilters) { + return Loading...; + } return ( ); diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index 44fd55e42c3..aa410be7004 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -200,15 +200,15 @@ export function RichHistoryQueriesTab(props: Props) {
    {!activeDatasourceOnly && ( -
    - -
    + )}
    ) => { const props: RichHistorySettingsProps = { @@ -15,7 +15,7 @@ const setup = (propOverrides?: Partial) => { Object.assign(props, propOverrides); - return render(); + return render(); }; describe('RichHistorySettings', () => { diff --git a/public/app/features/explore/RichHistory/RichHistorySettings.tsx b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx similarity index 74% rename from public/app/features/explore/RichHistory/RichHistorySettings.tsx rename to public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx index 2a83c6d04be..e482dfadc74 100644 --- a/public/app/features/explore/RichHistory/RichHistorySettings.tsx +++ b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { css } from '@emotion/css'; -import { stylesFactory, useTheme, Select, Button, Switch, Field } from '@grafana/ui'; +import { stylesFactory, useTheme, Select, Button, Field, InlineField, InlineSwitch } from '@grafana/ui'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; import appEvents from 'app/core/app_events'; import { ShowConfirmModalEvent } from '../../../types/events'; @@ -30,13 +30,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { input: css` max-width: 200px; `, - switch: css` - display: flex; - align-items: center; - `, - label: css` - margin-left: ${theme.spacing.md}; - `, }; }); @@ -47,7 +40,7 @@ const retentionPeriodOptions = [ { value: 14, label: '2 weeks' }, ]; -export function RichHistorySettings(props: RichHistorySettingsProps) { +export function RichHistorySettingsTab(props: RichHistorySettingsProps) { const { retentionPeriod, starredTabAsFirstTab, @@ -92,18 +85,20 @@ export function RichHistorySettings(props: RichHistorySettingsProps) { >
    - -
    - -
    Change the default active tab from “Query history” to “Starred”
    -
    -
    - -
    - -
    Only show queries for data source currently active in Explore
    -
    -
    + + + + + +
    { describe('select datasource', () => { it('should render select datasource if activeDatasourceOnly is false', () => { const wrapper = setup(); - expect(wrapper.find({ 'aria-label': 'Filter datasources' })).toHaveLength(1); + expect(wrapper.find({ 'aria-label': 'Filter queries for data sources(s)' }).exists()).toBeTruthy(); }); it('should not render select datasource if activeDatasourceOnly is true', () => { const wrapper = setup({ activeDatasourceOnly: true }); - expect(wrapper.find({ 'aria-label': 'Filter datasources' })).toHaveLength(0); + expect(wrapper.find({ 'aria-label': 'Filter queries for data sources(s)' }).exists()).toBeFalsy(); }); }); }); diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index b1db75f57f1..cbe7915f791 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -117,15 +117,15 @@ export function RichHistoryStarredTab(props: Props) {
    {!activeDatasourceOnly && ( -
    - -
    + )}
    { + expect(withinExplore(exploreId).getByRole('tab', { name: `Tab ${tabName}`, selected: true })).toBeInTheDocument(); +}; + +export const assertDataSourceFilterVisibility = (visible: boolean, exploreId: ExploreId = ExploreId.left) => { + const filterInput = withinExplore(exploreId).queryByLabelText('Filter queries for data sources(s)'); + if (visible) { + expect(filterInput).toBeInTheDocument(); + } else { + expect(filterInput).not.toBeInTheDocument(); + } +}; diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts index df8f51fb0bc..f1ed4542cc4 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -33,6 +33,30 @@ export const openQueryHistory = async (exploreId: ExploreId = ExploreId.left) => ).toBeInTheDocument(); }; +export const closeQueryHistory = async (exploreId: ExploreId = ExploreId.left) => { + const closeButton = withinExplore(exploreId).getByRole('button', { name: 'Close query history' }); + userEvent.click(closeButton); +}; + +export const switchToQueryHistoryTab = async ( + name: 'Settings' | 'Query History', + exploreId: ExploreId = ExploreId.left +) => { + userEvent.click(withinExplore(exploreId).getByRole('tab', { name: `Tab ${name}` })); +}; + +export const selectStarredTabFirst = (exploreId: ExploreId = ExploreId.left) => { + const checkbox = withinExplore(exploreId).getByRole('checkbox', { + name: 'Change the default active tab from “Query history” to “Starred”', + }); + userEvent.click(checkbox); +}; + +export const selectOnlyActiveDataSource = (exploreId: ExploreId = ExploreId.left) => { + const checkbox = withinExplore(exploreId).getByLabelText(/Only show queries for data source currently active.*/); + userEvent.click(checkbox); +}; + export const starQueryHistory = (queryIndex: number, exploreId: ExploreId = ExploreId.left) => { invokeAction(queryIndex, 'Star query', exploreId); }; diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index 976fbb7bcd8..b62c8d2558d 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -1,8 +1,24 @@ import React from 'react'; import { serializeStateToUrlParam } from '@grafana/data'; import { setupExplore, tearDown, waitForExplore } from './helper/setup'; -import { deleteQueryHistory, inputQuery, openQueryHistory, runQuery, starQueryHistory } from './helper/interactions'; -import { assertQueryHistory, assertQueryHistoryExists, assertQueryHistoryIsStarred } from './helper/assert'; +import { + closeQueryHistory, + deleteQueryHistory, + inputQuery, + openQueryHistory, + runQuery, + selectOnlyActiveDataSource, + selectStarredTabFirst, + starQueryHistory, + switchToQueryHistoryTab, +} from './helper/interactions'; +import { + assertDataSourceFilterVisibility, + assertQueryHistory, + assertQueryHistoryExists, + assertQueryHistoryIsStarred, + assertQueryHistoryTabIsSelected, +} from './helper/assert'; import { makeLogsQueryResponse } from './helper/query'; import { ExploreId } from '../../../types'; import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; @@ -106,4 +122,26 @@ describe('Explore: Query History', () => { await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.left); await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.right); }); + + it('updates query history settings', async () => { + // open settings page + setupExplore(); + await waitForExplore(); + await openQueryHistory(); + + // assert default values + assertQueryHistoryTabIsSelected('Query history'); + assertDataSourceFilterVisibility(true); + await switchToQueryHistoryTab('Settings'); + + // change settings + selectStarredTabFirst(); + selectOnlyActiveDataSource(); + await closeQueryHistory(); + await openQueryHistory(); + + // assert new settings + assertQueryHistoryTabIsSelected('Starred'); + assertDataSourceFilterVisibility(false); + }); }); diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 38b07eb3f5b..a9265a441fa 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -36,7 +36,7 @@ import { import { ThunkResult } from 'app/types'; import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors'; import { getDataSourceSrv } from '@grafana/runtime'; -import { richHistoryUpdatedAction, stateSave } from './main'; +import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction, stateSave } from './main'; import { keybindingSrv } from 'app/core/services/keybindingSrv'; // @@ -262,6 +262,14 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac }; } + if (richHistorySearchFiltersUpdatedAction.match(action)) { + const richHistorySearchFilters = action.payload.filters; + return { + ...state, + richHistorySearchFilters, + }; + } + if (changeSizeAction.match(action)) { const containerWidth = action.payload.width; return { ...state, containerWidth }; diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index a44329ba995..4c5982514db 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -3,13 +3,23 @@ import { deleteAllFromRichHistory, deleteQueryInRichHistory, getRichHistory, + getRichHistorySettings, + SortOrder, updateCommentInRichHistory, + updateRichHistorySettings, updateStarredInRichHistory, } from 'app/core/utils/richHistory'; import { ExploreId, ExploreItemState, ExploreState, RichHistoryQuery, ThunkResult } from 'app/types'; -import { richHistoryLimitExceededAction, richHistoryStorageFullAction, richHistoryUpdatedAction } from './main'; +import { + richHistoryLimitExceededAction, + richHistorySearchFiltersUpdatedAction, + richHistorySettingsUpdatedAction, + richHistoryStorageFullAction, + richHistoryUpdatedAction, +} from './main'; import { DataQuery, HistoryItem } from '@grafana/data'; import { AnyAction, createAction } from '@reduxjs/toolkit'; +import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; // // Actions and Payloads @@ -106,11 +116,68 @@ export const deleteRichHistory = (): ThunkResult => { export const loadRichHistory = (exploreId: ExploreId): ThunkResult => { return async (dispatch) => { + // TODO: #45379 pass currently applied search filters const richHistory = await getRichHistory(); dispatch(richHistoryUpdatedAction({ richHistory, exploreId })); }; }; +/** + * Initialize query history pane. To load history it requires settings to be loaded first + * (but only once per session) and filters initialised with default values based on settings. + */ +export const initRichHistory = (exploreId: ExploreId): ThunkResult => { + return async (dispatch, getState) => { + let settings = getState().explore.richHistorySettings; + if (!settings) { + settings = await getRichHistorySettings(); + dispatch(richHistorySettingsUpdatedAction(settings)); + } + + dispatch( + richHistorySearchFiltersUpdatedAction({ + exploreId, + filters: { + search: '', + sortOrder: SortOrder.Descending, + datasourceFilters: settings!.lastUsedDatasourceFilters || [], + from: 0, + to: settings!.retentionPeriod, + }, + }) + ); + + dispatch(loadRichHistory(exploreId)); + }; +}; + +export const updateHistorySettings = (settings: RichHistorySettings): ThunkResult => { + return async (dispatch) => { + dispatch(richHistorySettingsUpdatedAction(settings)); + await updateRichHistorySettings(settings); + }; +}; + +/** + * Assumed this can be called only when settings and filters are initialised + */ +export const updateHistorySearchFilters = ( + exploreId: ExploreId, + filters: RichHistorySearchFilters +): ThunkResult => { + return async (dispatch, getState) => { + // TODO: #45379 get new rich history list based on filters + dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters })); + const currentSettings = getState().explore.richHistorySettings!; + dispatch( + updateHistorySettings({ + ...currentSettings, + lastUsedDatasourceFilters: filters.datasourceFilters, + }) + ); + }; +}; + export const historyReducer = (state: ExploreItemState, action: AnyAction): ExploreItemState => { if (historyUpdatedAction.match(action)) { return { diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 249d7cda74c..cd439b72d85 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -9,6 +9,7 @@ import { getUrlStateFromPaneState, makeExplorePaneState } from './utils'; import { ThunkResult } from '../../../types'; import { TimeSrv } from '../../dashboard/services/TimeSrv'; import { PanelModel } from 'app/features/dashboard/state'; +import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; // // Actions and Payloads @@ -24,6 +25,12 @@ export const richHistoryUpdatedAction = export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction'); export const richHistoryLimitExceededAction = createAction('explore/richHistoryLimitExceededAction'); +export const richHistorySettingsUpdatedAction = createAction('explore/richHistorySettingsUpdated'); +export const richHistorySearchFiltersUpdatedAction = createAction<{ + exploreId: ExploreId; + filters: RichHistorySearchFilters; +}>('explore/richHistorySearchFiltersUpdatedAction'); + /** * Resets state for explore. */ @@ -243,6 +250,14 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction): }; } + if (richHistorySettingsUpdatedAction.match(action)) { + const richHistorySettings = action.payload; + return { + ...state, + richHistorySettings, + }; + } + if (action.payload) { const { exploreId } = action.payload; if (exploreId !== undefined) { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 626b894070b..ca313a28f06 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -15,6 +15,7 @@ import { DataQueryResponse, ExplorePanelsState, } from '@grafana/data'; +import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes'; export enum ExploreId { left = 'left', @@ -43,6 +44,11 @@ export interface ExploreState { */ right?: ExploreItemState; + /** + * Settings for rich history (note: filters are stored per each pane separately) + */ + richHistorySettings?: RichHistorySettings; + /** * True if local storage quota was exceeded when a rich history item was added. This is to prevent showing * multiple errors when local storage is full. @@ -153,6 +159,7 @@ export interface ExploreItemState { * History of all queries */ richHistory: RichHistoryQuery[]; + richHistorySearchFilters?: RichHistorySearchFilters; /** * We are using caching to store query responses of queries run from logs navigation. From a4381ebc9122d121e80cb7636c69c408422271fe Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Tue, 12 Apr 2022 19:34:04 +0200 Subject: [PATCH 050/121] Previews: create crawler auth setup service (#47349) * #46968: add `RetrieveServiceAccountIdByName` to serviceaccounts service * #46968: improve error logging in rendering service * #46968: add oss crawler account setup * #46968: fix tests * #46968: switch back to ROLE_ADMIN * #46968: rename to crawlerAuth * comment crawler_auth.go --- pkg/server/wireexts_oss.go | 3 ++ pkg/services/rendering/http_mode.go | 14 ++++--- .../serviceaccounts/database/database.go | 41 +++++++++++++++++++ .../serviceaccounts/database/database_test.go | 17 +++++--- .../serviceaccounts/manager/service.go | 8 ++++ .../serviceaccounts/serviceaccounts.go | 2 + pkg/services/serviceaccounts/tests/common.go | 31 +++++++++----- pkg/services/thumbs/crawler.go | 9 ++-- pkg/services/thumbs/crawler_auth.go | 41 +++++++++++++++++++ pkg/services/thumbs/models.go | 3 +- pkg/services/thumbs/service.go | 20 +++++---- 11 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 pkg/services/thumbs/crawler_auth.go diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 81b21fad048..84903cdb4bd 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -31,6 +31,7 @@ import ( "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/sqlstore/migrations" + "github.com/grafana/grafana/pkg/services/thumbs" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" ) @@ -46,6 +47,8 @@ var wireExtsBasicSet = wire.NewSet( ossaccesscontrol.ProvideService, wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.OSSAccessControlService)), wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.OSSAccessControlService)), + thumbs.ProvideCrawlerAuthSetupService, + wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)), validations.ProvideValidator, wire.Bind(new(models.PluginRequestValidator), new(*validations.OSSPluginRequestValidator)), provisioning.ProvideService, diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go index e8820b9e1f9..b086a36b787 100644 --- a/pkg/services/rendering/http_mode.go +++ b/pkg/services/rendering/http_mode.go @@ -46,7 +46,8 @@ func (rs *RenderingService) renderViaHTTP(ctx context.Context, renderKey string, } queryParams := rendererURL.Query() - queryParams.Add("url", rs.getURL(opts.Path)) + url := rs.getURL(opts.Path) + queryParams.Add("url", url) queryParams.Add("renderKey", renderKey) queryParams.Add("width", strconv.Itoa(opts.Width)) queryParams.Add("height", strconv.Itoa(opts.Height)) @@ -74,7 +75,7 @@ func (rs *RenderingService) renderViaHTTP(ctx context.Context, renderKey string, } }() - err = rs.readFileResponse(reqContext, resp, filePath) + err = rs.readFileResponse(reqContext, resp, filePath, url) if err != nil { return nil, err } @@ -94,7 +95,8 @@ func (rs *RenderingService) renderCSVViaHTTP(ctx context.Context, renderKey stri } queryParams := rendererURL.Query() - queryParams.Add("url", rs.getURL(opts.Path)) + url := rs.getURL(opts.Path) + queryParams.Add("url", url) queryParams.Add("renderKey", renderKey) queryParams.Add("domain", rs.domain) queryParams.Add("timezone", isoTimeOffsetToPosixTz(opts.Timezone)) @@ -125,7 +127,7 @@ func (rs *RenderingService) renderCSVViaHTTP(ctx context.Context, renderKey stri } downloadFileName := params["filename"] - err = rs.readFileResponse(reqContext, resp, filePath) + err = rs.readFileResponse(reqContext, resp, filePath, url) if err != nil { return nil, err } @@ -156,7 +158,7 @@ func (rs *RenderingService) doRequest(ctx context.Context, url *url.URL, headers return resp, nil } -func (rs *RenderingService) readFileResponse(ctx context.Context, resp *http.Response, filePath string) error { +func (rs *RenderingService) readFileResponse(ctx context.Context, resp *http.Response, filePath string, url string) error { // check for timeout first if errors.Is(ctx.Err(), context.DeadlineExceeded) { rs.log.Info("Rendering timed out") @@ -165,7 +167,7 @@ func (rs *RenderingService) readFileResponse(ctx context.Context, resp *http.Res // if we didn't get a 200 response, something went wrong. if resp.StatusCode != http.StatusOK { - rs.log.Error("Remote rendering request failed", "error", resp.Status) + rs.log.Error("Remote rendering request failed", "error", resp.Status, "url", url) return fmt.Errorf("remote rendering request failed, status code: %d, status: %s", resp.StatusCode, resp.Status) } diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index 61647755700..bd27655fc10 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -231,6 +231,47 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o return serviceAccount, nil } +func (s *ServiceAccountsStoreImpl) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { + serviceAccount := &struct { + Id int64 + }{} + + err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + sess := dbSession.Table("user") + + whereConditions := []string{ + fmt.Sprintf("%s.name = ?", + s.sqlStore.Dialect.Quote("user")), + fmt.Sprintf("%s.org_id = ?", + s.sqlStore.Dialect.Quote("user")), + fmt.Sprintf("%s.is_service_account = %s", + s.sqlStore.Dialect.Quote("user"), + s.sqlStore.Dialect.BooleanStr(true)), + } + whereParams := []interface{}{name, orgID} + + sess.Where(strings.Join(whereConditions, " AND "), whereParams...) + + sess.Cols( + "user.id", + ) + + if ok, err := sess.Get(serviceAccount); err != nil { + return err + } else if !ok { + return serviceaccounts.ErrServiceAccountNotFound + } + + return nil + }) + + if err != nil { + return 0, err + } + + return serviceAccount.Id, nil +} + func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { diff --git a/pkg/services/serviceaccounts/database/database_test.go b/pkg/services/serviceaccounts/database/database_test.go index d09aa46de4e..44c388034dd 100644 --- a/pkg/services/serviceaccounts/database/database_test.go +++ b/pkg/services/serviceaccounts/database/database_test.go @@ -15,17 +15,24 @@ import ( func TestStore_CreateServiceAccount(t *testing.T) { _, store := setupTestDatabase(t) t.Run("create service account", func(t *testing.T) { - saDTO, err := store.CreateServiceAccount(context.Background(), 1, "new Service Account") + serviceAccountName := "new Service Account" + serviceAccountOrgId := int64(1) + + saDTO, err := store.CreateServiceAccount(context.Background(), serviceAccountOrgId, serviceAccountName) require.NoError(t, err) assert.Equal(t, "sa-new-service-account", saDTO.Login) - assert.Equal(t, "new Service Account", saDTO.Name) + assert.Equal(t, serviceAccountName, saDTO.Name) assert.Equal(t, 0, int(saDTO.Tokens)) - retrieved, err := store.RetrieveServiceAccount(context.Background(), 1, saDTO.Id) + retrieved, err := store.RetrieveServiceAccount(context.Background(), serviceAccountOrgId, saDTO.Id) require.NoError(t, err) assert.Equal(t, "sa-new-service-account", retrieved.Login) - assert.Equal(t, "new Service Account", retrieved.Name) - assert.Equal(t, 1, int(retrieved.OrgId)) + assert.Equal(t, serviceAccountName, retrieved.Name) + assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) + + retrievedId, err := store.RetrieveServiceAccountIdByName(context.Background(), serviceAccountOrgId, serviceAccountName) + require.NoError(t, err) + assert.Equal(t, saDTO.Id, retrievedId) }) } diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 9d8967488b5..71aba8ae775 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -68,3 +68,11 @@ func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgI } return sa.store.DeleteServiceAccount(ctx, orgID, serviceAccountID) } + +func (sa *ServiceAccountsService) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { + if !sa.features.IsEnabled(featuremgmt.FlagServiceAccounts) { + sa.log.Debug(ServiceAccountFeatureToggleNotFound) + return 0, nil + } + return sa.store.RetrieveServiceAccountIdByName(ctx, orgID, name) +} diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index e7b186e8397..fc4c14ad869 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -10,6 +10,7 @@ import ( type Service interface { CreateServiceAccount(ctx context.Context, orgID int64, name string) (*ServiceAccountDTO, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error + RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) } type Store interface { @@ -19,6 +20,7 @@ type Store interface { UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error) + RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error UpgradeServiceAccounts(ctx context.Context) error ConvertToServiceAccounts(ctx context.Context, keys []int64) error diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index 6974ed1de14..b89a602dded 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -38,6 +38,10 @@ func SetupUserServiceAccount(t *testing.T, sqlStore *sqlstore.SQLStore, testUser // create mock for serviceaccountservice type ServiceAccountMock struct{} +func (s *ServiceAccountMock) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { + return 0, nil +} + func (s *ServiceAccountMock) CreateServiceAccount(ctx context.Context, orgID int64, name string) (*serviceaccounts.ServiceAccountDTO, error) { return nil, nil } @@ -65,24 +69,31 @@ func SetupMockAccesscontrol(t *testing.T, // this is a way to see // that the Mock implements the store interface var _ serviceaccounts.Store = new(ServiceAccountsStoreMock) +var _ serviceaccounts.Service = new(ServiceAccountMock) type Calls struct { - CreateServiceAccount []interface{} - RetrieveServiceAccount []interface{} - DeleteServiceAccount []interface{} - UpgradeServiceAccounts []interface{} - ConvertServiceAccounts []interface{} - ListTokens []interface{} - DeleteServiceAccountToken []interface{} - UpdateServiceAccount []interface{} - AddServiceAccountToken []interface{} - SearchOrgServiceAccounts []interface{} + CreateServiceAccount []interface{} + RetrieveServiceAccount []interface{} + DeleteServiceAccount []interface{} + UpgradeServiceAccounts []interface{} + ConvertServiceAccounts []interface{} + ListTokens []interface{} + DeleteServiceAccountToken []interface{} + UpdateServiceAccount []interface{} + AddServiceAccountToken []interface{} + SearchOrgServiceAccounts []interface{} + RetrieveServiceAccountIdByName []interface{} } type ServiceAccountsStoreMock struct { Calls Calls } +func (s *ServiceAccountsStoreMock) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { + s.Calls.RetrieveServiceAccountIdByName = append(s.Calls.RetrieveServiceAccountIdByName, []interface{}{ctx, orgID, name}) + return 0, nil +} + func (s *ServiceAccountsStoreMock) CreateServiceAccount(ctx context.Context, orgID int64, name string) (*serviceaccounts.ServiceAccountDTO, error) { // now we can test that the mock has these calls when we call the function s.Calls.CreateServiceAccount = append(s.Calls.CreateServiceAccount, []interface{}{ctx, orgID, name}) diff --git a/pkg/services/thumbs/crawler.go b/pkg/services/thumbs/crawler.go index 74dcc5051a6..c641455728c 100644 --- a/pkg/services/thumbs/crawler.go +++ b/pkg/services/thumbs/crawler.go @@ -26,6 +26,7 @@ type simpleCrawler struct { thumbnailRepo thumbnailRepo mode CrawlerMode thumbnailKind models.ThumbnailKind + auth CrawlerAuth opts rendering.Opts status crawlStatus statusMutex sync.RWMutex @@ -67,8 +68,8 @@ func (r *simpleCrawler) next(ctx context.Context) (*models.DashboardWithStaleThu authOpts := rendering.AuthOpts{ OrgID: v.OrgId, - UserID: r.opts.AuthOpts.UserID, - OrgRole: r.opts.AuthOpts.OrgRole, + UserID: r.auth.GetUserId(v.OrgId), + OrgRole: r.auth.GetOrgRole(), } if renderingSession, ok := r.renderingSessionByOrgId[v.OrgId]; ok { @@ -112,7 +113,7 @@ func (d byOrgId) Len() int { return len(d) } func (d byOrgId) Less(i, j int) bool { return d[i].OrgId > d[j].OrgId } func (d byOrgId) Swap(i, j int) { d[i], d[j] = d[j], d[i] } -func (r *simpleCrawler) Run(ctx context.Context, authOpts rendering.AuthOpts, mode CrawlerMode, theme models.Theme, thumbnailKind models.ThumbnailKind) error { +func (r *simpleCrawler) Run(ctx context.Context, auth CrawlerAuth, mode CrawlerMode, theme models.Theme, thumbnailKind models.ThumbnailKind) error { res, err := r.renderService.HasCapability(rendering.ScalingDownImages) if err != nil { return err @@ -150,8 +151,8 @@ func (r *simpleCrawler) Run(ctx context.Context, authOpts rendering.AuthOpts, mo r.mode = mode r.thumbnailKind = thumbnailKind + r.auth = auth r.opts = rendering.Opts{ - AuthOpts: authOpts, TimeoutOpts: rendering.TimeoutOpts{ Timeout: 20 * time.Second, RequestTimeoutMultiplier: 3, diff --git a/pkg/services/thumbs/crawler_auth.go b/pkg/services/thumbs/crawler_auth.go new file mode 100644 index 00000000000..b8dd6ba334c --- /dev/null +++ b/pkg/services/thumbs/crawler_auth.go @@ -0,0 +1,41 @@ +package thumbs + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) + +type CrawlerAuthSetupService interface { + Setup(ctx context.Context) (CrawlerAuth, error) +} + +func ProvideCrawlerAuthSetupService() *OSSCrawlerAuthSetupService { + return &OSSCrawlerAuthSetupService{} +} + +type OSSCrawlerAuthSetupService struct{} + +type CrawlerAuth interface { + GetUserId(orgId int64) int64 + GetOrgRole() models.RoleType +} + +type staticCrawlerAuth struct { + userId int64 + orgRole models.RoleType +} + +func (o *staticCrawlerAuth) GetOrgRole() models.RoleType { + return o.orgRole +} + +func (o *staticCrawlerAuth) GetUserId(orgId int64) int64 { + return o.userId +} + +func (o *OSSCrawlerAuthSetupService) Setup(ctx context.Context) (CrawlerAuth, error) { + // userId:0 and ROLE_ADMIN grants the crawler process permissions to view all dashboards in all folders & orgs + // the process doesn't and shouldn't actually need to edit/modify any resources from the UI + return &staticCrawlerAuth{userId: 0, orgRole: models.ROLE_ADMIN}, nil +} diff --git a/pkg/services/thumbs/models.go b/pkg/services/thumbs/models.go index a7c128d195c..78d30746c74 100644 --- a/pkg/services/thumbs/models.go +++ b/pkg/services/thumbs/models.go @@ -5,7 +5,6 @@ import ( "time" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/rendering" ) type CrawlerMode string @@ -66,7 +65,7 @@ type dashboardPreviewsSetupConfig struct { type dashRenderer interface { // Run Assumes you have already authenticated as admin. - Run(ctx context.Context, authOpts rendering.AuthOpts, mode CrawlerMode, theme models.Theme, kind models.ThumbnailKind) error + Run(ctx context.Context, auth CrawlerAuth, mode CrawlerMode, theme models.Theme, kind models.ThumbnailKind) error // Assumes you have already authenticated as admin. Stop() (crawlStatus, error) diff --git a/pkg/services/thumbs/service.go b/pkg/services/thumbs/service.go index 6979de50572..8f098780407 100644 --- a/pkg/services/thumbs/service.go +++ b/pkg/services/thumbs/service.go @@ -56,22 +56,24 @@ type crawlerScheduleOptions struct { maxCrawlDuration time.Duration crawlerMode CrawlerMode thumbnailKind models.ThumbnailKind - auth rendering.AuthOpts themes []models.Theme + auth CrawlerAuth } -func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockService *serverlock.ServerLockService, renderService rendering.Service, gl *live.GrafanaLive, store *sqlstore.SQLStore) Service { +func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockService *serverlock.ServerLockService, renderService rendering.Service, gl *live.GrafanaLive, store *sqlstore.SQLStore, authSetupService CrawlerAuthSetupService) Service { if !features.IsEnabled(featuremgmt.FlagDashboardPreviews) { return &dummyService{} } + logger := log.New("thumbnails_service") thumbnailRepo := newThumbnailRepo(store) - authOpts := rendering.AuthOpts{ - OrgID: 0, - UserID: 0, - OrgRole: models.ROLE_ADMIN, + crawlerAuth, err := authSetupService.Setup(context.Background()) + if err != nil { + logger.Error("Failed to setup auth for the dashboard previews crawler", "err", err) + return &dummyService{} } + return &thumbService{ renderingService: renderService, renderer: newSimpleCrawler(renderService, gl, thumbnailRepo), @@ -80,7 +82,7 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockS features: features, lockService: lockService, crawlLockServiceActionName: "dashboard-crawler", - log: log.New("thumbnails_service"), + log: logger, scheduleOptions: crawlerScheduleOptions{ tickerInterval: time.Hour, @@ -89,7 +91,7 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockS crawlerMode: CrawlerModeThumbs, thumbnailKind: models.ThumbnailKindDefault, themes: []models.Theme{models.ThemeDark, models.ThemeLight}, - auth: authOpts, + auth: crawlerAuth, }, } } @@ -377,7 +379,7 @@ func (hs *thumbService) runOnDemandCrawl(parentCtx context.Context, theme models // wait for at least a minute after the last completed run interval := time.Minute err := hs.lockService.LockAndExecute(crawlerCtx, hs.crawlLockServiceActionName, interval, func(ctx context.Context) { - if err := hs.renderer.Run(crawlerCtx, authOpts, mode, theme, kind); err != nil { + if err := hs.renderer.Run(crawlerCtx, hs.scheduleOptions.auth, mode, theme, kind); err != nil { hs.log.Error("On demand crawl error", "mode", mode, "theme", theme, "kind", kind, "userId", authOpts.UserID, "orgId", authOpts.OrgID, "orgRole", authOpts.OrgRole) } }) From 6042beab43221d23c277ae4dc82fd3554a3f2c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 Apr 2022 07:39:22 +0200 Subject: [PATCH 051/121] Performance: Create separate div for portal root to limit reflow -> recalc style (#47633) * Performance: Create separate div for portal root to limit reflow -> recalc style * refactoring --- .../grafana-ui/src/components/Portal/Portal.tsx | 16 +++++++++++++++- packages/grafana-ui/src/components/index.ts | 2 +- public/app/AppWrapper.tsx | 3 ++- .../NavBar/Next/NavBarItemMenuTrigger.tsx | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/Portal/Portal.tsx b/packages/grafana-ui/src/components/Portal/Portal.tsx index b2705ed006c..a5397721eba 100644 --- a/packages/grafana-ui/src/components/Portal/Portal.tsx +++ b/packages/grafana-ui/src/components/Portal/Portal.tsx @@ -9,9 +9,11 @@ interface Props { } export function Portal(props: PropsWithChildren) { - const { children, className, root: portalRoot = document.body, forwardedRef } = props; + const { children, className, root, forwardedRef } = props; const theme = useTheme2(); const node = useRef(null); + const portalRoot = root ?? getPortalContainer(); + if (!node.current) { node.current = document.createElement('div'); if (className) { @@ -25,6 +27,7 @@ export function Portal(props: PropsWithChildren) { if (node.current) { portalRoot.appendChild(node.current); } + return () => { if (node.current) { portalRoot.removeChild(node.current); @@ -35,7 +38,18 @@ export function Portal(props: PropsWithChildren) { return ReactDOM.createPortal(
    {children}
    , node.current); } +/** @internal */ +export function getPortalContainer() { + return window.document.getElementById('grafana-portal-container') ?? document.body; +} + +/** @internal */ +export function PortalContainer() { + return
    ; +} + export const RefForwardingPortal = React.forwardRef((props, ref) => { return ; }); + RefForwardingPortal.displayName = 'RefForwardingPortal'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 3d04933efe2..f4733b6ff7d 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -6,7 +6,7 @@ export { Tooltip } from './Tooltip/Tooltip'; export { PopoverContent } from './Tooltip/types'; export { PopoverController } from './Tooltip/PopoverController'; export { Popover } from './Tooltip/Popover'; -export { Portal } from './Portal/Portal'; +export { Portal, getPortalContainer, PortalContainer } from './Portal/Portal'; export { CustomScrollbar, ScrollbarPosition } from './CustomScrollbar/CustomScrollbar'; export { TabbedContainer, TabConfig } from './TabbedContainer/TabbedContainer'; diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index 47c00a004b7..95c0477196a 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -3,7 +3,7 @@ import { Router, Route, Redirect, Switch } from 'react-router-dom'; import { config, locationService, navigationLogger } from '@grafana/runtime'; import { Provider } from 'react-redux'; import { store } from 'app/store/store'; -import { ErrorBoundaryAlert, GlobalStyles, ModalRoot, ModalsProvider } from '@grafana/ui'; +import { ErrorBoundaryAlert, GlobalStyles, ModalRoot, ModalsProvider, PortalContainer } from '@grafana/ui'; import { GrafanaApp } from './app'; import { getAppRoutes } from 'app/routes/routes'; import { ConfigContext, ThemeProvider } from './core/utils/ConfigProvider'; @@ -113,6 +113,7 @@ export class AppWrapper extends React.Component + diff --git a/public/app/core/components/NavBar/Next/NavBarItemMenuTrigger.tsx b/public/app/core/components/NavBar/Next/NavBarItemMenuTrigger.tsx index 0d266ac2165..75a216aa3e8 100644 --- a/public/app/core/components/NavBar/Next/NavBarItemMenuTrigger.tsx +++ b/public/app/core/components/NavBar/Next/NavBarItemMenuTrigger.tsx @@ -1,6 +1,6 @@ import React, { ReactElement, useEffect, useState } from 'react'; import { css, cx } from '@emotion/css'; -import { Icon, IconName, Link, useTheme2 } from '@grafana/ui'; +import { getPortalContainer, Icon, IconName, Link, useTheme2 } from '@grafana/ui'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { MenuTriggerProps } from '@react-types/menu'; import { useMenuTriggerState } from '@react-stately/menu'; @@ -182,7 +182,7 @@ export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactE
    {element} {state.isOpen && ( - + Date: Wed, 13 Apr 2022 09:52:51 +0200 Subject: [PATCH 052/121] Chore: Bump minimist to 1.2.6 (#47401) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index edf380093a7..175a6fff990 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26013,9 +26013,9 @@ __metadata: linkType: hard "minimist@npm:^1.1.1, minimist@npm:^1.2.0, minimist@npm:^1.2.5": - version: 1.2.5 - resolution: "minimist@npm:1.2.5" - checksum: 86706ce5b36c16bfc35c5fe3dbb01d5acdc9a22f2b6cc810b6680656a1d2c0e44a0159c9a3ba51fb072bb5c203e49e10b51dcd0eec39c481f4c42086719bae52 + version: 1.2.6 + resolution: "minimist@npm:1.2.6" + checksum: d15428cd1e11eb14e1233bcfb88ae07ed7a147de251441d61158619dfb32c4d7e9061d09cab4825fdee18ecd6fce323228c8c47b5ba7cd20af378ca4048fb3fb languageName: node linkType: hard From 232d880fe07389a840c3dceec8ff3e54079d2bf9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Apr 2022 10:58:20 +0300 Subject: [PATCH 053/121] Bump go.opentelemetry.io/otel/trace from 1.2.0 to 1.6.3 (#47544) Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.2.0 to 1.6.3. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.2.0...v1.6.3) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel/trace dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 +++++--- go.sum | 17 +++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index ee9a6a8221e..28dc2616525 100644 --- a/go.mod +++ b/go.mod @@ -86,7 +86,7 @@ require ( github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/robfig/cron/v3 v3.0.1 github.com/russellhaering/goxmldsig v1.1.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.1 github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f github.com/uber/jaeger-client-go v2.29.1+incompatible @@ -98,10 +98,10 @@ require ( github.com/yudai/gojsondiff v1.0.0 go.opentelemetry.io/collector v0.31.0 go.opentelemetry.io/collector/model v0.31.0 - go.opentelemetry.io/otel v1.2.0 + go.opentelemetry.io/otel v1.6.3 go.opentelemetry.io/otel/exporters/jaeger v1.0.0 go.opentelemetry.io/otel/sdk v1.0.0 - go.opentelemetry.io/otel/trace v1.2.0 + go.opentelemetry.io/otel/trace v1.6.3 golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 golang.org/x/exp v0.0.0-20210220032938-85be41e4509f golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect @@ -274,6 +274,8 @@ require ( github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect github.com/getkin/kin-openapi v0.94.0 // indirect github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt v3.2.1+incompatible // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/klauspost/compress v1.13.6 // indirect diff --git a/go.sum b/go.sum index 45e79610803..8ffd434c46a 100644 --- a/go.sum +++ b/go.sum @@ -922,8 +922,12 @@ github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNV github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= @@ -2411,8 +2415,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -2636,8 +2641,8 @@ go.opentelemetry.io/contrib/zpages v0.0.0-20210722161726-7668016acb73/go.mod h1: go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= go.opentelemetry.io/otel v1.0.0-RC1/go.mod h1:x9tRa9HK4hSSq7jf2TKbqFbtt58/TGk0f9XiEYISI1I= go.opentelemetry.io/otel v1.0.0/go.mod h1:AjRVh9A5/5DE7S+mZtTR6t8vpKKryam+0lREnfmS4cg= -go.opentelemetry.io/otel v1.2.0 h1:YOQDvxO1FayUcT9MIhJhgMyNO1WqoduiyvQHzGN0kUQ= -go.opentelemetry.io/otel v1.2.0/go.mod h1:aT17Fk0Z1Nor9e0uisf98LrntPGMnk4frBO9+dkf69I= +go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE= +go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= go.opentelemetry.io/otel/exporters/jaeger v1.0.0 h1:cLhx8llHw02h5JTqGqaRbYn+QVKHmrzD9vEbKnSPk5U= go.opentelemetry.io/otel/exporters/jaeger v1.0.0/go.mod h1:q10N1AolE1JjqKrFJK2tYw0iZpmX+HBaXBtuCzRnBGQ= go.opentelemetry.io/otel/internal/metric v0.21.0/go.mod h1:iOfAaY2YycsXfYD4kaRSbLx2LKmfpKObWBEv9QK5zFo= @@ -2648,8 +2653,8 @@ go.opentelemetry.io/otel/sdk v1.0.0 h1:BNPMYUONPNbLneMttKSjQhOTlFLOD9U22HNG1KrIN go.opentelemetry.io/otel/sdk v1.0.0/go.mod h1:PCrDHlSy5x1kjezSdL37PhbFUMjrsLRshJ2zCzeXwbM= go.opentelemetry.io/otel/trace v1.0.0-RC1/go.mod h1:86UHmyHWFEtWjfWPSbu0+d0Pf9Q6e1U+3ViBOc+NXAg= go.opentelemetry.io/otel/trace v1.0.0/go.mod h1:PXTWqayeFUlJV1YDNhsJYB184+IvAH814St6o6ajzIs= -go.opentelemetry.io/otel/trace v1.2.0 h1:Ys3iqbqZhcf28hHzrm5WAquMkDHNZTUkw7KHbuNjej0= -go.opentelemetry.io/otel/trace v1.2.0/go.mod h1:N5FLswTubnxKxOJHM7XZC074qpeEdLy3CgAVsdMucK0= +go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc= +go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.starlark.net v0.0.0-20200901195727-6e684ef5eeee/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= From 06d2d44031b23930f5fc2ab3bf5dcd63d6de40b6 Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Wed, 13 Apr 2022 11:02:19 +0200 Subject: [PATCH 054/121] put the default logger format and feature toggle to fallback to old logger (#47584) --- pkg/infra/log/log.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index ea3b824388d..efe85bbf84e 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -446,7 +446,9 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { } var err error - root.gokitLogActivated, err = isNewLoggerActivated(cfg) + isOldLoggerActivated, err := isOldLoggerActivated(cfg) + root.gokitLogActivated = !isOldLoggerActivated + if err != nil { return err } @@ -459,13 +461,13 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { // This would be removed eventually, no need to make a fancy design. // For the sake of important cycle I just copied the function -func isNewLoggerActivated(cfg *ini.File) (bool, error) { +func isOldLoggerActivated(cfg *ini.File) (bool, error) { section := cfg.Section("feature_toggles") toggles, err := readFeatureTogglesFromInitFile(section) if err != nil { return false, err } - return toggles["newlog"], nil + return toggles["oldlog"], nil } func readFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) { From fe3a27a2ab2505dea03c57839e8d61c2b0b8b68f Mon Sep 17 00:00:00 2001 From: Shirley <4163034+fridgepoet@users.noreply.github.com> Date: Wed, 13 Apr 2022 11:15:22 +0200 Subject: [PATCH 055/121] Fix wildcard on branches-ignore backport (#47679) Fixes https://github.com/grafana/code-coverage/issues/11 --- .github/workflows/cloud-data-sources-code-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cloud-data-sources-code-coverage.yml b/.github/workflows/cloud-data-sources-code-coverage.yml index a1af9904e89..f49cd30e3a2 100644 --- a/.github/workflows/cloud-data-sources-code-coverage.yml +++ b/.github/workflows/cloud-data-sources-code-coverage.yml @@ -10,7 +10,7 @@ on: - 'public/app/plugins/datasource/cloud-monitoring/**' branches-ignore: - dependabot/** - - backport-** + - backport-* jobs: workflow-call: From 1957863b67b303999a823b687135e23256eb893b Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Wed, 13 Apr 2022 12:22:39 +0300 Subject: [PATCH 056/121] Update grabpl (#47643) --- .drone.yml | 70 ++++++++++++++++++------------------ scripts/drone/steps/lib.star | 2 +- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.drone.yml b/.drone.yml index d343d174c22..4fc307c5c50 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,7 +11,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -114,7 +114,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -356,7 +356,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -420,7 +420,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -503,7 +503,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -583,7 +583,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -684,7 +684,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1109,7 +1109,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1177,7 +1177,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -1264,7 +1264,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1348,7 +1348,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1650,7 +1650,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1771,7 +1771,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1855,7 +1855,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -1914,7 +1914,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2268,7 +2268,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2441,7 +2441,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2569,7 +2569,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -2644,7 +2644,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2722,7 +2722,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2783,7 +2783,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2862,7 +2862,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2924,7 +2924,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2962,7 +2962,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3011,7 +3011,7 @@ steps: name: initialize - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3061,7 +3061,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3124,7 +3124,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3389,7 +3389,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3503,7 +3503,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3580,7 +3580,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: initialize @@ -3628,7 +3628,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3969,7 +3969,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4132,7 +4132,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4250,7 +4250,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.33/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -4447,6 +4447,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: bc16f36771e14d7c3e167893a48406c750165f049e851641e90c71af4b3d9dd7 +hmac: 48fc25b65552d02f3d75f3ad1e24a9854c0485bebac8d4e062cfa8605e0a75ba ... diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 1743ecb7f68..3ba6b73a9d2 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1,6 +1,6 @@ load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket') -grabpl_version = 'v2.9.33' +grabpl_version = 'v2.9.34' build_image = 'grafana/build-container:1.5.3' publish_image = 'grafana/grafana-ci-deploy:1.3.1' deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image' From 46dc1e98fa36ecb4d5ea4797e2131887d5e17595 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Wed, 13 Apr 2022 11:54:49 +0200 Subject: [PATCH 057/121] Previews: track oss adoption --- pkg/services/thumbs/service.go | 35 ++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/pkg/services/thumbs/service.go b/pkg/services/thumbs/service.go index 8f098780407..4b837baaaa3 100644 --- a/pkg/services/thumbs/service.go +++ b/pkg/services/thumbs/service.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/serverlock" + "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/guardian" @@ -48,6 +49,7 @@ type thumbService struct { store sqlstore.Store crawlLockServiceActionName string log log.Logger + usageStatsService usagestats.Service } type crawlerScheduleOptions struct { @@ -60,7 +62,7 @@ type crawlerScheduleOptions struct { auth CrawlerAuth } -func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockService *serverlock.ServerLockService, renderService rendering.Service, gl *live.GrafanaLive, store *sqlstore.SQLStore, authSetupService CrawlerAuthSetupService) Service { +func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockService *serverlock.ServerLockService, renderService rendering.Service, gl *live.GrafanaLive, store *sqlstore.SQLStore, authSetupService CrawlerAuthSetupService, usageStatsService usagestats.Service) Service { if !features.IsEnabled(featuremgmt.FlagDashboardPreviews) { return &dummyService{} } @@ -73,8 +75,8 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockS logger.Error("Failed to setup auth for the dashboard previews crawler", "err", err) return &dummyService{} } - - return &thumbService{ + t := &thumbService{ + usageStatsService: usageStatsService, renderingService: renderService, renderer: newSimpleCrawler(renderService, gl, thumbnailRepo), thumbnailRepo: thumbnailRepo, @@ -94,6 +96,27 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockS auth: crawlerAuth, }, } + + t.registerUsageStats() + return t +} + +func (hs *thumbService) registerUsageStats() { + hs.usageStatsService.RegisterMetricsFunc(func(ctx context.Context) (map[string]interface{}, error) { + s := hs.getDashboardPreviewsSetupSettings(ctx) + + stats := make(map[string]interface{}) + + if s.SystemRequirements.Met { + stats["stats.dashboard_previews.system_req_met.count"] = 1 + } + + if s.ThumbnailsExist { + stats["stats.dashboard_previews.thumbnails_exist.count"] = 1 + } + + return stats, nil + }) } func (hs *thumbService) Enabled() bool { @@ -203,8 +226,12 @@ func (hs *thumbService) GetImage(c *models.ReqContext) { } func (hs *thumbService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig { + return hs.getDashboardPreviewsSetupSettings(c.Req.Context()) +} + +func (hs *thumbService) getDashboardPreviewsSetupSettings(ctx context.Context) dashboardPreviewsSetupConfig { systemRequirements := hs.getSystemRequirements() - thumbnailsExist, err := hs.thumbnailRepo.doThumbnailsExist(c.Req.Context()) + thumbnailsExist, err := hs.thumbnailRepo.doThumbnailsExist(ctx) if err != nil { return dashboardPreviewsSetupConfig{ From 463f00b93f7d77d556f5fd9382f16e8941071e55 Mon Sep 17 00:00:00 2001 From: Vardan Torosyan Date: Wed, 13 Apr 2022 11:58:18 +0200 Subject: [PATCH 058/121] Reload permissions cache when adding a datasource (#47658) --- public/app/features/datasources/state/actions.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index b2dcdadee76..54fa40cc03d 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -24,6 +24,7 @@ import { } from './reducers'; import { getDataSource, getDataSourceMeta } from './selectors'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; +import { contextSrv } from '../../../core/services/context_srv'; export interface DataSourceTypesLoadedPayload { plugins: DataSourcePluginMeta[]; @@ -217,6 +218,9 @@ export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult { const result = await getBackendSrv().post('/api/datasources', newInstance); await getDatasourceSrv().reload(); + + await contextSrv.fetchUserPermissions(); + locationService.push(`/datasources/edit/${result.datasource.uid}`); }; } From 32511f3d060740ca86cdd584fad4499c8d04533e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Apr 2022 12:26:19 +0200 Subject: [PATCH 059/121] Update Storybook updates to v6.4.21 (#47523) * Update Storybook updates to v6.4.21 * chore(yarn): update yarnrc storybook versions Co-authored-by: Renovate Bot Co-authored-by: Jack Westbrook --- .yarnrc.yml | 26 +- packages/grafana-ui/package.json | 26 +- yarn.lock | 728 +++++++++++++++---------------- 3 files changed, 390 insertions(+), 390 deletions(-) diff --git a/.yarnrc.yml b/.yarnrc.yml index 98678568a76..d14926ae5f9 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -9,28 +9,28 @@ packageExtensions: "@mdx-js/loader@1.6.22": peerDependencies: react: 17.0.1 - "@storybook/addon-docs@6.4.15": + "@storybook/addon-docs@6.4.21": peerDependencies: - "@storybook/manager-webpack5": 6.4.15 - "@storybook/addon-essentials@6.4.15": + "@storybook/manager-webpack5": 6.4.21 + "@storybook/addon-essentials@6.4.21": peerDependencies: - "@storybook/components": 6.4.15 - "@storybook/core-events": 6.4.15 - "@storybook/manager-webpack5": 6.4.15 - "@storybook/theming": 6.4.15 - "@storybook/core-server@6.4.15": + "@storybook/components": 6.4.21 + "@storybook/core-events": 6.4.21 + "@storybook/manager-webpack5": 6.4.21 + "@storybook/theming": 6.4.21 + "@storybook/core-server@6.4.21": peerDependencies: "@babel/core": ^7.0.0 - "@storybook/core@6.4.15": + "@storybook/core@6.4.21": peerDependencies: "@babel/core": ^7.0.0 - "@storybook/manager-webpack5": 6.4.15 - "@storybook/csf-tools@6.4.15": + "@storybook/manager-webpack5": 6.4.21 + "@storybook/csf-tools@6.4.21": peerDependencies: "@babel/core": ^7.0.0 - "@storybook/react@6.4.15": + "@storybook/react@6.4.21": peerDependencies: - "@storybook/manager-webpack5": 6.4.15 + "@storybook/manager-webpack5": 6.4.21 doctrine@3.0.0: dependencies: assert: 2.0.0 diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 10130b923a7..6d588bc85c1 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -100,20 +100,20 @@ "@rollup/plugin-commonjs": "21.0.2", "@rollup/plugin-image": "2.1.1", "@rollup/plugin-node-resolve": "13.1.3", - "@storybook/addon-a11y": "6.4.15", - "@storybook/addon-actions": "6.4.15", - "@storybook/addon-docs": "6.4.15", - "@storybook/addon-essentials": "6.4.15", + "@storybook/addon-a11y": "6.4.21", + "@storybook/addon-actions": "6.4.21", + "@storybook/addon-docs": "6.4.21", + "@storybook/addon-essentials": "6.4.21", "@storybook/addon-knobs": "6.4.0", - "@storybook/addon-storysource": "6.4.15", - "@storybook/addons": "6.4.15", - "@storybook/api": "6.4.15", - "@storybook/builder-webpack5": "6.4.15", - "@storybook/components": "6.4.15", - "@storybook/core-events": "6.4.15", - "@storybook/manager-webpack5": "6.4.15", - "@storybook/react": "6.4.15", - "@storybook/theming": "6.4.15", + "@storybook/addon-storysource": "6.4.21", + "@storybook/addons": "6.4.21", + "@storybook/api": "6.4.21", + "@storybook/builder-webpack5": "6.4.21", + "@storybook/components": "6.4.21", + "@storybook/core-events": "6.4.21", + "@storybook/manager-webpack5": "6.4.21", + "@storybook/react": "6.4.21", + "@storybook/theming": "6.4.21", "@swc/helpers": "0.3.8", "@testing-library/dom": "8.13.0", "@testing-library/jest-dom": "5.16.2", diff --git a/yarn.lock b/yarn.lock index 175a6fff990..d1a6ba3b777 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4369,20 +4369,20 @@ __metadata: "@rollup/plugin-image": 2.1.1 "@rollup/plugin-node-resolve": 13.1.3 "@sentry/browser": 6.19.1 - "@storybook/addon-a11y": 6.4.15 - "@storybook/addon-actions": 6.4.15 - "@storybook/addon-docs": 6.4.15 - "@storybook/addon-essentials": 6.4.15 + "@storybook/addon-a11y": 6.4.21 + "@storybook/addon-actions": 6.4.21 + "@storybook/addon-docs": 6.4.21 + "@storybook/addon-essentials": 6.4.21 "@storybook/addon-knobs": 6.4.0 - "@storybook/addon-storysource": 6.4.15 - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/builder-webpack5": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-events": 6.4.15 - "@storybook/manager-webpack5": 6.4.15 - "@storybook/react": 6.4.15 - "@storybook/theming": 6.4.15 + "@storybook/addon-storysource": 6.4.21 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/builder-webpack5": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-events": 6.4.21 + "@storybook/manager-webpack5": 6.4.21 + "@storybook/react": 6.4.21 + "@storybook/theming": 6.4.21 "@swc/helpers": 0.3.8 "@testing-library/dom": 8.13.0 "@testing-library/jest-dom": 5.16.2 @@ -7555,18 +7555,18 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-a11y@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-a11y@npm:6.4.15" +"@storybook/addon-a11y@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-a11y@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/channels": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/channels": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/theming": 6.4.15 + "@storybook/theming": 6.4.21 axe-core: ^4.2.0 core-js: ^3.8.2 global: ^4.4.0 @@ -7583,20 +7583,20 @@ __metadata: optional: true react-dom: optional: true - checksum: 996081f2279001e2d855f950eece21ea098ac765f9b3749c5f9c109585bd2b293d1e35459d4ea9dc9fb6abccb115414fcd44fdcb4b6878d546f14d7778561eed + checksum: 0e71c20a9516c3ff531f7accd3326177a1c7ede13eead5baa1794e350a9f8a6fc80021638a6419c92e1f88a0a936cb2b0355cb75295a12a2bfebeb890e8bd968 languageName: node linkType: hard -"@storybook/addon-actions@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-actions@npm:6.4.15" +"@storybook/addon-actions@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-actions@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/theming": 6.4.15 + "@storybook/theming": 6.4.21 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -7617,21 +7617,21 @@ __metadata: optional: true react-dom: optional: true - checksum: 311e082bebc9a93372c90feb03ccf3ab0fb591acb69e580e1845d0f76f8d007de12b5667ea35dc83e6ed891ac3f5cf40871cc4902980e4079ba2cd11015dc99f + checksum: 27f6cc73ecc6be33898c8623503d1c6a2db07228d390fa7bf82e03055f7d95faa64d9eab632929ac42cc2a3732de0b26fd06bc3c9397a0e756fb883302a235b6 languageName: node linkType: hard -"@storybook/addon-backgrounds@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-backgrounds@npm:6.4.15" +"@storybook/addon-backgrounds@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-backgrounds@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/theming": 6.4.15 + "@storybook/theming": 6.4.21 core-js: ^3.8.2 global: ^4.4.0 memoizerific: ^1.11.3 @@ -7646,23 +7646,23 @@ __metadata: optional: true react-dom: optional: true - checksum: 71ca7b45d0e3cc042394043959e45bce5f61dc0bfc003c01343b2153c84098abfab32853959c24ed393548bba65d4edc06f764f690855ed832dd30eba169e90b + checksum: 41f2358fce5616886d2e364d19000f284df561973e1fca44873b8f90d6a8fe45f9f84583dcb42fffc68f286e3845ef9576bd2a2970f496e6f363c20cb27be275 languageName: node linkType: hard -"@storybook/addon-controls@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-controls@npm:6.4.15" +"@storybook/addon-controls@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-controls@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-common": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-common": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/node-logger": 6.4.15 - "@storybook/store": 6.4.15 - "@storybook/theming": 6.4.15 + "@storybook/node-logger": 6.4.21 + "@storybook/store": 6.4.21 + "@storybook/theming": 6.4.21 core-js: ^3.8.2 lodash: ^4.17.21 ts-dedent: ^2.0.0 @@ -7674,13 +7674,13 @@ __metadata: optional: true react-dom: optional: true - checksum: d7daa8cdd199d0c1b0d68c027977efa4b60e75aed25e0b60e79e0d4631afc41b6f2df4a6ffd880a014789e0e8b889eea81658e8b8ddaa89abfeba671ccc0f4fc + checksum: c47aaffd6d13afbeac9c471033efe87837a1c46007cf4559fb55c8e4876552acf4e2814c87485cbf4afe70cb18b88e0bbfc239ed6fb8fb3eff72485b179ec12f languageName: node linkType: hard -"@storybook/addon-docs@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-docs@npm:6.4.15" +"@storybook/addon-docs@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-docs@npm:6.4.21" dependencies: "@babel/core": ^7.12.10 "@babel/generator": ^7.12.11 @@ -7691,21 +7691,21 @@ __metadata: "@mdx-js/loader": ^1.6.22 "@mdx-js/mdx": ^1.6.22 "@mdx-js/react": ^1.6.22 - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/builder-webpack4": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/builder-webpack4": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/csf-tools": 6.4.15 - "@storybook/node-logger": 6.4.15 - "@storybook/postinstall": 6.4.15 - "@storybook/preview-web": 6.4.15 - "@storybook/source-loader": 6.4.15 - "@storybook/store": 6.4.15 - "@storybook/theming": 6.4.15 + "@storybook/csf-tools": 6.4.21 + "@storybook/node-logger": 6.4.21 + "@storybook/postinstall": 6.4.21 + "@storybook/preview-web": 6.4.21 + "@storybook/source-loader": 6.4.21 + "@storybook/store": 6.4.21 + "@storybook/theming": 6.4.21 acorn: ^7.4.1 acorn-jsx: ^5.3.1 acorn-walk: ^7.2.0 @@ -7729,12 +7729,12 @@ __metadata: ts-dedent: ^2.0.0 util-deprecate: ^1.0.2 peerDependencies: - "@storybook/angular": 6.4.15 - "@storybook/html": 6.4.15 - "@storybook/react": 6.4.15 - "@storybook/vue": 6.4.15 - "@storybook/vue3": 6.4.15 - "@storybook/web-components": 6.4.15 + "@storybook/angular": 6.4.21 + "@storybook/html": 6.4.21 + "@storybook/react": 6.4.21 + "@storybook/vue": 6.4.21 + "@storybook/vue3": 6.4.21 + "@storybook/web-components": 6.4.21 lit: ^2.0.0 lit-html: ^1.4.1 || ^2.0.0 react: ^16.8.0 || ^17.0.0 @@ -7772,32 +7772,32 @@ __metadata: optional: true webpack: optional: true - checksum: a4245258c9497c50b025bf6c39d60e18c1ca5af736ac24f312f88a6376cb965d1fcc301d507eb859b0ef8b0761264fb0adcf288030ec1d889bf9a5c0f287a8a7 + checksum: df37cdb51c8a2656bb8dbf586f0cae2437f1c654375913e1a500d83a58bd32d3e59b0ae66ec00a5ec2c3352622078e594f6f3b8ac09662de7e7b7ce93cc1b1ba languageName: node linkType: hard -"@storybook/addon-essentials@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-essentials@npm:6.4.15" +"@storybook/addon-essentials@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-essentials@npm:6.4.21" dependencies: - "@storybook/addon-actions": 6.4.15 - "@storybook/addon-backgrounds": 6.4.15 - "@storybook/addon-controls": 6.4.15 - "@storybook/addon-docs": 6.4.15 - "@storybook/addon-measure": 6.4.15 - "@storybook/addon-outline": 6.4.15 - "@storybook/addon-toolbars": 6.4.15 - "@storybook/addon-viewport": 6.4.15 - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/node-logger": 6.4.15 + "@storybook/addon-actions": 6.4.21 + "@storybook/addon-backgrounds": 6.4.21 + "@storybook/addon-controls": 6.4.21 + "@storybook/addon-docs": 6.4.21 + "@storybook/addon-measure": 6.4.21 + "@storybook/addon-outline": 6.4.21 + "@storybook/addon-toolbars": 6.4.21 + "@storybook/addon-viewport": 6.4.21 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/node-logger": 6.4.21 core-js: ^3.8.2 regenerator-runtime: ^0.13.7 ts-dedent: ^2.0.0 peerDependencies: "@babel/core": ^7.9.6 - "@storybook/vue": 6.4.15 - "@storybook/web-components": 6.4.15 + "@storybook/vue": 6.4.21 + "@storybook/web-components": 6.4.21 babel-loader: ^8.0.0 lit-html: ^1.4.1 || ^2.0.0-rc.3 react: ^16.8.0 || ^17.0.0 @@ -7816,7 +7816,7 @@ __metadata: optional: true webpack: optional: true - checksum: 2e05c01380393b870898bca8f5965c9b236d9e31b27f748369620b4d732e6303f930147bd47240bce2416cc7740aa7679808eeba9e2caa41fbbbb1b55563dcae + checksum: aee028eaebb6574a00c41d0540189ac9427d424972cb33c1d2fade68a5ec98560d8848969674acf5f54357c5c97428d14512ebe49cf1c069599d3ea3cac4cb03 languageName: node linkType: hard @@ -7852,15 +7852,15 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-measure@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-measure@npm:6.4.15" +"@storybook/addon-measure@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-measure@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 core-js: ^3.8.2 global: ^4.4.0 @@ -7872,19 +7872,19 @@ __metadata: optional: true react-dom: optional: true - checksum: fe19eeca1ed3957268c6e2d42c4f4e6ce58f513f097a7b5dc34576a4c2fdaa82584365d428119227f4180a7b3a10b3bdc4e88181675526403461c4a5371d9bf1 + checksum: ef3004d789ff258c3eca31c6a7465678d5dbb89b2e7d7739e54749669b7bff927a6185798bd56e0e2137010a46c171f86df1166407334cb5daf7e4a5a7510887 languageName: node linkType: hard -"@storybook/addon-outline@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-outline@npm:6.4.15" +"@storybook/addon-outline@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-outline@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 core-js: ^3.8.2 global: ^4.4.0 @@ -7898,21 +7898,21 @@ __metadata: optional: true react-dom: optional: true - checksum: 6bbfd68754ddecd975f0ea720790973e79f91e9e01f9442fb1199c1adf049330ddd1c01806c0c6c3399a2cda6a9933b49e56b831d8842ce746e41ea70ff5a738 + checksum: 980878641a779084ed28c5c6460ad2e15b3c2fce9b9a4ca425b2a779956b0c84bdc70cfa22109337da1b72253cb8718c1ee2b778dd41200424f2c1e0ba9dba79 languageName: node linkType: hard -"@storybook/addon-storysource@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-storysource@npm:6.4.15" +"@storybook/addon-storysource@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-storysource@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/router": 6.4.15 - "@storybook/source-loader": 6.4.15 - "@storybook/theming": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/router": 6.4.21 + "@storybook/source-loader": 6.4.21 + "@storybook/theming": 6.4.21 core-js: ^3.8.2 estraverse: ^5.2.0 loader-utils: ^2.0.0 @@ -7928,18 +7928,18 @@ __metadata: optional: true react-dom: optional: true - checksum: d87b3bbd79c815a97e794b177267569cb2005b74b3f892bca324e560c433360986371b2c9156baf00586a06838845b2b0fe0503f5393830f9801c9456dbea749 + checksum: 41b1aca46f7cb5aa73dbccd3faf6a959e70846453847bff5009096f020dec290fc0db7d116439c4fbc339347367c6d279ac13f44ed80d67d08893be9df782a53 languageName: node linkType: hard -"@storybook/addon-toolbars@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-toolbars@npm:6.4.15" +"@storybook/addon-toolbars@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-toolbars@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/theming": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/theming": 6.4.21 core-js: ^3.8.2 regenerator-runtime: ^0.13.7 peerDependencies: @@ -7950,20 +7950,20 @@ __metadata: optional: true react-dom: optional: true - checksum: bdccfc40e9e6763c3a0f0787fb274c1b6c5c1b1375717d6637539df663575fe84df3e980148c9cc5ce0123e05c3b2635e151460f9989af0a7d46eed1a452e372 + checksum: ab1460ee855772fc1ea61e65de4f228af9fd5d83e93908bffd50445ca391584299d1f7517ea750cde7c29d56815ccdd1463b91e1f872016956e1a253fe6983fd languageName: node linkType: hard -"@storybook/addon-viewport@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addon-viewport@npm:6.4.15" +"@storybook/addon-viewport@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addon-viewport@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-events": 6.4.15 - "@storybook/theming": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-events": 6.4.21 + "@storybook/theming": 6.4.21 core-js: ^3.8.2 global: ^4.4.0 memoizerific: ^1.11.3 @@ -7977,21 +7977,21 @@ __metadata: optional: true react-dom: optional: true - checksum: 44fe3b03a17fd358bb1e47e37e483c4942adf03e55a0cb38b21a7070631bc20728443657250fc0ccf573722d77a28b9738d8a2c58ce3d8b5c1cfab308cfa6a78 + checksum: 76966f2597f2bb7811d885ee828728cec4e210af8c87dd56589d5b01df4447de163c83a2467f07e420e77584906e04e42e8632a14a9eb358d9b3c9400c30701a languageName: node linkType: hard -"@storybook/addons@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/addons@npm:6.4.15" +"@storybook/addons@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/addons@npm:6.4.21" dependencies: - "@storybook/api": 6.4.15 - "@storybook/channels": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/api": 6.4.21 + "@storybook/channels": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/router": 6.4.15 - "@storybook/theming": 6.4.15 + "@storybook/router": 6.4.21 + "@storybook/theming": 6.4.21 "@types/webpack-env": ^1.16.0 core-js: ^3.8.2 global: ^4.4.0 @@ -7999,21 +7999,21 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: c853099f08ca2ac98826609709a05ba169634f357d93ba033a28820e063fd3a49fb2520d1ec63f87f5f9e7427b94487f57453bbb2fb579528d6742769dcee7e9 + checksum: 16c173711a8261193d003a8b5521789d0e45d977e3aedaafe3d7f5afa0e0ab69864cbda5778935eb8c95267a4cf12ef14163a8a9fb20c27222606a7d1d1471ac languageName: node linkType: hard -"@storybook/api@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/api@npm:6.4.15" +"@storybook/api@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/api@npm:6.4.21" dependencies: - "@storybook/channels": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/channels": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/router": 6.4.15 + "@storybook/router": 6.4.21 "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.4.15 + "@storybook/theming": 6.4.21 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -8027,13 +8027,13 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: 1f071611f5e0c42fa161deaacdf3606004cfdf1f444eddb31ec78afb803915d09b6964f4c8aba5416d2dd0ef0b24ed694107264ec80a1649544b7b37d033cb5e + checksum: c3c32c3f3589d553d8d182aae804c9337ce2d6c8e238b3973fb1cadc1dddd49fff142fc863a9b6fa1f076c802475abbb1b60d461a8cef7d18c6d45b17f8d7721 languageName: node linkType: hard -"@storybook/builder-webpack4@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/builder-webpack4@npm:6.4.15" +"@storybook/builder-webpack4@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/builder-webpack4@npm:6.4.21" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-proposal-class-properties": ^7.12.1 @@ -8056,22 +8056,22 @@ __metadata: "@babel/preset-env": ^7.12.11 "@babel/preset-react": ^7.12.10 "@babel/preset-typescript": ^7.12.7 - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/channel-postmessage": 6.4.15 - "@storybook/channels": 6.4.15 - "@storybook/client-api": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-common": 6.4.15 - "@storybook/core-events": 6.4.15 - "@storybook/node-logger": 6.4.15 - "@storybook/preview-web": 6.4.15 - "@storybook/router": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/channel-postmessage": 6.4.21 + "@storybook/channels": 6.4.21 + "@storybook/client-api": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-common": 6.4.21 + "@storybook/core-events": 6.4.21 + "@storybook/node-logger": 6.4.21 + "@storybook/preview-web": 6.4.21 + "@storybook/router": 6.4.21 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.4.15 - "@storybook/theming": 6.4.15 - "@storybook/ui": 6.4.15 + "@storybook/store": 6.4.21 + "@storybook/theming": 6.4.21 + "@storybook/ui": 6.4.21 "@types/node": ^14.0.10 "@types/webpack": ^4.41.26 autoprefixer: ^9.8.6 @@ -8110,13 +8110,13 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 43f1e999d1ac86887cf0d53862352d95aa26239807c010c46c890b02840b8fb7a7a446c77fa8c34524241665e8e432fbc74b73f4693536239a0d9ec6604e3a1b + checksum: 321a77c4e6f945b588feaeccc954adc314c02aa2d8a9428367a1ddf74c142504b1bb1a9e78d3e4e94ece4331478b56d8a93a86e5a625968ddbec835c2f4e33ba languageName: node linkType: hard -"@storybook/builder-webpack5@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/builder-webpack5@npm:6.4.15" +"@storybook/builder-webpack5@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/builder-webpack5@npm:6.4.21" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-proposal-class-properties": ^7.12.1 @@ -8138,21 +8138,21 @@ __metadata: "@babel/preset-env": ^7.12.11 "@babel/preset-react": ^7.12.10 "@babel/preset-typescript": ^7.12.7 - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/channel-postmessage": 6.4.15 - "@storybook/channels": 6.4.15 - "@storybook/client-api": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-common": 6.4.15 - "@storybook/core-events": 6.4.15 - "@storybook/node-logger": 6.4.15 - "@storybook/preview-web": 6.4.15 - "@storybook/router": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/channel-postmessage": 6.4.21 + "@storybook/channels": 6.4.21 + "@storybook/client-api": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-common": 6.4.21 + "@storybook/core-events": 6.4.21 + "@storybook/node-logger": 6.4.21 + "@storybook/preview-web": 6.4.21 + "@storybook/router": 6.4.21 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.4.15 - "@storybook/theming": 6.4.15 + "@storybook/store": 6.4.21 + "@storybook/theming": 6.4.21 "@types/node": ^14.0.10 babel-loader: ^8.0.0 babel-plugin-macros: ^3.0.1 @@ -8181,60 +8181,60 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: b9be6ed8f093cbcb77bbbaccbe5af5e544e837f480500ed3ee4a3c595a14963cd7b4364d815dc965d5a01adeeb20f8a7fc6912be00b17199f944768cf1a2ec61 + checksum: 1837c75680f0158ec620759c4d09c6ae7fb1d50373b2174c37ff6bf0957dc8bb9bb8edcf15d745c41aaf96efc18776ffcce47c1963307b91919e662003f480ca languageName: node linkType: hard -"@storybook/channel-postmessage@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/channel-postmessage@npm:6.4.15" +"@storybook/channel-postmessage@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/channel-postmessage@npm:6.4.21" dependencies: - "@storybook/channels": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/channels": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/core-events": 6.4.21 core-js: ^3.8.2 global: ^4.4.0 qs: ^6.10.0 telejson: ^5.3.2 - checksum: f47439ca301cc29dbf55dd13166cb9c7fe44084b73f65cc729bd2beb3f4ca5af517f6b789de676793f1b2ee0fd902deb5a4fa619632874b277bc74b180f65f60 + checksum: 00eafe2ab4cab45caefceb542a415296942c311eca062c66de62098e7a716a7c63854c2cf5fcdece00a57da7a5be8d010c3925d3841fccdbd4be5643d14fc842 languageName: node linkType: hard -"@storybook/channel-websocket@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/channel-websocket@npm:6.4.15" +"@storybook/channel-websocket@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/channel-websocket@npm:6.4.21" dependencies: - "@storybook/channels": 6.4.15 - "@storybook/client-logger": 6.4.15 + "@storybook/channels": 6.4.21 + "@storybook/client-logger": 6.4.21 core-js: ^3.8.2 global: ^4.4.0 telejson: ^5.3.2 - checksum: d74ff19d83de1227e26d361ed24b503ee63353c00a20668bbd71b76f45fd8cf825ad698caaa69fd4bf2de383567bd2670a3f3981e8aa71081db32ae9d53029ab + checksum: 4f41091f6026ab10f4fd260508a0db848dadad05eb7d2faaac59fe8ce52d73d4465970e362914e0618313ca9c072e0b9c2d7e28fbfb51f5bd2d60efdb247bda5 languageName: node linkType: hard -"@storybook/channels@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/channels@npm:6.4.15" +"@storybook/channels@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/channels@npm:6.4.21" dependencies: core-js: ^3.8.2 ts-dedent: ^2.0.0 util-deprecate: ^1.0.2 - checksum: 15467a58ddf1da417eb099e9569a85e65804d463f5928fdfa91825c412478d671af4606089d01d6e9ff06eb235bef5d2844bc7dd7a54b793ef480ad2a5cc05a0 + checksum: 8953ac7dc34486406edca55f77e3e592283a9355f52c1608acdfa3845ed5df08b909d08467d8e4df1f6c0f7eb1e97ce3ee8e183d72a79ea6e29da4859da26681 languageName: node linkType: hard -"@storybook/client-api@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/client-api@npm:6.4.15" +"@storybook/client-api@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/client-api@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/channel-postmessage": 6.4.15 - "@storybook/channels": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/channel-postmessage": 6.4.21 + "@storybook/channels": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/store": 6.4.15 + "@storybook/store": 6.4.21 "@types/qs": ^6.9.5 "@types/webpack-env": ^1.16.0 core-js: ^3.8.2 @@ -8251,28 +8251,28 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: f948d6bd8823fe84c1875171d2d77ed00e991d7f491ec6d1667f34fd58c4d92d800ddf7766450a959cbf378397361952b7d1c9f4e2925ee049e1ac79f44a728a + checksum: 895c4eebf092c6804a4215548b0b52d2be2eca4bad1b9d7c050f6a22eaec314f261689c1e9f637c59e91aa1a3588ae791c75580d00b44bac4fe45c4875e1a53d languageName: node linkType: hard -"@storybook/client-logger@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/client-logger@npm:6.4.15" +"@storybook/client-logger@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/client-logger@npm:6.4.21" dependencies: core-js: ^3.8.2 global: ^4.4.0 - checksum: c4fd16d3743cc0baa3c6e8fb0598b12e0f9f34f712055110eefef87ce3e28ff37c6b5de597233718de78ebde492ebdfe0b3735810091b5c2ab684d1426320e1f + checksum: e85ed5379cd214ea0d0441239bc7e9504f9a466dc353409945e4474b5baf1a2c33c437810ddc3c3bacd23df7dc5f498436ec5c58c0c2ef2ab88ebc74a800ecd5 languageName: node linkType: hard -"@storybook/components@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/components@npm:6.4.15" +"@storybook/components@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/components@npm:6.4.21" dependencies: "@popperjs/core": ^2.6.0 - "@storybook/client-logger": 6.4.15 + "@storybook/client-logger": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/theming": 6.4.15 + "@storybook/theming": 6.4.21 "@types/color-convert": ^2.0.0 "@types/overlayscrollbars": ^1.12.0 "@types/react-syntax-highlighter": 11.0.5 @@ -8296,24 +8296,24 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: a49885ff2ed49a565dd74da1e6ed8f24242324b807f318ea2d5540a8d780f88053cdcea9d68a34a6f90d45a37d80723e35800fdd53fa7e1ab1df07a65870c14b + checksum: 24159e630a6fd643672c377c0f118ee5f61a10e4171fa4cdcdac09b6bbfa09b814e60fb65322077b4402cab7e22b782d6d260ce92238d540821ab2e9def6e3e7 languageName: node linkType: hard -"@storybook/core-client@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/core-client@npm:6.4.15" +"@storybook/core-client@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/core-client@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/channel-postmessage": 6.4.15 - "@storybook/channel-websocket": 6.4.15 - "@storybook/client-api": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/channel-postmessage": 6.4.21 + "@storybook/channel-websocket": 6.4.21 + "@storybook/client-api": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/preview-web": 6.4.15 - "@storybook/store": 6.4.15 - "@storybook/ui": 6.4.15 + "@storybook/preview-web": 6.4.21 + "@storybook/store": 6.4.21 + "@storybook/ui": 6.4.21 airbnb-js-shims: ^2.2.1 ansi-to-html: ^0.6.11 core-js: ^3.8.2 @@ -8331,13 +8331,13 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: b2ddd518076475e7dddcbe8bf1f81d945a0becedfc0bf14ee49ee4e1bd3fb213c91bf379f9dad6ffb84fc1ab117d7c35fac1a222334992980ac1aa6c58381f10 + checksum: e30c95770539a31f248e8b322fed371100bf50934e86d61331fde5e8f77a1bbb82ca162c5f9fcc177c38b62482f1ed47681ab8544fb0d977ed766be2ab52c73f languageName: node linkType: hard -"@storybook/core-common@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/core-common@npm:6.4.15" +"@storybook/core-common@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/core-common@npm:6.4.21" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-proposal-class-properties": ^7.12.1 @@ -8360,7 +8360,7 @@ __metadata: "@babel/preset-react": ^7.12.10 "@babel/preset-typescript": ^7.12.7 "@babel/register": ^7.12.1 - "@storybook/node-logger": 6.4.15 + "@storybook/node-logger": 6.4.21 "@storybook/semver": ^7.3.2 "@types/node": ^14.0.10 "@types/pretty-hrtime": ^1.0.0 @@ -8394,34 +8394,34 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: a24aef6d2cdacadae35041ee07dfd0ac4192b1cd9cbbc3faa841d62ffde124da601728a35ca7e5821a725a030c29ab80607b556d2a9563e58b981c324124e486 + checksum: 12d417ec8692ecd467bfbebac083ca762b0b23e7e64162ad1e31b2daf0965e16bc25ffed92aae74639fd209267cc5cafd6b98e1ce27f2ef85368bee66e37d854 languageName: node linkType: hard -"@storybook/core-events@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/core-events@npm:6.4.15" +"@storybook/core-events@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/core-events@npm:6.4.21" dependencies: core-js: ^3.8.2 - checksum: 72dfd7b0d8424955a1347b5fabd133512e87ef0a2b8c75a7a7bfe1a0f0a23ac2ad7f70be79f9aa0ce907f78b992cc274738f938facde28b122d55d0d62abbba5 + checksum: 70c4e9f30f894f176d76a1a0b19b84efc70e0bbf9295e08f7c2595103f903c06b44279d2e6ece73f174d894e68a2e03c9511c8c9cd4b01f5d5e9941c6feaf9a7 languageName: node linkType: hard -"@storybook/core-server@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/core-server@npm:6.4.15" +"@storybook/core-server@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/core-server@npm:6.4.21" dependencies: "@discoveryjs/json-ext": ^0.5.3 - "@storybook/builder-webpack4": 6.4.15 - "@storybook/core-client": 6.4.15 - "@storybook/core-common": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/builder-webpack4": 6.4.21 + "@storybook/core-client": 6.4.21 + "@storybook/core-common": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/csf-tools": 6.4.15 - "@storybook/manager-webpack4": 6.4.15 - "@storybook/node-logger": 6.4.15 + "@storybook/csf-tools": 6.4.21 + "@storybook/manager-webpack4": 6.4.21 + "@storybook/node-logger": 6.4.21 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.4.15 + "@storybook/store": 6.4.21 "@types/node": ^14.0.10 "@types/node-fetch": ^2.5.7 "@types/pretty-hrtime": ^1.0.0 @@ -8454,8 +8454,8 @@ __metadata: webpack: 4 ws: ^8.2.3 peerDependencies: - "@storybook/builder-webpack5": 6.4.15 - "@storybook/manager-webpack5": 6.4.15 + "@storybook/builder-webpack5": 6.4.21 + "@storybook/manager-webpack5": 6.4.21 react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 peerDependenciesMeta: @@ -8465,18 +8465,18 @@ __metadata: optional: true typescript: optional: true - checksum: 52be7c418ddaa60a4e9b8f268d5dea1d4dec1dd518efe8e57e8246b1c09d0290bae7b4c98b02ecc597b78f5dbd46df7e27cbd0cb4c31e51abab2677670193a7c + checksum: 7efb1c4c71dfe5af4cfb392ec0efaacf1c85ce88579196c3475799a96722ddf1c8aeda814e08173621eea17d24d4c8e147e043291abe1518956e15f5e51223f6 languageName: node linkType: hard -"@storybook/core@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/core@npm:6.4.15" +"@storybook/core@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/core@npm:6.4.21" dependencies: - "@storybook/core-client": 6.4.15 - "@storybook/core-server": 6.4.15 + "@storybook/core-client": 6.4.21 + "@storybook/core-server": 6.4.21 peerDependencies: - "@storybook/builder-webpack5": 6.4.15 + "@storybook/builder-webpack5": 6.4.21 react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 webpack: "*" @@ -8485,13 +8485,13 @@ __metadata: optional: true typescript: optional: true - checksum: d3bb7166c4c5ce51a684b8aef89a57b84f6fcf197e83f31a0cf2b60406f56a94997830dc48cf947c622b5a1144c60bc08bb1d6a38bdb68c15c609b0c729f1d95 + checksum: d7be99431d933b8abcb4420f271fb6859a42b5dca5afb88a1c35b7a99e7d624a9c5f9d34be8c47badf589b16020ed182affac0bd4590a5cda3728dff20a52b08 languageName: node linkType: hard -"@storybook/csf-tools@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/csf-tools@npm:6.4.15" +"@storybook/csf-tools@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/csf-tools@npm:6.4.21" dependencies: "@babel/core": ^7.12.10 "@babel/generator": ^7.12.11 @@ -8510,7 +8510,7 @@ __metadata: prettier: ">=2.2.1 <=2.3.0" regenerator-runtime: ^0.13.7 ts-dedent: ^2.0.0 - checksum: f9eb5b5f85ec4ac34c22b616d4536475dd61b66fe4d20f13e3dbcdab511cd7ee74c5ac8568eed3752ceac6d5411504e213f4fb764ddc2ed0c0e47ba36535a6df + checksum: 3c23ad1781bc681d0a8eb9eebeb084613d951c03221e597d8cee5b85514afcb4ae6ebc9748b4f7c34a8bee1b068f14ca443591bbc67de74b7aa1bdd2b7657a61 languageName: node linkType: hard @@ -8523,19 +8523,19 @@ __metadata: languageName: node linkType: hard -"@storybook/manager-webpack4@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/manager-webpack4@npm:6.4.15" +"@storybook/manager-webpack4@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/manager-webpack4@npm:6.4.21" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-transform-template-literals": ^7.12.1 "@babel/preset-react": ^7.12.10 - "@storybook/addons": 6.4.15 - "@storybook/core-client": 6.4.15 - "@storybook/core-common": 6.4.15 - "@storybook/node-logger": 6.4.15 - "@storybook/theming": 6.4.15 - "@storybook/ui": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/core-client": 6.4.21 + "@storybook/core-common": 6.4.21 + "@storybook/node-logger": 6.4.21 + "@storybook/theming": 6.4.21 + "@storybook/ui": 6.4.21 "@types/node": ^14.0.10 "@types/webpack": ^4.41.26 babel-loader: ^8.0.0 @@ -8569,23 +8569,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 88345b8850261a06c829e0424ae9ff7fc803f123b76b022d2b946b91b4d9f2b60419e5f195a5b3d14f0cd354ae1eef8290a3876fb9078c7441aa091cf3fe9656 + checksum: c085f6d81a2ba93eed32098efbb3d80637786e4cda71b2ea3e3f2f13044aaf5245e7532cebc39d954e695ba3aea35fff44d2c057731bfbcf08666a0252082b15 languageName: node linkType: hard -"@storybook/manager-webpack5@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/manager-webpack5@npm:6.4.15" +"@storybook/manager-webpack5@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/manager-webpack5@npm:6.4.21" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-transform-template-literals": ^7.12.1 "@babel/preset-react": ^7.12.10 - "@storybook/addons": 6.4.15 - "@storybook/core-client": 6.4.15 - "@storybook/core-common": 6.4.15 - "@storybook/node-logger": 6.4.15 - "@storybook/theming": 6.4.15 - "@storybook/ui": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/core-client": 6.4.21 + "@storybook/core-common": 6.4.21 + "@storybook/node-logger": 6.4.21 + "@storybook/theming": 6.4.21 + "@storybook/ui": 6.4.21 "@types/node": ^14.0.10 babel-loader: ^8.0.0 case-sensitive-paths-webpack-plugin: ^2.3.0 @@ -8616,42 +8616,42 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: d81668a8bef94195095ed6981c8c6d9b9926fe5cc094fc8a9c36e5aa8875e02e4628f5bbf7423ee77a7f4eff0c9b55d56ad1b6cb04a4db39efff357e44673786 + checksum: 97b154a3e40a9019b2eb2156ad661809d184ae4c53084a0cf15f7b730f6677328302cb6a536f64bc658a255acfe56212e14cee4bfd219fe4ed3a4a19f15109f9 languageName: node linkType: hard -"@storybook/node-logger@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/node-logger@npm:6.4.15" +"@storybook/node-logger@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/node-logger@npm:6.4.21" dependencies: "@types/npmlog": ^4.1.2 chalk: ^4.1.0 core-js: ^3.8.2 npmlog: ^5.0.1 pretty-hrtime: ^1.0.3 - checksum: 3e37a29574b218f363c0687116d115caffe2c6e38380adb68abcd749f406fd15fc59c61b1cc21b91ff47eb2e003087935b9e6c6618deab4528b8464ff5409e52 + checksum: d3b37def30185a3cb8df33fa34f4f85b0d152cfb871922e9591cb7c3fc78e481916cf0aaed3ee45631c3f60fea0242cfca46eab8dd2a7180eebd94719f453537 languageName: node linkType: hard -"@storybook/postinstall@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/postinstall@npm:6.4.15" +"@storybook/postinstall@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/postinstall@npm:6.4.21" dependencies: core-js: ^3.8.2 - checksum: fc81669e1772723e61f9b72084b98cfc3b6450a28c4f6e8884002fee917aca8816b3dc57ccd5cc67b9ff79541e800e15c3f80e65e2e7bfa7e75aec7abde51e47 + checksum: f90c57c85d144e8fd177646fbb19540f109d5f1d399b8dcc7a782048edc97391d8520ef4bbd6ad9e1d577cde61b6d91e5b7e7b9234692202e6b586b7d1fe6e9d languageName: node linkType: hard -"@storybook/preview-web@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/preview-web@npm:6.4.15" +"@storybook/preview-web@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/preview-web@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/channel-postmessage": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/channel-postmessage": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/store": 6.4.15 + "@storybook/store": 6.4.21 ansi-to-html: ^0.6.11 core-js: ^3.8.2 global: ^4.4.0 @@ -8665,7 +8665,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: 66d20941f612d9163852309e545a44f87c98b6dafb5416b3213e8c0ad2eed86a7c55febcba5224403d7de80dce7eeb5a36ff5eb56ca7da55ac47ccabb3d8d169 + checksum: 686175bcc867962c287e1f35a0d2a78b76bdccc55d7120f04fe3c6d10745588575f561ee7523a58e1129798d1c4e5dbc9faecf0960add89313a6d103a810f5be languageName: node linkType: hard @@ -8687,21 +8687,21 @@ __metadata: languageName: node linkType: hard -"@storybook/react@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/react@npm:6.4.15" +"@storybook/react@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/react@npm:6.4.21" dependencies: "@babel/preset-flow": ^7.12.1 "@babel/preset-react": ^7.12.10 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.1 - "@storybook/addons": 6.4.15 - "@storybook/core": 6.4.15 - "@storybook/core-common": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/core": 6.4.21 + "@storybook/core-common": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 - "@storybook/node-logger": 6.4.15 + "@storybook/node-logger": 6.4.21 "@storybook/react-docgen-typescript-plugin": 1.0.2-canary.253f8c1.0 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.4.15 + "@storybook/store": 6.4.21 "@types/webpack-env": ^1.16.0 babel-plugin-add-react-displayname: ^0.0.5 babel-plugin-named-asset-import: ^0.3.1 @@ -8728,15 +8728,15 @@ __metadata: build-storybook: bin/build.js start-storybook: bin/index.js storybook-server: bin/index.js - checksum: 8bd420f025af38029c89526d5374f904770be606e19d1ea10749846185a686d1674f07811f4151a3ba1958940738fc704075873528272ca10c1f65a6c0d1c744 + checksum: 42262eb1e017f7b2ced5945af8b0d1179767028ee8e1f88818989dae9c4f8f3046fa97bd98cff72045680de0aeef1c654874aba2ae0570c62f82922d94fdc47a languageName: node linkType: hard -"@storybook/router@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/router@npm:6.4.15" +"@storybook/router@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/router@npm:6.4.21" dependencies: - "@storybook/client-logger": 6.4.15 + "@storybook/client-logger": 6.4.21 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -8750,7 +8750,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: 10bb86b31bfd4ae2f75986330328f935161ee68472a8401e8bec4c1cab820a2e991bc344ad04d0e595c79bf8f251d70cd39efe1b0685612fc9a48d607759c9f5 + checksum: bd0ce2c5d58efc6326a02002eeb10535c3a558b40995aa25d7664421ab3e2c79f36eeb824da9a6921b032cd3cdeba87b164f61d8f469a816931ffd92ae7a2087 languageName: node linkType: hard @@ -8766,12 +8766,12 @@ __metadata: languageName: node linkType: hard -"@storybook/source-loader@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/source-loader@npm:6.4.15" +"@storybook/source-loader@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/source-loader@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/client-logger": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/client-logger": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 core-js: ^3.8.2 estraverse: ^5.2.0 @@ -8783,17 +8783,17 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: 85e91d62d541d8a41db70280dabcba700a85a24eb57dbe2973403d2733fd57e8575c88b69d40e6042ec9b3c451e87ea5bd121f3fd0853df656d55e8ec60306d9 + checksum: 21aefe8cdfc88d598838b2e5c4814b29d38b263ca27071c450ba9f28942265c533ee5c8bcc0613d98600e63a1a3c6f7d6c7379b1b9829127a1fcd32031e7d293 languageName: node linkType: hard -"@storybook/store@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/store@npm:6.4.15" +"@storybook/store@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/store@npm:6.4.21" dependencies: - "@storybook/addons": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/core-events": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/core-events": 6.4.21 "@storybook/csf": 0.0.2--canary.87bc651.0 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 @@ -8809,18 +8809,18 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: f0123dec020d19badaa3388137e8712f7607067fef976bfc2747f8818f58b22b2daed94c17ae5a91ba4058f8a8cf354bfd54e9dfdaff3e75e01cde14dc4a9998 + checksum: beaa302c6e3c24e53307f8c8219c1c30c4b8bf5ecb7be46fbb1fafd99c24963052a29c8d46c7b7934ab52da67ae262025246bc8ae47ba21c008769eba1bcd359 languageName: node linkType: hard -"@storybook/theming@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/theming@npm:6.4.15" +"@storybook/theming@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/theming@npm:6.4.21" dependencies: "@emotion/core": ^10.1.1 "@emotion/is-prop-valid": ^0.8.6 "@emotion/styled": ^10.0.27 - "@storybook/client-logger": 6.4.15 + "@storybook/client-logger": 6.4.21 core-js: ^3.8.2 deep-object-diff: ^1.1.0 emotion-theming: ^10.0.27 @@ -8832,24 +8832,24 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: 2a9217bbc0ecbf85733fb64fc506e210e33897980fb9907aaa722412728391f702922a1c3ab53fd6c1c683268b72934f29c8c5b5f3232cf07c2674ea48216a49 + checksum: 11ab5d5f1708b648a0ab5c516e8d84ebb165d2fd7167af80ac38a549f40b7b1d0e3ea4e5e2a94bf71fb45f8d4e8cb1d48fee1f63c12d181e8906a7ef149601ea languageName: node linkType: hard -"@storybook/ui@npm:6.4.15": - version: 6.4.15 - resolution: "@storybook/ui@npm:6.4.15" +"@storybook/ui@npm:6.4.21": + version: 6.4.21 + resolution: "@storybook/ui@npm:6.4.21" dependencies: "@emotion/core": ^10.1.1 - "@storybook/addons": 6.4.15 - "@storybook/api": 6.4.15 - "@storybook/channels": 6.4.15 - "@storybook/client-logger": 6.4.15 - "@storybook/components": 6.4.15 - "@storybook/core-events": 6.4.15 - "@storybook/router": 6.4.15 + "@storybook/addons": 6.4.21 + "@storybook/api": 6.4.21 + "@storybook/channels": 6.4.21 + "@storybook/client-logger": 6.4.21 + "@storybook/components": 6.4.21 + "@storybook/core-events": 6.4.21 + "@storybook/router": 6.4.21 "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.4.15 + "@storybook/theming": 6.4.21 copy-to-clipboard: ^3.3.1 core-js: ^3.8.2 core-js-pure: ^3.8.2 @@ -8871,7 +8871,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 react-dom: ^16.8.0 || ^17.0.0 - checksum: dc0f8b84964ca68cc90da007a32470897b5fc16889e234de3008842e770c586877b146ec19361b54b123e5f245c346cd79f60943eee619f76818ce93bed03e44 + checksum: 8db7c619e6e91f8ca2cb6249bcb911be50e0cd3c463ad99b11e85b326449a78b32f78a359d6ebf4fd6b7dd0841bba04a5dd9fbba33d096432b7cc7b1a87d4b92 languageName: node linkType: hard From d7516f11b360df2a04b257c07110528c72a92081 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 13 Apr 2022 14:05:29 +0200 Subject: [PATCH 060/121] Add permission checking to alert rules in the folders view (#47686) --- public/app/features/folders/state/navModel.ts | 19 +++++++++++-------- public/app/routes/routes.tsx | 2 ++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts index 628c5e0089e..7f4a04e9b88 100644 --- a/public/app/features/folders/state/navModel.ts +++ b/public/app/features/folders/state/navModel.ts @@ -1,6 +1,7 @@ import { NavModel, NavModelItem } from '@grafana/data'; +import { contextSrv } from 'app/core/services/context_srv'; -import { FolderDTO } from 'app/types'; +import { AccessControlAction, FolderDTO } from 'app/types'; export function buildNavModel(folder: FolderDTO): NavModelItem { const model = { @@ -29,13 +30,15 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { url: `${folder.url}/library-panels`, }); - model.children.push({ - active: false, - icon: 'bell', - id: `folder-alerting-${folder.uid}`, - text: 'Alert rules', - url: `${folder.url}/alerting`, - }); + if (contextSrv.hasPermission(AccessControlAction.AlertingRuleRead)) { + model.children.push({ + active: false, + icon: 'bell', + id: `folder-alerting-${folder.uid}`, + text: 'Alert rules', + url: `${folder.url}/alerting`, + }); + } if (folder.canAdmin) { model.children.push({ diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 022b0b42aee..bb8964345ca 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -406,6 +406,8 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/dashboards/f/:uid/:slug/alerting', + roles: () => + contextSrv.evaluatePermission(() => ['Viewer', 'Editor', 'Admin'], [AccessControlAction.AlertingRuleRead]), component: SafeDynamicImport( () => import(/* webpackChunkName: "FolderAlerting"*/ 'app/features/folders/FolderAlerting') ), From ab5e2e10cecdbe2c037912444ca2c14ca757dc75 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Wed, 13 Apr 2022 15:06:22 +0300 Subject: [PATCH 061/121] Remove brackets (#47691) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de012ddeaa7..92f076ff019 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "*pkg/**/*.go": [ "gofmt -w -s" ], - "*.{star}": [ + "*.star": [ "make drone" ] }, From 673a2ab49e932aac3e969470e6ea14b4f7ad4d9e Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Wed, 13 Apr 2022 13:40:58 +0100 Subject: [PATCH 062/121] fix: bug where disabled didnt disable the use of service account (#47688) --- pkg/models/user.go | 1 + pkg/services/contexthandler/contexthandler.go | 19 +++++++++++++------ pkg/services/sqlstore/user.go | 1 + 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/models/user.go b/pkg/models/user.go index 4d4b2bace22..b2419637364 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -182,6 +182,7 @@ type SignedInUser struct { OrgCount int IsGrafanaAdmin bool IsAnonymous bool + IsDisabled bool HelpFlags1 HelpFlags1 LastSeenAt time.Time Teams []int64 diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index de9752c1fca..386ea1ea1b4 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -4,6 +4,7 @@ package contexthandler import ( "context" "errors" + "net/http" "net/url" "strconv" "strings" @@ -255,20 +256,26 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo //There is a service account attached to the API key //Use service account linked to API key as the signed in user - query := models.GetSignedInUserQuery{UserId: *apikey.ServiceAccountId, OrgId: apikey.OrgId} - if err := h.SQLStore.GetSignedInUserWithCacheCtx(reqContext.Req.Context(), &query); err != nil { + querySignedInUser := models.GetSignedInUserQuery{UserId: *apikey.ServiceAccountId, OrgId: apikey.OrgId} + if err := h.SQLStore.GetSignedInUserWithCacheCtx(reqContext.Req.Context(), &querySignedInUser); err != nil { reqContext.Logger.Error( "Failed to link API key to service account in", - "id", query.UserId, - "org", query.OrgId, + "id", querySignedInUser.UserId, + "org", querySignedInUser.OrgId, "err", err, ) - reqContext.JsonApiErr(500, "Unable to link API key to service account", err) + reqContext.JsonApiErr(http.StatusInternalServerError, "Unable to link API key to service account", err) + return true + } + + // disabled service accounts are not allowed to access the API + if querySignedInUser.Result.IsDisabled { + reqContext.JsonApiErr(http.StatusUnauthorized, "Service account is disabled", nil) return true } reqContext.IsSignedIn = true - reqContext.SignedInUser = query.Result + reqContext.SignedInUser = querySignedInUser.Result return true } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index b159dcd6bf8..d119422ce1c 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -535,6 +535,7 @@ func (ss *SQLStore) GetSignedInUser(ctx context.Context, query *models.GetSigned u.email as email, u.login as login, u.name as name, + u.is_disabled as is_disabled, u.help_flags1 as help_flags1, u.last_seen_at as last_seen_at, (SELECT COUNT(*) FROM org_user where org_user.user_id = u.id) as org_count, From c3db3121b0c08e316df013135b33952a9ffcc5b4 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 13 Apr 2022 14:46:58 +0200 Subject: [PATCH 063/121] Chore: Bump moment to 2.29.2 (#47680) * chore: bump moment to 2.29.2 * chore(yarn): clean up remaining moment nested deps --- package.json | 4 ++-- packages/grafana-data/package.json | 2 +- packages/grafana-ui/package.json | 2 +- packages/jaeger-ui-components/package.json | 2 +- yarn.lock | 20 ++++++++++---------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 92f076ff019..50c6939f8ca 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "@types/papaparse": "5.3.2", "@types/pluralize": "^0.0.29", "@types/prismjs": "1.26.0", - "@types/rc-time-picker": "^3", + "@types/rc-time-picker": "3.4.1", "@types/react": "17.0.42", "@types/react-beautiful-dnd": "13.1.2", "@types/react-dom": "17.0.14", @@ -325,7 +325,7 @@ "lru-cache": "7.7.1", "memoize-one": "6.0.0", "minisearch": "5.0.0-beta1", - "moment": "2.29.1", + "moment": "2.29.2", "moment-timezone": "0.5.34", "monaco-editor": "^0.31.1", "monaco-promql": "^1.7.2", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 2f761e9a3f3..b4252fa7bb3 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -29,7 +29,7 @@ "eventemitter3": "4.0.7", "lodash": "4.17.21", "marked": "4.0.12", - "moment": "2.29.1", + "moment": "2.29.2", "moment-timezone": "0.5.34", "ol": "6.14.1", "papaparse": "5.3.2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 6d588bc85c1..a9485cc8168 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -58,7 +58,7 @@ "jquery": "3.6.0", "lodash": "4.17.21", "memoize-one": "6.0.0", - "moment": "2.29.1", + "moment": "2.29.2", "monaco-editor": "^0.31.1", "ol": "6.14.1", "prismjs": "1.27.0", diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index b69f93aad83..1ede766e773 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -39,7 +39,7 @@ "lodash": "4.17.21", "lru-memoize": "^1.1.0", "memoize-one": "6.0.0", - "moment": "2.29.1", + "moment": "2.29.2", "moment-timezone": "0.5.34", "prop-types": "15.8.1", "react": "17.0.2", diff --git a/yarn.lock b/yarn.lock index d1a6ba3b777..3255bdf855f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4018,7 +4018,7 @@ __metadata: history: 4.10.1 lodash: 4.17.21 marked: 4.0.12 - moment: 2.29.1 + moment: 2.29.2 moment-timezone: 0.5.34 ol: 6.14.1 papaparse: 5.3.2 @@ -4440,7 +4440,7 @@ __metadata: lodash: 4.17.21 memoize-one: 6.0.0 mock-raf: 1.0.1 - moment: 2.29.1 + moment: 2.29.2 monaco-editor: ^0.31.1 ol: 6.14.1 postcss: 8.4.12 @@ -4614,7 +4614,7 @@ __metadata: lodash: 4.17.21 lru-memoize: ^1.1.0 memoize-one: 6.0.0 - moment: 2.29.1 + moment: 2.29.2 moment-timezone: 0.5.34 prop-types: 15.8.1 react: 17.0.2 @@ -10374,7 +10374,7 @@ __metadata: languageName: node linkType: hard -"@types/rc-time-picker@npm:^3": +"@types/rc-time-picker@npm:3.4.1": version: 3.4.1 resolution: "@types/rc-time-picker@npm:3.4.1" dependencies: @@ -20494,7 +20494,7 @@ __metadata: "@types/papaparse": 5.3.2 "@types/pluralize": ^0.0.29 "@types/prismjs": 1.26.0 - "@types/rc-time-picker": ^3 + "@types/rc-time-picker": 3.4.1 "@types/react": 17.0.42 "@types/react-beautiful-dnd": 13.1.2 "@types/react-dom": 17.0.14 @@ -20605,7 +20605,7 @@ __metadata: memoize-one: 6.0.0 mini-css-extract-plugin: 2.6.0 minisearch: 5.0.0-beta1 - moment: 2.29.1 + moment: 2.29.2 moment-timezone: 0.5.34 monaco-editor: ^0.31.1 monaco-promql: ^1.7.2 @@ -26255,10 +26255,10 @@ __metadata: languageName: node linkType: hard -"moment@npm:2.29.1, moment@npm:2.x, moment@npm:>= 2.9.0, moment@npm:^2.19.4, moment@npm:^2.20.1": - version: 2.29.1 - resolution: "moment@npm:2.29.1" - checksum: 1e14d5f422a2687996be11dd2d50c8de3bd577c4a4ca79ba5d02c397242a933e5b941655de6c8cb90ac18f01cc4127e55b4a12ae3c527a6c0a274e455979345e +"moment@npm:2.29.2, moment@npm:2.x, moment@npm:>= 2.9.0, moment@npm:^2.19.4, moment@npm:^2.20.1": + version: 2.29.2 + resolution: "moment@npm:2.29.2" + checksum: ee850b5776485e2af0775ceb3cfebaa7d7638f0a750fe0678fcae24c310749f96c1938808384bd422a55e5703834a71fcb09c8a1d36d9cf847f6ed0205d7a3e5 languageName: node linkType: hard From 59a0c715f48b1554c3318ca2ac457087b9fa3314 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Apr 2022 13:51:06 +0100 Subject: [PATCH 064/121] Update dependency react-select-event to v5.5.0 (#47308) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 50c6939f8ca..9177778c959 100644 --- a/package.json +++ b/package.json @@ -219,7 +219,7 @@ "prettier": "2.6.0", "raw-loader": "4.0.2", "react-refresh": "0.11.0", - "react-select-event": "^5.1.0", + "react-select-event": "5.5.0", "react-test-renderer": "17.0.2", "redux-mock-store": "1.5.4", "rimraf": "3.0.2", diff --git a/yarn.lock b/yarn.lock index 3255bdf855f..5962185a9ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20651,7 +20651,7 @@ __metadata: react-reverse-portal: ^2.0.1 react-router-dom: ^5.2.0 react-select: 5.2.2 - react-select-event: ^5.1.0 + react-select-event: 5.5.0 react-split-pane: 0.1.92 react-table: ^7.7.0 react-test-renderer: 17.0.2 @@ -31096,6 +31096,15 @@ __metadata: languageName: node linkType: hard +"react-select-event@npm:5.5.0": + version: 5.5.0 + resolution: "react-select-event@npm:5.5.0" + dependencies: + "@testing-library/dom": ">=7" + checksum: 03fd3f5aee9f31667d6c965c532d56e8bb3de993f3c150c4d1a44f0c97e90395152b2fbc06c3707bee12a640b76213d119a12b8749f6692e7b0d90b7b797d83e + languageName: node + linkType: hard + "react-select-event@npm:^5.1.0": version: 5.3.0 resolution: "react-select-event@npm:5.3.0" From 337d5bf423ab23be91c3a6d30fa1a9c39269fd00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Apr 2022 13:10:24 +0000 Subject: [PATCH 065/121] Update dependency @betterer/betterer to v5.2.1 (#47700) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 9177778c959..1647c9e05b0 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "@babel/preset-env": "7.16.11", "@babel/preset-react": "7.16.7", "@babel/preset-typescript": "7.16.7", - "@betterer/betterer": "5.1.7", + "@betterer/betterer": "5.2.1", "@betterer/cli": "5.1.7", "@betterer/regexp": "5.1.7", "@emotion/eslint-plugin": "11.7.0", diff --git a/yarn.lock b/yarn.lock index 5962185a9ca..f79e96fbfa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3239,7 +3239,34 @@ __metadata: languageName: node linkType: hard -"@betterer/betterer@npm:5.1.7, @betterer/betterer@npm:^5.1.7": +"@betterer/betterer@npm:5.2.1, @betterer/betterer@npm:^5.2.1": + version: 5.2.1 + resolution: "@betterer/betterer@npm:5.2.1" + dependencies: + "@betterer/constraints": ^5.0.0 + "@betterer/errors": ^5.0.0 + "@betterer/logger": ^5.2.0 + "@betterer/reporter": ^5.2.1 + "@phenomnomnominal/debug": ^0.2.5 + "@phenomnomnominal/worker-require": ^0.0.34 + chokidar: ^3.3.1 + djb2a: ^1.2.0 + fast-memoize: ^2.5.2 + lines-and-columns: ^2.0.3 + minimatch: ^5.0.1 + prettier: ^2.3.2 + simple-git: ^3.1.1 + ts-node: ^10.2.1 + tslib: ^2.3.1 + typescript: ">=2.7" + dependenciesMeta: + typescript: + optional: true + checksum: 7af5030332d4161c966ab4ff0c1e4cd1cd02fa242b4cd5d9f8a52cba48b10c00235d3d280e806fca096537e4062a5a94fdf49e25988faa7e27ec561b142d11d6 + languageName: node + linkType: hard + +"@betterer/betterer@npm:^5.1.7": version: 5.1.7 resolution: "@betterer/betterer@npm:5.1.7" dependencies: @@ -3321,6 +3348,20 @@ __metadata: languageName: node linkType: hard +"@betterer/logger@npm:^5.2.0": + version: 5.2.0 + resolution: "@betterer/logger@npm:5.2.0" + dependencies: + "@babel/code-frame": ^7.10.3 + jest-diff: ^27.0.6 + lines-and-columns: ^2.0.3 + react: ">=16.8.4 || >=17.0.0" + react-dom: ">=16.8.4 || >=17.0.0" + tslib: ^2.3.1 + checksum: c9789050d3f95e39a5165a97dcafdc9da194b57bb351ed993b92a37e6a602c345a00825a8ad6e3a4037e4c428532c502f231d45dfa27a29c569cd76d6b504105 + languageName: node + linkType: hard + "@betterer/regexp@npm:5.1.7": version: 5.1.7 resolution: "@betterer/regexp@npm:5.1.7" @@ -3349,6 +3390,23 @@ __metadata: languageName: node linkType: hard +"@betterer/reporter@npm:^5.2.1": + version: 5.2.1 + resolution: "@betterer/reporter@npm:5.2.1" + dependencies: + "@betterer/betterer": ^5.2.1 + "@betterer/errors": ^5.0.0 + "@betterer/logger": ^5.2.0 + "@betterer/tasks": ^5.2.0 + ink: ^3.0.9 + ink-text-input: ^4.0.1 + react: ">=16.8.4 || >=17.0.0" + react-dom: ">=16.8.4 || >=17.0.0" + tslib: ^2.3.1 + checksum: 53b58bfcbd3333480cb13e62d2526b70ace020768e6cccd147088b7b6ecf3d011292e529dced0967b6196cf029748458412ff4b2a16469e41fa75e4e962bc8e8 + languageName: node + linkType: hard + "@betterer/tasks@npm:^5.1.7": version: 5.1.7 resolution: "@betterer/tasks@npm:5.1.7" @@ -3364,6 +3422,21 @@ __metadata: languageName: node linkType: hard +"@betterer/tasks@npm:^5.2.0": + version: 5.2.0 + resolution: "@betterer/tasks@npm:5.2.0" + dependencies: + "@betterer/errors": ^5.0.0 + "@betterer/logger": ^5.2.0 + chalk: ^4.1.2 + ink: ^3.0.9 + react: ">=16.8.4 || >=17.0.0" + react-dom: ">=16.8.4 || >=17.0.0" + tslib: ^2.3.1 + checksum: aab9c9c76c21396b553aa1f0e86d8760864f3156f222b724d13f53ac0264aa2a67ee37b9ec515ddf56b819bbaf6c2fd0138d540ca7b5f0eba404b0207b07c1ec + languageName: node + linkType: hard + "@braintree/sanitize-url@npm:6.0.0": version: 6.0.0 resolution: "@braintree/sanitize-url@npm:6.0.0" @@ -13825,6 +13898,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: ^1.0.0 + checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 + languageName: node + linkType: hard + "brace@npm:0.11.1": version: 0.11.1 resolution: "brace@npm:0.11.1" @@ -20405,7 +20487,7 @@ __metadata: "@babel/preset-env": 7.16.11 "@babel/preset-react": 7.16.7 "@babel/preset-typescript": 7.16.7 - "@betterer/betterer": 5.1.7 + "@betterer/betterer": 5.2.1 "@betterer/cli": 5.1.7 "@betterer/regexp": 5.1.7 "@emotion/css": 11.7.1 @@ -24830,6 +24912,13 @@ __metadata: languageName: node linkType: hard +"lines-and-columns@npm:^2.0.3": + version: 2.0.3 + resolution: "lines-and-columns@npm:2.0.3" + checksum: 5955363dfd7d3d7c476d002eb47944dbe0310d57959e2112dce004c0dc76cecfd479cf8c098fd479ff344acdf04ee0e82b455462a26492231ac152f6c48d17a1 + languageName: node + linkType: hard + "lint-staged@npm:12.3.7": version: 12.3.7 resolution: "lint-staged@npm:12.3.7" @@ -25994,6 +26083,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^5.0.1": + version: 5.0.1 + resolution: "minimatch@npm:5.0.1" + dependencies: + brace-expansion: ^2.0.1 + checksum: b34b98463da4754bc526b244d680c69d4d6089451ebe512edaf6dd9eeed0279399cfa3edb19233513b8f830bf4bfcad911dddcdf125e75074100d52f724774f0 + languageName: node + linkType: hard + "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" From e86b6662a187c77656f72bef3b0022bf5ced8b98 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Wed, 13 Apr 2022 15:24:13 +0200 Subject: [PATCH 066/121] Chore: Remove bus.Bus field (#47695) * Chore: Remove bus.Bus field * fix integration test --- pkg/api/admin_users_test.go | 6 -- pkg/api/common_test.go | 4 -- pkg/api/dashboard_test.go | 8 +-- pkg/api/folder_test.go | 2 - pkg/api/frontendsettings_test.go | 2 - pkg/api/http_server.go | 5 +- pkg/api/login_oauth_test.go | 2 - pkg/api/pluginproxy/ds_proxy_test.go | 57 +++++++++---------- pkg/api/user_test.go | 3 +- pkg/api/user_token_test.go | 6 -- pkg/bus/bus.go | 5 -- pkg/cmd/grafana-cli/commands/commands.go | 3 +- pkg/plugins/plugincontext/plugincontext.go | 5 +- pkg/services/alerting/engine.go | 5 +- .../alerting/engine_integration_test.go | 2 +- pkg/services/alerting/engine_test.go | 4 +- pkg/services/alerting/service.go | 5 +- pkg/services/alerting/service_test.go | 3 +- .../dashboardsnapshots/dashboardsnapshots.go | 5 +- .../datasources/service/datasource_service.go | 5 +- .../service/datasource_service_test.go | 39 +++++++------ pkg/services/live/live.go | 5 +- .../authinfoservice/database/database.go | 5 +- .../login/authinfoservice/user_auth_test.go | 3 +- .../login/loginservice/loginservice.go | 5 +- .../login/loginservice/loginservice_test.go | 4 -- pkg/services/search/service.go | 5 +- pkg/services/sqlstore/sqlstore.go | 11 ++-- .../alerting/api_admin_configuration_test.go | 3 - .../api_alertmanager_configuration_test.go | 7 --- .../api/alerting/api_alertmanager_test.go | 15 ----- .../alerting/api_available_channel_test.go | 2 - .../alerting/api_notification_channel_test.go | 10 ---- pkg/tests/api/alerting/api_prometheus_test.go | 7 --- .../api/alerting/api_provisioning_test.go | 3 - pkg/tests/api/alerting/api_ruler_test.go | 7 --- pkg/tests/api/plugins/api_plugins_test.go | 3 - pkg/tsdb/legacydata/service/service_test.go | 3 +- 38 files changed, 70 insertions(+), 204 deletions(-) diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index 0ba11679d3c..1ab6c368b38 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" @@ -269,7 +268,6 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := HTTPServer{ - Bus: bus.GetBus(), AuthTokenService: auth.NewFakeUserAuthTokenService(), SQLStore: sqlStore, } @@ -297,7 +295,6 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ - Bus: bus.GetBus(), AuthTokenService: fakeAuthTokenService, SQLStore: sqlStore, } @@ -326,7 +323,6 @@ func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, route fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ - Bus: bus.GetBus(), AuthTokenService: fakeAuthTokenService, SQLStore: sqlStore, } @@ -355,7 +351,6 @@ func adminDisableUserScenario(t *testing.T, desc string, action string, url stri authInfoService := &logintest.AuthInfoServiceFake{} hs := HTTPServer{ - Bus: bus.GetBus(), SQLStore: mockstore.NewSQLStoreMock(), AuthTokenService: fakeAuthTokenService, authInfoService: authInfoService, @@ -405,7 +400,6 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := HTTPServer{ - Bus: bus.GetBus(), Login: loginservice.LoginServiceMock{ ExpectedUserForm: cmd, NoExistingOrgId: nonExistingOrgID, diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index c438c28a4c1..18986610b82 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" @@ -236,7 +235,6 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin store := sqlstore.InitTestDB(t) hs := &HTTPServer{ Cfg: cfg, - Bus: bus.GetBus(), Live: newTestLive(t, store), Features: features, QuotaService: "a.QuotaService{Cfg: cfg}, @@ -327,7 +325,6 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer { return &HTTPServer{ Cfg: cfg, Features: features, - Bus: bus.GetBus(), AccessControl: accesscontrolmock.New().WithDisabled(), } } @@ -378,7 +375,6 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo hs := &HTTPServer{ Cfg: cfg, Features: features, - Bus: bus.GetBus(), Live: newTestLive(t, db), QuotaService: "a.QuotaService{Cfg: cfg}, RouteRegister: routeRegister, diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index dc2a22629ff..0a3735e284a 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" @@ -46,7 +45,7 @@ func TestGetHomeDashboard(t *testing.T) { cfg.StaticRootPath = "../../public/" hs := &HTTPServer{ - Cfg: cfg, Bus: bus.New(), + Cfg: cfg, pluginStore: &fakePluginStore{}, SQLStore: mockstore.NewSQLStoreMock(), } @@ -96,7 +95,7 @@ func newTestLive(t *testing.T, store *sqlstore.SQLStore) *live.GrafanaLive { nil, &usagestats.UsageStatsMock{T: t}, nil, - features, nil, accesscontrolmock.New()) + features, accesscontrolmock.New()) require.NoError(t, err) return gLive } @@ -991,7 +990,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { cfg := setting.NewCfg() hs := HTTPServer{ - Bus: bus.GetBus(), Cfg: cfg, ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), Live: newTestLive(t, sqlstore.InitTestDB(t)), @@ -1027,7 +1025,6 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string cfg := setting.NewCfg() hs := HTTPServer{ Cfg: cfg, - Bus: bus.GetBus(), ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), Live: newTestLive(t, sqlstore.InitTestDB(t)), QuotaService: "a.QuotaService{Cfg: cfg}, @@ -1062,7 +1059,6 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout mockSQLStore := mockstore.NewSQLStoreMock() hs := HTTPServer{ Cfg: cfg, - Bus: bus.GetBus(), ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), Live: newTestLive(t, sqlstore.InitTestDB(t)), QuotaService: "a.QuotaService{Cfg: cfg}, diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index aa437ea2cf5..d62b4ad68c5 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -139,7 +138,6 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st cmd models.CreateFolderCommand, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := HTTPServer{ - Bus: bus.GetBus(), Cfg: setting.NewCfg(), folderService: folderService, Features: featuremgmt.WithFeatures(), diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index 707145f7378..26c3a939090 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -7,7 +7,6 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/bus" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" @@ -45,7 +44,6 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt. hs := &HTTPServer{ Cfg: cfg, Features: features, - Bus: bus.GetBus(), License: &licensing.OSSLicensingService{Cfg: cfg}, RenderService: &rendering.RenderingService{ Cfg: cfg, diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index fc87b642ca5..a2ff9d8e6d9 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/routing" httpstatic "github.com/grafana/grafana/pkg/api/static" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" @@ -85,7 +84,6 @@ type HTTPServer struct { PluginContextProvider *plugincontext.Provider RouteRegister routing.RouteRegister - Bus bus.Bus RenderService rendering.Service Cfg *setting.Cfg Features *featuremgmt.FeatureManager @@ -155,7 +153,7 @@ type ServerOptions struct { Listener net.Listener } -func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routing.RouteRegister, bus bus.Bus, +func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routing.RouteRegister, renderService rendering.Service, licensing models.Licensing, hooksService *hooks.HooksService, cacheService *localcache.CacheService, sqlStore *sqlstore.SQLStore, alertEngine *alerting.AlertEngine, pluginRequestValidator models.PluginRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver, @@ -188,7 +186,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi hs := &HTTPServer{ Cfg: cfg, RouteRegister: routeRegister, - Bus: bus, RenderService: renderService, License: licensing, HooksService: hooksService, diff --git a/pkg/api/login_oauth_test.go b/pkg/api/login_oauth_test.go index a9af0b79544..cc0c41f745d 100644 --- a/pkg/api/login_oauth_test.go +++ b/pkg/api/login_oauth_test.go @@ -13,7 +13,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/licensing" @@ -34,7 +33,6 @@ func setupOAuthTest(t *testing.T, cfg *setting.Cfg) *web.Mux { hs := &HTTPServer{ Cfg: cfg, - Bus: bus.GetBus(), License: &licensing.OSSLicensingService{Cfg: cfg}, SQLStore: sqlStore, SocialService: social.ProvideService(cfg), diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 39ff80a704c..d0784e839eb 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -13,7 +13,6 @@ import ( "time" "github.com/grafana/grafana/pkg/api/datasource" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/tracing" @@ -129,7 +128,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When matching route path", func(t *testing.T) { ctx, req := setUp() - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -142,7 +141,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When matching route path and has dynamic url", func(t *testing.T) { ctx, req := setUp() - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) proxy.matchedRoute = routes[3] @@ -154,7 +153,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When matching route path with no url", func(t *testing.T) { ctx, req := setUp() - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) proxy.matchedRoute = routes[4] @@ -165,7 +164,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When matching route path and has dynamic body", func(t *testing.T) { ctx, req := setUp() - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) proxy.matchedRoute = routes[5] @@ -179,7 +178,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("Validating request", func(t *testing.T) { t.Run("plugin route with valid role", func(t *testing.T) { ctx, _ := setUp() - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) err = proxy.validateRequest() @@ -188,7 +187,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("plugin route with admin role and user is editor", func(t *testing.T) { ctx, _ := setUp() - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) err = proxy.validateRequest() @@ -198,7 +197,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("plugin route with admin role and user is admin", func(t *testing.T) { ctx, _ := setUp() ctx.SignedInUser.OrgRole = models.ROLE_ADMIN - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) err = proxy.validateRequest() @@ -287,7 +286,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { }, } - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg) @@ -303,7 +302,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) client = newFakeHTTPClient(t, json2) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg) @@ -320,7 +319,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { require.NoError(t, err) client = newFakeHTTPClient(t, []byte{}) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg) @@ -342,7 +341,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { ctx := &models.ReqContext{} secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) @@ -368,7 +367,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { ctx := &models.ReqContext{} var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -392,7 +391,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { ctx := &models.ReqContext{} var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -420,7 +419,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { ctx := &models.ReqContext{} var pluginRoutes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -443,7 +442,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { ctx := &models.ReqContext{} var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) @@ -492,7 +491,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer, secretsService) require.NoError(t, err) req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) @@ -626,7 +625,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { ctx, ds := setUp(t) var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -644,7 +643,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { }) var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -658,7 +657,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { ctx, ds := setUp(t) var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -680,7 +679,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { }) var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -705,7 +704,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil) var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -729,7 +728,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil) var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -754,7 +753,7 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) { require.NoError(t, err) var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) _, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService) require.Error(t, err) assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`)) @@ -775,7 +774,7 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) { var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) _, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -818,7 +817,7 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) { var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService) if tc.err == nil { require.NoError(t, err) @@ -845,7 +844,7 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *sett var routes []*plugins.Route secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) @@ -969,7 +968,7 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, cfg *se require.NoError(t, err) var routes []*plugins.Route - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) @@ -1012,7 +1011,7 @@ func Test_PathCheck(t *testing.T) { } ctx, _ := setUp() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) proxy, err := NewDataSourceProxy(&models.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService) require.NoError(t, err) diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index fbf77357ad7..4e59ac05d8c 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -13,7 +13,6 @@ import ( "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" @@ -48,7 +47,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { loggedInUserScenario(t, "When calling GET on", "api/users/1", "api/users/:id", func(sc *scenarioContext) { fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC) secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) - authInfoStore := authinfostore.ProvideAuthInfoStore(sqlStore, bus.New(), secretsService) + authInfoStore := authinfostore.ProvideAuthInfoStore(sqlStore, secretsService) srv := authinfoservice.ProvideAuthInfoService(&authinfoservice.OSSUserProtectionImpl{}, authInfoStore) hs.authInfoService = srv diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go index 4a07a6b737e..8131f9f28a5 100644 --- a/pkg/api/user_token_test.go +++ b/pkg/api/user_token_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -151,7 +150,6 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ - Bus: bus.GetBus(), AuthTokenService: fakeAuthTokenService, SQLStore: sqlStore, } @@ -179,7 +177,6 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ - Bus: bus.GetBus(), AuthTokenService: fakeAuthTokenService, SQLStore: sqlStore, } @@ -204,7 +201,6 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) { t.Run(desc, func(t *testing.T) { hs := HTTPServer{ - Bus: bus.GetBus(), AuthTokenService: auth.NewFakeUserAuthTokenService(), SQLStore: sqlStore, } @@ -231,7 +227,6 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.R fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ - Bus: bus.GetBus(), AuthTokenService: fakeAuthTokenService, SQLStore: sqlStore, } @@ -257,7 +252,6 @@ func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models. fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ - Bus: bus.GetBus(), AuthTokenService: fakeAuthTokenService, SQLStore: sqlStore, } diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go index 28fdc4b002e..4ca374e93b0 100644 --- a/pkg/bus/bus.go +++ b/pkg/bus/bus.go @@ -54,11 +54,6 @@ func New() *InProcBus { return bus } -// Want to get rid of global bus -func GetBus() Bus { - return globalBus -} - // PublishCtx function publish a message to the bus listener. func (b *InProcBus) Publish(ctx context.Context, msg Msg) error { var msgName = reflect.TypeOf(msg).Elem().Name() diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 82bcb8ffac3..37354ab98ae 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/fatih/color" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/datamigrations" "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/secretsmigrations" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" @@ -56,7 +55,7 @@ func runDbCommand(command func(commandLine utils.CommandLine, sqlStore *sqlstore return errutil.Wrap("failed to initialize tracer service", err) } - sqlStore, err := sqlstore.ProvideService(cfg, nil, bus.GetBus(), &migrations.OSSMigrations{}, tracer) + sqlStore, err := sqlstore.ProvideService(cfg, nil, &migrations.OSSMigrations{}, tracer) if err != nil { return errutil.Wrap("failed to initialize SQL store", err) } diff --git a/pkg/plugins/plugincontext/plugincontext.go b/pkg/plugins/plugincontext/plugincontext.go index 865a42905fa..652417534b7 100644 --- a/pkg/plugins/plugincontext/plugincontext.go +++ b/pkg/plugins/plugincontext/plugincontext.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" @@ -20,11 +19,10 @@ import ( "github.com/grafana/grafana/pkg/util/errutil" ) -func ProvideService(bus bus.Bus, cacheService *localcache.CacheService, pluginStore plugins.Store, +func ProvideService(cacheService *localcache.CacheService, pluginStore plugins.Store, dataSourceCache datasources.CacheService, secretsService secrets.Service, pluginSettingsService pluginsettings.Service) *Provider { return &Provider{ - bus: bus, cacheService: cacheService, pluginStore: pluginStore, dataSourceCache: dataSourceCache, @@ -35,7 +33,6 @@ func ProvideService(bus bus.Bus, cacheService *localcache.CacheService, pluginSt } type Provider struct { - bus bus.Bus cacheService *localcache.CacheService pluginStore plugins.Store dataSourceCache datasources.CacheService diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 9566d7725b4..7fda9c47e85 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -10,7 +10,6 @@ import ( "go.opentelemetry.io/otel/attribute" "golang.org/x/sync/errgroup" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" @@ -41,7 +40,6 @@ type AlertStore interface { // are sent. type AlertEngine struct { RenderService rendering.Service - Bus bus.Bus RequestValidator models.PluginRequestValidator DataService legacydata.RequestHandler Cfg *setting.Cfg @@ -65,14 +63,13 @@ func (e *AlertEngine) IsDisabled() bool { } // ProvideAlertEngine returns a new AlertEngine. -func ProvideAlertEngine(renderer rendering.Service, bus bus.Bus, requestValidator models.PluginRequestValidator, +func ProvideAlertEngine(renderer rendering.Service, requestValidator models.PluginRequestValidator, dataService legacydata.RequestHandler, usageStatsService usagestats.Service, encryptionService encryption.Internal, notificationService *notifications.NotificationService, tracer tracing.Tracer, sqlStore AlertStore, cfg *setting.Cfg, dashAlertExtractor DashAlertExtractor) *AlertEngine { e := &AlertEngine{ Cfg: cfg, RenderService: renderer, - Bus: bus, RequestValidator: requestValidator, DataService: dataService, usageStatsService: usageStatsService, diff --git a/pkg/services/alerting/engine_integration_test.go b/pkg/services/alerting/engine_integration_test.go index 05725068215..06df00a9d4d 100644 --- a/pkg/services/alerting/engine_integration_test.go +++ b/pkg/services/alerting/engine_integration_test.go @@ -24,7 +24,7 @@ func TestEngineTimeouts(t *testing.T) { usMock := &usagestats.UsageStatsMock{T: t} tracer, err := tracing.InitializeTracerForTest() require.NoError(t, err) - engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, nil, setting.NewCfg(), nil) + engine := ProvideAlertEngine(nil, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, nil, setting.NewCfg(), nil) setting.AlertingNotificationTimeout = 30 * time.Second setting.AlertingMaxAttempts = 3 engine.resultHandler = &FakeResultHandler{} diff --git a/pkg/services/alerting/engine_test.go b/pkg/services/alerting/engine_test.go index 3ed3ee42362..e64f8a14dff 100644 --- a/pkg/services/alerting/engine_test.go +++ b/pkg/services/alerting/engine_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" @@ -100,13 +99,12 @@ func (a *AlertStoreMock) SetAlertState(_ context.Context, _ *models.SetAlertStat } func TestEngineProcessJob(t *testing.T) { - bus := bus.New() usMock := &usagestats.UsageStatsMock{T: t} tracer, err := tracing.InitializeTracerForTest() require.NoError(t, err) store := &AlertStoreMock{} - engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, store, setting.NewCfg(), nil) + engine := ProvideAlertEngine(nil, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, store, setting.NewCfg(), nil) setting.AlertingEvaluationTimeout = 30 * time.Second setting.AlertingNotificationTimeout = 30 * time.Second setting.AlertingMaxAttempts = 3 diff --git a/pkg/services/alerting/service.go b/pkg/services/alerting/service.go index 8db32e18728..34e683561fc 100644 --- a/pkg/services/alerting/service.go +++ b/pkg/services/alerting/service.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/notifications" @@ -13,16 +12,14 @@ import ( ) type AlertNotificationService struct { - Bus bus.Bus SQLStore *sqlstore.SQLStore EncryptionService encryption.Internal NotificationService *notifications.NotificationService } -func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, encryptionService encryption.Internal, +func ProvideService(store *sqlstore.SQLStore, encryptionService encryption.Internal, notificationService *notifications.NotificationService) *AlertNotificationService { s := &AlertNotificationService{ - Bus: bus, SQLStore: store, EncryptionService: encryptionService, NotificationService: notificationService, diff --git a/pkg/services/alerting/service_test.go b/pkg/services/alerting/service_test.go index 020003ffacd..edd58a5da30 100644 --- a/pkg/services/alerting/service_test.go +++ b/pkg/services/alerting/service_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/encryption/ossencryption" @@ -20,7 +19,7 @@ func TestService(t *testing.T) { nType := "test" registerTestNotifier(nType) - s := ProvideService(bus.New(), sqlStore, ossencryption.ProvideService(), nil) + s := ProvideService(sqlStore, ossencryption.ProvideService(), nil) origSecret := setting.SecretKey setting.SecretKey = "alert_notification_service_test" diff --git a/pkg/services/dashboardsnapshots/dashboardsnapshots.go b/pkg/services/dashboardsnapshots/dashboardsnapshots.go index 88745a181ed..51587cfa2dd 100644 --- a/pkg/services/dashboardsnapshots/dashboardsnapshots.go +++ b/pkg/services/dashboardsnapshots/dashboardsnapshots.go @@ -3,7 +3,6 @@ package dashboardsnapshots import ( "context" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/secrets" @@ -11,14 +10,12 @@ import ( ) type Service struct { - Bus bus.Bus SQLStore sqlstore.Store SecretsService secrets.Service } -func ProvideService(bus bus.Bus, store sqlstore.Store, secretsService secrets.Service) *Service { +func ProvideService(store sqlstore.Store, secretsService secrets.Service) *Service { s := &Service{ - Bus: bus, SQLStore: store, SecretsService: secretsService, } diff --git a/pkg/services/datasources/service/datasource_service.go b/pkg/services/datasources/service/datasource_service.go index 30381852578..914f5ed0b26 100644 --- a/pkg/services/datasources/service/datasource_service.go +++ b/pkg/services/datasources/service/datasource_service.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azhttpclient" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/models" @@ -29,7 +28,6 @@ import ( ) type Service struct { - Bus bus.Bus SQLStore *sqlstore.SQLStore SecretsService secrets.Service cfg *setting.Cfg @@ -61,11 +59,10 @@ type cachedDecryptedJSON struct { } func ProvideService( - bus bus.Bus, store *sqlstore.SQLStore, secretsService secrets.Service, cfg *setting.Cfg, features featuremgmt.FeatureToggles, + store *sqlstore.SQLStore, secretsService secrets.Service, cfg *setting.Cfg, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices, ) *Service { s := &Service{ - Bus: bus, SQLStore: store, SecretsService: secretsService, ptc: proxyTransportCache{ diff --git a/pkg/services/datasources/service/datasource_service_test.go b/pkg/services/datasources/service/datasource_service_test.go index 72a85504867..511b0f8c9b2 100644 --- a/pkg/services/datasources/service/datasource_service_test.go +++ b/pkg/services/datasources/service/datasource_service_test.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/models" @@ -39,7 +38,7 @@ func TestService(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) - s := ProvideService(bus.New(), sqlStore, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + s := ProvideService(sqlStore, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) var ds *models.DataSource @@ -239,7 +238,7 @@ func TestService_GetHttpTransport(t *testing.T) { } secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) rt1, err := dsService.GetHTTPTransport(&ds, provider) require.NoError(t, err) @@ -272,7 +271,7 @@ func TestService_GetHttpTransport(t *testing.T) { json.Set("tlsAuthWithCACert", true) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) tlsCaCert, err := secretsService.Encrypt(context.Background(), []byte(caCert), secrets.WithoutScope()) require.NoError(t, err) @@ -322,7 +321,7 @@ func TestService_GetHttpTransport(t *testing.T) { json.Set("tlsAuth", true) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) tlsClientCert, err := secretsService.Encrypt(context.Background(), []byte(clientCert), secrets.WithoutScope()) require.NoError(t, err) @@ -365,7 +364,7 @@ func TestService_GetHttpTransport(t *testing.T) { json.Set("serverName", "server-name") secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) tlsCaCert, err := secretsService.Encrypt(context.Background(), []byte(caCert), secrets.WithoutScope()) require.NoError(t, err) @@ -402,7 +401,7 @@ func TestService_GetHttpTransport(t *testing.T) { json.Set("tlsSkipVerify", true) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) ds := models.DataSource{ Id: 1, @@ -433,7 +432,7 @@ func TestService_GetHttpTransport(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) encryptedData, err := secretsService.Encrypt(context.Background(), []byte(`Bearer xf5yhfkpsnmgo`), secrets.WithoutScope()) require.NoError(t, err) @@ -492,7 +491,7 @@ func TestService_GetHttpTransport(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) ds := models.DataSource{ Id: 1, @@ -525,7 +524,7 @@ func TestService_GetHttpTransport(t *testing.T) { require.NoError(t, err) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) ds := models.DataSource{ Type: models.DS_ES, @@ -560,7 +559,7 @@ func TestService_getTimeout(t *testing.T) { } secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) for _, tc := range testCases { ds := &models.DataSource{ @@ -575,7 +574,7 @@ func TestService_DecryptedValue(t *testing.T) { t.Run("When datasource hasn't been updated, encrypted JSON should be fetched from cache", func(t *testing.T) { secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) encryptedJsonData, err := secretsService.EncryptJsonData( context.Background(), @@ -629,7 +628,7 @@ func TestService_DecryptedValue(t *testing.T) { SecureJsonData: encryptedJsonData, } - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) // Populate cache password, ok := dsService.DecryptedValue(&ds, "password") @@ -680,7 +679,7 @@ func TestService_HTTPClientOptions(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) opts, err := dsService.httpClientOptions(&ds) require.NoError(t, err) @@ -697,7 +696,7 @@ func TestService_HTTPClientOptions(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) opts, err := dsService.httpClientOptions(&ds) require.NoError(t, err) @@ -716,7 +715,7 @@ func TestService_HTTPClientOptions(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) _, err := dsService.httpClientOptions(&ds) assert.Error(t, err) @@ -734,7 +733,7 @@ func TestService_HTTPClientOptions(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) opts, err := dsService.httpClientOptions(&ds) require.NoError(t, err) @@ -752,7 +751,7 @@ func TestService_HTTPClientOptions(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) opts, err := dsService.httpClientOptions(&ds) require.NoError(t, err) @@ -774,7 +773,7 @@ func TestService_HTTPClientOptions(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) _, err := dsService.httpClientOptions(&ds) assert.Error(t, err) @@ -794,7 +793,7 @@ func TestService_HTTPClientOptions(t *testing.T) { }) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) opts, err := dsService.httpClientOptions(&ds) require.NoError(t, err) diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 085e3b40786..ea144b3f391 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" @@ -71,7 +70,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r pluginStore plugins.Store, cacheService *localcache.CacheService, dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore, secretsService secrets.Service, usageStatsService usagestats.Service, queryDataService *query.Service, toggles featuremgmt.FeatureToggles, - bus bus.Bus, accessControl accesscontrol.AccessControl) (*GrafanaLive, error) { + accessControl accesscontrol.AccessControl) (*GrafanaLive, error) { g := &GrafanaLive{ Cfg: cfg, Features: toggles, @@ -83,7 +82,6 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r SQLStore: sqlStore, SecretsService: secretsService, queryDataService: queryDataService, - bus: bus, channels: make(map[string]models.ChannelHandler), GrafanaScope: CoreGrafanaScope{ Features: make(map[string]models.ChannelHandlerFactory), @@ -408,7 +406,6 @@ type GrafanaLive struct { SecretsService secrets.Service pluginStore plugins.Store queryDataService *query.Service - bus bus.Bus node *centrifuge.Node surveyCaller *survey.Caller diff --git a/pkg/services/login/authinfoservice/database/database.go b/pkg/services/login/authinfoservice/database/database.go index 2ac7ad076e1..efd4f2cadac 100644 --- a/pkg/services/login/authinfoservice/database/database.go +++ b/pkg/services/login/authinfoservice/database/database.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/secrets" @@ -16,15 +15,13 @@ var GetTime = time.Now type AuthInfoStore struct { sqlStore sqlstore.Store - bus bus.Bus secretsService secrets.Service logger log.Logger } -func ProvideAuthInfoStore(sqlStore sqlstore.Store, bus bus.Bus, secretsService secrets.Service) *AuthInfoStore { +func ProvideAuthInfoStore(sqlStore sqlstore.Store, secretsService secrets.Service) *AuthInfoStore { store := &AuthInfoStore{ sqlStore: sqlStore, - bus: bus, secretsService: secretsService, logger: log.New("login.authinfo.store"), } diff --git a/pkg/services/login/authinfoservice/user_auth_test.go b/pkg/services/login/authinfoservice/user_auth_test.go index 1795714292a..f719366adb7 100644 --- a/pkg/services/login/authinfoservice/user_auth_test.go +++ b/pkg/services/login/authinfoservice/user_auth_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" secretstore "github.com/grafana/grafana/pkg/services/secrets/database" @@ -20,7 +19,7 @@ import ( func TestUserAuth(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) secretsService := secretsManager.SetupTestService(t, secretstore.ProvideSecretsStore(sqlStore)) - authInfoStore := database.ProvideAuthInfoStore(sqlStore, bus.New(), secretsService) + authInfoStore := database.ProvideAuthInfoStore(sqlStore, secretsService) srv := ProvideAuthInfoService(&OSSUserProtectionImpl{}, authInfoStore) t.Run("Given 5 users", func(t *testing.T) { diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go index ae82f81317e..ea9338602b7 100644 --- a/pkg/services/login/loginservice/loginservice.go +++ b/pkg/services/login/loginservice/loginservice.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" @@ -16,10 +15,9 @@ var ( logger = log.New("login.ext_user") ) -func ProvideService(sqlStore sqlstore.Store, bus bus.Bus, quotaService *quota.QuotaService, authInfoService login.AuthInfoService) *Implementation { +func ProvideService(sqlStore sqlstore.Store, quotaService *quota.QuotaService, authInfoService login.AuthInfoService) *Implementation { s := &Implementation{ SQLStore: sqlStore, - Bus: bus, QuotaService: quotaService, AuthInfoService: authInfoService, } @@ -28,7 +26,6 @@ func ProvideService(sqlStore sqlstore.Store, bus bus.Bus, quotaService *quota.Qu type Implementation struct { SQLStore sqlstore.Store - Bus bus.Bus AuthInfoService login.AuthInfoService QuotaService *quota.QuotaService TeamSync login.TeamSyncFunc diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go index 924506e5845..1e687de9e00 100644 --- a/pkg/services/login/loginservice/loginservice_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/go-kit/log" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log/level" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -28,7 +27,6 @@ func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T) } login := Implementation{ - Bus: bus.New(), QuotaService: "a.QuotaService{}, AuthInfoService: authInfoMock, SQLStore: store, @@ -53,7 +51,6 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) { } login := Implementation{ - Bus: bus.New(), QuotaService: "a.QuotaService{}, AuthInfoService: authInfoMock, SQLStore: store, @@ -67,7 +64,6 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) { func Test_teamSync(t *testing.T) { authInfoMock := &logintest.AuthInfoServiceFake{} login := Implementation{ - Bus: bus.New(), QuotaService: "a.QuotaService{}, AuthInfoService: authInfoMock, } diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index 1656f6ee123..809e5ba8c11 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -7,14 +7,12 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) -func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlstore *sqlstore.SQLStore) *SearchService { +func ProvideService(cfg *setting.Cfg, sqlstore *sqlstore.SQLStore) *SearchService { s := &SearchService{ Cfg: cfg, - Bus: bus, sortOptions: map[string]models.SortOption{ SortAlphaAsc.Name: SortAlphaAsc, SortAlphaDesc.Name: SortAlphaDesc, @@ -47,7 +45,6 @@ type Service interface { } type SearchService struct { - Bus bus.Bus Cfg *setting.Cfg sortOptions map[string]models.SortOption sqlstore sqlstore.Store diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index ca6ec8d6135..7baa450cb99 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -15,7 +15,6 @@ import ( _ "github.com/lib/pq" "xorm.io/xorm" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" @@ -44,7 +43,6 @@ type ContextSessionKey struct{} type SQLStore struct { Cfg *setting.Cfg - Bus bus.Bus CacheService *localcache.CacheService dbCfg DatabaseConfig @@ -56,12 +54,12 @@ type SQLStore struct { tracer tracing.Tracer } -func ProvideService(cfg *setting.Cfg, cacheService *localcache.CacheService, bus bus.Bus, migrations registry.DatabaseMigrator, tracer tracing.Tracer) (*SQLStore, error) { +func ProvideService(cfg *setting.Cfg, cacheService *localcache.CacheService, migrations registry.DatabaseMigrator, tracer tracing.Tracer) (*SQLStore, error) { // This change will make xorm use an empty default schema for postgres and // by that mimic the functionality of how it was functioning before // xorm's changes above. xorm.DefaultPostgresSchema = "" - s, err := newSQLStore(cfg, cacheService, bus, nil, migrations, tracer) + s, err := newSQLStore(cfg, cacheService, nil, migrations, tracer) if err != nil { return nil, err } @@ -81,11 +79,10 @@ func ProvideServiceForTests(migrations registry.DatabaseMigrator) (*SQLStore, er return initTestDB(migrations, InitTestDBOpt{EnsureDefaultOrgAndUser: true}) } -func newSQLStore(cfg *setting.Cfg, cacheService *localcache.CacheService, b bus.Bus, engine *xorm.Engine, +func newSQLStore(cfg *setting.Cfg, cacheService *localcache.CacheService, engine *xorm.Engine, migrations registry.DatabaseMigrator, tracer tracing.Tracer, opts ...InitTestDBOpt) (*SQLStore, error) { ss := &SQLStore{ Cfg: cfg, - Bus: b, CacheService: cacheService, log: log.New("sqlstore"), skipEnsureDefaultOrgAndUser: false, @@ -543,7 +540,7 @@ func initTestDB(migration registry.DatabaseMigrator, opts ...InitTestDBOpt) (*SQ if err != nil { return nil, err } - testSQLStore, err = newSQLStore(cfg, localcache.New(5*time.Minute, 10*time.Minute), bus.GetBus(), engine, migration, tracer, opts...) + testSQLStore, err = newSQLStore(cfg, localcache.New(5*time.Minute, 10*time.Minute), engine, migration, tracer, opts...) if err != nil { return nil, err } diff --git a/pkg/tests/api/alerting/api_admin_configuration_test.go b/pkg/tests/api/alerting/api_admin_configuration_test.go index 777ea9f05aa..c114d046494 100644 --- a/pkg/tests/api/alerting/api_admin_configuration_test.go +++ b/pkg/tests/api/alerting/api_admin_configuration_test.go @@ -12,7 +12,6 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -35,8 +34,6 @@ func TestAdminConfiguration_SendingToExternalAlertmanagers(t *testing.T) { }) grafanaListedAddr, s := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - s.Bus = bus.GetBus() // Create a user to make authenticated requests userID := createUser(t, s, models.CreateUserCommand{ diff --git a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go index d51d3752b60..dd6e1fcbbc3 100644 --- a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -34,9 +33,6 @@ func TestAlertmanagerConfigurationIsTransactional(t *testing.T) { // editor from main organisation requests configuration alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() - // create user under main organisation userID := createUser(t, store, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -145,9 +141,6 @@ func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) { grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() - createUser(t, store, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), Password: "editor", diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index 1505ade9311..0d1a6e86db1 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -16,7 +16,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" @@ -40,8 +39,6 @@ func TestAMConfigAccess(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create a users to make authenticated requests createUser(t, store, models.CreateUserCommand{ @@ -403,8 +400,6 @@ func TestAlertAndGroupsQuery(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // unauthenticated request to get the alerts should fail { @@ -574,8 +569,6 @@ func TestRulerAccess(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create the namespace we'll save our alerts to. _, err = createFolder(t, store, 0, "default") @@ -704,8 +697,6 @@ func TestDeleteFolderWithRules(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create the namespace we'll save our alerts to. namespaceUID, err := createFolder(t, store, 0, "default") @@ -866,8 +857,6 @@ func TestAlertRuleCRUD(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() createUser(t, store, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -2001,8 +1990,6 @@ func TestQuota(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create the namespace we'll save our alerts to. _, err = createFolder(t, store, 0, "default") @@ -2248,8 +2235,6 @@ func TestEval(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() createUser(t, store, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), diff --git a/pkg/tests/api/alerting/api_available_channel_test.go b/pkg/tests/api/alerting/api_available_channel_test.go index 3a53f2a1bfa..c54d1a1ae6b 100644 --- a/pkg/tests/api/alerting/api_available_channel_test.go +++ b/pkg/tests/api/alerting/api_available_channel_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" @@ -28,7 +27,6 @@ func TestAvailableChannels(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - store.Bus = bus.GetBus() // Create a user to make authenticated requests createUser(t, store, models.CreateUserCommand{ diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 686de9827a3..20cd1b0eac5 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -20,7 +20,6 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -43,7 +42,6 @@ func TestTestReceivers(t *testing.T) { }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - env.SQLStore.Bus = bus.GetBus() createUser(t, env.SQLStore, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -77,7 +75,6 @@ func TestTestReceivers(t *testing.T) { }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - env.SQLStore.Bus = bus.GetBus() createUser(t, env.SQLStore, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -162,7 +159,6 @@ func TestTestReceivers(t *testing.T) { }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - env.SQLStore.Bus = bus.GetBus() createUser(t, env.SQLStore, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -242,7 +238,6 @@ func TestTestReceivers(t *testing.T) { }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - env.SQLStore.Bus = bus.GetBus() createUser(t, env.SQLStore, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -332,7 +327,6 @@ func TestTestReceivers(t *testing.T) { }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - env.SQLStore.Bus = bus.GetBus() createUser(t, env.SQLStore, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -448,7 +442,6 @@ func TestTestReceiversAlertCustomization(t *testing.T) { }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - env.SQLStore.Bus = bus.GetBus() createUser(t, env.SQLStore, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -544,7 +537,6 @@ func TestTestReceiversAlertCustomization(t *testing.T) { }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - env.SQLStore.Bus = bus.GetBus() createUser(t, env.SQLStore, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -635,7 +627,6 @@ func TestTestReceiversAlertCustomization(t *testing.T) { }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - env.SQLStore.Bus = bus.GetBus() createUser(t, env.SQLStore, models.CreateUserCommand{ DefaultOrgRole: string(models.ROLE_EDITOR), @@ -727,7 +718,6 @@ func TestNotificationChannels(t *testing.T) { }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - env.SQLStore.Bus = bus.GetBus() mockChannel := newMockNotificationChannel(t, grafanaListedAddr) amConfig := getAlertmanagerConfig(mockChannel.server.Addr) diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index 8a834a19292..95b059be794 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -35,8 +34,6 @@ func TestPrometheusRules(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create the namespace under default organisation (orgID = 1) where we'll save our alerts to. _, err = createFolder(t, store, 0, "default") @@ -330,8 +327,6 @@ func TestPrometheusRulesFilterByDashboard(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create the namespace under default organisation (orgID = 1) where we'll save our alerts to. dashboardUID, err := createFolder(t, store, 0, "default") @@ -625,8 +620,6 @@ func TestPrometheusRulesPermissions(t *testing.T) { grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) dashboardsStore := dashboardsstore.ProvideDashboardStore(store) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create a user to make authenticated requests createUser(t, store, models.CreateUserCommand{ diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index 1cdcef6055b..ad4e70e32b2 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -7,7 +7,6 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -28,8 +27,6 @@ func TestProvisioning(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create a users to make authenticated requests createUser(t, store, models.CreateUserCommand{ diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 3e59d152ddd..7af16c013d0 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -37,8 +36,6 @@ func TestAlertRulePermissions(t *testing.T) { grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) dashboardsStore := dashboardsstore.ProvideDashboardStore(store) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create a user to make authenticated requests createUser(t, store, models.CreateUserCommand{ @@ -345,8 +342,6 @@ func TestAlertRuleConflictingTitle(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create the namespace we'll save our alerts to. _, err = createFolder(t, store, 0, "folder1") @@ -469,8 +464,6 @@ func TestRulerRulesFilterByDashboard(t *testing.T) { }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) - // override bus to get the GetSignedInUserQuery handler - store.Bus = bus.GetBus() // Create the namespace under default organisation (orgID = 1) where we'll save our alerts to. dashboardUID, err := createFolder(t, store, 0, "default") diff --git a/pkg/tests/api/plugins/api_plugins_test.go b/pkg/tests/api/plugins/api_plugins_test.go index 04073f43e1a..bf44d6202f4 100644 --- a/pkg/tests/api/plugins/api_plugins_test.go +++ b/pkg/tests/api/plugins/api_plugins_test.go @@ -10,7 +10,6 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -42,8 +41,6 @@ func TestPlugins(t *testing.T) { } t.Run("Install", func(t *testing.T) { - store.Bus = bus.GetBus() - createUser(t, store, models.CreateUserCommand{Login: usernameNonAdmin, Password: defaultPassword, IsAdmin: false}) createUser(t, store, models.CreateUserCommand{Login: usernameAdmin, Password: defaultPassword, IsAdmin: true}) diff --git a/pkg/tsdb/legacydata/service/service_test.go b/pkg/tsdb/legacydata/service/service_test.go index b309f97b982..7e895da093a 100644 --- a/pkg/tsdb/legacydata/service/service_test.go +++ b/pkg/tsdb/legacydata/service/service_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -40,7 +39,7 @@ func TestHandleRequest(t *testing.T) { return backend.NewQueryDataResponse(), nil } secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) s := ProvideService(client, nil, dsService) ds := &models.DataSource{Id: 12, Type: "unregisteredType", JsonData: simplejson.New()} From 38809d73c246a75d0892ee9754c5737b38ac67a4 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 13 Apr 2022 14:41:48 +0100 Subject: [PATCH 067/121] Nav: Icon clickable area fills all available space (#47334) Closes #47255 --- .../app/core/components/NavBar/Next/NavBarItemWithoutMenu.tsx | 1 - public/app/core/components/NavBar/Next/NavBarMenu.tsx | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/NavBar/Next/NavBarItemWithoutMenu.tsx b/public/app/core/components/NavBar/Next/NavBarItemWithoutMenu.tsx index d990ff34e55..a8dd795fb10 100644 --- a/public/app/core/components/NavBar/Next/NavBarItemWithoutMenu.tsx +++ b/public/app/core/components/NavBar/Next/NavBarItemWithoutMenu.tsx @@ -71,7 +71,6 @@ export function getNavBarItemWithoutMenuStyles(theme: GrafanaTheme2, isActive?: position: 'relative', color: isActive ? theme.colors.text.primary : theme.colors.text.secondary, display: 'grid', - placeItems: 'center', '&:hover': { backgroundColor: theme.colors.action.hover, diff --git a/public/app/core/components/NavBar/Next/NavBarMenu.tsx b/public/app/core/components/NavBar/Next/NavBarMenu.tsx index c7dc7e3017f..224e32ff66e 100644 --- a/public/app/core/components/NavBar/Next/NavBarMenu.tsx +++ b/public/app/core/components/NavBar/Next/NavBarMenu.tsx @@ -356,6 +356,7 @@ function CollapsibleNavItem({ onClose(); }} className={styles.collapsibleMenuItem} + elClassName={styles.collapsibleIcon} > {link.img && ( {`${link.text} @@ -392,6 +393,9 @@ const getCollapsibleStyles = (theme: GrafanaTheme2) => ({ height: theme.spacing(6), width: theme.spacing(7), display: 'grid', + }), + collapsibleIcon: css({ + display: 'grid', placeContent: 'center', }), collapsibleSectionWrapper: css({ From a57716f8689ae5cfb8a0cebf2df0720bbdbfd26f Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 13 Apr 2022 06:56:32 -0700 Subject: [PATCH 068/121] AzureMonitor: Include macros and template variables as suggestions (#47689) --- .../azure_log_analytics_datasource.test.ts | 22 ++++ .../azure_log_analytics_datasource.ts | 2 +- .../azure_log_analytics/response_parser.ts | 100 ++++++++++++++---- .../components/LogsQueryEditor/QueryField.tsx | 1 + 4 files changed, 103 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 35a26d345c2..9f3a7fd0903 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -28,6 +28,7 @@ describe('AzureLogAnalyticsDatasource', () => { beforeEach(() => { templateSrv.init([singleVariable]); + templateSrv.getVariables = jest.fn().mockReturnValue([singleVariable]); ctx.instanceSettings = { jsonData: { subscriptionId: 'xxx' }, url: 'http://azureloganalyticsapi', @@ -120,6 +121,27 @@ describe('AzureLogAnalyticsDatasource', () => { await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace/$var1'); expect(ctx.mockGetResource).lastCalledWith('loganalytics/v1myWorkspace/var1-foo/metadata'); }); + + it('should include macros as suggested functions', async () => { + const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace'); + expect(result.database.functions.map((f: { name: string }) => f.name)).toEqual([ + 'Func1', + '_AzureBackup_GetVaults', + '$__timeFilter', + '$__timeFrom', + '$__timeTo', + '$__escapeMulti', + '$__contains', + ]); + }); + + it('should include template variables as global parameters', async () => { + const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace'); + expect(result.globalParameters.map((f: { name: string }) => f.name)).toEqual([ + `$${singleVariable.name}`, + '$__timeFilter', + ]); + }); }); describe('When performing annotationQuery', () => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index c98aac12ece..6f1ece771ba 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -112,7 +112,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< const templateSrv = getTemplateSrv(); const interpolatedUri = templateSrv.replace(resourceUri, {}, interpolateVariable); const metadata = await this.getMetadata(interpolatedUri); - return transformMetadataToKustoSchema(metadata, interpolatedUri); + return transformMetadataToKustoSchema(metadata, interpolatedUri, templateSrv.getVariables()); } applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): AzureMonitorQuery { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts index 412eb8f1e7a..bce81329529 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts @@ -1,4 +1,4 @@ -import { AnnotationEvent, dateTime, TimeSeries } from '@grafana/data'; +import { AnnotationEvent, dateTime, TimeSeries, VariableModel } from '@grafana/data'; import { concat, find, flattenDeep, forEach, get, map } from 'lodash'; import { AzureLogsTableData, AzureLogsVariable } from '../types'; @@ -213,7 +213,11 @@ function transformMetadataFunction(sourceSchema: AzureLogAnalyticsMetadata) { }); } -export function transformMetadataToKustoSchema(sourceSchema: AzureLogAnalyticsMetadata, nameOrIdOrSomething: string) { +export function transformMetadataToKustoSchema( + sourceSchema: AzureLogAnalyticsMetadata, + nameOrIdOrSomething: string, + templateVariables: VariableModel[] +) { const database = { name: nameOrIdOrSomething, tables: sourceSchema.tables, @@ -222,25 +226,78 @@ export function transformMetadataToKustoSchema(sourceSchema: AzureLogAnalyticsMe minorVersion: 0, }; - // TODO: We should define macros here as functions so they are interpreted as valid - // But we cannot do so for the issues listed here: https://github.com/Azure/monaco-kusto/issues/189 - // For example: - // database.functions.push( - // { - // name: '$__timeFilter', - // body: '', - // inputParameters: [ - // { - // name: 'timeColumn', - // type: 'System.String', - // cslType: 'string', - // cslDefaultValue: 'Timestamp', - // }, - // ], - // docString: 'Filter by a time column', - // functionKind: 'Unknown', - // }, - // ); + // Adding macros as known functions + database.functions.push( + { + name: '$__timeFilter', + body: '{ true }', + inputParameters: [ + { + name: 'timeColumn', + type: 'System.String', + defaultValue: 'TimeGenerated', + cslDefaultValue: 'TimeGenerated', + }, + ], + }, + { + name: '$__timeFrom', + body: '{ datetime(2018-06-05T18:09:58.907Z) }', + inputParameters: [], + }, + { + name: '$__timeTo', + body: '{ datetime(2018-06-05T20:09:58.907Z) }', + inputParameters: [], + }, + { + name: '$__escapeMulti', + body: `{ @'\\grafana-vm\Network(eth0)\Total', @'\\hello!'}`, + inputParameters: [ + { + name: '$myVar', + type: 'System.String', + defaultValue: '$myVar', + cslDefaultValue: '$myVar', + }, + ], + }, + { + name: '$__contains', + body: `{ colName in ('value1','value2') }`, + inputParameters: [ + { + name: 'colName', + type: 'System.String', + defaultValue: 'colName', + cslDefaultValue: 'colName', + }, + { + name: '$myVar', + type: 'System.String', + defaultValue: '$myVar', + cslDefaultValue: '$myVar', + }, + ], + } + ); + + // Adding macros as global parameters + const globalParameters = templateVariables.map((v) => { + return { + name: `$${v.name}`, + type: 'dynamic', + }; + }); + + // It's not possible to define optional paramaters in Kusto + // and it's not possible to define the same function twice so + // we are defining $__timeFilter also as a parameter when used + // with no arguments as a workaround + globalParameters.push({ + name: `$__timeFilter`, + type: 'boolean', + }); return { clusterType: 'Engine', @@ -249,5 +306,6 @@ export function transformMetadataToKustoSchema(sourceSchema: AzureLogAnalyticsMe databases: [database], }, database: database, + globalParameters, }; } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx index df33e362b61..b435ca4955f 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx @@ -1,6 +1,7 @@ import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui'; import { Deferred } from 'app/core/utils/deferred'; import React, { useCallback, useEffect, useRef } from 'react'; + import { AzureQueryEditorFieldProps } from '../../types'; import { setKustoQuery } from './setQueryValue'; From 6db470c11e0a810dcf0befaf0e2838746f893478 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Apr 2022 18:17:07 +0300 Subject: [PATCH 069/121] Explore: no data returned (#46987) * Explore: no data message display no data text when query executed succesfully but no data was returned * Explore: 'no-data' display logic and styling fix * pipeline trigger * Revert "pipeline trigger" This reverts commit f08d246ba767e66344ad252577523f391832ec18. * Add more parameters for when to show no data * Change check to use frame length Co-authored-by: Kristina Durivage --- public/app/features/explore/Explore.tsx | 15 ++++++++++ public/app/features/explore/NoData.tsx | 37 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 public/app/features/explore/NoData.tsx diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index a793f1f537f..38ec9457441 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -34,6 +34,7 @@ import appEvents from 'app/core/app_events'; import { AbsoluteTimeEvent } from 'app/types/events'; import { Unsubscribable } from 'rxjs'; import { getNodeGraphDataFrames } from 'app/plugins/panel/nodeGraph/utils'; +import { NoData } from './NoData'; const getStyles = (theme: GrafanaTheme2) => { return { @@ -213,6 +214,10 @@ export class Explore extends React.PureComponent { ); } + renderNoData() { + return ; + } + renderGraphPanel(width: number) { const { graphResult, absoluteRange, timeZone, splitOpen, queryResponse, loading, theme, graphStyle } = this.props; const spacing = parseInt(theme.spacing(2).slice(0, -2), 10); @@ -334,6 +339,15 @@ export class Explore extends React.PureComponent { const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted; const showRichHistory = openDrawer === ExploreDrawer.RichHistory; const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector; + const showNoData = + queryResponse.state === LoadingState.Done && + [ + queryResponse.logsFrames, + queryResponse.graphFrames, + queryResponse.nodeGraphFrames, + queryResponse.tableFrames, + queryResponse.traceFrames, + ].every((e) => e.length === 0); return ( { {showLogs && {this.renderLogsPanel(width)}} {showNodeGraph && {this.renderNodeGraphPanel()}} {showTrace && {this.renderTraceViewPanel()}} + {showNoData && {this.renderNoData()}} )} {showRichHistory && ( diff --git a/public/app/features/explore/NoData.tsx b/public/app/features/explore/NoData.tsx new file mode 100644 index 00000000000..7b9c8df5f0d --- /dev/null +++ b/public/app/features/explore/NoData.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { css, cx } from '@emotion/css'; + +import { useStyles2 } from '@grafana/ui'; + +import { GrafanaTheme2 } from '@grafana/data/src'; + +export const NoData = () => { + const css = useStyles2(getStyles); + return ( + <> +
    + {'No data'} +
    + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css` + label: no-data-card; + padding: ${theme.spacing(3)}; + background: ${theme.colors.background.primary}; + border-radius: ${theme.shape.borderRadius(2)}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 1; + `, + message: css` + margin-bottom: ${theme.spacing(3)}; + font-size: 2em; + padding: 6em 1em; + color: ${theme.colors.text.disabled}; + `, +}); From 939a7781113b0d2a0e5bd7b078891a9fd6695b85 Mon Sep 17 00:00:00 2001 From: Connor Lindsey Date: Wed, 13 Apr 2022 08:18:40 -0700 Subject: [PATCH 070/121] NodeGraph: Detect dataframes more accurately based on fields (#47213) * NodeGraph: Detect dataframes more accurately based on fields * Make get fields case insensitive * Update node graph docs --- docs/sources/visualizations/node-graph.md | 26 ++--- packages/grafana-data/src/utils/nodeGraph.ts | 6 +- .../plugins/datasource/jaeger/testResponse.ts | 6 +- .../datasource/tempo/datasource.test.ts | 6 +- .../datasource/tempo/graphTransform.test.ts | 30 ++--- .../datasource/zipkin/utils/testResponse.ts | 6 +- .../app/plugins/panel/nodeGraph/utils.test.ts | 104 ++++++++++++++++-- public/app/plugins/panel/nodeGraph/utils.ts | 48 +++++--- 8 files changed, 167 insertions(+), 65 deletions(-) diff --git a/docs/sources/visualizations/node-graph.md b/docs/sources/visualizations/node-graph.md index 2545d108325..e0fcc69fed5 100644 --- a/docs/sources/visualizations/node-graph.md +++ b/docs/sources/visualizations/node-graph.md @@ -88,14 +88,14 @@ Required fields: Optional fields: -| Field name | Type | Description | -| ------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| title | string | Name of the node visible in just under the node. | -| subTitle | string | Additional, name, type or other identifier that will be shown right under the title. | -| mainStat | string/number | First stat shown inside the node itself. Can be either string in which case the value will be shown as it is or it can be a number in which case any unit associated with that field will be also shown. | -| secondaryStat | string/number | Same as mainStat but shown right under it inside the node. | -| arc\_\_\* | number | Any field prefixed with `arc__` will be used to create the color circle around the node. All values in these fields should add up to 1. You can specify color using `config.color.fixedColor`. | -| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the node. Use `config.displayName` for more human readable label. | +| Field name | Type | Description | +| ------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| title | string | Name of the node visible in just under the node. | +| subtitle | string | Additional, name, type or other identifier shown under the title. | +| mainstat | string/number | First stat shown inside the node itself. It can either be a string showing the value as is or a number. If it is a number, any unit associated with that field is also shown. | +| secondarystat | string/number | Same as mainStat, but shown under it inside the node. | +| arc\_\_\* | number | Any field prefixed with `arc__` will be used to create the color circle around the node. All values in these fields should add up to 1. You can specify color using `config.color.fixedColor`. | +| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the node. Use `config.displayName` for more human readable label. | ### Edge parameters @@ -109,8 +109,8 @@ Required fields: Optional fields: -| Field name | Type | Description | -| ------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| mainStat | string/number | First stat shown in the overlay when hovering over the edge. Can be either string in which case the value will be shown as it is or it can be a number in which case any unit associated with that field will be also shown | -| secondaryStat | string/number | Same as mainStat but shown right under it. | -| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. | +| Field name | Type | Description | +| ------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| mainstat | string/number | First stat shown in the overlay when hovering over the edge. It can be a string showing the value as is or it can be a number. If it is a number, any unit associated with that field is also shown | +| secondarystat | string/number | Same as mainStat, but shown right under it. | +| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. | diff --git a/packages/grafana-data/src/utils/nodeGraph.ts b/packages/grafana-data/src/utils/nodeGraph.ts index 03a1c48eda9..caccc59e5ef 100644 --- a/packages/grafana-data/src/utils/nodeGraph.ts +++ b/packages/grafana-data/src/utils/nodeGraph.ts @@ -1,9 +1,9 @@ export enum NodeGraphDataFrameFieldNames { id = 'id', title = 'title', - subTitle = 'subTitle', - mainStat = 'mainStat', - secondaryStat = 'secondaryStat', + subTitle = 'subtitle', + mainStat = 'mainstat', + secondaryStat = 'secondarystat', source = 'source', target = 'target', detail = 'detail__', diff --git a/public/app/plugins/datasource/jaeger/testResponse.ts b/public/app/plugins/datasource/jaeger/testResponse.ts index 39c5b4121c3..e9627249eae 100644 --- a/public/app/plugins/datasource/jaeger/testResponse.ts +++ b/public/app/plugins/datasource/jaeger/testResponse.ts @@ -111,9 +111,9 @@ export function toNodesFrame(values: any[]) { return toVectors([ { name: 'id', values: values[0] }, { name: 'title', values: values[1] }, - { name: 'subTitle', values: values[2] }, - { name: 'mainStat', values: values[3] }, - { name: 'secondaryStat', values: values[4] }, + { name: 'subtitle', values: values[2] }, + { name: 'mainstat', values: values[3] }, + { name: 'secondarystat', values: values[4] }, { name: 'color', config: { diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index e5884138fe9..e3c14e675b3 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -77,9 +77,9 @@ describe('Tempo data source', () => { ).toMatchObject([ { name: 'id', values: ['4322526419282105830'] }, { name: 'title', values: ['service'] }, - { name: 'subTitle', values: ['store.validateQueryTimeRange'] }, - { name: 'mainStat', values: ['14.98ms (100%)'] }, - { name: 'secondaryStat', values: ['14.98ms (100%)'] }, + { name: 'subtitle', values: ['store.validateQueryTimeRange'] }, + { name: 'mainstat', values: ['14.98ms (100%)'] }, + { name: 'secondarystat', values: ['14.98ms (100%)'] }, { name: 'color', values: [1.000007560204647] }, ]); diff --git a/public/app/plugins/datasource/tempo/graphTransform.test.ts b/public/app/plugins/datasource/tempo/graphTransform.test.ts index a6dd5b417cf..666ce5a8945 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.test.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.test.ts @@ -13,18 +13,18 @@ describe('createGraphFrames', () => { expect(view.get(0)).toMatchObject({ id: '4322526419282105830', title: 'loki-all', - subTitle: 'store.validateQueryTimeRange', - mainStat: '0ms (0.02%)', - secondaryStat: '0ms (100%)', + subtitle: 'store.validateQueryTimeRange', + mainstat: '0ms (0.02%)', + secondarystat: '0ms (100%)', color: 0.00021968356127648162, }); expect(view.get(29)).toMatchObject({ id: '4450900759028499335', title: 'loki-all', - subTitle: 'HTTP GET - loki_api_v1_query_range', - mainStat: '18.21ms (100%)', - secondaryStat: '3.22ms (17.71%)', + subtitle: 'HTTP GET - loki_api_v1_query_range', + mainstat: '18.21ms (100%)', + secondarystat: '3.22ms (17.71%)', color: 0.17707117189595056, }); @@ -43,9 +43,9 @@ describe('createGraphFrames', () => { expect(view.get(0)).toMatchObject({ id: '4322526419282105830', title: 'loki-all', - subTitle: 'store.validateQueryTimeRange', - mainStat: '14.98ms (100%)', - secondaryStat: '14.98ms (100%)', + subtitle: 'store.validateQueryTimeRange', + mainstat: '14.98ms (100%)', + secondarystat: '14.98ms (100%)', color: 1.000007560204647, }); }); @@ -75,8 +75,8 @@ describe('mapPromMetricsToServiceMap', () => { expect(nodes.fields).toMatchObject([ { name: 'id', values: new ArrayVector(['db', 'app', 'lb']) }, { name: 'title', values: new ArrayVector(['db', 'app', 'lb']) }, - { name: 'mainStat', values: new ArrayVector([1000, 2000, NaN]) }, - { name: 'secondaryStat', values: new ArrayVector([0.17, 0.33, NaN]) }, + { name: 'mainstat', values: new ArrayVector([1000, 2000, NaN]) }, + { name: 'secondarystat', values: new ArrayVector([0.17, 0.33, NaN]) }, { name: 'arc__success', values: new ArrayVector([0.8, 0.25, 1]) }, { name: 'arc__failed', values: new ArrayVector([0.2, 0.75, 0]) }, ]); @@ -84,8 +84,8 @@ describe('mapPromMetricsToServiceMap', () => { { name: 'id', values: new ArrayVector(['app_db', 'lb_app']) }, { name: 'source', values: new ArrayVector(['app', 'lb']) }, { name: 'target', values: new ArrayVector(['db', 'app']) }, - { name: 'mainStat', values: new ArrayVector([10, 20]) }, - { name: 'secondaryStat', values: new ArrayVector([1000, 2000]) }, + { name: 'mainstat', values: new ArrayVector([10, 20]) }, + { name: 'secondarystat', values: new ArrayVector([1000, 2000]) }, ]); }); @@ -107,8 +107,8 @@ describe('mapPromMetricsToServiceMap', () => { expect(nodes.fields).toMatchObject([ { name: 'id', values: new ArrayVector(['db', 'app', 'lb']) }, { name: 'title', values: new ArrayVector(['db', 'app', 'lb']) }, - { name: 'mainStat', values: new ArrayVector([1000, 2000, NaN]) }, - { name: 'secondaryStat', values: new ArrayVector([0.17, 0.33, NaN]) }, + { name: 'mainstat', values: new ArrayVector([1000, 2000, NaN]) }, + { name: 'secondarystat', values: new ArrayVector([0.17, 0.33, NaN]) }, { name: 'arc__success', values: new ArrayVector([0, 0, 1]) }, { name: 'arc__failed', values: new ArrayVector([1, 1, 0]) }, ]); diff --git a/public/app/plugins/datasource/zipkin/utils/testResponse.ts b/public/app/plugins/datasource/zipkin/utils/testResponse.ts index bdf76fdb35d..7a6ba459102 100644 --- a/public/app/plugins/datasource/zipkin/utils/testResponse.ts +++ b/public/app/plugins/datasource/zipkin/utils/testResponse.ts @@ -95,9 +95,9 @@ export function toNodesFrame(values: any[]) { return toVectors([ { name: 'id', values: values[0] }, { name: 'title', values: values[1] }, - { name: 'subTitle', values: values[2] }, - { name: 'mainStat', values: values[3] }, - { name: 'secondaryStat', values: values[4] }, + { name: 'subtitle', values: values[2] }, + { name: 'mainstat', values: values[3] }, + { name: 'secondarystat', values: values[4] }, { name: 'color', config: { diff --git a/public/app/plugins/panel/nodeGraph/utils.test.ts b/public/app/plugins/panel/nodeGraph/utils.test.ts index b2e25b0b98e..d95e4e72fdd 100644 --- a/public/app/plugins/panel/nodeGraph/utils.test.ts +++ b/public/app/plugins/panel/nodeGraph/utils.test.ts @@ -1,5 +1,12 @@ -import { ArrayVector, createTheme } from '@grafana/data'; -import { makeEdgesDataFrame, makeNodesDataFrame, processNodes } from './utils'; +import { ArrayVector, createTheme, DataFrame, FieldType, MutableDataFrame } from '@grafana/data'; +import { + getEdgeFields, + getNodeFields, + getNodeGraphDataFrames, + makeEdgesDataFrame, + makeNodesDataFrame, + processNodes, +} from './utils'; describe('processNodes', () => { const theme = createTheme(); @@ -62,14 +69,14 @@ describe('processNodes', () => { mainStat: { config: {}, index: 3, - name: 'mainStat', + name: 'mainstat', type: 'number', values: new ArrayVector([0.1, 0.1, 0.1]), }, secondaryStat: { config: {}, index: 4, - name: 'secondaryStat', + name: 'secondarystat', type: 'number', values: new ArrayVector([2, 2, 2]), }, @@ -106,14 +113,14 @@ describe('processNodes', () => { mainStat: { config: {}, index: 3, - name: 'mainStat', + name: 'mainstat', type: 'number', values: new ArrayVector([0.1, 0.1, 0.1]), }, secondaryStat: { config: {}, index: 4, - name: 'secondaryStat', + name: 'secondarystat', type: 'number', values: new ArrayVector([2, 2, 2]), }, @@ -150,14 +157,14 @@ describe('processNodes', () => { mainStat: { config: {}, index: 3, - name: 'mainStat', + name: 'mainstat', type: 'number', values: new ArrayVector([0.1, 0.1, 0.1]), }, secondaryStat: { config: {}, index: 4, - name: 'secondaryStat', + name: 'secondarystat', type: 'number', values: new ArrayVector([2, 2, 2]), }, @@ -204,4 +211,85 @@ describe('processNodes', () => { }, ]); }); + + it('detects dataframes correctly', () => { + const validFrames = [ + new MutableDataFrame({ + refId: 'hasPreferredVisualisationType', + fields: [], + meta: { + preferredVisualisationType: 'nodeGraph', + }, + }), + new MutableDataFrame({ + refId: 'hasName', + fields: [], + name: 'nodes', + }), + new MutableDataFrame({ + refId: 'nodes', // hasRefId + fields: [], + }), + new MutableDataFrame({ + refId: 'hasValidNodesShape', + fields: [{ name: 'id', type: FieldType.string }], + }), + new MutableDataFrame({ + refId: 'hasValidEdgesShape', + fields: [ + { name: 'id', type: FieldType.string }, + { name: 'source', type: FieldType.string }, + { name: 'target', type: FieldType.string }, + ], + }), + ]; + const invalidFrames = [ + new MutableDataFrame({ + refId: 'invalidData', + fields: [], + }), + ]; + const frames = [...validFrames, ...invalidFrames]; + + const nodeGraphFrames = getNodeGraphDataFrames(frames as DataFrame[]); + expect(nodeGraphFrames.length).toBe(5); + expect(nodeGraphFrames).toEqual(validFrames); + }); + + it('getting fields is case insensitive', () => { + const nodeFrame = new MutableDataFrame({ + refId: 'nodes', + fields: [ + { name: 'id', type: FieldType.string, values: ['id'] }, + { name: 'title', type: FieldType.string, values: ['title'] }, + { name: 'SUBTITLE', type: FieldType.string, values: ['subTitle'] }, + { name: 'mainstat', type: FieldType.string, values: ['mainStat'] }, + { name: 'seconDarysTat', type: FieldType.string, values: ['secondaryStat'] }, + ], + }); + + const nodeFields = getNodeFields(nodeFrame); + expect(nodeFields.id).toBeDefined(); + expect(nodeFields.title).toBeDefined(); + expect(nodeFields.subTitle).toBeDefined(); + expect(nodeFields.mainStat).toBeDefined(); + expect(nodeFields.secondaryStat).toBeDefined(); + + const edgeFrame = new MutableDataFrame({ + refId: 'nodes', + fields: [ + { name: 'id', type: FieldType.string, values: ['id'] }, + { name: 'source', type: FieldType.string, values: ['title'] }, + { name: 'TARGET', type: FieldType.string, values: ['subTitle'] }, + { name: 'mainstat', type: FieldType.string, values: ['mainStat'] }, + { name: 'secondarystat', type: FieldType.string, values: ['secondaryStat'] }, + ], + }); + const edgeFields = getEdgeFields(edgeFrame); + expect(edgeFields.id).toBeDefined(); + expect(edgeFields.source).toBeDefined(); + expect(edgeFields.target).toBeDefined(); + expect(edgeFields.mainStat).toBeDefined(); + expect(edgeFields.secondaryStat).toBeDefined(); + }); }); diff --git a/public/app/plugins/panel/nodeGraph/utils.ts b/public/app/plugins/panel/nodeGraph/utils.ts index c79274427f1..0aac55cedf8 100644 --- a/public/app/plugins/panel/nodeGraph/utils.ts +++ b/public/app/plugins/panel/nodeGraph/utils.ts @@ -35,13 +35,17 @@ export function shortenLine(line: Line, length: number): Line { } export function getNodeFields(nodes: DataFrame) { - const fieldsCache = new FieldCache(nodes); + const normalizedFrames = { + ...nodes, + fields: nodes.fields.map((field) => ({ ...field, name: field.name.toLowerCase() })), + }; + const fieldsCache = new FieldCache(normalizedFrames); return { - id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id), - title: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.title), - subTitle: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.subTitle), - mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat), - secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat), + id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id.toLowerCase()), + title: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.title.toLowerCase()), + subTitle: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.subTitle.toLowerCase()), + mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat.toLowerCase()), + secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat.toLowerCase()), arc: findFieldsByPrefix(nodes, NodeGraphDataFrameFieldNames.arc), details: findFieldsByPrefix(nodes, NodeGraphDataFrameFieldNames.detail), color: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.color), @@ -49,14 +53,18 @@ export function getNodeFields(nodes: DataFrame) { } export function getEdgeFields(edges: DataFrame) { - const fieldsCache = new FieldCache(edges); + const normalizedFrames = { + ...edges, + fields: edges.fields.map((field) => ({ ...field, name: field.name.toLowerCase() })), + }; + const fieldsCache = new FieldCache(normalizedFrames); return { - id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id), - source: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.source), - target: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.target), - mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat), - secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat), - details: findFieldsByPrefix(edges, NodeGraphDataFrameFieldNames.detail), + id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id.toLowerCase()), + source: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.source.toLowerCase()), + target: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.target.toLowerCase()), + mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat.toLowerCase()), + secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat.toLowerCase()), + details: findFieldsByPrefix(edges, NodeGraphDataFrameFieldNames.detail.toLowerCase()), }; } @@ -172,11 +180,11 @@ function makeNode(index: number) { return { id: index.toString(), title: `service:${index}`, - subTitle: 'service', + subtitle: 'service', arc__success: 0.5, arc__errors: 0.5, - mainStat: 0.1, - secondaryStat: 2, + mainstat: 0.1, + secondarystat: 2, color: 0.5, }; } @@ -327,7 +335,13 @@ export function getNodeGraphDataFrames(frames: DataFrame[]) { if (frame.meta?.preferredVisualisationType === 'nodeGraph') { return true; } - if (frame.name === 'nodes' || frame.name === 'edges') { + + if (frame.name === 'nodes' || frame.name === 'edges' || frame.refId === 'nodes' || frame.refId === 'edges') { + return true; + } + + const fieldsCache = new FieldCache(frame); + if (fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id)) { return true; } From a30ab515500644b1f9e314114386410971209073 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 13 Apr 2022 17:19:54 +0200 Subject: [PATCH 071/121] Alerting: FGAC for alert rule view and edit page (#47441) * Add permission check to the find route, add query not accessible warning for the view page * Hide grafana or mimir rule buttons depending on user's permissions * Add grafana and cloud read rules checking on the alert rules list view * Improve missing data source handling, refactor edit and remove permissions handling * Add tests for rule edit permissions * PR feedback --- public/app/features/alerting/routes.tsx | 22 +-- .../features/alerting/unified/RuleViewer.tsx | 59 ++++--- .../rule-editor/AlertTypeStep.test.tsx | 75 +++++++++ .../components/rule-editor/AlertTypeStep.tsx | 23 ++- .../components/rule-editor/PreviewRule.tsx | 39 +++-- .../rule-editor/rule-types/RuleTypePicker.tsx | 47 +++--- .../components/rules/RuleDetails.test.tsx | 26 +-- .../rules/RuleDetailsActionButtons.tsx | 51 +++--- .../rules/RuleListGroupView.test.tsx | 108 ++++++++++++ .../components/rules/RuleListGroupView.tsx | 10 +- .../unified/hooks/useAlertQueriesStatus.ts | 12 ++ .../unified/hooks/useIsRuleEditable.test.tsx | 156 ++++++++++++++++++ .../unified/hooks/useIsRuleEditable.ts | 15 +- public/app/features/alerting/unified/mocks.ts | 16 ++ 14 files changed, 541 insertions(+), 118 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.test.tsx create mode 100644 public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx create mode 100644 public/app/features/alerting/unified/hooks/useAlertQueriesStatus.ts create mode 100644 public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index ea93183aa40..d8eaa9cd338 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -1,12 +1,11 @@ -import React from 'react'; -import { Redirect } from 'react-router-dom'; +import { OrgRole } from '@grafana/data'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { config } from 'app/core/config'; import { RouteDescriptor } from 'app/core/navigation/types'; -import { uniq } from 'lodash'; -import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; -import { OrgRole } from '@grafana/data'; +import { uniq } from 'lodash'; +import React from 'react'; +import { Redirect } from 'react-router-dom'; import { evaluateAccess } from './unified/utils/access-control'; const commonRoutes: RouteDescriptor[] = [ @@ -101,11 +100,10 @@ const unifiedRoutes: RouteDescriptor[] = [ }, { path: '/alerting/routes', - roles: () => - contextSrv.evaluatePermission(config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : () => [], [ - AccessControlAction.AlertingNotificationsRead, - AccessControlAction.AlertingNotificationsExternalRead, - ]), + roles: evaluateAccess( + [AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead], + [OrgRole.Editor, OrgRole.Admin] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes') ), @@ -263,6 +261,10 @@ const unifiedRoutes: RouteDescriptor[] = [ { path: '/alerting/:sourceName/:name/find', pageClass: 'page-alerting', + roles: evaluateAccess( + [AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead], + [OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertingRedirectToRule"*/ 'app/features/alerting/unified/RedirectToRuleViewer') ), diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 1b904541fdc..105e078bc4a 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -1,36 +1,37 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useObservable } from 'react-use'; import { css } from '@emotion/css'; import { GrafanaTheme2, LoadingState, PanelData } from '@grafana/data'; import { - withErrorBoundary, - useStyles2, Alert, + Button, + Icon, LoadingPlaceholder, PanelChromeLoadingIndicator, - Icon, - Button, + useStyles2, VerticalGroup, + withErrorBoundary, } from '@grafana/ui'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { AlertingQueryRunner } from './state/AlertingQueryRunner'; -import { useCombinedRule } from './hooks/useCombinedRule'; -import { alertRuleToQueries } from './utils/query'; -import { RuleState } from './components/rules/RuleState'; -import { getRulesSourceByName } from './utils/datasource'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useObservable } from 'react-use'; +import { AlertQuery } from '../../../types/unified-alerting-dto'; +import { AlertLabels } from './components/AlertLabels'; import { DetailsField } from './components/DetailsField'; -import { RuleHealth } from './components/rules/RuleHealth'; +import { RuleViewerLayout, RuleViewerLayoutContent } from './components/rule-viewer/RuleViewerLayout'; import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization'; import { RuleDetailsActionButtons } from './components/rules/RuleDetailsActionButtons'; -import { RuleDetailsMatchingInstances } from './components/rules/RuleDetailsMatchingInstances'; -import { RuleDetailsDataSources } from './components/rules/RuleDetailsDataSources'; -import { RuleViewerLayout, RuleViewerLayoutContent } from './components/rule-viewer/RuleViewerLayout'; -import { AlertLabels } from './components/AlertLabels'; -import { RuleDetailsExpression } from './components/rules/RuleDetailsExpression'; import { RuleDetailsAnnotations } from './components/rules/RuleDetailsAnnotations'; -import * as ruleId from './utils/rule-id'; -import { AlertQuery } from '../../../types/unified-alerting-dto'; +import { RuleDetailsDataSources } from './components/rules/RuleDetailsDataSources'; +import { RuleDetailsExpression } from './components/rules/RuleDetailsExpression'; import { RuleDetailsFederatedSources } from './components/rules/RuleDetailsFederatedSources'; +import { RuleDetailsMatchingInstances } from './components/rules/RuleDetailsMatchingInstances'; +import { RuleHealth } from './components/rules/RuleHealth'; +import { RuleState } from './components/rules/RuleState'; +import { useAlertQueriesStatus } from './hooks/useAlertQueriesStatus'; +import { useCombinedRule } from './hooks/useCombinedRule'; +import { AlertingQueryRunner } from './state/AlertingQueryRunner'; +import { getRulesSourceByName } from './utils/datasource'; +import { alertRuleToQueries } from './utils/query'; +import * as ruleId from './utils/rule-id'; import { isFederatedRuleGroup } from './utils/rules'; type RuleViewerProps = GrafanaRouteComponentProps<{ id?: string; sourceName?: string }>; @@ -49,19 +50,23 @@ export function RuleViewer({ match }: RuleViewerProps) { const queries2 = useMemo(() => alertRuleToQueries(rule), [rule]); const [queries, setQueries] = useState([]); + const { allDataSourcesAvailable } = useAlertQueriesStatus(queries2); + const onRunQueries = useCallback(() => { - if (queries.length > 0) { + if (queries.length > 0 && allDataSourcesAvailable) { runner.run(queries); } - }, [queries, runner]); + }, [queries, runner, allDataSourcesAvailable]); useEffect(() => { setQueries(queries2); }, [queries2]); useEffect(() => { - onRunQueries(); - }, [onRunQueries]); + if (allDataSourcesAvailable) { + onRunQueries(); + } + }, [onRunQueries, allDataSourcesAvailable]); useEffect(() => { return () => runner.destroy(); @@ -192,6 +197,11 @@ export function RuleViewer({ match }: RuleViewerProps) { )} + {!isFederatedRule && !allDataSourcesAvailable && ( + + Cannot display the query preview. Some of the data sources used in the queries are not available. + + )} ); } @@ -219,6 +229,9 @@ const getStyles = (theme: GrafanaTheme2) => { border-bottom: 1px solid ${theme.colors.border.medium}; padding: ${theme.spacing(2)}; `, + queryWarning: css` + margin: ${theme.spacing(4, 0)}; + `, details: css` display: flex; flex-direction: row; diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.test.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.test.tsx new file mode 100644 index 00000000000..7eacd6a1fcb --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.test.tsx @@ -0,0 +1,75 @@ +import { render } from '@testing-library/react'; +import { contextSrv } from 'app/core/services/context_srv'; +import { configureStore } from 'app/store/configureStore'; +import { AccessControlAction } from 'app/types'; +import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { Provider } from 'react-redux'; +import { byText } from 'testing-library-selector'; +import { AlertTypeStep } from './AlertTypeStep'; + +const ui = { + ruleTypePicker: { + grafanaManagedButton: byText('Grafana managed alert'), + mimirOrLokiButton: byText('Mimir or Loki alert'), + mimirOrLokiRecordingButton: byText('Mimir or Loki recording rule'), + }, +}; + +const FormProviderWrapper: React.FC = ({ children }) => { + const methods = useForm({}); + return {children}; +}; + +function renderAlertTypeStep() { + const store = configureStore(); + + render( + + + , + { wrapper: FormProviderWrapper } + ); +} + +describe('RuleTypePicker', () => { + describe('FGAC', () => { + it('Should display grafana, mimir alert and mimir recording buttons when user has rule create and write permissions', async () => { + jest.spyOn(contextSrv, 'hasPermission').mockImplementation((action) => { + return [AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite].includes( + action as AccessControlAction + ); + }); + + renderAlertTypeStep(); + + expect(ui.ruleTypePicker.grafanaManagedButton.get()).toBeInTheDocument(); + expect(ui.ruleTypePicker.mimirOrLokiButton.get()).toBeInTheDocument(); + expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.get()).toBeInTheDocument(); + }); + + it('Should hide grafana button when user does not have rule create permission', () => { + jest.spyOn(contextSrv, 'hasPermission').mockImplementation((action) => { + return [AccessControlAction.AlertingRuleExternalWrite].includes(action as AccessControlAction); + }); + + renderAlertTypeStep(); + + expect(ui.ruleTypePicker.grafanaManagedButton.query()).not.toBeInTheDocument(); + expect(ui.ruleTypePicker.mimirOrLokiButton.get()).toBeInTheDocument(); + expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.get()).toBeInTheDocument(); + }); + + it('Should hide mimir alert and mimir recording when user does not have rule external write permission', () => { + jest.spyOn(contextSrv, 'hasPermission').mockImplementation((action) => { + return [AccessControlAction.AlertingRuleCreate].includes(action as AccessControlAction); + }); + + renderAlertTypeStep(); + + expect(ui.ruleTypePicker.grafanaManagedButton.get()).toBeInTheDocument(); + expect(ui.ruleTypePicker.mimirOrLokiButton.query()).not.toBeInTheDocument(); + expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.query()).not.toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx index b06e7e2497c..c0ebdaa73bf 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx @@ -10,6 +10,8 @@ import { GroupAndNamespaceFields } from './GroupAndNamespaceFields'; import { CloudRulesSourcePicker } from './CloudRulesSourcePicker'; import { checkForPathSeparator } from './util'; import { RuleTypePicker } from './rule-types/RuleTypePicker'; +import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction } from 'app/types'; interface Props { editingExistingRule: boolean; @@ -24,6 +26,8 @@ const recordingRuleNameValidationPattern = { export const AlertTypeStep: FC = ({ editingExistingRule }) => { const styles = useStyles2(getStyles); + const { enabledRuleTypes, defaultRuleType } = getAvailableRuleTypes(); + const { register, control, @@ -44,8 +48,9 @@ export const AlertTypeStep: FC = ({ editingExistingRule }) => { render={({ field: { onChange } }) => ( )} name="type" @@ -140,6 +145,22 @@ export const AlertTypeStep: FC = ({ editingExistingRule }) => { ); }; +function getAvailableRuleTypes() { + const canCreateGrafanaRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleCreate); + const canCreateCloudRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite); + const defaultRuleType = canCreateGrafanaRules ? RuleFormType.grafana : RuleFormType.cloudAlerting; + + const enabledRuleTypes: RuleFormType[] = []; + if (canCreateGrafanaRules) { + enabledRuleTypes.push(RuleFormType.grafana); + } + if (canCreateCloudRules) { + enabledRuleTypes.push(RuleFormType.cloudAlerting, RuleFormType.cloudRecording); + } + + return { enabledRuleTypes, defaultRuleType }; +} + const getStyles = (theme: GrafanaTheme2) => ({ formInput: css` width: 330px; diff --git a/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx b/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx index 9cac7d83d27..4b2bf6a9510 100644 --- a/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx @@ -1,33 +1,44 @@ -import React, { useCallback, useState } from 'react'; import { css } from '@emotion/css'; -import { useFormContext } from 'react-hook-form'; -import { takeWhile } from 'rxjs/operators'; -import { useMountedState } from 'react-use'; -import { Button, HorizontalGroup, useStyles2 } from '@grafana/ui'; import { dateTimeFormatISO, GrafanaTheme2, LoadingState } from '@grafana/data'; -import { RuleFormType } from '../../types/rule-form'; -import { PreviewRuleRequest, PreviewRuleResponse } from '../../types/preview'; +import { Alert, Button, HorizontalGroup, useStyles2 } from '@grafana/ui'; +import React, { useCallback, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useMountedState } from 'react-use'; +import { takeWhile } from 'rxjs/operators'; import { previewAlertRule } from '../../api/preview'; +import { useAlertQueriesStatus } from '../../hooks/useAlertQueriesStatus'; +import { PreviewRuleRequest, PreviewRuleResponse } from '../../types/preview'; +import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { PreviewRuleResult } from './PreviewRuleResult'; -const fields: string[] = ['type', 'dataSourceName', 'condition', 'queries', 'expression']; +const fields: Array = ['type', 'dataSourceName', 'condition', 'queries', 'expression']; export function PreviewRule(): React.ReactElement | null { const styles = useStyles2(getStyles); const [preview, onPreview] = usePreview(); - const { watch } = useFormContext(); - const [type, condition] = watch(['type', 'condition']); + const { watch } = useFormContext(); + const [type, condition, queries] = watch(['type', 'condition', 'queries']); + const { allDataSourcesAvailable } = useAlertQueriesStatus(queries); if (type === RuleFormType.cloudRecording || type === RuleFormType.cloudAlerting) { return null; } + const isPreviewAvailable = Boolean(condition) && allDataSourcesAvailable; + return (
    - + {allDataSourcesAvailable && ( + + )} + {!allDataSourcesAvailable && ( + + Cannot display the query preview. Some of the data sources used in the queries are not available. + + )}
    @@ -36,7 +47,7 @@ export function PreviewRule(): React.ReactElement | null { function usePreview(): [PreviewRuleResponse | undefined, () => void] { const [preview, setPreview] = useState(); - const { getValues } = useFormContext(); + const { getValues } = useFormContext(); const isMounted = useMountedState(); const onPreview = useCallback(() => { diff --git a/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx b/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx index 85e3365cd7f..e9dcc0c3450 100644 --- a/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx @@ -1,3 +1,6 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data/src'; +import { Stack } from '@grafana/experimental'; import { useStyles2 } from '@grafana/ui'; import { isEmpty } from 'lodash'; import React, { FC } from 'react'; @@ -6,16 +9,14 @@ import { RuleFormType } from '../../../types/rule-form'; import { GrafanaManagedRuleType } from './GrafanaManagedAlert'; import { MimirFlavoredType } from './MimirOrLokiAlert'; import { RecordingRuleType } from './MimirOrLokiRecordingRule'; -import { css } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data/src'; -import { Stack } from '@grafana/experimental'; interface RuleTypePickerProps { onChange: (value: RuleFormType) => void; selected: RuleFormType; + enabledTypes: RuleFormType[]; } -const RuleTypePicker: FC = ({ selected, onChange }) => { +const RuleTypePicker: FC = ({ selected, onChange, enabledTypes }) => { const rulesSourcesWithRuler = useRulesSourcesWithRuler(); const hasLotexDatasources = !isEmpty(rulesSourcesWithRuler); @@ -24,22 +25,30 @@ const RuleTypePicker: FC = ({ selected, onChange }) => { return ( <> - - - + {enabledTypes.includes(RuleFormType.grafana) && ( + + )} + {enabledTypes.includes(RuleFormType.cloudAlerting) && ( + + )} + {enabledTypes.includes(RuleFormType.cloudRecording) && ( + + )} - - Select “Grafana managed” unless you have a Mimir, Loki or Cortex data source with the Ruler API - enabled. - + {enabledTypes.includes(RuleFormType.grafana) && ( + + Select “Grafana managed” unless you have a Mimir, Loki or Cortex data source with the Ruler API + enabled. + + )} ); }; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx index 4c78bd50443..6e5c6147bf0 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx @@ -28,13 +28,11 @@ const ui = { jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true); describe('RuleDetails FGAC', () => { - mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true }); - describe('Grafana rules action buttons', () => { const grafanaRule = getGrafanaRule({ name: 'Grafana' }); it('Should not render Edit button for users without the update permission', () => { // Arrange - jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false }); // Act renderRuleDetails(grafanaRule); @@ -45,7 +43,7 @@ describe('RuleDetails FGAC', () => { it('Should not render Delete button for users without the delete permission', () => { // Arrange - jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false }); // Act renderRuleDetails(grafanaRule); @@ -56,9 +54,7 @@ describe('RuleDetails FGAC', () => { it('Should render Edit button for users with the update permission', () => { // Arrange - jest - .spyOn(contextSrv, 'hasPermission') - .mockImplementation((action) => action === AccessControlAction.AlertingRuleUpdate); + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true }); // Act renderRuleDetails(grafanaRule); @@ -69,9 +65,7 @@ describe('RuleDetails FGAC', () => { it('Should render Delete button for users with the delete permission', () => { // Arrange - jest - .spyOn(contextSrv, 'hasPermission') - .mockImplementation((action) => action === AccessControlAction.AlertingRuleDelete); + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true }); // Act renderRuleDetails(grafanaRule); @@ -109,7 +103,7 @@ describe('RuleDetails FGAC', () => { const cloudRule = getCloudRule({ name: 'Cloud' }); it('Should not render Edit button for users without the update permission', () => { // Arrange - jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false }); // Act renderRuleDetails(cloudRule); @@ -120,7 +114,7 @@ describe('RuleDetails FGAC', () => { it('Should not render Delete button for users without the delete permission', () => { // Arrange - jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false }); // Act renderRuleDetails(cloudRule); @@ -131,9 +125,7 @@ describe('RuleDetails FGAC', () => { it('Should render Edit button for users with the update permission', () => { // Arrange - jest - .spyOn(contextSrv, 'hasPermission') - .mockImplementation((action) => action === AccessControlAction.AlertingRuleExternalWrite); + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true }); // Act renderRuleDetails(cloudRule); @@ -144,9 +136,7 @@ describe('RuleDetails FGAC', () => { it('Should render Delete button for users with the delete permission', () => { // Arrange - jest - .spyOn(contextSrv, 'hasPermission') - .mockImplementation((action) => action === AccessControlAction.AlertingRuleExternalWrite); + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true }); // Act renderRuleDetails(cloudRule); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 7f26d28ba87..43604a68733 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -4,7 +4,6 @@ import { config } from '@grafana/runtime'; import { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/services/context_srv'; -import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control'; import { AccessControlAction } from 'app/types'; import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; @@ -41,16 +40,13 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { : getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name; const rulesSourceName = getRulesSourceName(rulesSource); - const rulesPermissions = getRulesPermissions(rulesSourceName); - const hasEditPermission = contextSrv.hasPermission(rulesPermissions.update); - const hasDeletePermission = contextSrv.hasPermission(rulesPermissions.delete); const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore); const leftButtons: JSX.Element[] = []; const rightButtons: JSX.Element[] = []; const isFederated = isFederatedRuleGroup(group); - const { isEditable } = useIsRuleEditable(rulesSourceName, rulerRule); + const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule); const returnTo = location.pathname + location.search; const isViewMode = inViewMode(location.pathname); @@ -187,7 +183,6 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { ); } - // TODO Maybe there is a way to unify isEditable with FGAC permissions if (isEditable && rulerRule && !isFederated) { const sourceName = getRulesSourceName(rulesSource); const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule); @@ -218,29 +213,29 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { ); } - if (hasEditPermission) { - rightButtons.push( - - Edit - - ); - } - if (hasDeletePermission) { - rightButtons.push( - - ); - } + rightButtons.push( + + Edit + + ); } + + if (isRemovable && rulerRule && !isFederated) { + rightButtons.push( + + ); + } + if (leftButtons.length || rightButtons.length) { return ( <> diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx new file mode 100644 index 00000000000..e7d2192e5d8 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx @@ -0,0 +1,108 @@ +import { render } from '@testing-library/react'; +import { contextSrv } from 'app/core/services/context_srv'; +import { configureStore } from 'app/store/configureStore'; +import { AccessControlAction } from 'app/types'; +import { CombinedRuleNamespace } from 'app/types/unified-alerting'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { byRole } from 'testing-library-selector'; +import { mockCombinedRule, mockDataSource } from '../../mocks'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; +import { RuleListGroupView } from './RuleListGroupView'; + +const ui = { + grafanaRulesHeading: byRole('heading', { name: 'Grafana' }), + cloudRulesHeading: byRole('heading', { name: 'Mimir / Cortex / Loki' }), +}; + +describe('RuleListGroupView', () => { + describe('FGAC', () => { + jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true); + + it('Should display Grafana rules when the user has the alert rule read permission', () => { + const grafanaNamespace = getGrafanaNamespace(); + const namespaces: CombinedRuleNamespace[] = [grafanaNamespace]; + + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((action) => action === AccessControlAction.AlertingRuleRead); + + renderRuleList(namespaces); + + expect(ui.grafanaRulesHeading.get()).toBeInTheDocument(); + }); + + it('Should display Cloud rules when the user has the external rules read permission', () => { + const cloudNamespace = getCloudNamespace(); + const namespaces: CombinedRuleNamespace[] = [cloudNamespace]; + + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((action) => action === AccessControlAction.AlertingRuleExternalRead); + + renderRuleList(namespaces); + + expect(ui.cloudRulesHeading.get()).toBeInTheDocument(); + }); + + it('Should not display Grafana rules when the user does not have alert rule read permission', () => { + const grafanaNamespace = getGrafanaNamespace(); + const namespaces: CombinedRuleNamespace[] = [grafanaNamespace]; + + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + + renderRuleList(namespaces); + + expect(ui.grafanaRulesHeading.query()).not.toBeInTheDocument(); + }); + + it('Should not display Cloud rules when the user does not have the external rules read permission', () => { + const cloudNamespace = getCloudNamespace(); + + const namespaces: CombinedRuleNamespace[] = [cloudNamespace]; + renderRuleList(namespaces); + + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + + renderRuleList(namespaces); + + expect(ui.cloudRulesHeading.query()).not.toBeInTheDocument(); + }); + }); +}); + +function renderRuleList(namespaces: CombinedRuleNamespace[]) { + const store = configureStore(); + + render( + + + + ); +} + +function getGrafanaNamespace(): CombinedRuleNamespace { + return { + name: 'Grafana Test Namespace', + rulesSource: GRAFANA_RULES_SOURCE_NAME, + groups: [ + { + name: 'default', + rules: [mockCombinedRule()], + }, + ], + }; +} + +function getCloudNamespace(): CombinedRuleNamespace { + return { + name: 'Cloud Test Namespace', + rulesSource: mockDataSource(), + groups: [ + { + name: 'Prom group', + rules: [mockCombinedRule()], + }, + ], + }; +} diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx index e2a51b2b390..9bb64492da7 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx @@ -1,6 +1,8 @@ +import { AccessControlAction } from 'app/types'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; import React, { FC, useMemo } from 'react'; import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource'; +import { Authorize } from '../Authorize'; import { CloudRules } from './CloudRules'; import { GrafanaRules } from './GrafanaRules'; @@ -25,8 +27,12 @@ export const RuleListGroupView: FC = ({ namespaces, expandAll }) => { return ( <> - - + + + + + + ); }; diff --git a/public/app/features/alerting/unified/hooks/useAlertQueriesStatus.ts b/public/app/features/alerting/unified/hooks/useAlertQueriesStatus.ts new file mode 100644 index 00000000000..b5e3c61eb05 --- /dev/null +++ b/public/app/features/alerting/unified/hooks/useAlertQueriesStatus.ts @@ -0,0 +1,12 @@ +import { getDataSourceSrv } from '@grafana/runtime'; +import { AlertQuery } from 'app/types/unified-alerting-dto'; +import { useMemo } from 'react'; + +export function useAlertQueriesStatus(queries: AlertQuery[]) { + const allDataSourcesAvailable = useMemo( + () => queries.every((query) => Boolean(getDataSourceSrv().getInstanceSettings(query.datasourceUid))), + [queries] + ); + + return { allDataSourcesAvailable }; +} diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx new file mode 100644 index 00000000000..7925df6f9d6 --- /dev/null +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { contextSrv } from 'app/core/services/context_srv'; +import { configureStore } from 'app/store/configureStore'; +import { AccessControlAction, FolderDTO, StoreState } from 'app/types'; +import { Provider } from 'react-redux'; +import { mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule } from '../mocks'; +import { useFolder } from './useFolder'; +import { useIsRuleEditable } from './useIsRuleEditable'; +import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector'; + +jest.mock('./useFolder'); + +const mocks = { + useFolder: jest.mocked(useFolder), + useUnifiedAlertingSelector: jest.mocked(useUnifiedAlertingSelector), +}; + +describe('useIsRuleEditable', () => { + describe('FGAC enabled', () => { + jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true); + describe('Grafana rules', () => { + it('Should allow editing when the user has the alert rule update permission and folder permissions', () => { + mockPermissions([AccessControlAction.AlertingRuleUpdate]); + mockUseFolder({ canSave: true }); + const wrapper = getProviderWrapper(); + + const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); + + expect(result.current.loading).toBe(false); + expect(result.current.isEditable).toBe(true); + }); + + it('Should allow deleting when the user has the alert rule delete permission and folder permissions', () => { + mockPermissions([AccessControlAction.AlertingRuleDelete]); + mockUseFolder({ canSave: true }); + const wrapper = getProviderWrapper(); + + const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); + + expect(result.current.loading).toBe(false); + expect(result.current.isRemovable).toBe(true); + }); + + it('Should forbid editing when the user has no alert rule update permission and has folder permissions', () => { + mockPermissions([]); + mockUseFolder({ canSave: true }); + const wrapper = getProviderWrapper(); + + const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); + + expect(result.current.loading).toBe(false); + expect(result.current.isEditable).toBe(false); + }); + + it('Should forbid deleting when the user has no alert rule delete permission and has folder permissions', () => { + mockPermissions([]); + mockUseFolder({ canSave: true }); + const wrapper = getProviderWrapper(); + + const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); + + expect(result.current.loading).toBe(false); + expect(result.current.isRemovable).toBe(false); + }); + + it('Should forbid editing and deleting when the user has aler rule permissions but does not have folder permissions', () => { + mockPermissions([AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleDelete]); + mockUseFolder({ canSave: false }); + const wrapper = getProviderWrapper(); + + const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); + + expect(result.current.loading).toBe(false); + expect(result.current.isEditable).toBe(false); + expect(result.current.isRemovable).toBe(false); + }); + }); + + describe('Cloud rules', () => { + it('Should allow editing and deleting when the user has alert rule external write permission', () => { + mockPermissions([AccessControlAction.AlertingRuleExternalWrite]); + const wrapper = getProviderWrapper(); + + const { result } = renderHook(() => useIsRuleEditable('cortex', mockRulerAlertingRule()), { wrapper }); + + expect(result.current.loading).toBe(false); + expect(result.current.isEditable).toBe(true); + expect(result.current.isRemovable).toBe(true); + }); + + it('Should forbid editing and deleting when the user has no alert rule external write permission', () => { + mockPermissions([]); + const wrapper = getProviderWrapper(); + + const { result } = renderHook(() => useIsRuleEditable('cortex', mockRulerAlertingRule()), { wrapper }); + + expect(result.current.loading).toBe(false); + expect(result.current.isEditable).toBe(false); + expect(result.current.isRemovable).toBe(false); + }); + }); + }); +}); + +function mockUseFolder(partial?: Partial) { + mocks.useFolder.mockReturnValue({ loading: false, folder: mockFolder(partial) }); +} + +function mockPermissions(grantedPermissions: AccessControlAction[]) { + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((action) => grantedPermissions.includes(action as AccessControlAction)); +} + +function getProviderWrapper() { + const dataSources = getMockedDataSources(); + const store = mockStore({ dataSources }); + const wrapper: React.FC = ({ children }) => {children}; + return wrapper; +} + +function getMockedDataSources(): StoreState['unifiedAlerting']['dataSources'] { + return { + grafana: { + loading: false, + dispatched: false, + result: { + id: 'grafana', + name: 'grafana', + rulerConfig: { dataSourceName: 'grafana', apiVersion: 'legacy' }, + }, + }, + cortex: { + loading: false, + dispatched: false, + result: { + id: 'cortex', + name: 'Cortex', + rulerConfig: { dataSourceName: 'cortex', apiVersion: 'legacy' }, + }, + }, + }; +} + +function mockStore(unifiedAlerting?: Partial) { + const defaultState = configureStore().getState(); + + return configureStore({ + ...defaultState, + unifiedAlerting: { + ...defaultState.unifiedAlerting, + ...unifiedAlerting, + }, + }); +} diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts index 97695319adf..5d42819b433 100644 --- a/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts @@ -1,11 +1,13 @@ import { contextSrv } from 'app/core/services/context_srv'; import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; +import { getRulesPermissions } from '../utils/access-control'; import { isGrafanaRulerRule } from '../utils/rules'; import { useFolder } from './useFolder'; import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector'; interface ResultBag { isEditable?: boolean; + isRemovable?: boolean; loading: boolean; } @@ -13,10 +15,14 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): const dataSources = useUnifiedAlertingSelector((state) => state.dataSources); const folderUID = rule && isGrafanaRulerRule(rule) ? rule.grafana_alert.namespace_uid : undefined; + const rulePermission = getRulesPermissions(rulesSourceName); + const hasEditPermission = contextSrv.hasAccess(rulePermission.update, contextSrv.isEditor); + const hasRemovePermission = contextSrv.hasAccess(rulePermission.delete, contextSrv.isEditor); + const { folder, loading } = useFolder(folderUID); if (!rule) { - return { isEditable: false, loading: false }; + return { isEditable: false, isRemovable: false, loading: false }; } // grafana rules can be edited if user can edit the folder they're in @@ -27,14 +33,17 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): ); } return { - isEditable: folder?.canSave, + isEditable: hasEditPermission && folder?.canSave, + isRemovable: hasRemovePermission && folder?.canSave, loading, }; } // prom rules are only editable by users with Editor role and only if rules source supports editing + const isRulerAvailable = Boolean(dataSources[rulesSourceName]?.result?.rulerConfig); return { - isEditable: contextSrv.isEditor && Boolean(dataSources[rulesSourceName]?.result?.rulerConfig), + isEditable: hasEditPermission && isRulerAvailable, + isRemovable: hasRemovePermission && isRulerAvailable, loading: dataSources[rulesSourceName]?.loading, }; } diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index eca52ac0931..952037e6f44 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -29,6 +29,7 @@ import { Silence, SilenceState, } from 'app/plugins/datasource/alertmanager/types'; +import { FolderDTO } from 'app/types'; let nextDataSourceId = 1; @@ -449,3 +450,18 @@ export const mockCombinedRule = (partial?: Partial): CombinedRule rulerRule: mockRulerAlertingRule(), ...partial, }); + +export const mockFolder = (partial?: Partial): FolderDTO => { + return { + id: 1, + uid: 'gdev-1', + title: 'Gdev', + version: 1, + url: '', + canAdmin: true, + canDelete: true, + canEdit: true, + canSave: true, + ...partial, + }; +}; From 3456793e0f610847f6e644abf78e815ee55a4d02 Mon Sep 17 00:00:00 2001 From: Jguer Date: Wed, 13 Apr 2022 15:22:53 +0000 Subject: [PATCH 072/121] AccessControl: fix value copying in method access and add LogID (#47694) * AccessControl: fix value copying in method access and add LogID Co-authored-by: Gabriel MABILLE * Lint. Co-authored-by: Gabriel MABILLE Co-authored-by: gamab --- pkg/services/accesscontrol/models.go | 30 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 11d856c4494..0d4a2fc6f36 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -2,6 +2,7 @@ package accesscontrol import ( "encoding/json" + "fmt" "strings" "time" @@ -32,15 +33,15 @@ type Role struct { Created time.Time `json:"created"` } -func (r Role) Global() bool { +func (r *Role) Global() bool { return r.OrgID == GlobalOrgID } -func (r Role) IsFixed() bool { +func (r *Role) IsFixed() bool { return strings.HasPrefix(r.Name, FixedRolePrefix) } -func (r Role) GetDisplayName() string { +func (r *Role) GetDisplayName() string { if r.IsFixed() && r.DisplayName == "" { r.DisplayName = fallbackDisplayName(r.Name) } @@ -78,7 +79,22 @@ type RoleDTO struct { Created time.Time `json:"created"` } -func (r RoleDTO) Role() Role { +func (r *RoleDTO) LogID() string { + var org string + + if r.Global() { + org = "Global" + } else { + org = fmt.Sprintf("OrgId:%v", r.OrgID) + } + + if r.UID != "" { + return fmt.Sprintf("[%s RoleUID:%v]", org, r.UID) + } + return fmt.Sprintf("[%s Role:%v]", org, r.Name) +} + +func (r *RoleDTO) Role() Role { return Role{ ID: r.ID, OrgID: r.OrgID, @@ -94,15 +110,15 @@ func (r RoleDTO) Role() Role { } } -func (r RoleDTO) Global() bool { +func (r *RoleDTO) Global() bool { return r.OrgID == GlobalOrgID } -func (r RoleDTO) IsFixed() bool { +func (r *RoleDTO) IsFixed() bool { return strings.HasPrefix(r.Name, FixedRolePrefix) } -func (r RoleDTO) GetDisplayName() string { +func (r *RoleDTO) GetDisplayName() string { if r.IsFixed() && r.DisplayName == "" { r.DisplayName = fallbackDisplayName(r.Name) } From dfdfe3f4288d3c3d67364925bb5f08a32a009391 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 13 Apr 2022 09:29:03 -0600 Subject: [PATCH 073/121] TimeSeries & BarChart: refactor stacking (#47373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Bedi Co-authored-by: Dominik Prokop Co-authored-by: Ryan McKinley --- .../panel-graph/graph-ng-stacking2.json | 2957 +++++++++++++++++ .../src/components/GraphNG/GraphNG.tsx | 16 +- .../GraphNG/__snapshots__/utils.test.ts.snap | 16 - .../src/components/Sparkline/Sparkline.tsx | 6 +- .../src/components/TimeSeries/TimeSeries.tsx | 3 +- .../src/components/TimeSeries/utils.ts | 24 +- .../src/components/uPlot/Plot.test.tsx | 34 +- .../uPlot/config/UPlotConfigBuilder.ts | 24 +- .../src/components/uPlot/utils.test.ts | 570 ++-- .../grafana-ui/src/components/uPlot/utils.ts | 345 +- public/app/plugins/panel/barchart/bars.ts | 15 +- public/app/plugins/panel/barchart/utils.ts | 18 +- .../app/plugins/panel/state-timeline/utils.ts | 4 +- 13 files changed, 3578 insertions(+), 454 deletions(-) create mode 100644 devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json diff --git a/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json b/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json new file mode 100644 index 00000000000..29bda45b50f --- /dev/null +++ b/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json @@ -0,0 +1,2957 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 6, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "0,2,2,3,4,5" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "5,4,3,2,2,0" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,10,25,5,0" + } + ], + "title": "Pos Only", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 4, + "y": 0 + }, + "id": 7, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 6, + "refId": "A" + } + ], + "title": "Stack Normal", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 8, + "y": 0 + }, + "id": 8, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 6, + "refId": "A" + } + ], + "title": "Stack 100%", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 12, + "y": 0 + }, + "id": 3, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "+100", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "100,100,100,100,100,100,100,100" + }, + { + "alias": "+50", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "50,50,50,50,50,50,50,50" + }, + { + "alias": "+25", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "25,25,25,25,25,25,25,25" + }, + { + "alias": "-50", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "-50,-50,-50,-50,-50,-50,-50,-50" + }, + { + "alias": "-100", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "-100,-100,-100,-100,-100,-100,-100,-100" + } + ], + "title": "Pos & Neg", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 16, + "y": 0 + }, + "id": 4, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 3, + "refId": "A" + } + ], + "title": "Stack Normal", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 20, + "y": 0 + }, + "id": 12, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 3, + "refId": "A" + } + ], + "title": "Stacked 100%", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NegY" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 0, + "y": 9 + }, + "id": 9, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "NegY", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,10,25,5,0" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "0,2,2,3,4,5" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "5,4,3,2,2,0" + }, + { + "alias": "Neg", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "-15,-5,-15,-5,-15,-5" + } + ], + "title": "Pos & Neg & NegY", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NegY" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 4, + "y": 9 + }, + "id": 10, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 9, + "refId": "A" + } + ], + "title": "Stack Normal", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NegY" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 8, + "y": 9 + }, + "id": 11, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 9, + "refId": "A" + } + ], + "title": "Stack 100%", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 0, + "scaleDistribution": { + "type": "linear" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 12, + "y": 9 + }, + "id": 17, + "options": { + "barRadius": 0, + "barWidth": 1, + "groupWidth": 0.89, + "legend": { + "calcs": [ + "max" + ], + "displayMode": "hidden", + "placement": "right" + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "text": {}, + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "targets": [ + { + "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8\nA, 10, 15,8,3,4,12,14,1\nB, 19, 5,8,3,4,12,14,6\nC, 15, 5,8,3,4,10,4,6\n", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "Barchart", + "type": "barchart" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 0, + "scaleDistribution": { + "type": "linear" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 16, + "y": 9 + }, + "id": 18, + "options": { + "barRadius": 0, + "barWidth": 1, + "groupWidth": 0.89, + "legend": { + "calcs": [ + "max" + ], + "displayMode": "hidden", + "placement": "right" + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "normal", + "text": {}, + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 17, + "refId": "A" + } + ], + "title": "Stack Normal", + "type": "barchart" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 0, + "scaleDistribution": { + "type": "linear" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 20, + "y": 9 + }, + "id": 19, + "options": { + "barRadius": 0, + "barWidth": 1, + "groupWidth": 0.89, + "legend": { + "calcs": [ + "max" + ], + "displayMode": "hidden", + "placement": "right" + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "percent", + "text": {}, + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 17, + "refId": "A" + } + ], + "title": "Stack 100%", + "type": "barchart" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NegY" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 0, + "y": 18 + }, + "id": 13, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 9, + "refId": "A" + } + ], + "title": "Neg & NegY Bars", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NegY" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 4, + "y": 18 + }, + "id": 14, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 9, + "refId": "A" + } + ], + "title": "Stack Normal", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NegY" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 8, + "y": 18 + }, + "id": 15, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 9, + "refId": "A" + } + ], + "title": "Stack 100%", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "(A|B)-series.*" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "bars" + }, + { + "id": "custom.lineWidth", + "value": 0 + }, + { + "id": "custom.fillOpacity", + "value": 100 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 12, + "y": 18 + }, + "id": 21, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "18,25,43,11,5" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "41,29,20,23,27" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "169,109,170,150,116" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "160,131,117,134,139" + } + ], + "title": "Mixed Styles", + "transformations": [], + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "(A|B)-series.*" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "bars" + }, + { + "id": "custom.lineWidth", + "value": 0 + }, + { + "id": "custom.fillOpacity", + "value": 100 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 16, + "y": 18 + }, + "id": 22, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 21, + "refId": "A" + } + ], + "title": "Stack Normal Auto-grouped", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "(A|B)-series.*" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "bars" + }, + { + "id": "custom.lineWidth", + "value": 0 + }, + { + "id": "custom.fillOpacity", + "value": 100 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 20, + "y": 18 + }, + "id": 23, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 21, + "refId": "A" + } + ], + "title": "Stack 100% Auto-grouped", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 0, + "y": 27 + }, + "id": 24, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "+100", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "100,100,100,100,100,null,100,100" + }, + { + "alias": "+50", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "50,50,50,50,50,null,50,50" + }, + { + "alias": "+25", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "25,25,25,25,25,null,25,25" + }, + { + "alias": "-50", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "-50,-50,null,-50,-50,-50,-50,-50" + }, + { + "alias": "-100", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "-100,-100,null,-100,-100,-100,-100,-100" + } + ], + "title": "Null through all", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 4, + "y": 27 + }, + "id": 25, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 24, + "refId": "A" + } + ], + "title": "Stack Normal", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NegY" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 8, + "y": 27 + }, + "id": 2, + "maxDataPoints": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "50,50,50,null,50,50,50,50" + }, + { + "alias": "NegY", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "50,50,50,50,null,50,50" + } + ], + "title": "NegY nulls", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 8, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 12, + "y": 27 + }, + "id": 26, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,2,1,2,1" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "3,3,3,3,3" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "2,2,2,2,2" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "4,4,4,4,4" + } + ], + "title": "1,2,3,4 Unstacked", + "transformations": [], + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 8, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "(A|B)-series.*" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": "B", + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 16, + "y": 27 + }, + "id": 27, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 26, + "refId": "A" + } + ], + "title": "Stack Normal Groups A + B / C + D", + "transformations": [], + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 8, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "(A|B)-series.*" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": "B", + "mode": "percent" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 20, + "y": 27 + }, + "id": 28, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 26, + "refId": "A" + } + ], + "title": "Stack 100% Groups A + B / C + D", + "transformations": [], + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "bars" + }, + { + "id": "color", + "value": { + "mode": "palette-classic" + } + }, + { + "id": "custom.gradientMode", + "value": "none" + }, + { + "id": "custom.barAlignment", + "value": 1 + }, + { + "id": "custom.stacking", + "value": { + "group": "A", + "mode": "normal" + } + }, + { + "id": "custom.spanNulls", + "value": 3600000 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 36 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "csvContent": "\"Time\",\"Sum daily.cost_per_kwh\"\n1639612800000,\n1639872000000,0\n1639958400000,272\n1640044800000,272\n1640131200000,327\n1640217600000,290\n1640304000000,286\n1640390400000,256\n1640476800000,397\n1640563200000,295\n1640649600000,310\n1640736000000,224\n1640822400000,291\n1640908800000,265\n1640995200000,256\n1641081600000,399\n1641168000000,471\n1641254400000,325\n1641340800000,363\n1641427200000,375\n1641513600000,365\n1641600000000,492\n1641686400000,409\n1641772800000,371\n1641859200000,448\n1641945600000,372\n1642032000000,0\n1642118400000,0\n1642204800000,0\n1642291200000,0\n1642377600000,0\n1642464000000,0\n\n", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "A", + "scenarioId": "csv_content" + }, + { + "csvContent": "\"Time\",\"Sum morning.cost_per_kwh\",\"Sum afternoon.cost_per_kwh\",\"Sum evening.cost_per_kwh\",\"Sum night.cost_per_kwh\",\"Sum late.cost_per_kwh\"\n1639612800000,0,0,0,0,0\n1639872000000,,,,,\n1639958400000,,,,,\n1640044800000,,,,,\n1640131200000,,,,,\n1640217600000,32,28,62,63,32\n1640304000000,,,,,\n1640390400000,,,,,\n1640476800000,,,,,\n1640563200000,,,,,\n1640649600000,,,,,\n1640736000000,,,,,\n1640822400000,47,53,68,96,45\n1640908800000,,,,,\n1640995200000,,,,,\n1641081600000,,,,,\n1641168000000,,,,,\n1641254400000,,,,,\n1641340800000,,,,,\n1641427200000,112,130,147,152,115\n1641513600000,,,,,\n1641600000000,,,,,\n1641686400000,,,,,\n1641772800000,,,,,\n1641859200000,,,,,\n1641945600000,,,,,\n1642032000000,0,0,0,0,0\n1642118400000,,,,,\n1642204800000,,,,,\n1642291200000,,,,,\n1642377600000,,,,,\n1642464000000,,,,,\n\n", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "B", + "scenarioId": "csv_content" + } + ], + "title": "https://github.com/grafana/grafana/pull/44359", + "transformations": [], + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "line" + }, + { + "id": "color", + "value": { + "mode": "palette-classic" + } + }, + { + "id": "custom.gradientMode", + "value": "none" + }, + { + "id": "custom.barAlignment", + "value": 1 + }, + { + "id": "custom.stacking", + "value": { + "group": "A", + "mode": "normal" + } + }, + { + "id": "custom.spanNulls", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 36 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "csvContent": "\"Time\",\"Sum daily.cost_per_kwh\"\n1639612800000,\n1639872000000,0\n1639958400000,272\n1640044800000,272\n1640131200000,327\n1640217600000,290\n1640304000000,286\n1640390400000,256\n1640476800000,397\n1640563200000,295\n1640649600000,310\n1640736000000,224\n1640822400000,291\n1640908800000,265\n1640995200000,256\n1641081600000,399\n1641168000000,471\n1641254400000,325\n1641340800000,363\n1641427200000,375\n1641513600000,365\n1641600000000,492\n1641686400000,409\n1641772800000,371\n1641859200000,448\n1641945600000,372\n1642032000000,0\n1642118400000,0\n1642204800000,0\n1642291200000,0\n1642377600000,0\n1642464000000,0\n\n", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "A", + "scenarioId": "csv_content" + }, + { + "csvContent": "\"Time\",\"Sum morning.cost_per_kwh\",\"Sum afternoon.cost_per_kwh\",\"Sum evening.cost_per_kwh\",\"Sum night.cost_per_kwh\",\"Sum late.cost_per_kwh\"\n1639612800000,0,0,0,0,0\n1639872000000,,,,,\n1639958400000,,,,,\n1640044800000,,,,,\n1640131200000,,,,,\n1640217600000,32,28,62,63,32\n1640304000000,,,,,\n1640390400000,,,,,\n1640476800000,,,,,\n1640563200000,,,,,\n1640649600000,,,,,\n1640736000000,,,,,\n1640822400000,47,53,68,96,45\n1640908800000,,,,,\n1640995200000,,,,,\n1641081600000,,,,,\n1641168000000,,,,,\n1641254400000,,,,,\n1641340800000,,,,,\n1641427200000,112,130,147,152,115\n1641513600000,,,,,\n1641600000000,,,,,\n1641686400000,,,,,\n1641772800000,,,,,\n1641859200000,,,,,\n1641945600000,,,,,\n1642032000000,0,0,0,0,0\n1642118400000,,,,,\n1642204800000,,,,,\n1642291200000,,,,,\n1642377600000,,,,,\n1642464000000,,,,,\n\n", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "B", + "scenarioId": "csv_content" + } + ], + "title": "Connect null values = always", + "transformations": [], + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "line" + }, + { + "id": "color", + "value": { + "mode": "palette-classic" + } + }, + { + "id": "custom.gradientMode", + "value": "none" + }, + { + "id": "custom.barAlignment", + "value": 1 + }, + { + "id": "custom.stacking", + "value": { + "group": "A", + "mode": "normal" + } + }, + { + "id": "custom.spanNulls", + "value": false + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 36 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "csvContent": "\"Time\",\"Sum daily.cost_per_kwh\"\n1639612800000,\n1639872000000,0\n1639958400000,272\n1640044800000,272\n1640131200000,327\n1640217600000,290\n1640304000000,286\n1640390400000,256\n1640476800000,397\n1640563200000,295\n1640649600000,310\n1640736000000,224\n1640822400000,291\n1640908800000,265\n1640995200000,256\n1641081600000,399\n1641168000000,471\n1641254400000,325\n1641340800000,363\n1641427200000,375\n1641513600000,365\n1641600000000,492\n1641686400000,409\n1641772800000,371\n1641859200000,448\n1641945600000,372\n1642032000000,0\n1642118400000,0\n1642204800000,0\n1642291200000,0\n1642377600000,0\n1642464000000,0\n\n", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "A", + "scenarioId": "csv_content" + }, + { + "csvContent": "\"Time\",\"Sum morning.cost_per_kwh\",\"Sum afternoon.cost_per_kwh\",\"Sum evening.cost_per_kwh\",\"Sum night.cost_per_kwh\",\"Sum late.cost_per_kwh\"\n1639612800000,0,0,0,0,0\n1639872000000,,,,,\n1639958400000,,,,,\n1640044800000,,,,,\n1640131200000,,,,,\n1640217600000,32,28,62,63,32\n1640304000000,,,,,\n1640390400000,,,,,\n1640476800000,,,,,\n1640563200000,,,,,\n1640649600000,,,,,\n1640736000000,,,,,\n1640822400000,47,53,68,96,45\n1640908800000,,,,,\n1640995200000,,,,,\n1641081600000,,,,,\n1641168000000,,,,,\n1641254400000,,,,,\n1641340800000,,,,,\n1641427200000,112,130,147,152,115\n1641513600000,,,,,\n1641600000000,,,,,\n1641686400000,,,,,\n1641772800000,,,,,\n1641859200000,,,,,\n1641945600000,,,,,\n1642032000000,0,0,0,0,0\n1642118400000,,,,,\n1642204800000,,,,,\n1642291200000,,,,,\n1642377600000,,,,,\n1642464000000,,,,,\n\n", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "B", + "scenarioId": "csv_content" + } + ], + "title": "Connect null values = never", + "transformations": [], + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 32, + "style": "dark", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "templating": { + "list": [] + }, + "time": { + "from": "2021-12-14T04:26:06.000Z", + "to": "2022-01-19T08:44:05.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "TimeSeries & BarChart Stacking", + "uid": "1KxMUdE7k", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index 9758bfcb61a..e02a291226a 100755 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -82,7 +82,7 @@ function sameProps(prevProps: any, nextProps: any, propsToDiff: Array { constructor(props: GraphNGProps) { super(props); - this.state = this.prepState(props); + let state = this.prepState(props); + state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData; + this.state = state; this.plotInstance = React.createRef(); } @@ -131,7 +133,6 @@ export class GraphNG extends React.Component { state = { alignedFrame, - alignedData: config!.prepData!([alignedFrame]) as AlignedData, config, }; @@ -229,12 +230,13 @@ export class GraphNG extends React.Component { if (shouldReconfig) { newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange); - newState.alignedData = newState.config.prepData!([newState.alignedFrame]) as AlignedData; pluginLog('GraphNG', false, 'config recreated', newState.config); } - } - newState && this.setState(newState); + newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData; + + this.setState(newState); + } } } @@ -255,7 +257,7 @@ export class GraphNG extends React.Component { {(vizWidth: number, vizHeight: number) => ( { const alignedDataFrame = preparePlotFrame(props.sparkline, props.config); this.state = { - data: preparePlotData([alignedDataFrame]), + data: preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame)), alignedDataFrame, configBuilder: this.prepareConfig(alignedDataFrame), }; @@ -65,7 +65,7 @@ export class Sparkline extends PureComponent { return { ...state, - data: preparePlotData([frame]), + data: preparePlotData2(frame, getStackingGroups(frame)), alignedDataFrame: frame, }; } diff --git a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx index 2b0813d2ee4..e3d54e93ce5 100644 --- a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx +++ b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx @@ -19,7 +19,7 @@ export class UnthemedTimeSeries extends React.Component { prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { const { eventBus, sync } = this.context as PanelContext; - const { theme, timeZone, legend, renderers, tweakAxis, tweakScale } = this.props; + const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props; return preparePlotConfigBuilder({ frame: alignedFrame, @@ -29,7 +29,6 @@ export class UnthemedTimeSeries extends React.Component { eventBus, sync, allFrames, - legend, renderers, tweakScale, tweakAxis, diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index d812a8dc93d..ed9e084759e 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -23,10 +23,9 @@ import { VisibilityMode, ScaleDirection, ScaleOrientation, - VizLegendOptions, StackingMode, } from '@grafana/schema'; -import { collectStackingGroups, INTERNAL_NEGATIVE_Y_PREFIX, orderIdsByCalcs, preparePlotData } from '../uPlot/utils'; +import { getStackingGroups, preparePlotData2 } from '../uPlot/utils'; import uPlot from 'uplot'; import { buildScaleKey } from '../GraphNG/utils'; @@ -40,7 +39,6 @@ const defaultConfig: GraphFieldConfig = { export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync?: () => DashboardCursorSync; - legend?: VizLegendOptions; }> = ({ frame, theme, @@ -50,13 +48,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync, allFrames, renderers, - legend, tweakScale = (opts) => opts, tweakAxis = (opts) => opts, }) => { const builder = new UPlotConfigBuilder(timeZone); - builder.setPrepData((prepData) => preparePlotData(prepData, undefined, legend)); + builder.setPrepData((frames) => preparePlotData2(frames[0], builder.getStackingGroups())); // X is the first field in the aligned frame const xField = frame.fields[0]; @@ -122,8 +119,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ let customRenderedFields = renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? []; - const stackingGroups: Map = new Map(); - let indexByName: Map | undefined; for (let i = 1; i < frame.fields.length; i++) { @@ -328,20 +323,11 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ }); } } - collectStackingGroups(field, stackingGroups, seriesIndex); } - if (stackingGroups.size !== 0) { - for (const [group, seriesIds] of stackingGroups.entries()) { - const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame }); - for (let j = seriesIdxs.length - 1; j > 0; j--) { - builder.addBand({ - series: [seriesIdxs[j], seriesIdxs[j - 1]], - dir: group.startsWith(INTERNAL_NEGATIVE_Y_PREFIX) ? 1 : -1, - }); - } - } - } + let stackingGroups = getStackingGroups(frame); + + builder.setStackingGroups(stackingGroups); // hook up custom/composite renderers renderers?.forEach((r) => { diff --git a/packages/grafana-ui/src/components/uPlot/Plot.test.tsx b/packages/grafana-ui/src/components/uPlot/Plot.test.tsx index 26640d5b532..7505c6cb26f 100644 --- a/packages/grafana-ui/src/components/uPlot/Plot.test.tsx +++ b/packages/grafana-ui/src/components/uPlot/Plot.test.tsx @@ -6,7 +6,7 @@ import { GraphFieldConfig, GraphDrawStyle } from '@grafana/schema'; import uPlot from 'uplot'; import createMockRaf from 'mock-raf'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; -import { preparePlotData } from './utils'; +import { preparePlotData2, getStackingGroups } from './utils'; import { SeriesProps } from './config/UPlotSeriesBuilder'; const mockRaf = createMockRaf(); @@ -55,7 +55,7 @@ const mockData = () => { const config = new UPlotConfigBuilder(); config.addSeries({} as SeriesProps); - return { data: [data], timeRange, config }; + return { data: data, timeRange, config }; }; describe('UPlotChart', () => { @@ -75,7 +75,7 @@ describe('UPlotChart', () => { const { unmount } = render( { const { rerender } = render( { expect(uPlot).toBeCalledTimes(1); - data[0].fields[1].values.set(0, 1); + data.fields[1].values.set(0, 1); rerender( { it('skips uPlot intialization for width and height equal 0', async () => { const { data, timeRange, config } = mockData(); const { queryAllByTestId } = render( - + ); expect(queryAllByTestId('uplot-main-div')).toHaveLength(1); @@ -136,7 +142,7 @@ describe('UPlotChart', () => { const { rerender } = render( { nextConfig.addSeries({} as SeriesProps); rerender( - + ); expect(destroyMock).toBeCalledTimes(1); @@ -162,7 +174,7 @@ describe('UPlotChart', () => { const { rerender } = render( { // we wait 1 frame for plugins initialisation logic to finish rerender( AlignedData | FacetedData; +type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData; export class UPlotConfigBuilder { private series: UPlotSeriesBuilder[] = []; private axes: Record = {}; private scales: UPlotScaleBuilder[] = []; private bands: Band[] = []; + private stackingGroups: StackingGroup[] = []; private cursor: Cursor | undefined; private select: uPlot.Select | undefined; private hasLeftAxis = false; @@ -143,6 +145,14 @@ export class UPlotConfigBuilder { this.bands.push(band); } + setStackingGroups(groups: StackingGroup[]) { + this.stackingGroups = groups; + } + + getStackingGroups() { + return this.stackingGroups; + } + setTooltipInterpolator(interpolator: PlotTooltipInterpolator) { this.tooltipInterpolator = interpolator; } @@ -151,10 +161,10 @@ export class UPlotConfigBuilder { return this.tooltipInterpolator; } - setPrepData(prepData: PrepData) { + setPrepData(prepData: PreDataStacked) { this.prepData = (frames) => { this.frames = frames; - return prepData(frames); + return prepData(frames, this.getStackingGroups()); }; } @@ -221,6 +231,14 @@ export class UPlotConfigBuilder { config.tzDate = this.tzDate; config.padding = this.padding; + if (this.stackingGroups.length) { + this.stackingGroups.forEach((group) => { + getStackingBands(group).forEach((band) => { + this.addBand(band); + }); + }); + } + if (this.bands.length) { config.bands = this.bands; } diff --git a/packages/grafana-ui/src/components/uPlot/utils.test.ts b/packages/grafana-ui/src/components/uPlot/utils.test.ts index 8cf0e7c1445..3ccb288de24 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.test.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.test.ts @@ -1,6 +1,7 @@ -import { orderIdsByCalcs, preparePlotData, timeFormatToTemplate } from './utils'; +import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils'; import { FieldType, MutableDataFrame } from '@grafana/data'; -import { GraphTransform, StackingMode } from '@grafana/schema'; +import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema'; +import Units from 'ol/proj/Units'; describe('timeFormatToTemplate', () => { it.each` @@ -16,7 +17,7 @@ describe('timeFormatToTemplate', () => { }); }); -describe('preparePlotData', () => { +describe('preparePlotData2', () => { const df = new MutableDataFrame({ fields: [ { name: 'time', type: FieldType.time, values: [9997, 9998, 9999] }, @@ -27,7 +28,7 @@ describe('preparePlotData', () => { }); it('creates array from DataFrame', () => { - expect(preparePlotData([df])).toMatchInlineSnapshot(` + expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(` Array [ Array [ 9997, @@ -63,7 +64,7 @@ describe('preparePlotData', () => { { name: 'c', values: [20, 20, 20], config: { custom: { transform: GraphTransform.NegativeY } } }, ], }); - expect(preparePlotData([df])).toMatchInlineSnapshot(` + expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(` Array [ Array [ 9997, @@ -104,7 +105,7 @@ describe('preparePlotData', () => { { name: 'i', values: [20, undefined, 20, 20], config: { custom: { transform: GraphTransform.NegativeY } } }, ], }); - expect(preparePlotData([df])).toMatchInlineSnapshot(` + expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(` Array [ Array [ 9997, @@ -178,7 +179,7 @@ describe('preparePlotData', () => { { name: 'c', values: [20, 20, 20] }, ], }); - expect(preparePlotData([df])).toMatchInlineSnapshot(` + expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(` Array [ Array [ 9997, @@ -226,7 +227,7 @@ describe('preparePlotData', () => { }, ], }); - expect(preparePlotData([df])).toMatchInlineSnapshot(` + expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(` Array [ Array [ 9997, @@ -273,7 +274,7 @@ describe('preparePlotData', () => { }, ], }); - expect(preparePlotData([df])).toMatchInlineSnapshot(` + expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(` Array [ Array [ 9997, @@ -329,7 +330,7 @@ describe('preparePlotData', () => { }, ], }); - expect(preparePlotData([df])).toMatchInlineSnapshot(` + expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(` Array [ Array [ 9997, @@ -397,7 +398,7 @@ describe('preparePlotData', () => { ], }); - expect(preparePlotData([df])).toMatchInlineSnapshot(` + expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(` Array [ Array [ 9997, @@ -472,7 +473,7 @@ describe('preparePlotData', () => { ], }); - expect(preparePlotData([df])).toMatchInlineSnapshot(` + expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(` Array [ Array [ 9997, @@ -507,239 +508,330 @@ describe('preparePlotData', () => { ] `); }); - - describe('ignores nullish-only stacks', () => { - test('single stacking group', () => { - const df = new MutableDataFrame({ - fields: [ - { name: 'time', type: FieldType.time, values: [9997, 9998, 9999] }, - { - name: 'a', - values: [-10, null, 10], - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } }, - }, - { - name: 'b', - values: [10, null, null], - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } }, - }, - { - name: 'c', - values: [20, undefined, 20], - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } }, - }, - ], - }); - - expect(preparePlotData([df])).toMatchInlineSnapshot(` - Array [ - Array [ - 9997, - 9998, - 9999, - ], - Array [ - -10, - null, - 10, - ], - Array [ - 0, - null, - 10, - ], - Array [ - 20, - null, - 30, - ], - ] - `); - }); - test('multiple stacking groups', () => { - const df = new MutableDataFrame({ - fields: [ - { name: 'time', type: FieldType.time, values: [9997, 9998, 9999] }, - { - name: 'a', - values: [-10, undefined, 10], - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } }, - }, - { - name: 'b', - values: [10, undefined, 10], - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } }, - }, - { - name: 'c', - values: [20, undefined, 20], - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } }, - }, - { - name: 'd', - values: [1, 2, null], - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } }, - }, - { - name: 'e', - values: [1, 2, null], - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } }, - }, - { - name: 'f', - values: [1, 2, null], - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } }, - }, - ], - }); - - expect(preparePlotData([df])).toMatchInlineSnapshot(` - Array [ - Array [ - 9997, - 9998, - 9999, - ], - Array [ - -10, - null, - 10, - ], - Array [ - 0, - null, - 20, - ], - Array [ - 20, - null, - 40, - ], - Array [ - 1, - 2, - null, - ], - Array [ - 2, - 4, - null, - ], - Array [ - 3, - 6, - null, - ], - ] - `); - }); - }); - describe('with legend sorted', () => { - it('should affect when single group', () => { - const df = new MutableDataFrame({ - fields: [ - { name: 'time', type: FieldType.time, values: [9997, 9998, 9999] }, - { - name: 'a', - values: [-10, 20, 10], - state: { calcs: { max: 20 } }, - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } }, - }, - { - name: 'b', - values: [10, 10, 10], - state: { calcs: { max: 10 } }, - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } }, - }, - { - name: 'c', - values: [20, 20, 20], - state: { calcs: { max: 20 } }, - config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } }, - }, - ], - }); - - expect(preparePlotData([df], undefined, { sortBy: 'Max', sortDesc: false } as any)).toMatchInlineSnapshot(` - Array [ - Array [ - 9997, - 9998, - 9999, - ], - Array [ - 0, - 30, - 20, - ], - Array [ - 10, - 10, - 10, - ], - Array [ - 20, - 50, - 40, - ], - ] - `); - expect(preparePlotData([df], undefined, { sortBy: 'Max', sortDesc: true } as any)).toMatchInlineSnapshot(` - Array [ - Array [ - 9997, - 9998, - 9999, - ], - Array [ - -10, - 20, - 10, - ], - Array [ - 20, - 50, - 40, - ], - Array [ - 10, - 40, - 30, - ], - ] - `); - }); - }); }); }); -describe('orderIdsByCalcs', () => { - const ids = [1, 2, 3, 4]; - const frame = new MutableDataFrame({ - fields: [ - { name: 'time', type: FieldType.time, values: [9997, 9998, 9999] }, - { name: 'a', values: [-10, 20, 10], state: { calcs: { min: -10 } } }, - { name: 'b', values: [20, 20, 20], state: { calcs: { min: 20 } } }, - { name: 'c', values: [10, 10, 10], state: { calcs: { min: 10 } } }, - { name: 'd', values: [30, 30, 30] }, - ], +describe('auto stacking groups', () => { + test('split on stacking mode', () => { + const df = new MutableDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2] }, + { + name: 'b', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Percent } } }, + }, + { + name: 'c', + values: [4, 5, 6], + config: { custom: { stacking: { mode: StackingMode.Normal } } }, + }, + ], + }); + + expect(getStackingGroups(df)).toMatchInlineSnapshot(` + Array [ + Object { + "dir": 1, + "series": Array [ + 1, + ], + }, + Object { + "dir": 1, + "series": Array [ + 2, + ], + }, + ] + `); }); - it.each([ - { legend: undefined }, - { legend: { sortBy: 'Min' } }, - { legend: { sortDesc: false } }, - { legend: {} }, - { sortBy: 'Mik', sortDesc: true }, - ])('should return without ordering if legend option is %o', (legend: any) => { - const result = orderIdsByCalcs({ ids, frame, legend }); - expect(result).toEqual([1, 2, 3, 4]); + test('split pos/neg', () => { + // since we expect most series to be Pos, we try to bail early when scanning all values + // as soon as we find a value >= 0, it's assumed Pos, else Neg + + const df = new MutableDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2] }, + { + name: 'a', + values: [-1, null, -3], + config: { custom: { stacking: { mode: StackingMode.Normal } } }, + }, + { + name: 'b', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Normal } } }, + }, + { + name: 'c', + values: [0, 0, 0], + config: { custom: { stacking: { mode: StackingMode.Normal } } }, + }, + ], + }); + + expect(getStackingGroups(df)).toMatchInlineSnapshot(` + Array [ + Object { + "dir": -1, + "series": Array [ + 1, + 3, + ], + }, + Object { + "dir": 1, + "series": Array [ + 2, + ], + }, + ] + `); }); - it('should order the ids based on the frame stat', () => { - const resultDesc = orderIdsByCalcs({ ids, frame, legend: { sortBy: 'Min', sortDesc: true } as any }); - expect(resultDesc).toEqual([4, 2, 3, 1]); - const resultAsc = orderIdsByCalcs({ ids, frame, legend: { sortBy: 'Min', sortDesc: false } as any }); - expect(resultAsc).toEqual([1, 3, 2, 4]); + test('split pos/neg with NegY', () => { + const df = new MutableDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2] }, + { + name: 'a', + values: [-1, null, -3], + config: { custom: { stacking: { mode: StackingMode.Normal }, transform: GraphTransform.NegativeY } }, + }, + { + name: 'b', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Normal } } }, + }, + { + name: 'c', + values: [0, 0, 0], + config: { custom: { stacking: { mode: StackingMode.Normal } } }, + }, + ], + }); + + expect(getStackingGroups(df)).toMatchInlineSnapshot(` + Array [ + Object { + "dir": 1, + "series": Array [ + 1, + 2, + ], + }, + Object { + "dir": -1, + "series": Array [ + 3, + ], + }, + ] + `); + }); + + test('split on drawStyle, lineInterpolation, barAlignment', () => { + const df = new MutableDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2] }, + { + name: 'a', + values: [1, 2, 3], + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + barAlignment: BarAlignment.After, + stacking: { mode: StackingMode.Normal }, + }, + }, + }, + { + name: 'b', + values: [1, 2, 3], + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + barAlignment: BarAlignment.Before, + stacking: { mode: StackingMode.Normal }, + }, + }, + }, + { + name: 'c', + values: [1, 2, 3], + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + lineInterpolation: LineInterpolation.Linear, + stacking: { mode: StackingMode.Normal }, + }, + }, + }, + { + name: 'd', + values: [1, 2, 3], + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + lineInterpolation: LineInterpolation.Smooth, + stacking: { mode: StackingMode.Normal }, + }, + }, + }, + { + name: 'e', + values: [1, 2, 3], + config: { custom: { drawStyle: GraphDrawStyle.Points, stacking: { mode: StackingMode.Normal } } }, + }, + ], + }); + + expect(getStackingGroups(df)).toMatchInlineSnapshot(` + Array [ + Object { + "dir": 1, + "series": Array [ + 1, + ], + }, + Object { + "dir": 1, + "series": Array [ + 2, + ], + }, + Object { + "dir": 1, + "series": Array [ + 3, + ], + }, + Object { + "dir": 1, + "series": Array [ + 4, + ], + }, + Object { + "dir": 1, + "series": Array [ + 5, + ], + }, + ] + `); + }); + + test('split on axis & units (scaleKey)', () => { + const df = new MutableDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2] }, + { + name: 'a', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Normal } }, unit: Units.FEET }, + }, + { + name: 'b', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Normal } }, unit: Units.DEGREES }, + }, + ], + }); + + expect(getStackingGroups(df)).toMatchInlineSnapshot(` + Array [ + Object { + "dir": 1, + "series": Array [ + 1, + ], + }, + Object { + "dir": 1, + "series": Array [ + 2, + ], + }, + ] + `); + }); + + test('split on explicit stacking group & mode & pos/neg w/NegY', () => { + const df = new MutableDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2] }, + { + name: 'a', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Normal, group: 'A' } } }, + }, + { + name: 'b', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Normal, group: 'A' } } }, + }, + { + name: 'c', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Percent, group: 'A' } } }, + }, + { + name: 'd', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Normal, group: 'B' } } }, + }, + { + name: 'e', + values: [1, 2, 3], + config: { custom: { stacking: { mode: StackingMode.Percent, group: 'B' } } }, + }, + { + name: 'e', + values: [1, 2, 3], + config: { + custom: { stacking: { mode: StackingMode.Percent, group: 'B' }, transform: GraphTransform.NegativeY }, + }, + }, + ], + }); + + expect(getStackingGroups(df)).toMatchInlineSnapshot(` + Array [ + Object { + "dir": 1, + "series": Array [ + 1, + 2, + ], + }, + Object { + "dir": 1, + "series": Array [ + 3, + ], + }, + Object { + "dir": 1, + "series": Array [ + 4, + ], + }, + Object { + "dir": 1, + "series": Array [ + 5, + ], + }, + Object { + "dir": -1, + "series": Array [ + 6, + ], + }, + ] + `); }); }); diff --git a/packages/grafana-ui/src/components/uPlot/utils.ts b/packages/grafana-ui/src/components/uPlot/utils.ts index 96ceaf2eb5d..4bdaadc8b8d 100755 --- a/packages/grafana-ui/src/components/uPlot/utils.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.ts @@ -1,12 +1,11 @@ -import { DataFrame, ensureTimeField, Field, FieldType } from '@grafana/data'; -import { GraphFieldConfig, GraphTransform, StackingMode, VizLegendOptions } from '@grafana/schema'; -import { orderBy } from 'lodash'; +import { DataFrame, ensureTimeField, FieldType } from '@grafana/data'; +import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema'; import uPlot, { AlignedData, Options, PaddingSide } from 'uplot'; import { attachDebugger } from '../../utils'; import { createLogger } from '../../utils/logger'; +import { buildScaleKey } from '../GraphNG/utils'; const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g; -export const INTERNAL_NEGATIVE_Y_PREFIX = '__internalNegY'; export function timeFormatToTemplate(f: string) { return f.replace(ALLOWED_FORMAT_STRINGS_REGEX, (match) => `{${match}}`); @@ -41,115 +40,225 @@ interface StackMeta { } /** @internal */ -export function preparePlotData( - frames: DataFrame[], - onStackMeta?: (meta: StackMeta) => void, - legend?: VizLegendOptions -): AlignedData { - const frame = frames[0]; - const result: any[] = []; - const stackingGroups: Map = new Map(); - let seriesIndex = 0; - - for (let i = 0; i < frame.fields.length; i++) { - const f = frame.fields[i]; - - if (f.type === FieldType.time) { - result.push(ensureTimeField(f).values.toArray()); - seriesIndex++; - continue; - } - - collectStackingGroups(f, stackingGroups, seriesIndex); - const customConfig: GraphFieldConfig = f.config.custom || {}; - - const values = f.values.toArray(); - - if (customConfig.transform === GraphTransform.NegativeY) { - result.push(values.map((v) => (v == null ? v : v * -1))); - } else if (customConfig.transform === GraphTransform.Constant) { - result.push(new Array(values.length).fill(values[0])); - } else { - result.push(values); - } - seriesIndex++; - } - - // Stacking - if (stackingGroups.size !== 0) { - const byPct = frame.fields[1].config.custom?.stacking?.mode === StackingMode.Percent; - const dataLength = result[0].length; - const alignedTotals = Array(stackingGroups.size); - alignedTotals[0] = null; - - // array or stacking groups - for (const [_, seriesIds] of stackingGroups.entries()) { - const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame }); - const noValueStack = Array(dataLength).fill(true); - const groupTotals = byPct ? Array(dataLength).fill(0) : null; - - if (byPct) { - for (let j = 0; j < seriesIdxs.length; j++) { - const currentlyStacking = result[seriesIdxs[j]]; - - for (let k = 0; k < dataLength; k++) { - const v = currentlyStacking[k]; - groupTotals![k] += v == null ? 0 : +v; - } - } - } - - const acc = Array(dataLength).fill(0); - - for (let j = 0; j < seriesIdxs.length; j++) { - let seriesIdx = seriesIdxs[j]; - - alignedTotals[seriesIdx] = groupTotals; - - const currentlyStacking = result[seriesIdx]; - - for (let k = 0; k < dataLength; k++) { - const v = currentlyStacking[k]; - if (v != null && noValueStack[k]) { - noValueStack[k] = false; - } - acc[k] += v == null ? 0 : v / (byPct ? groupTotals![k] : 1); - } - - result[seriesIdx] = acc.slice().map((v, i) => (noValueStack[i] ? null : v)); - } - } - - onStackMeta && - onStackMeta({ - totals: alignedTotals as AlignedData, - }); - } - - return result as AlignedData; +export interface StackingGroup { + series: number[]; + dir: StackDirection; } -export function collectStackingGroups(f: Field, groups: Map, seriesIdx: number) { - const customConfig = f.config.custom; - if (!customConfig) { - return; - } - if ( - customConfig.stacking?.mode !== StackingMode.None && - customConfig.stacking?.group && - !customConfig.hideFrom?.viz - ) { - const group = - customConfig.transform === GraphTransform.NegativeY - ? `${INTERNAL_NEGATIVE_Y_PREFIX}-${customConfig.stacking.group}` - : customConfig.stacking.group; +/** @internal */ +const enum StackDirection { + Pos = 1, + Neg = -1, +} - if (!groups.has(group)) { - groups.set(group, [seriesIdx]); - } else { - groups.set(group, groups.get(group)!.concat(seriesIdx)); +// generates bands between adjacent group series +/** @internal */ +export function getStackingBands(group: StackingGroup) { + let bands: uPlot.Band[] = []; + let { series, dir } = group; + let lastIdx = series.length - 1; + + let rSeries = series.slice().reverse(); + + rSeries.forEach((si, i) => { + if (i !== lastIdx) { + let nextIdx = rSeries[i + 1]; + bands.push({ + series: [si, nextIdx], + // fill direction is inverted from stack direction + dir: (-1 * dir) as 1 | -1, + }); } + }); + + return bands; +} + +// expects an AlignedFrame +/** @internal */ +export function getStackingGroups(frame: DataFrame) { + let groups: Map = new Map(); + + frame.fields.forEach(({ config, values }, i) => { + // skip x or time field + if (i === 0) { + return; + } + + let { custom } = config; + + if (custom == null) { + return; + } + + // TODO: currently all AlignedFrame fields end up in uplot series & data, even custom.hideFrom?.viz + // ideally hideFrom.viz fields would be excluded so we can remove this + if (custom.hideFrom?.viz) { + return; + } + + let { stacking } = custom; + + if (stacking == null) { + return; + } + + let { mode: stackingMode, group: stackingGroup } = stacking; + + // not stacking + if (stackingMode === StackingMode.None) { + return; + } + + // will this be stacked up or down after any transforms applied + let vals = values.toArray(); + let transform = custom.transform; + let stackDir = + transform === GraphTransform.Constant + ? vals[0] > 0 + ? StackDirection.Pos + : StackDirection.Neg + : transform === GraphTransform.NegativeY + ? vals.some((v) => v > 0) + ? StackDirection.Neg + : StackDirection.Pos + : vals.some((v) => v > 0) + ? StackDirection.Pos + : StackDirection.Neg; + + let drawStyle = custom.drawStyle as GraphDrawStyle; + let drawStyle2 = + drawStyle === GraphDrawStyle.Bars + ? (custom.barAlignment as BarAlignment) + : drawStyle === GraphDrawStyle.Line + ? (custom.lineInterpolation as LineInterpolation) + : null; + + let stackKey = `${stackDir}|${stackingMode}|${stackingGroup}|${buildScaleKey(config)}|${drawStyle}|${drawStyle2}`; + + let group = groups.get(stackKey); + + if (group == null) { + group = { + series: [], + dir: stackDir, + }; + + groups.set(stackKey, group); + } + + group.series.push(i); + }); + + return [...groups.values()]; +} + +/** @internal */ +export function preparePlotData2( + frame: DataFrame, + stackingGroups: StackingGroup[], + onStackMeta?: (meta: StackMeta) => void +) { + let data = Array(frame.fields.length) as AlignedData; + + let dataLen = frame.length; + let zeroArr = Array(dataLen).fill(0); + let accums = Array.from({ length: stackingGroups.length }, () => zeroArr.slice()); + + frame.fields.forEach((field, i) => { + let vals = field.values.toArray(); + + if (i === 0) { + if (field.type === FieldType.time) { + data[i] = ensureTimeField(field).values.toArray(); + } else { + data[i] = vals; + } + return; + } + + let { custom } = field.config; + + if (!custom || custom.hideFrom?.viz) { + data[i] = vals; + return; + } + + // apply transforms + if (custom.transform === GraphTransform.Constant) { + vals = Array(vals.length).fill(vals[0]); + } else { + vals = vals.slice(); + + if (custom.transform === GraphTransform.NegativeY) { + for (let i = 0; i < vals.length; i++) { + if (vals[i] != null) { + vals[i] *= -1; + } + } + } + } + + let stackingMode = custom.stacking?.mode; + + if (!stackingMode || stackingMode === StackingMode.None) { + data[i] = vals; + } else { + let stackIdx = stackingGroups.findIndex((group) => group.series.indexOf(i) > -1); + + let accum = accums[stackIdx]; + let stacked = (data[i] = Array(dataLen)); + + for (let i = 0; i < dataLen; i++) { + let v = vals[i]; + + if (v != null) { + stacked[i] = accum[i] += v; + } else { + stacked[i] = v; // we may want to coerce to 0 here + } + } + } + }); + + if (onStackMeta) { + let accumsBySeriesIdx = data.map((vals, i) => { + let stackIdx = stackingGroups.findIndex((group) => group.series.indexOf(i) > -1); + return stackIdx !== -1 ? accums[stackIdx] : vals; + }); + + onStackMeta({ + totals: accumsBySeriesIdx as AlignedData, + }); } + + // re-compute by percent + frame.fields.forEach((field, i) => { + if (i === 0 || field.config.custom?.hideFrom?.viz) { + return; + } + + let stackingMode = field.config.custom?.stacking?.mode; + + if (stackingMode === StackingMode.Percent) { + let stackIdx = stackingGroups.findIndex((group) => group.series.indexOf(i) > -1); + let accum = accums[stackIdx]; + let group = stackingGroups[stackIdx]; + + let stacked = data[i]; + + for (let i = 0; i < dataLen; i++) { + let v = stacked[i]; + + if (v != null) { + // v / accum will always be pos, so properly (re)sign by group stacking dir + stacked[i] = group.dir * (v / accum[i]); + } + } + } + }); + + return data; } /** @@ -215,23 +324,3 @@ export const pluginLogger = createLogger('uPlot'); export const pluginLog = pluginLogger.logger; // pluginLogger.enable(); attachDebugger('graphng', undefined, pluginLogger); - -type OrderIdsByCalcsOptions = { - legend?: VizLegendOptions; - ids: number[]; - frame: DataFrame; -}; -export function orderIdsByCalcs({ legend, ids, frame }: OrderIdsByCalcsOptions) { - if (!legend?.sortBy || legend.sortDesc == null) { - return ids; - } - const orderedIds = orderBy( - ids, - (id) => { - return frame.fields[id].state?.calcs?.[legend.sortBy!.toLowerCase()]; - }, - legend.sortDesc ? 'desc' : 'asc' - ); - - return orderedIds; -} diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index ef7cc7f6336..316baf93ecb 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -11,7 +11,7 @@ import { VizTextDisplayOptions, VizLegendOptions, } from '@grafana/schema'; -import { preparePlotData } from '../../../../../packages/grafana-ui/src/components/uPlot/utils'; +import { preparePlotData2, StackingGroup } from '../../../../../packages/grafana-ui/src/components/uPlot/utils'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { formatTime } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; @@ -564,16 +564,11 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { let alignedTotals: AlignedData | null = null; - function prepData(frames: DataFrame[]) { + function prepData(frames: DataFrame[], stackingGroups: StackingGroup[]) { alignedTotals = null; - - return preparePlotData( - frames, - ({ totals }) => { - alignedTotals = totals; - }, - opts.legend - ); + return preparePlotData2(frames[0], stackingGroups, ({ totals }) => { + alignedTotals = totals; + }); } return { diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 992138a0753..9cb28a986c1 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -25,9 +25,9 @@ import { StackingMode, VizLegendOptions, } from '@grafana/schema'; -import { collectStackingGroups, orderIdsByCalcs } from '../../../../../packages/grafana-ui/src/components/uPlot/utils'; import { orderBy } from 'lodash'; import { findField } from 'app/features/dimensions'; +import { getStackingGroups } from '@grafana/ui/src/components/uPlot/utils'; function getBarCharScaleOrientation(orientation: VizOrientation) { if (orientation === VizOrientation.Vertical) { @@ -156,7 +156,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ let seriesIndex = 0; const legendOrdered = isLegendOrdered(legend); - const stackingGroups: Map = new Map(); // iterate the y values for (let i = 1; i < frame.fields.length; i++) { @@ -237,20 +236,11 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ grid: { show: customConfig.axisGridShow }, }); } - - collectStackingGroups(field, stackingGroups, seriesIndex); } - if (stackingGroups.size !== 0) { - for (const [_, seriesIds] of stackingGroups.entries()) { - const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame }); - for (let j = seriesIdxs.length - 1; j > 0; j--) { - builder.addBand({ - series: [seriesIdxs[j], seriesIdxs[j - 1]], - }); - } - } - } + let stackingGroups = getStackingGroups(frame); + + builder.setStackingGroups(stackingGroups); return builder; }; diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index 0bf27bd90ba..e84b6a87fbb 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -31,7 +31,7 @@ import { getConfig, TimelineCoreOptions } from './timeline'; import { VizLegendOptions, AxisPlacement, ScaleDirection, ScaleOrientation } from '@grafana/schema'; import { TimelineFieldConfig, TimelineOptions } from './types'; import { PlotTooltipInterpolator } from '@grafana/ui/src/components/uPlot/types'; -import { preparePlotData } from '../../../../../packages/grafana-ui/src/components/uPlot/utils'; +import { preparePlotData2, getStackingGroups } from '../../../../../packages/grafana-ui/src/components/uPlot/utils'; import uPlot from 'uplot'; const defaultConfig: TimelineFieldConfig = { @@ -153,7 +153,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ builder.setTooltipInterpolator(interpolateTooltip); - builder.setPrepData(preparePlotData); + builder.setPrepData((frames) => preparePlotData2(frames[0], getStackingGroups(frames[0]))); builder.setCursor(coreConfig.cursor); From f62c2619008d4a803e53710eabf9d4b6c0da268c Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Wed, 13 Apr 2022 17:37:56 +0200 Subject: [PATCH 074/121] fix terminal format (#47718) --- pkg/infra/log/term/terminal_logger.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/infra/log/term/terminal_logger.go b/pkg/infra/log/term/terminal_logger.go index d98644a87dc..b9aeac0e8a4 100644 --- a/pkg/infra/log/term/terminal_logger.go +++ b/pkg/infra/log/term/terminal_logger.go @@ -26,8 +26,7 @@ const ( ) func SetTimeFormatGokitLog() { - timeFormat = "2006-01-02T15:04:05.000-0700" - termTimeFormat = "01-02|15:04:05.000" + timeFormat = time.RFC3339Nano } type terminalLogger struct { @@ -104,10 +103,16 @@ func getRecord(keyvals ...interface{}) *record { if k == "t" { t, ok := v.(fmt.Stringer) if ok { - time, err := time.Parse("2006-01-02T15:04:05.999999999-0700", t.String()) + parsedTime, err := time.Parse("2006-01-02T15:04:05.999999999-0700", t.String()) if err == nil { - r.time = time + r.time = parsedTime continue + } else { + parsedTime, err := time.Parse(time.RFC3339Nano, t.String()) + if err == nil { + r.time = parsedTime + continue + } } } From a245531f0c22d6fd7fe55b720677b506ebe1b4ef Mon Sep 17 00:00:00 2001 From: Ieva Date: Wed, 13 Apr 2022 17:11:03 +0100 Subject: [PATCH 075/121] Access control: service account role check (#47710) * forbid setting role higher than user's role * change response code * can assign API key permissions to non-admin users * add: assign viewer role directly upon creation * refactor: add AddSATcommand infavor of AddAPIkey * refactor: frontend fixes for ServiceAccountToken Co-authored-by: eleijonmarck --- pkg/api/api.go | 2 +- pkg/api/apikey.go | 3 +++ pkg/services/serviceaccounts/api/api.go | 3 +++ pkg/services/serviceaccounts/api/api_test.go | 24 +++++++++++++++---- pkg/services/serviceaccounts/api/token.go | 20 ++++++---------- .../serviceaccounts/api/token_test.go | 3 +-- .../serviceaccounts/database/stats_test.go | 4 ++-- .../serviceaccounts/database/token_store.go | 5 ++-- .../database/token_store_test.go | 7 +++--- pkg/services/serviceaccounts/models.go | 9 +++++++ .../serviceaccounts/serviceaccounts.go | 2 +- pkg/services/serviceaccounts/tests/common.go | 2 +- .../CreateServiceAccountTokenModal.tsx | 9 ++++--- .../serviceaccounts/ServiceAccountPage.tsx | 4 ++-- .../features/serviceaccounts/state/actions.ts | 5 ++-- 15 files changed, 64 insertions(+), 38 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index bc663b6939f..f54e5ee7a94 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -279,7 +279,7 @@ func (hs *HTTPServer) registerRoutes() { keysRoute.Get("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyRead, ac.ScopeAPIKeysAll)), routing.Wrap(hs.GetAPIKeys)) keysRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyCreate)), quota("api_key"), routing.Wrap(hs.AddAPIKey)) keysRoute.Delete("/:id", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyDelete, apikeyIDScope)), routing.Wrap(hs.DeleteAPIKey)) - }, reqOrgAdmin) + }) // Preferences apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) { diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index a282ee2f257..15599f037f6 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -70,6 +70,9 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response { if !cmd.Role.IsValid() { return response.Error(400, "Invalid role specified", nil) } + if !c.OrgRole.Includes(cmd.Role) { + return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil) + } if hs.Cfg.ApiKeyMaxSecondsToLive != -1 { if cmd.SecondsToLive == 0 { diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index 7c177329a26..d4da2814925 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -187,6 +187,9 @@ func (api *ServiceAccountsAPI) updateServiceAccount(c *models.ReqContext) respon if cmd.Role != nil && !cmd.Role.IsValid() { return response.Error(http.StatusBadRequest, "Invalid role specified", nil) } + if cmd.Role != nil && !c.OrgRole.Includes(*cmd.Role) { + return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil) + } resp, err := api.store.UpdateServiceAccount(c.Req.Context(), c.OrgId, scopeID, &cmd) if err != nil { diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index a5ed51dcde1..c017b3dee0d 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -227,7 +227,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, m := web.New() signedUser := &models.SignedInUser{ OrgId: 1, - OrgRole: models.ROLE_ADMIN, + OrgRole: models.ROLE_VIEWER, } m.Use(func(c *web.Context) { @@ -344,13 +344,14 @@ func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) { Id int } - role := models.ROLE_ADMIN + viewerRole := models.ROLE_VIEWER + editorRole := models.ROLE_EDITOR var invalidRole models.RoleType = "InvalidRole" testCases := []testUpdateSATestCase{ { desc: "should be ok to update serviceaccount with permissions", - user: &tests.TestUser{Login: "servicetest1@admin", IsServiceAccount: true, Role: "Editor", Name: "Unaltered"}, - body: &serviceaccounts.UpdateServiceAccountForm{Name: newString("New Name"), Role: &role}, + user: &tests.TestUser{Login: "servicetest1@admin", IsServiceAccount: true, Role: "Viewer", Name: "Unaltered"}, + body: &serviceaccounts.UpdateServiceAccountForm{Name: newString("New Name"), Role: &viewerRole}, acmock: tests.SetupMockAccesscontrol( t, func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) { @@ -360,6 +361,19 @@ func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) { ), expectedCode: http.StatusOK, }, + { + desc: "should be forbidden to set role higher than user's role", + user: &tests.TestUser{Login: "servicetest2@admin", IsServiceAccount: true, Role: "Viewer", Name: "Unaltered 2"}, + body: &serviceaccounts.UpdateServiceAccountForm{Name: newString("New Name 2"), Role: &editorRole}, + acmock: tests.SetupMockAccesscontrol( + t, + func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) { + return []*accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: serviceaccounts.ScopeAll}}, nil + }, + false, + ), + expectedCode: http.StatusForbidden, + }, { desc: "bad request when invalid role", user: &tests.TestUser{Login: "servicetest3@admin", IsServiceAccount: true, Role: "Invalid", Name: "Unaltered"}, @@ -375,7 +389,7 @@ func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) { }, { desc: "should be forbidden to update serviceaccount if no permissions", - user: &tests.TestUser{Login: "servicetest2@admin", IsServiceAccount: true}, + user: &tests.TestUser{Login: "servicetest4@admin", IsServiceAccount: true}, body: nil, acmock: tests.SetupMockAccesscontrol( t, diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index 787d06fdadb..dec1d498280 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -17,13 +17,12 @@ import ( const failedToDeleteMsg = "Failed to delete API key" type TokenDTO struct { - Id int64 `json:"id"` - Name string `json:"name"` - Role models.RoleType `json:"role"` - Created *time.Time `json:"created"` - Expiration *time.Time `json:"expiration"` - SecondsUntilExpiration *float64 `json:"secondsUntilExpiration"` - HasExpired bool `json:"hasExpired"` + Id int64 `json:"id"` + Name string `json:"name"` + Created *time.Time `json:"created"` + Expiration *time.Time `json:"expiration"` + SecondsUntilExpiration *float64 `json:"secondsUntilExpiration"` + HasExpired bool `json:"hasExpired"` } func hasExpired(expiration *int64) bool { @@ -60,7 +59,6 @@ func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Respo result[i] = &TokenDTO{ Id: t.Id, Name: t.Name, - Role: t.Role, Created: &t.Created, Expiration: expiration, SecondsUntilExpiration: &secondsUntilExpiration, @@ -91,7 +89,7 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon } } - cmd := models.AddApiKeyCommand{} + cmd := serviceaccounts.AddServiceAccountTokenCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "Bad request data", err) } @@ -99,10 +97,6 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon // Force affected service account to be the one referenced in the URL cmd.OrgId = c.OrgId - if !cmd.Role.IsValid() { - return response.Error(http.StatusBadRequest, "Invalid role specified", nil) - } - if api.cfg.ApiKeyMaxSecondsToLive != -1 { if cmd.SecondsToLive == 0 { return response.Error(http.StatusBadRequest, "Number of seconds before expiration should be set", nil) diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index 38eb95de0bb..7f04cc8d1bb 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -34,9 +34,8 @@ func createTokenforSA(t *testing.T, store serviceaccounts.Store, keyName string, key, err := apikeygen.New(orgID, keyName) require.NoError(t, err) - cmd := models.AddApiKeyCommand{ + cmd := serviceaccounts.AddServiceAccountTokenCommand{ Name: keyName, - Role: "Viewer", OrgId: orgID, Key: key.HashedKey, SecondsToLive: secondsToLive, diff --git a/pkg/services/serviceaccounts/database/stats_test.go b/pkg/services/serviceaccounts/database/stats_test.go index 067bf756d9d..55405509c4b 100644 --- a/pkg/services/serviceaccounts/database/stats_test.go +++ b/pkg/services/serviceaccounts/database/stats_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,9 +21,8 @@ func TestStore_UsageStats(t *testing.T) { key, err := apikeygen.New(sa.OrgId, keyName) require.NoError(t, err) - cmd := models.AddApiKeyCommand{ + cmd := serviceaccounts.AddServiceAccountTokenCommand{ Name: keyName, - Role: "Viewer", OrgId: sa.OrgId, Key: key.HashedKey, SecondsToLive: 0, diff --git a/pkg/services/serviceaccounts/database/token_store.go b/pkg/services/serviceaccounts/database/token_store.go index e69317e55c0..320bc2f1ed5 100644 --- a/pkg/services/serviceaccounts/database/token_store.go +++ b/pkg/services/serviceaccounts/database/token_store.go @@ -5,10 +5,11 @@ import ( "time" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/sqlstore" ) -func (s *ServiceAccountsStoreImpl) AddServiceAccountToken(ctx context.Context, saID int64, cmd *models.AddApiKeyCommand) error { +func (s *ServiceAccountsStoreImpl) AddServiceAccountToken(ctx context.Context, saID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) error { return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { key := models.ApiKey{OrgId: cmd.OrgId, Name: cmd.Name} exists, _ := sess.Get(&key) @@ -28,7 +29,7 @@ func (s *ServiceAccountsStoreImpl) AddServiceAccountToken(ctx context.Context, s t := models.ApiKey{ OrgId: cmd.OrgId, Name: cmd.Name, - Role: cmd.Role, + Role: models.ROLE_VIEWER, Key: cmd.Key, Created: updated, Updated: updated, diff --git a/pkg/services/serviceaccounts/database/token_store_test.go b/pkg/services/serviceaccounts/database/token_store_test.go index 2e24caa9c37..1560e19ffcd 100644 --- a/pkg/services/serviceaccounts/database/token_store_test.go +++ b/pkg/services/serviceaccounts/database/token_store_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/stretchr/testify/require" ) @@ -28,9 +29,8 @@ func TestStore_AddServiceAccountToken(t *testing.T) { key, err := apikeygen.New(user.OrgId, keyName) require.NoError(t, err) - cmd := models.AddApiKeyCommand{ + cmd := serviceaccounts.AddServiceAccountTokenCommand{ Name: keyName, - Role: "Viewer", OrgId: user.OrgId, Key: key.HashedKey, SecondsToLive: tc.secondsToLive, @@ -79,9 +79,8 @@ func TestStore_DeleteServiceAccountToken(t *testing.T) { key, err := apikeygen.New(user.OrgId, keyName) require.NoError(t, err) - cmd := models.AddApiKeyCommand{ + cmd := serviceaccounts.AddServiceAccountTokenCommand{ Name: keyName, - Role: "Viewer", OrgId: user.OrgId, Key: key.HashedKey, SecondsToLive: 0, diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index f350f259a90..ffc354ce7f0 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -40,6 +40,15 @@ type ServiceAccountDTO struct { AvatarUrl string `json:"avatarUrl"` AccessControl map[string]bool `json:"accessControl,omitempty"` } + +type AddServiceAccountTokenCommand struct { + Name string `json:"name" binding:"Required"` + OrgId int64 `json:"-"` + Key string `json:"-"` + SecondsToLive int64 `json:"secondsToLive"` + Result *models.ApiKey `json:"-"` +} + type SearchServiceAccountsResult struct { TotalCount int64 `json:"totalCount"` ServiceAccounts []*ServiceAccountDTO `json:"serviceAccounts"` diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index fc4c14ad869..632aa42a928 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -26,6 +26,6 @@ type Store interface { ConvertToServiceAccounts(ctx context.Context, keys []int64) error ListTokens(ctx context.Context, orgID int64, serviceAccount int64) ([]*models.ApiKey, error) DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error - AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *models.AddApiKeyCommand) error + AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *AddServiceAccountTokenCommand) error GetUsageMetrics(ctx context.Context) (map[string]interface{}, error) } diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index b89a602dded..67c30d20a10 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -151,7 +151,7 @@ func (s *ServiceAccountsStoreMock) DeleteServiceAccountToken(ctx context.Context return nil } -func (s *ServiceAccountsStoreMock) AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *models.AddApiKeyCommand) error { +func (s *ServiceAccountsStoreMock) AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) error { s.Calls.AddServiceAccountToken = append(s.Calls.AddServiceAccountToken, []interface{}{ctx, cmd}) return nil } diff --git a/public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx b/public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx index 996ee89962e..5cc322dc664 100644 --- a/public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx +++ b/public/app/features/serviceaccounts/CreateServiceAccountTokenModal.tsx @@ -15,17 +15,21 @@ import { RadioButtonGroup, useStyles2, } from '@grafana/ui'; -import { ApiKey, OrgRole } from 'app/types'; const EXPIRATION_OPTIONS = [ { label: 'No expiration', value: false }, { label: 'Set expiration date', value: true }, ]; +export type ServiceAccountToken = { + name: string; + secondsToLive: number; +}; + interface CreateTokenModalProps { isOpen: boolean; token: string; - onCreateToken: (token: ApiKey) => void; + onCreateToken: (token: ServiceAccountToken) => void; onClose: () => void; } @@ -95,7 +99,6 @@ export const CreateTokenModal = ({ isOpen, token, onCreateToken, onClose }: Crea onClick={() => onCreateToken({ name: newTokenName, - role: OrgRole.Viewer, secondsToLive: getSecondsToLive(newTokenExpirationDate), }) } diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index e8aaa35b572..66e59e0cc83 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -17,7 +17,7 @@ import { import { ServiceAccountTokensTable } from './ServiceAccountTokensTable'; import { getTimeZone, NavModel } from '@grafana/data'; import { Button } from '@grafana/ui'; -import { CreateTokenModal } from './CreateServiceAccountTokenModal'; +import { CreateTokenModal, ServiceAccountToken } from './CreateServiceAccountTokenModal'; import { contextSrv } from 'app/core/core'; interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { @@ -86,7 +86,7 @@ const ServiceAccountPageUnconnected = ({ deleteServiceAccountToken(parseInt(match.params.id, 10), key.id!); }; - const onCreateToken = (token: ApiKey) => { + const onCreateToken = (token: ServiceAccountToken) => { createServiceAccountToken(serviceAccount.id, token, setNewToken); }; diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 0e1a6d750c7..48bfe6f1c2c 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -1,4 +1,4 @@ -import { ApiKey, ServiceAccountDTO, ThunkResult, ServiceAccountFilter } from '../../../types'; +import { ServiceAccountDTO, ThunkResult, ServiceAccountFilter } from '../../../types'; import { getBackendSrv, locationService } from '@grafana/runtime'; import { acOptionsLoaded, @@ -16,6 +16,7 @@ import { import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { fetchBuiltinRoles, fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { debounce } from 'lodash'; +import { ServiceAccountToken } from '../CreateServiceAccountTokenModal'; const BASE_URL = `/api/serviceaccounts`; @@ -55,7 +56,7 @@ export function loadServiceAccount(saID: number): ThunkResult { export function createServiceAccountToken( saID: number, - token: ApiKey, + token: ServiceAccountToken, onTokenCreated: (key: string) => void ): ThunkResult { return async (dispatch) => { From 17bd741fc00827c23c85da057fcd138a2868daf2 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Wed, 13 Apr 2022 17:18:59 +0100 Subject: [PATCH 076/121] SAML: fix pkcs8 doc and refactoring SAML docs (#47337) * refactoring: saml docs * refactoring: relreferences * adding error as presented in issue * refactor: update sentence * refactor: more relref fixes that was missing * refactor: more relref fixes that was missing * Apply suggestions from code review Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * refactor: move saml with okta * fix: spell and small corrections * add: enterprise tag Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- docs/sources/auth/_index.md | 22 +-- docs/sources/auth/overview.md | 26 +-- docs/sources/auth/saml.md | 2 +- docs/sources/enterprise/_index.md | 4 +- docs/sources/enterprise/auditing.md | 2 +- docs/sources/enterprise/saml/_index.md | 15 ++ docs/sources/enterprise/saml/about-saml.md | 74 ++++++++ .../{saml.md => saml/configure-saml.md} | 168 +++--------------- docs/sources/enterprise/saml/enable-saml.md | 65 +++++++ .../enterprise/saml/setup-saml-with-okta.md | 45 +++++ .../enterprise/saml/troubleshoot-saml.md | 31 ++++ docs/sources/enterprise/team-sync.md | 2 +- 12 files changed, 281 insertions(+), 175 deletions(-) create mode 100644 docs/sources/enterprise/saml/_index.md create mode 100644 docs/sources/enterprise/saml/about-saml.md rename docs/sources/enterprise/{saml.md => saml/configure-saml.md} (60%) create mode 100644 docs/sources/enterprise/saml/enable-saml.md create mode 100644 docs/sources/enterprise/saml/setup-saml-with-okta.md create mode 100644 docs/sources/enterprise/saml/troubleshoot-saml.md diff --git a/docs/sources/auth/_index.md b/docs/sources/auth/_index.md index 719b5c758aa..16bdaacf067 100644 --- a/docs/sources/auth/_index.md +++ b/docs/sources/auth/_index.md @@ -12,14 +12,14 @@ Here is a table showing all supported authentication providers and the features See also, [Grafana Authentication]({{< relref "grafana.md" >}}). -| Provider | Support | Role mapping | Team sync
    _(Enterprise only)_ | Active sync
    _(Enterprise only)_ | -| ---------------------------------------------------------------- | :-----: | :----------: | :-------------------------------: | :---------------------------------: | -| [Auth Proxy]({{< relref "auth-proxy.md" >}}) | v2.1+ | - | v6.3+ | - | -| [Azure AD OAuth]({{< relref "azuread.md" >}}) | v6.7+ | v6.7+ | v6.7+ | - | -| [Generic OAuth]({{< relref "generic-oauth.md" >}}) | v4.0+ | v6.5+ | - | - | -| [GitHub OAuth]({{< relref "github.md" >}}) | v2.0+ | - | v6.3+ | - | -| [GitLab OAuth]({{< relref "gitlab.md" >}}) | v5.3+ | - | v6.4+ | - | -| [Google OAuth]({{< relref "google.md" >}}) | v2.0+ | - | - | - | -| [LDAP]({{< relref "ldap.md" >}}) | v2.1+ | v2.1+ | v5.3+ | v6.3+ | -| [Okta OAuth]({{< relref "okta.md" >}}) | v7.0+ | v7.0+ | v7.0+ | - | -| [SAML]({{< relref "../enterprise/saml.md" >}}) (Enterprise only) | v6.3+ | v7.0+ | v7.0+ | - | +| Provider | Support | Role mapping | Team sync
    _(Enterprise only)_ | Active sync
    _(Enterprise only)_ | +| -------------------------------------------------------------- | :-----: | :----------: | :-------------------------------: | :---------------------------------: | +| [Auth Proxy]({{< relref "auth-proxy.md" >}}) | v2.1+ | - | v6.3+ | - | +| [Azure AD OAuth]({{< relref "azuread.md" >}}) | v6.7+ | v6.7+ | v6.7+ | - | +| [Generic OAuth]({{< relref "generic-oauth.md" >}}) | v4.0+ | v6.5+ | - | - | +| [GitHub OAuth]({{< relref "github.md" >}}) | v2.0+ | - | v6.3+ | - | +| [GitLab OAuth]({{< relref "gitlab.md" >}}) | v5.3+ | - | v6.4+ | - | +| [Google OAuth]({{< relref "google.md" >}}) | v2.0+ | - | - | - | +| [LDAP]({{< relref "ldap.md" >}}) | v2.1+ | v2.1+ | v5.3+ | v6.3+ | +| [Okta OAuth]({{< relref "okta.md" >}}) | v7.0+ | v7.0+ | v7.0+ | - | +| [SAML]({{< relref "../enterprise/saml/" >}}) (Enterprise only) | v6.3+ | v7.0+ | v7.0+ | - | diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index 61e0dfe54f2..f9cba2e119b 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -8,20 +8,20 @@ weight = 1 Grafana provides many ways to authenticate users. Some authentication integrations also enable syncing user permissions and org memberships. -Here is a table showing all supported authentication providers and the features available for them. [Team sync]({{< relref "../enterprise/team-sync.md" >}}) and [active sync]({{< relref "../enterprise/enhanced_ldap.md#active-ldap-synchronization" >}}) are only available in Grafana Enterprise. +The following table shows all supported authentication providers and the features available for them. [Team sync]({{< relref "../enterprise/team-sync.md" >}}) and [active sync]({{< relref "../enterprise/enhanced_ldap.md#active-ldap-synchronization" >}}) are only available in Grafana Enterprise. -| Provider | Support | Role mapping | Team sync
    _(Enterprise only)_ | Active sync
    _(Enterprise only)_ | -| ---------------------------------------------------------------- | :-----: | :----------: | :-------------------------------: | :---------------------------------: | -| [Auth Proxy]({{< relref "auth-proxy.md" >}}) | v2.1+ | - | v6.3+ | - | -| [Azure AD OAuth]({{< relref "azuread.md" >}}) | v6.7+ | v6.7+ | v6.7+ | - | -| [Generic OAuth]({{< relref "generic-oauth.md" >}}) | v4.0+ | v6.5+ | - | - | -| [GitHub OAuth]({{< relref "github.md" >}}) | v2.0+ | - | v6.3+ | - | -| [GitLab OAuth]({{< relref "gitlab.md" >}}) | v5.3+ | - | v6.4+ | - | -| [Google OAuth]({{< relref "google.md" >}}) | v2.0+ | - | - | - | -| [JWT]({{< relref "jwt.md" >}}) | v8.0+ | - | - | - | -| [LDAP]({{< relref "ldap.md" >}}) | v2.1+ | v2.1+ | v5.3+ | v6.3+ | -| [Okta OAuth]({{< relref "okta.md" >}}) | v7.0+ | v7.0+ | v7.0+ | - | -| [SAML]({{< relref "../enterprise/saml.md" >}}) (Enterprise only) | v6.3+ | v7.0+ | v7.0+ | - | +| Provider | Support | Role mapping | Team sync
    _(Enterprise only)_ | Active sync
    _(Enterprise only)_ | +| -------------------------------------------------------------- | :-----: | :----------: | :-------------------------------: | :---------------------------------: | +| [Auth Proxy]({{< relref "auth-proxy.md" >}}) | v2.1+ | - | v6.3+ | - | +| [Azure AD OAuth]({{< relref "azuread.md" >}}) | v6.7+ | v6.7+ | v6.7+ | - | +| [Generic OAuth]({{< relref "generic-oauth.md" >}}) | v4.0+ | v6.5+ | - | - | +| [GitHub OAuth]({{< relref "github.md" >}}) | v2.0+ | - | v6.3+ | - | +| [GitLab OAuth]({{< relref "gitlab.md" >}}) | v5.3+ | - | v6.4+ | - | +| [Google OAuth]({{< relref "google.md" >}}) | v2.0+ | - | - | - | +| [JWT]({{< relref "jwt.md" >}}) | v8.0+ | - | - | - | +| [LDAP]({{< relref "ldap.md" >}}) | v2.1+ | v2.1+ | v5.3+ | v6.3+ | +| [Okta OAuth]({{< relref "okta.md" >}}) | v7.0+ | v7.0+ | v7.0+ | - | +| [SAML]({{< relref "../enterprise/saml/" >}}) (Enterprise only) | v6.3+ | v7.0+ | v7.0+ | - | ## Grafana Auth diff --git a/docs/sources/auth/saml.md b/docs/sources/auth/saml.md index 923715756a5..57157ad9ca4 100644 --- a/docs/sources/auth/saml.md +++ b/docs/sources/auth/saml.md @@ -10,4 +10,4 @@ weight = 1100 The SAML authentication integration allows your Grafana users to log in by using an external SAML Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. -> SAML authentication integration is available in Grafana Cloud Pro and Advanced and in Grafana Enterprise. For more information, refer to [SAML authentication]({{< relref "../enterprise/saml.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}). +> SAML authentication integration is available in Grafana Cloud Pro and Advanced and in Grafana Enterprise. For more information, refer to [SAML authentication]({{< relref "../enterprise/saml/" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}). diff --git a/docs/sources/enterprise/_index.md b/docs/sources/enterprise/_index.md index bc7d1ac99a4..958578d0820 100644 --- a/docs/sources/enterprise/_index.md +++ b/docs/sources/enterprise/_index.md @@ -29,7 +29,7 @@ Supported auth providers: - [GitLab OAuth]({{< relref "../auth/gitlab.md#team-sync-enterprise-only" >}}) - [LDAP]({{< relref "enhanced_ldap.md#ldap-group-synchronization-for-teams" >}}) - [Okta]({{< relref "../auth/okta.md#team-sync-enterprise-only" >}}) -- [SAML]({{< relref "saml.md#configure-team-sync" >}}) +- [SAML]({{< relref "./saml/configure-saml.md#configure-team-sync" >}}) ### Enhanced LDAP integration @@ -37,7 +37,7 @@ With Grafana Enterprise [enhanced LDAP]({{< relref "enhanced_ldap.md" >}}), you ### SAML authentication -[SAML authentication]({{< relref "saml.md" >}}) enables your Grafana Enterprise users to authenticate with SAML. +[SAML authentication]({{< relref "./saml" >}}) enables your Grafana Enterprise users to authenticate with SAML. ## Enterprise features diff --git a/docs/sources/enterprise/auditing.md b/docs/sources/enterprise/auditing.md index 01982586f82..ab8bd49db70 100644 --- a/docs/sources/enterprise/auditing.md +++ b/docs/sources/enterprise/auditing.md @@ -122,7 +122,7 @@ pattern of the `requestUri` field is given. \* Where `AUTH-MODULE` is the name of the authentication module: `grafana`, `saml`, `ldap`, etc. \ -\*\* Includes manual log out, token expired/revoked, and [SAML Single Logout]({{< relref "saml.md#single-logout" >}}). +\*\* Includes manual log out, token expired/revoked, and [SAML Single Logout]({{< relref "./saml/configure-saml.md#single-logout" >}}). #### User management diff --git a/docs/sources/enterprise/saml/_index.md b/docs/sources/enterprise/saml/_index.md new file mode 100644 index 00000000000..3350533e54b --- /dev/null +++ b/docs/sources/enterprise/saml/_index.md @@ -0,0 +1,15 @@ +--- +title: 'SAML authentication' +description: 'Grafana SAML authentication' +keywords: ['grafana', 'saml', 'documentation', 'saml-auth', 'enterprise'] +aliases: ['/docs/grafana/latest/auth/saml/'] +weight: 10 +--- + +# SAML authentication + +SAML authentication integration enables your Grafana users to log in by using an external SAML 2.0 Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. + +> Only available in Grafana Enterprise v6.3+. If you experience any issues with our implementation, contact our [Technical Support team](https://grafana.com/contact?plcmt=top-nav&cta=contactus) + +{{< section >}} diff --git a/docs/sources/enterprise/saml/about-saml.md b/docs/sources/enterprise/saml/about-saml.md new file mode 100644 index 00000000000..787c0552809 --- /dev/null +++ b/docs/sources/enterprise/saml/about-saml.md @@ -0,0 +1,74 @@ +--- +title: 'About SAML authentication in Grafana' +menuTitle: 'About SAML authentication' +description: 'SAML authentication' +keywords: ['grafana', 'saml', 'documentation', 'saml-auth', 'enterprise'] +aliases: ['/docs/grafana/latest/auth/saml/'] +weight: 20 +--- + +# About SAML authentication + +SAML authentication integration allows your Grafana users to log in by using an external SAML 2.0 Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. + +The SAML single sign-on (SSO) standard is varied and flexible. Our implementation contains a subset of features needed to provide a smooth authentication experience into Grafana. + +> Only available in Grafana Enterprise v6.3+. If you encounter any problems with our implementation, please don't hesitate to contact us. + +## Supported SAML + +Grafana supports the following SAML 2.0 bindings: + +- From the Service Provider (SP) to the Identity Provider (IdP): + + - `HTTP-POST` binding + - `HTTP-Redirect` binding + +- From the Identity Provider (IdP) to the Service Provider (SP): + - `HTTP-POST` binding + +In terms of security: + +- Grafana supports signed and encrypted assertions. +- Grafana does not support signed or encrypted requests. + +In terms of initiation, Grafana supports: + +- SP-initiated requests +- IdP-initiated requests + +By default, SP-initiated requests are enabled. For instructions on how to enable IdP-initiated logins, see https://grafana.com/docs/grafana/latest/enterprise/saml/#idp-initiated-single-sign-on-sso. + +### Edit SAML options in the Grafana config file + +Once you have enabled saml, you can configure Grafana to use it for SAML authentication. Refer to [Configuration]({{< relref "../../administration/configuration.md" >}}) to get more information about how to configure Grafana. + +**Edit SAML options in Grafana config file:** + +1. In the `[auth.saml]` section in the Grafana configuration file, set [`enabled`]({{< relref ".././enterprise-configuration.md#enabled" >}}) to `true`. +1. Configure the [certificate and private key]({{< relref "#certificate-and-private-key" >}}). +1. On the Okta application page where you have been redirected after application created, navigate to the **Sign On** tab and find **Identity Provider metadata** link in the **Settings** section. +1. Set the [`idp_metadata_url`]({{< relref ".././enterprise-configuration.md#idp-metadata-url" >}}) to the URL obtained from the previous step. The URL should look like `https://.okta.com/app//sso/saml/metadata`. +1. Set the following options to the attribute names configured at the **step 10** of the SAML integration setup. You can find this attributes on the **General** tab of the application page (**ATTRIBUTE STATEMENTS** and **GROUP ATTRIBUTE STATEMENTS** in the **SAML Settings** section). + - [`assertion_attribute_login`]({{< relref ".././enterprise-configuration.md#assertion-attribute-login" >}}) + - [`assertion_attribute_email`]({{< relref ".././enterprise-configuration.md#assertion-attribute-email" >}}) + - [`assertion_attribute_name`]({{< relref ".././enterprise-configuration.md#assertion-attribute-name" >}}) + - [`assertion_attribute_groups`]({{< relref ".././enterprise-configuration.md#assertion-attribute-groups" >}}) +1. Save the configuration file and and then restart the Grafana server. + +When you are finished, the Grafana configuration might look like this example: + +```bash +[server] +root_url = https://grafana.example.com + +[auth.saml] +enabled = true +private_key_path = "/path/to/private_key.pem" +certificate_path = "/path/to/certificate.cert" +idp_metadata_url = "https://my-org.okta.com/app/my-application/sso/saml/metadata" +assertion_attribute_name = DisplayName +assertion_attribute_login = Login +assertion_attribute_email = Email +assertion_attribute_groups = Group +``` diff --git a/docs/sources/enterprise/saml.md b/docs/sources/enterprise/saml/configure-saml.md similarity index 60% rename from docs/sources/enterprise/saml.md rename to docs/sources/enterprise/saml/configure-saml.md index 3e4c695d9f2..bf697c51377 100644 --- a/docs/sources/enterprise/saml.md +++ b/docs/sources/enterprise/saml/configure-saml.md @@ -1,46 +1,15 @@ -+++ -title = "SAML Authentication" -description = "Grafana SAML Authentication" -keywords = ["grafana", "saml", "documentation", "saml-auth"] -aliases = ["/docs/grafana/latest/auth/saml/"] -weight = 900 -+++ +--- +title: 'Configure SAML authentication in Grafana' +menuTitle: 'Configure SAML' +description: 'This contains information on how to configure SAML authentication in Grafana' +keywords: ['grafana', 'saml', 'documentation', 'saml-configuration', 'enterprise'] +aliases: ['/docs/grafana/latest/auth/saml/'] +weight: 40 +--- -# SAML authentication +# Configure SAML authentication in Grafana -SAML authentication integration allows your Grafana users to log in by using an external SAML 2.0 Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. - -The SAML single sign-on (SSO) standard is varied and flexible. Our implementation contains a subset of features needed to provide a smooth authentication experience into Grafana. - -> Only available in Grafana Enterprise v6.3+. If you encounter any problems with our implementation, please don't hesitate to contact us. - -## Supported SAML - -Grafana supports the following SAML 2.0 bindings: - -- From the Service Provider (SP) to the Identity Provider (IdP): - - - `HTTP-POST` binding - - `HTTP-Redirect` binding - -- From the Identity Provider (IdP) to the Service Provider (SP): - - `HTTP-POST` binding - -In terms of security: - -- Grafana supports signed and encrypted assertions. -- Grafana does not support signed or encrypted requests. - -In terms of initiation, Grafana supports: - -- SP-initiated requests -- IdP-initiated requests - -By default, SP-initiated requests are enabled. For instructions on how to enable IdP-initiated logins, see https://grafana.com/docs/grafana/latest/enterprise/saml/#idp-initiated-single-sign-on-sso. - -## Set up SAML authentication - -The table below describes all SAML configuration options. Continue reading below for details on specific options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../administration/configuration.md#configure-with-environment-variables" >}}). +The table below describes all SAML configuration options. Continue reading below for details on specific options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../../administration/configuration.md#configure-with-environment-variables" >}}). | Setting | Required | Description | Default | | ---------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | @@ -65,24 +34,7 @@ The table below describes all SAML configuration options. Continue reading below | `org_mapping` | No | List of comma- or space-separated Organization:OrgId:Role mappings. Organization can be `*` meaning "All users". Role is optional and can have the following values: `Viewer`, `Editor` or `Admin`. | | | `role_values_editor` | No | List of comma- or space-separated roles which will be mapped into the Editor role | | | `role_values_admin` | No | List of comma- or space-separated roles which will be mapped into the Admin role | | -| `role_values_grafana_admin` | No | List of comma- or space-separated roles which will be mapped into the Grafana Admin (Super Admin) role | | - -### Enable SAML authentication - -To use the SAML integration, in the `auth.saml` section of in the Grafana custom configuration file, set `enabled` to `true`. - -Refer to [Configuration]({{< relref "../administration/configuration.md" >}}) for more information about configuring Grafana. - -### Certificate and private key - -The SAML SSO standard uses asymmetric encryption to exchange information between the SP (Grafana) and the IdP. To perform such encryption, you need a public part and a private part. In this case, the X.509 certificate provides the public part, while the private key provides the private part. - -Grafana supports two ways of specifying both the `certificate` and `private_key`. - -- Without a suffix (`certificate` or `private_key`), the configuration assumes you've supplied the base64-encoded file contents. -- With the `_path` suffix (`certificate_path` or `private_key_path`), then Grafana treats the value entered as a file path and attempts to read the file from the file system. - -You can only use one form of each configuration option. Using multiple forms, such as both `certificate` and `certificate_path`, results in an error. +| `role_values_grafana_admin` | No | List of comma- or space-separated roles which will be mapped into the Grafana Admin (Super Admin) role | ### Signature algorithm @@ -151,24 +103,24 @@ By default, new Grafana users using SAML authentication will have an account cre > Team sync support for SAML only available in Grafana v7.0+ -To use SAML Team sync, set [`assertion_attribute_groups`]({{< relref "./enterprise-configuration.md#assertion-attribute-groups" >}}) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab. +To use SAML Team sync, set [`assertion_attribute_groups`]({{< relref ".././enterprise-configuration.md#assertion-attribute-groups" >}}) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab. -[Learn more about Team Sync]({{< relref "../enterprise/team-sync.md" >}}) +[Learn more about Team Sync]({{< relref "../../enterprise/team-sync.md" >}}) ### Configure role sync > Only available in Grafana v7.0+ -Role sync allows you to map user roles from an identity provider to Grafana. To enable role sync, configure role attribute and possible values for the Editor, Admin, and Grafana Admin roles. For more information about user roles, refer to [About users and permissions]({{< relref "../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}). +Role sync allows you to map user roles from an identity provider to Grafana. To enable role sync, configure role attribute and possible values for the Editor, Admin, and Grafana Admin roles. For more information about user roles, refer to [About users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}). -1. In the configuration file, set [`assertion_attribute_role`]({{< relref "./enterprise-configuration.md#assertion-attribute-role" >}}) option to the attribute name where the role information will be extracted from. -1. Set the [`role_values_editor`]({{< relref "./enterprise-configuration.md#role-values-editor" >}}) option to the values mapped to the `Editor` role. -1. Set the [`role_values_admin`]({{< relref "./enterprise-configuration.md#role-values-admin" >}}) option to the values mapped to the organization `Admin` role. -1. Set the [`role_values_grafana_admin`]({{< relref "./enterprise-configuration.md#role-values-grafana-admin" >}}) option to the values mapped to the `Grafana Admin` role. +1. In the configuration file, set [`assertion_attribute_role`]({{< relref ".././enterprise-configuration.md#assertion-attribute-role" >}}) option to the attribute name where the role information will be extracted from. +1. Set the [`role_values_editor`]({{< relref ".././enterprise-configuration.md#role-values-editor" >}}) option to the values mapped to the `Editor` role. +1. Set the [`role_values_admin`]({{< relref ".././enterprise-configuration.md#role-values-admin" >}}) option to the values mapped to the organization `Admin` role. +1. Set the [`role_values_grafana_admin`]({{< relref ".././enterprise-configuration.md#role-values-grafana-admin" >}}) option to the values mapped to the `Grafana Admin` role. If a user role doesn't match any of configured values, then the `Viewer` role will be assigned. -Refer to [About users and permissions]({{< relref "../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}) for more information about roles and permissions in Grafana. +Refer to [About users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}) for more information about roles and permissions in Grafana. Example configuration: @@ -188,8 +140,8 @@ role_values_grafana_admin = superadmin Organization mapping allows you to assign users to particular organization in Grafana depending on attribute value obtained from identity provider. -1. In configuration file, set [`assertion_attribute_org`]({{< relref "./enterprise-configuration.md#assertion-attribute-org" >}}) to the attribute name you store organization info in. This attribute can be an array if you want a user to be in multiple organizations. -1. Set [`org_mapping`]({{< relref "./enterprise-configuration.md#org-mapping" >}}) option to the comma-separated list of `Organization:OrgId` pairs to map organization from IdP to Grafana organization specified by id. If you want users to have different roles in multiple organizations, you can set this option to a comma-separated list of `Organization:OrgId:Role` mappings. +1. In configuration file, set [`assertion_attribute_org`]({{< relref ".././enterprise-configuration.md#assertion-attribute-org" >}}) to the attribute name you store organization info in. This attribute can be an array if you want a user to be in multiple organizations. +1. Set [`org_mapping`]({{< relref ".././enterprise-configuration.md#org-mapping" >}}) option to the comma-separated list of `Organization:OrgId` pairs to map organization from IdP to Grafana organization specified by id. If you want users to have different roles in multiple organizations, you can set this option to a comma-separated list of `Organization:OrgId:Role` mappings. For example, use following configuration to assign users from `Engineering` organization to the Grafana organization with id `2` as Editor and users from `Sales` - to the org with id `3` as Admin, based on `Org` assertion attribute value: @@ -212,7 +164,7 @@ You can use `*` as an Organization if you want all your users to be in some orga > Only available in Grafana v7.0+ -With the [`allowed_organizations`]({{< relref "./enterprise-configuration.md#allowed-organizations" >}}) option you can specify a list of organizations where the user must be a member of at least one of them to be able to log in to Grafana. +With the [`allowed_organizations`]({{< relref ".././enterprise-configuration.md#allowed-organizations" >}}) option you can specify a list of organizations where the user must be a member of at least one of them to be able to log in to Grafana. ## Example SAML configuration @@ -237,79 +189,3 @@ role_values_grafana_admin = superadmin org_mapping = Engineering:2:Editor, Engineering:3:Viewer, Sales:3:Editor, *:1:Editor allowed_organizations = Engineering, Sales ``` - -## Set up SAML with Okta - -This guide will follow you through the steps of configuring SAML authentication in Grafana with [Okta](https://okta.com/). You need to be an admin in your Okta organization to access Admin Console and create SAML integration. You also need permissions to edit Grafana config file and restart Grafana server. - -### Configure the SAML integration in Okta - -To configure SAML integration with Okta, create integration inside the Okta organization first. - -1. Log in to the [Okta portal](https://login.okta.com/). -1. Go to the Admin Console in your Okta organization by clicking **Admin** in the upper-right corner. If you are in the Developer Console, then click **Developer Console** in the upper-left corner and then click **Classic UI** to switch over to the Admin Console. -1. In the Admin Console, navigate to **Applications** > **Applications**. -1. Click **Add Application**. -1. Click **Create New App** to start the Application Integration Wizard. -1. Choose **Web** as a platform. -1. Select **SAML 2.0** in the Sign on method section. -1. Click **Create**. -1. On the **General Settings** tab, enter a name for your Grafana integration. You can also upload a logo. -1. On the **Configure SAML** tab, enter the SAML information related to your Grafana instance: - - - In the **Single sign on URL** field, use the `/saml/acs` endpoint URL of your Grafana instance, for example, `https://grafana.example.com/saml/acs`. - - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, for example, `https://grafana.example.com/saml/metadata`. - - Leave the default values for **Name ID format** and **Application username**. - - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana, for example: - - | Attribute name (in Grafana) | Value (in Okta profile) | - | --------------------------- | -------------------------------------- | - | Login | `user.login` | - | Email | `user.email` | - | DisplayName | `user.firstName + " " + user.lastName` | - - - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`) and set filter to `Matches regex .*` to return all user groups. - -1. Click **Next**. -1. On the final Feedback tab, fill out the form and then click **Finish**. - -### Edit SAML options in the Grafana config file - -Once the application is created, configure Grafana to use it for SAML authentication. Refer to [Configuration]({{< relref "../administration/configuration.md" >}}) to get more information about how to configure Grafana. - -1. In the `[auth.saml]` section in the Grafana configuration file, set [`enabled`]({{< relref "./enterprise-configuration.md#enabled" >}}) to `true`. -1. Configure the [certificate and private key]({{< relref "#certificate-and-private-key" >}}). -1. On the Okta application page where you have been redirected after application created, navigate to the **Sign On** tab and find **Identity Provider metadata** link in the **Settings** section. -1. Set the [`idp_metadata_url`]({{< relref "./enterprise-configuration.md#idp-metadata-url" >}}) to the URL obtained from the previous step. The URL should look like `https://.okta.com/app//sso/saml/metadata`. -1. Set the following options to the attribute names configured at the **step 10** of the SAML integration setup. You can find this attributes on the **General** tab of the application page (**ATTRIBUTE STATEMENTS** and **GROUP ATTRIBUTE STATEMENTS** in the **SAML Settings** section). - - [`assertion_attribute_login`]({{< relref "./enterprise-configuration.md#assertion-attribute-login" >}}) - - [`assertion_attribute_email`]({{< relref "./enterprise-configuration.md#assertion-attribute-email" >}}) - - [`assertion_attribute_name`]({{< relref "./enterprise-configuration.md#assertion-attribute-name" >}}) - - [`assertion_attribute_groups`]({{< relref "./enterprise-configuration.md#assertion-attribute-groups" >}}) -1. Save the configuration file and and then restart the Grafana server. - -When you are finished, the Grafana configuration might look like this example: - -```bash -[server] -root_url = https://grafana.example.com - -[auth.saml] -enabled = true -private_key_path = "/path/to/private_key.pem" -certificate_path = "/path/to/certificate.cert" -idp_metadata_url = "https://my-org.okta.com/app/my-application/sso/saml/metadata" -assertion_attribute_name = DisplayName -assertion_attribute_login = Login -assertion_attribute_email = Email -assertion_attribute_groups = Group -``` - -## Troubleshoot SAML authentication - -To troubleshoot and get more log information, enable SAML debug logging in the configuration file. Refer to [Configuration]({{< relref "../administration/configuration.md#filters" >}}) for more information. - -```bash -[log] -filters = saml.auth:debug -``` diff --git a/docs/sources/enterprise/saml/enable-saml.md b/docs/sources/enterprise/saml/enable-saml.md new file mode 100644 index 00000000000..22067f4d12d --- /dev/null +++ b/docs/sources/enterprise/saml/enable-saml.md @@ -0,0 +1,65 @@ +--- +title: 'Enable SAML authentication in Grafana' +menuTitle: 'Enable SAML authentication' +description: 'This contains information to enable SAML authentication in Grafana' +keywords: ['grafana', 'saml', 'documentation', 'saml-auth', 'enterprise'] +aliases: ['/docs/grafana/latest/auth/saml/'] +weight: 30 +--- + +# Enable SAML authentication in Grafana + +To use the SAML integration, in the `auth.saml` section of in the Grafana custom configuration file, set `enabled` to `true`. + +Refer to [Configuration]({{< relref "../../administration/configuration.md" >}}) for more information about configuring Grafana. + +## Certificate and private key + +The SAML SSO standard uses asymmetric encryption to exchange information between the SP (Grafana) and the IdP. To perform such encryption, you need a public part and a private part. In this case, the X.509 certificate provides the public part, while the private key provides the private part. The private key needs to be issued in a [PKCS#8](https://en.wikipedia.org/wiki/PKCS_8) format. + +Grafana supports two ways of specifying both the `certificate` and `private_key`. + +- Without a suffix (`certificate` or `private_key`), the configuration assumes you've supplied the base64-encoded file contents. +- With the `_path` suffix (`certificate_path` or `private_key_path`), then Grafana treats the value entered as a file path and attempts to read the file from the file system. + +> **Note:** You can only use one form of each configuration option. Using multiple forms, such as both `certificate` and `certificate_path`, results in an error. + +--- + +### **Example** of how to generate SAML credentials: + +An example of how to generate a self-signed certificate and private key that's valid for one year: + +```sh +$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes​ +``` + +Base64-encode the cert.pem and key.pem files: +(-w0 switch is not needed on Mac, only for Linux) + +```sh +$ base64 -w0 key.pem > key.pem.base64 +$ base64 -w0 cert.pem > cert.pem.base64 +``` + +The base64-encoded values (`key.pem.base64, cert.pem.base64` files) are then used for certificate and private_key. + +The keys you provide should look like: + +It should look like: + +``` +-----BEGIN PRIVATE KEY----- +... +... +-----END PRIVATE KEY----- +``` + +If you have a key that looks like: + +``` +-----BEGIN CERTIFICATE----- +... +... +-----END CERTIFICATE----- +``` diff --git a/docs/sources/enterprise/saml/setup-saml-with-okta.md b/docs/sources/enterprise/saml/setup-saml-with-okta.md new file mode 100644 index 00000000000..6daa4655f50 --- /dev/null +++ b/docs/sources/enterprise/saml/setup-saml-with-okta.md @@ -0,0 +1,45 @@ +--- +title: 'Setup SAML authentication with Okta in Grafana' +menuTitle: 'SAML authentication with Okta' +description: 'This is a guide to setup SAML authentication with Okta in Grafana' +keywords: ['grafana', 'saml', 'documentation', 'saml-auth', 'enterprise'] +weight: 30 +--- + +# Setup SAML with Okta + +Grafana supports user authentication through Okta, which is useful when you want your users to access Grafana using single sign on. This guide will follow you through the steps of configuring SAML authentication in Grafana with [Okta](https://okta.com/). You need to be an admin in your Okta organization to access Admin Console and create SAML integration. You also need permissions to edit Grafana config file and restart Grafana server. + +## Before you begin + +- To configure SAML integration with Okta, create integration inside the Okta organization first. [Add integration in Okta](https://help.okta.com/en/prod/Content/Topics/Apps/apps-overview-add-apps.htm) +- Ensure you have permission to administer SAML authentication. For more information about permissions, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#">}}). + +**To setup SAML with Okta:** + +1. Log in to the [Okta portal](https://login.okta.com/). +1. Go to the Admin Console in your Okta organization by clicking **Admin** in the upper-right corner. If you are in the Developer Console, then click **Developer Console** in the upper-left corner and then click **Classic UI** to switch over to the Admin Console. +1. In the Admin Console, navigate to **Applications** > **Applications**. +1. Click **Add Application**. +1. Click **Create New App** to start the Application Integration Wizard. +1. Choose **Web** as a platform. +1. Select **SAML 2.0** in the Sign on method section. +1. Click **Create**. +1. On the **General Settings** tab, enter a name for your Grafana integration. You can also upload a logo. +1. On the **Configure SAML** tab, enter the SAML information related to your Grafana instance: + + - In the **Single sign on URL** field, use the `/saml/acs` endpoint URL of your Grafana instance, for example, `https://grafana.example.com/saml/acs`. + - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, for example, `https://grafana.example.com/saml/metadata`. + - Leave the default values for **Name ID format** and **Application username**. + - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana, for example: + + | Attribute name (in Grafana) | Value (in Okta profile) | + | --------------------------- | -------------------------------------- | + | Login | `user.login` | + | Email | `user.email` | + | DisplayName | `user.firstName + " " + user.lastName` | + + - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`) and set filter to `Matches regex .*` to return all user groups. + +1. Click **Next**. +1. On the final Feedback tab, fill out the form and then click **Finish**. diff --git a/docs/sources/enterprise/saml/troubleshoot-saml.md b/docs/sources/enterprise/saml/troubleshoot-saml.md new file mode 100644 index 00000000000..0c59e148046 --- /dev/null +++ b/docs/sources/enterprise/saml/troubleshoot-saml.md @@ -0,0 +1,31 @@ +--- +title: 'Troubleshoot SAML Authentication in Grafana' +menuTitle: 'Troubleshoot SAML Authentication' +description: 'This contains information on how to troubleshoot SAML authentication in Grafana' +keywords: ['grafana', 'saml', 'documentation', 'saml-auth', 'enterprise'] +aliases: ['/docs/grafana/latest/auth/saml/'] +weight: 50 +--- + +# Troubleshoot SAML authentication in Grafana + +To troubleshoot and get more log information, enable SAML debug logging in the configuration file. Refer to [Configuration]({{< relref "../../administration/configuration.md#filters" >}}) for more information. + +```bash +[log] +filters = saml.auth:debug +``` + +## Known issues + +### SAML authentication fails with error: + +- `asn1: structure error: tags don't match` + +We only support one private key format: PKCS#8. + +The keys may be in a different format (PKCS#1 or PKCS#12); in that case, it may be necessary to convert the private key format. + +```bash +$ openssl pkcs8 -topk8 -nocrypt -in -out private.pem +``` diff --git a/docs/sources/enterprise/team-sync.md b/docs/sources/enterprise/team-sync.md index 735cb260c6e..93888a7ed39 100644 --- a/docs/sources/enterprise/team-sync.md +++ b/docs/sources/enterprise/team-sync.md @@ -29,7 +29,7 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea - [GitLab OAuth]({{< relref "../auth/gitlab.md#team-sync-enterprise-only" >}}) - [LDAP]({{< relref "enhanced_ldap.md#ldap-group-synchronization-for-teams" >}}) - [Okta]({{< relref "../auth/okta.md#team-sync-enterprise-only" >}}) -- [SAML]({{< relref "saml.md#configure-team-sync" >}}) +- [SAML]({{< relref "./saml/configure-saml.md#configure-team-sync" >}}) ## Synchronize a Grafana team with an external group From 884c885289bde4d1483999cc56b870378f59c2f7 Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Wed, 13 Apr 2022 14:45:29 -0400 Subject: [PATCH 077/121] Alerting: Support OK option for Error state (#47670) * support OK state for Error --- pkg/services/ngalert/CHANGELOG.md | 1 + pkg/services/ngalert/state/manager_test.go | 146 +++++++++++++++++++++ pkg/services/ngalert/state/state.go | 14 +- 3 files changed, 157 insertions(+), 4 deletions(-) diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md index 0b6fff8a661..5e1552efc72 100644 --- a/pkg/services/ngalert/CHANGELOG.md +++ b/pkg/services/ngalert/CHANGELOG.md @@ -46,4 +46,5 @@ Scopes must have an order to ensure consistency and ease of search, this helps u ## Grafana Alerting - main / unreleased - [CHANGE] Prometheus Compatible API: Use float-like values for `api/prometheus/grafana/api/v1/alerts` and `api/prometheus/grafana/api/v1/rules` instead of the evaluation string #47216 +- [BUGFIX] Scheduler: Fix state manager to support OK option of `AlertRule.ExecErrState` #47670 - [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971 \ No newline at end of file diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index df34f4b4f3d..7ef5a293282 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -1397,6 +1397,152 @@ func TestProcessEvalResults(t *testing.T) { }, }, }, + { + desc: "normal -> normal when result is Error and ExecErrState is OK", + alertRule: &models.AlertRule{ + OrgID: 1, + Title: "test_title", + UID: "test_alert_rule_uid_2", + NamespaceUID: "test_namespace_uid", + Data: []models.AlertQuery{{ + RefID: "A", + DatasourceUID: "datasource_uid_1", + }}, + Annotations: map[string]string{"annotation": "test"}, + Labels: map[string]string{"label": "test"}, + IntervalSeconds: 10, + For: 1 * time.Minute, + ExecErrState: models.OkErrState, + }, + evalResults: []eval.Results{ + { + eval.Result{ + Instance: data.Labels{"instance_label": "test"}, + State: eval.Normal, + EvaluatedAt: evaluationTime, + EvaluationDuration: evaluationDuration, + }, + }, + { + eval.Result{ + Instance: data.Labels{"instance_label": "test"}, + Error: expr.QueryError{ + RefID: "A", + Err: errors.New("this is an error"), + }, + State: eval.Error, + EvaluatedAt: evaluationTime.Add(10 * time.Second), + EvaluationDuration: evaluationDuration, + }, + }, + }, + expectedAnnotations: 0, + expectedStates: map[string]*state.State{ + `[["__alert_rule_namespace_uid__","test_namespace_uid"],["__alert_rule_uid__","test_alert_rule_uid_2"],["alertname","test_title"],["instance_label","test"],["label","test"]]`: { + AlertRuleUID: "test_alert_rule_uid_2", + OrgID: 1, + CacheId: `[["__alert_rule_namespace_uid__","test_namespace_uid"],["__alert_rule_uid__","test_alert_rule_uid_2"],["alertname","test_title"],["instance_label","test"],["label","test"]]`, + Labels: data.Labels{ + "__alert_rule_namespace_uid__": "test_namespace_uid", + "__alert_rule_uid__": "test_alert_rule_uid_2", + "alertname": "test_title", + "label": "test", + "instance_label": "test", + }, + State: eval.Normal, + Error: nil, + Results: []state.Evaluation{ + { + EvaluationTime: evaluationTime, + EvaluationState: eval.Normal, + Values: make(map[string]*float64), + }, + { + EvaluationTime: evaluationTime.Add(10 * time.Second), + EvaluationState: eval.Error, + Values: make(map[string]*float64), + }, + }, + LastEvaluationTime: evaluationTime.Add(10 * time.Second), + EvaluationDuration: evaluationDuration, + Annotations: map[string]string{"annotation": "test"}, + }, + }, + }, + { + desc: "alerting -> normal when result is Error and ExecErrState is OK", + alertRule: &models.AlertRule{ + OrgID: 1, + Title: "test_title", + UID: "test_alert_rule_uid_2", + NamespaceUID: "test_namespace_uid", + Data: []models.AlertQuery{{ + RefID: "A", + DatasourceUID: "datasource_uid_1", + }}, + Annotations: map[string]string{"annotation": "test"}, + Labels: map[string]string{"label": "test"}, + IntervalSeconds: 10, + For: 1 * time.Minute, + ExecErrState: models.OkErrState, + }, + evalResults: []eval.Results{ + { + eval.Result{ + Instance: data.Labels{"instance_label": "test"}, + State: eval.Alerting, + EvaluatedAt: evaluationTime, + EvaluationDuration: evaluationDuration, + }, + }, + { + eval.Result{ + Instance: data.Labels{"instance_label": "test"}, + Error: expr.QueryError{ + RefID: "A", + Err: errors.New("this is an error"), + }, + State: eval.Error, + EvaluatedAt: evaluationTime.Add(10 * time.Second), + EvaluationDuration: evaluationDuration, + }, + }, + }, + expectedAnnotations: 2, + expectedStates: map[string]*state.State{ + `[["__alert_rule_namespace_uid__","test_namespace_uid"],["__alert_rule_uid__","test_alert_rule_uid_2"],["alertname","test_title"],["instance_label","test"],["label","test"]]`: { + AlertRuleUID: "test_alert_rule_uid_2", + OrgID: 1, + CacheId: `[["__alert_rule_namespace_uid__","test_namespace_uid"],["__alert_rule_uid__","test_alert_rule_uid_2"],["alertname","test_title"],["instance_label","test"],["label","test"]]`, + Labels: data.Labels{ + "__alert_rule_namespace_uid__": "test_namespace_uid", + "__alert_rule_uid__": "test_alert_rule_uid_2", + "alertname": "test_title", + "label": "test", + "instance_label": "test", + }, + State: eval.Normal, + Error: nil, + Results: []state.Evaluation{ + { + EvaluationTime: evaluationTime, + EvaluationState: eval.Alerting, + Values: make(map[string]*float64), + }, + { + EvaluationTime: evaluationTime.Add(10 * time.Second), + EvaluationState: eval.Error, + Values: make(map[string]*float64), + }, + }, + StartsAt: evaluationTime.Add(10 * time.Second), + EndsAt: evaluationTime.Add(10 * time.Second), + LastEvaluationTime: evaluationTime.Add(10 * time.Second), + EvaluationDuration: evaluationDuration, + Annotations: map[string]string{"annotation": "test"}, + }, + }, + }, { desc: "normal -> alerting -> error -> alerting - it should clear the error", alertRule: &models.AlertRule{ diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index 63f3b9d5930..cf5c5b849d2 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -2,6 +2,7 @@ package state import ( "errors" + "fmt" "strings" "time" @@ -51,8 +52,7 @@ func NewEvaluationValues(m map[string]eval.NumberValueCapture) map[string]*float } func (a *State) resultNormal(_ *ngModels.AlertRule, result eval.Result) { - a.Error = result.Error // should be nil since state is not error - + a.Error = nil // should be nil since state is not error if a.State != eval.Normal { a.EndsAt = result.EvaluatedAt a.StartsAt = result.EvaluatedAt @@ -88,9 +88,10 @@ func (a *State) resultError(alertRule *ngModels.AlertRule, result eval.Result) { a.Error = result.Error execErrState := eval.Error - if alertRule.ExecErrState == ngModels.AlertingErrState { + switch alertRule.ExecErrState { + case ngModels.AlertingErrState: execErrState = eval.Alerting - } else if alertRule.ExecErrState == ngModels.ErrorErrState { + case ngModels.ErrorErrState: // If the evaluation failed because a query returned an error then // update the state with the Datasource UID as a label and the error // message as an annotation so other code can use this metadata to @@ -107,6 +108,11 @@ func (a *State) resultError(alertRule *ngModels.AlertRule, result eval.Result) { a.Annotations["Error"] = queryError.Error() } execErrState = eval.Error + case ngModels.OkErrState: + a.resultNormal(alertRule, result) + return + default: + a.Error = fmt.Errorf("cannot map error to a state because option [%s] is not supported. evaluation error: %w", alertRule.ExecErrState, a.Error) } switch a.State { From e00db6a826428d0480f0ea704f55346af2cbec9b Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Wed, 13 Apr 2022 22:27:03 +0300 Subject: [PATCH 078/121] =?UTF-8?q?Live:=20collect=20more=20usage=20stats?= =?UTF-8?q?=20=E2=80=93=20enabled,=20ha=20enabled,=20num=20nodes,=20channe?= =?UTF-8?q?ls,=20subs=20(#47378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/services/live/live.go | 103 ++++++++++++++++++++++----------- pkg/services/live/live_test.go | 42 ++++++++++++++ 2 files changed, 110 insertions(+), 35 deletions(-) diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index ea144b3f391..3924d6f810e 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "os" + "strconv" "strings" "sync" "time" @@ -1365,6 +1366,11 @@ func handleLog(msg centrifuge.LogEntry) { func (g *GrafanaLive) sampleLiveStats() { numClients := g.node.Hub().NumClients() numUsers := g.node.Hub().NumUsers() + numChannels := g.node.Hub().NumChannels() + var numNodes int + if info, err := g.node.Info(); err == nil { + numNodes = len(info.Nodes) + } g.usageStats.sampleCount++ g.usageStats.numClientsSum += numClients @@ -1374,16 +1380,16 @@ func (g *GrafanaLive) sampleLiveStats() { g.usageStats.numClientsMax = numClients } - if numClients < g.usageStats.numClientsMin { - g.usageStats.numClientsMin = numClients - } - if numUsers > g.usageStats.numUsersMax { g.usageStats.numUsersMax = numUsers } - if numUsers < g.usageStats.numUsersMin { - g.usageStats.numUsersMin = numUsers + if numNodes > g.usageStats.numNodesMax { + g.usageStats.numNodesMax = numNodes + } + + if numChannels > g.usageStats.numChannelsMax { + g.usageStats.numChannelsMax = numChannels } } @@ -1391,38 +1397,65 @@ func (g *GrafanaLive) resetLiveStats() { g.usageStats = usageStats{} } +func getHistogramMetric(val int, bounds []int, metricPrefix string) string { + for _, bound := range bounds { + if val <= bound { + return metricPrefix + "le_" + strconv.Itoa(bound) + } + } + return metricPrefix + "le_inf" +} + +func (g *GrafanaLive) collectLiveStats(_ context.Context) (map[string]interface{}, error) { + liveUsersAvg := 0 + liveClientsAvg := 0 + + if g.usageStats.sampleCount > 0 { + liveUsersAvg = g.usageStats.numUsersSum / g.usageStats.sampleCount + liveClientsAvg = g.usageStats.numClientsSum / g.usageStats.sampleCount + } + + var liveEnabled int + if g.Cfg.LiveMaxConnections != 0 { + liveEnabled = 1 + } + + var liveHAEnabled int + if g.Cfg.LiveHAEngine != "" { + liveHAEnabled = 1 + } + + metrics := map[string]interface{}{ + "stats.live_enabled.count": liveEnabled, + "stats.live_ha_enabled.count": liveHAEnabled, + "stats.live_samples.count": g.usageStats.sampleCount, + "stats.live_users_max.count": g.usageStats.numUsersMax, + "stats.live_users_avg.count": liveUsersAvg, + "stats.live_clients_max.count": g.usageStats.numClientsMax, + "stats.live_clients_avg.count": liveClientsAvg, + "stats.live_channels_max.count": g.usageStats.numChannelsMax, + "stats.live_nodes_max.count": g.usageStats.numNodesMax, + } + + metrics[getHistogramMetric(g.usageStats.numClientsMax, []int{0, 10, 100, 1000, 10000, 100000}, "stats.live_clients_")] = 1 + metrics[getHistogramMetric(g.usageStats.numUsersMax, []int{0, 10, 100, 1000, 10000, 100000}, "stats.live_users_")] = 1 + metrics[getHistogramMetric(g.usageStats.numChannelsMax, []int{0, 10, 100, 1000, 10000, 100000}, "stats.live_channels_")] = 1 + metrics[getHistogramMetric(g.usageStats.numNodesMax, []int{1, 3, 9}, "stats.live_nodes_")] = 1 + + return metrics, nil +} + func (g *GrafanaLive) registerUsageMetrics() { g.usageStatsService.RegisterSendReportCallback(g.resetLiveStats) - - g.usageStatsService.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) { - liveUsersAvg := 0 - liveClientsAvg := 0 - - if g.usageStats.sampleCount > 0 { - liveUsersAvg = g.usageStats.numUsersSum / g.usageStats.sampleCount - liveClientsAvg = g.usageStats.numClientsSum / g.usageStats.sampleCount - } - - metrics := map[string]interface{}{ - "stats.live_samples.count": g.usageStats.sampleCount, - "stats.live_users_max.count": g.usageStats.numUsersMax, - "stats.live_users_min.count": g.usageStats.numUsersMin, - "stats.live_users_avg.count": liveUsersAvg, - "stats.live_clients_max.count": g.usageStats.numClientsMax, - "stats.live_clients_min.count": g.usageStats.numClientsMin, - "stats.live_clients_avg.count": liveClientsAvg, - } - - return metrics, nil - }) + g.usageStatsService.RegisterMetricsFunc(g.collectLiveStats) } type usageStats struct { - numClientsMax int - numClientsMin int - numClientsSum int - numUsersMax int - numUsersMin int - numUsersSum int - sampleCount int + numClientsMax int + numClientsSum int + numUsersMax int + numUsersSum int + sampleCount int + numNodesMax int + numChannelsMax int } diff --git a/pkg/services/live/live_test.go b/pkg/services/live/live_test.go index 9c738c0aa30..d4dff995fcd 100644 --- a/pkg/services/live/live_test.go +++ b/pkg/services/live/live_test.go @@ -157,3 +157,45 @@ func TestCheckOrigin(t *testing.T) { }) } } + +func Test_getHistogramMetric(t *testing.T) { + type args struct { + val int + bounds []int + metricPrefix string + } + + tests := []struct { + name string + args args + want string + }{ + { + "zero", + args{0, []int{0, 10, 100, 1000, 10000, 100000}, "live_users_"}, + "live_users_le_0", + }, + { + "equal_to_bound", + args{10, []int{0, 10, 100, 1000, 10000, 100000}, "live_users_"}, + "live_users_le_10", + }, + { + "in_the_middle", + args{30000, []int{0, 10, 100, 1000, 10000, 100000}, "live_users_"}, + "live_users_le_100000", + }, + { + "more_than_upper_bound", + args{300000, []int{0, 10, 100, 1000, 10000, 100000}, "live_users_"}, + "live_users_le_inf", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getHistogramMetric(tt.args.val, tt.args.bounds, tt.args.metricPrefix); got != tt.want { + t.Errorf("getHistogramMetric() = %v, want %v", got, tt.want) + } + }) + } +} From 5fb80498b1da8fbb053e39f5ee8c7cb1845539da Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 13 Apr 2022 17:04:10 -0300 Subject: [PATCH 079/121] Apply templating on alert notifications on OK state (#47355) * OK notifications using previous evaluation data * copy rule.EvalMatches to avoid changes to the underlying array * test cases added/modified * delete trailing newline * fix double newline in go import * add change to the changelog * specify that this only affects legacy alerting (changelog) * use current eval data instead of prev eval data * create evalMatch just once * code comments, renamings, getTemplateMatches() function * changelog and docs updated --- .../old-alerting/add-notification-template.md | 4 +- pkg/services/alerting/conditions/query.go | 20 ++++-- pkg/services/alerting/eval_context.go | 20 +++++- pkg/services/alerting/eval_context_test.go | 63 +++++++++++++++++++ pkg/services/alerting/eval_handler.go | 1 + pkg/services/alerting/interfaces.go | 1 + pkg/services/alerting/notifier_test.go | 1 + pkg/services/ngalert/CHANGELOG.md | 1 + 8 files changed, 102 insertions(+), 9 deletions(-) diff --git a/docs/sources/alerting/old-alerting/add-notification-template.md b/docs/sources/alerting/old-alerting/add-notification-template.md index e9c276b5833..bd952bce72f 100644 --- a/docs/sources/alerting/old-alerting/add-notification-template.md +++ b/docs/sources/alerting/old-alerting/add-notification-template.md @@ -9,7 +9,9 @@ aliases = ["/docs/grafana/latest/alerting/add-notification-template/"] You can provide detailed information to alert notification recipients by injecting alert query data into an alert notification. This topic explains how you can use alert query labels in alert notifications. -Labels that exist from the evaluation of the alert query can be used in the alert rule name and in the alert notification message fields. The alert label data is injected into the notification fields when the alert is in the alerting state. When there are multiple unique values for the same label, the values are comma-separated. +Labels that exist from the evaluation of the alert query can be used in the alert rule name and in the alert notification message fields. The alert label data is injected into the notification fields when the alert is evaluated and the notification is about to be sent. When there are multiple unique values for the same label, the values are comma-separated. + +When the alert is in `firing` state, the data used to parse the templates corresponds to the series that are violating the alert condition at the moment the notification is sent. For `resolved` alert notifications, all series are used since there are none that are violating the condition. This topic explains how you can use alert query labels in alert notifications. diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index 9aee49e5b1b..4eb86714dd6 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -56,7 +56,11 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext, requestHandler lega emptySeriesCount := 0 evalMatchCount := 0 + + // matches represents all the series that violate the alert condition var matches []*alerting.EvalMatch + // allMatches capture all evaluation matches irregardless on whether the condition is met or not + var allMatches []*alerting.EvalMatch for _, series := range seriesList { reducedValue := c.Reducer.Reduce(series) @@ -72,14 +76,17 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext, requestHandler lega }) } + em := alerting.EvalMatch{ + Metric: series.Name, + Value: reducedValue, + Tags: series.Tags, + } + + allMatches = append(allMatches, &em) + if evalMatch { evalMatchCount++ - - matches = append(matches, &alerting.EvalMatch{ - Metric: series.Name, - Value: reducedValue, - Tags: series.Tags, - }) + matches = append(matches, &em) } } @@ -105,6 +112,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext, requestHandler lega NoDataFound: emptySeriesCount == len(seriesList), Operator: c.Operator, EvalMatches: matches, + AllMatches: allMatches, }, nil } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index de0d2d155ab..7eb3caad471 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -17,6 +17,7 @@ type EvalContext struct { IsTestRun bool IsDebug bool EvalMatches []*EvalMatch + AllMatches []*EvalMatch Logs []*ResultLogEntry Error error ConditionEvals string @@ -47,6 +48,7 @@ func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator model Rule: rule, Logs: make([]*ResultLogEntry, 0), EvalMatches: make([]*EvalMatch, 0), + AllMatches: make([]*EvalMatch, 0), Log: log.New("alerting.evalContext"), PrevAlertState: rule.State, RequestValidator: requestValidator, @@ -188,11 +190,13 @@ func getNewStateInternal(c *EvalContext) models.AlertStateType { // evaluateNotificationTemplateFields will treat the alert evaluation rule's name and message fields as // templates, and evaluate the templates using data from the alert evaluation's tags func (c *EvalContext) evaluateNotificationTemplateFields() error { - if len(c.EvalMatches) < 1 { + matches := c.getTemplateMatches() + if len(matches) < 1 { + // if there are no series to parse the templates with, return return nil } - templateDataMap, err := buildTemplateDataMap(c.EvalMatches) + templateDataMap, err := buildTemplateDataMap(matches) if err != nil { return err } @@ -212,6 +216,18 @@ func (c *EvalContext) evaluateNotificationTemplateFields() error { return nil } +// getTemplateMatches returns the values we should use to parse the templates +func (c *EvalContext) getTemplateMatches() []*EvalMatch { + // EvalMatches represent series violating the rule threshold, + // if we have any, this means the alert is firing and we should use this data to parse the templates. + if len(c.EvalMatches) > 0 { + return c.EvalMatches + } + + // If we don't have any alerting values, use all values to parse the templates. + return c.AllMatches +} + func evaluateTemplate(s string, m map[string]string) (string, error) { for k, v := range m { re, err := regexp.Compile(fmt.Sprintf(`\${%s}`, regexp.QuoteMeta(k))) diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 30c3114ed15..6ac5da6fab0 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -340,3 +340,66 @@ func TestEvaluateTemplate(t *testing.T) { }) } } + +func TestEvaluateNotificationTemplateFields(t *testing.T) { + tests := []struct { + name string + evalMatches []*EvalMatch + allMatches []*EvalMatch + expectedName string + expectedMessage string + }{ + { + "with evaluation matches", + []*EvalMatch{{ + Tags: map[string]string{"value1": "test1", "value2": "test2"}, + }}, + []*EvalMatch{{ + Tags: map[string]string{"value1": "test1", "value2": "test2"}, + }}, + "Rule name: test1", + "Rule message: test2", + }, + { + "missing key", + []*EvalMatch{{ + Tags: map[string]string{"value1": "test1", "value3": "test2"}, + }}, + []*EvalMatch{{ + Tags: map[string]string{"value1": "test1", "value3": "test2"}, + }}, + "Rule name: test1", + "Rule message: ${value2}", + }, + { + "no evaluation matches, with series", + []*EvalMatch{}, + []*EvalMatch{{ + Tags: map[string]string{"value1": "test1", "value2": "test2"}, + }}, + "Rule name: test1", + "Rule message: test2", + }, + { + "no evaluation matches, no series", + []*EvalMatch{}, + []*EvalMatch{}, + "Rule name: ${value1}", + "Rule message: ${value2}", + }, + } + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + evalContext := NewEvalContext(context.Background(), &Rule{Name: "Rule name: ${value1}", Message: "Rule message: ${value2}", Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil) + evalContext.EvalMatches = test.evalMatches + evalContext.AllMatches = test.allMatches + + err := evalContext.evaluateNotificationTemplateFields() + + require.NoError(tt, err) + require.Equal(tt, test.expectedName, evalContext.Rule.Name) + require.Equal(tt, test.expectedMessage, evalContext.Rule.Message) + }) + } +} diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 88a609a714b..f516d0718e6 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -68,6 +68,7 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { } context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...) + context.AllMatches = append(context.AllMatches, cr.AllMatches...) } context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing) diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 5d40116021b..b5f2fce922d 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -56,6 +56,7 @@ type ConditionResult struct { NoDataFound bool Operator string EvalMatches []*EvalMatch + AllMatches []*EvalMatch } // Condition is responsible for evaluating an alert condition. diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index c921908c6b8..4e1ce660709 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -25,6 +25,7 @@ func TestNotificationService(t *testing.T) { evalCtx := NewEvalContext(context.Background(), testRule, &validations.OSSPluginRequestValidator{}, store) testRuleTemplated := &Rule{Name: "Test latency ${quantile}", Message: "Something is bad on instance ${instance}"} + evalCtxWithMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store) evalCtxWithMatch.EvalMatches = []*EvalMatch{{ Tags: map[string]string{ diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md index 5e1552efc72..c5650250638 100644 --- a/pkg/services/ngalert/CHANGELOG.md +++ b/pkg/services/ngalert/CHANGELOG.md @@ -46,5 +46,6 @@ Scopes must have an order to ensure consistency and ease of search, this helps u ## Grafana Alerting - main / unreleased - [CHANGE] Prometheus Compatible API: Use float-like values for `api/prometheus/grafana/api/v1/alerts` and `api/prometheus/grafana/api/v1/rules` instead of the evaluation string #47216 +- [BUGFIX] (Legacy) Templates: Parse notification templates using all the matches of the alert rule when going from `Alerting` to `OK` in legacy alerting #47355 - [BUGFIX] Scheduler: Fix state manager to support OK option of `AlertRule.ExecErrState` #47670 - [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971 \ No newline at end of file From 388ecb40377f7818ed9e29d1f9f34532c63279b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Wed, 13 Apr 2022 22:15:55 +0200 Subject: [PATCH 080/121] Alerting: Provisioning API - Contact points (#47197) --- pkg/services/ngalert/api/api.go | 6 +- pkg/services/ngalert/api/api_provisioning.go | 53 ++- pkg/services/ngalert/api/authorization.go | 8 +- .../ngalert/api/authorization_test.go | 2 +- .../ngalert/api/forked_provisioning.go | 16 + .../api/generated_base_api_provisioning.go | 68 +++ .../definitions/provisioning_contactpoints.go | 157 +++++++ ...ovisioning.go => provisioning_policies.go} | 0 pkg/services/ngalert/api/tooling/post.json | 161 ++++++- pkg/services/ngalert/api/tooling/spec.json | 151 ++++++- pkg/services/ngalert/models/provisioning.go | 4 +- pkg/services/ngalert/ngalert.go | 2 + .../ngalert/provisioning/contactpoints.go | 402 ++++++++++++++++++ .../provisioning/contactpoints_test.go | 195 +++++++++ .../notification_policies_test.go | 6 +- pkg/services/ngalert/provisioning/persist.go | 2 + pkg/services/ngalert/provisioning/testing.go | 25 +- .../ngalert/store/provisioning_store.go | 29 ++ .../ngalert/store/provisioning_store_test.go | 46 +- .../api/alerting/api_provisioning_test.go | 92 ++++ 20 files changed, 1384 insertions(+), 41 deletions(-) create mode 100644 pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go rename pkg/services/ngalert/api/tooling/definitions/{provisioning.go => provisioning_policies.go} (100%) create mode 100644 pkg/services/ngalert/provisioning/contactpoints.go create mode 100644 pkg/services/ngalert/provisioning/contactpoints_test.go diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index c17b50795a3..be3a4623a7a 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -77,6 +77,7 @@ type API struct { SecretsService secrets.Service AccessControl accesscontrol.AccessControl Policies *provisioning.NotificationPolicyService + ContactPointService *provisioning.ContactPointService } // RegisterAPIEndpoints registers API handlers @@ -132,8 +133,9 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { if api.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAlertProvisioning) { api.RegisterProvisioningApiEndpoints(NewForkedProvisioningApi(&ProvisioningSrv{ - log: logger, - policies: api.Policies, + log: logger, + policies: api.Policies, + contactPointService: api.ContactPointService, }), m) } } diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 679e432107c..2b6e632c58e 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -9,20 +9,29 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - domain "github.com/grafana/grafana/pkg/services/ngalert/models" + alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/web" ) type ProvisioningSrv struct { - log log.Logger - policies NotificationPolicyService + log log.Logger + policies NotificationPolicyService + contactPointService ContactPointService +} + +type ContactPointService interface { + GetContactPoints(ctx context.Context, orgID int64) ([]apimodels.EmbeddedContactPoint, error) + CreateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, p alerting_models.Provenance) (apimodels.EmbeddedContactPoint, error) + UpdateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, p alerting_models.Provenance) error + DeleteContactPoint(ctx context.Context, orgID int64, uid string) error } type NotificationPolicyService interface { GetPolicyTree(ctx context.Context, orgID int64) (provisioning.EmbeddedRoutingTree, error) - UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p domain.Provenance) error + UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p alerting_models.Provenance) error } func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Response { @@ -39,7 +48,7 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Re func (srv *ProvisioningSrv) RoutePostPolicyTree(c *models.ReqContext, tree apimodels.Route) response.Response { // TODO: lift validation out of definitions.Rotue.UnmarshalJSON and friends into a dedicated validator. - err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgId, tree, domain.ProvenanceApi) + err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgId, tree, alerting_models.ProvenanceAPI) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") } @@ -49,3 +58,37 @@ func (srv *ProvisioningSrv) RoutePostPolicyTree(c *models.ReqContext, tree apimo return response.JSON(http.StatusAccepted, util.DynMap{"message": "policies updated"}) } + +func (srv *ProvisioningSrv) RouteGetContactPoints(c *models.ReqContext) response.Response { + cps, err := srv.contactPointService.GetContactPoints(c.Req.Context(), c.OrgId) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, cps) +} + +func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { + // TODO: provenance is hardcoded for now, change it later to make it more flexible + contactPoint, err := srv.contactPointService.CreateContactPoint(c.Req.Context(), c.OrgId, cp, alerting_models.ProvenanceAPI) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusAccepted, contactPoint) +} + +func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { + err := srv.contactPointService.UpdateContactPoint(c.Req.Context(), c.OrgId, cp, alerting_models.ProvenanceAPI) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusAccepted, util.DynMap{"message": "contactpoint updated"}) +} + +func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *models.ReqContext) response.Response { + cpID := web.Params(c.Req)[":ID"] + err := srv.contactPointService.DeleteContactPoint(c.Req.Context(), c.OrgId, cpID) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusAccepted, util.DynMap{"message": "contactpoint deleted"}) +} diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 317c7b21037..6394ec50ac3 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -181,10 +181,14 @@ func (api *API) authorize(method, path string) web.Handler { return middleware.ReqOrgAdmin // Grafana-only Provisioning Read Paths - case http.MethodGet + "/api/provisioning/policies": + case http.MethodGet + "/api/provisioning/policies", + http.MethodGet + "/api/provisioning/contact-points": return middleware.ReqSignedIn - case http.MethodPost + "/api/provisioning/policies": + case http.MethodPost + "/api/provisioning/policies", + http.MethodPost + "/api/provisioning/contact-points", + http.MethodPut + "/api/provisioning/contact-points", + http.MethodDelete + "/api/provisioning/contact-points/{ID}": return middleware.ReqEditorRole } diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index b39597a9fb8..0f6579760e6 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -47,7 +47,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 30) + require.Len(t, paths, 32) ac := acmock.New() api := &API{AccessControl: ac} diff --git a/pkg/services/ngalert/api/forked_provisioning.go b/pkg/services/ngalert/api/forked_provisioning.go index 74213e3c783..a808d9eca3e 100644 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ b/pkg/services/ngalert/api/forked_provisioning.go @@ -26,3 +26,19 @@ func (f *ForkedProvisioningApi) forkRouteGetPolicyTree(ctx *models.ReqContext) r func (f *ForkedProvisioningApi) forkRoutePostPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response { return f.svc.RoutePostPolicyTree(ctx, route) } + +func (f *ForkedProvisioningApi) forkRouteGetContactpoints(ctx *models.ReqContext) response.Response { + return f.svc.RouteGetContactPoints(ctx) +} + +func (f *ForkedProvisioningApi) forkRoutePostContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { + return f.svc.RoutePostContactPoint(ctx, cp) +} + +func (f *ForkedProvisioningApi) forkRoutePutContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { + return f.svc.RoutePutContactPoint(ctx, cp) +} + +func (f *ForkedProvisioningApi) forkRouteDeleteContactpoints(ctx *models.ReqContext) response.Response { + return f.svc.RouteDeleteContactPoint(ctx) +} diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index 2d5e72ae571..f310cdaddd5 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -20,14 +20,34 @@ import ( ) type ProvisioningApiForkingService interface { + RouteDeleteContactpoints(*models.ReqContext) response.Response + RouteGetContactpoints(*models.ReqContext) response.Response RouteGetPolicyTree(*models.ReqContext) response.Response + RoutePostContactpoints(*models.ReqContext) response.Response RoutePostPolicyTree(*models.ReqContext) response.Response + RoutePutContactpoints(*models.ReqContext) response.Response +} + +func (f *ForkedProvisioningApi) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response { + return f.forkRouteDeleteContactpoints(ctx) +} + +func (f *ForkedProvisioningApi) RouteGetContactpoints(ctx *models.ReqContext) response.Response { + return f.forkRouteGetContactpoints(ctx) } func (f *ForkedProvisioningApi) RouteGetPolicyTree(ctx *models.ReqContext) response.Response { return f.forkRouteGetPolicyTree(ctx) } +func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) response.Response { + conf := apimodels.EmbeddedContactPoint{} + if err := web.Bind(ctx.Req, &conf); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + return f.forkRoutePostContactpoints(ctx, conf) +} + func (f *ForkedProvisioningApi) RoutePostPolicyTree(ctx *models.ReqContext) response.Response { conf := apimodels.Route{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -36,8 +56,36 @@ func (f *ForkedProvisioningApi) RoutePostPolicyTree(ctx *models.ReqContext) resp return f.forkRoutePostPolicyTree(ctx, conf) } +func (f *ForkedProvisioningApi) RoutePutContactpoints(ctx *models.ReqContext) response.Response { + conf := apimodels.EmbeddedContactPoint{} + if err := web.Bind(ctx.Req, &conf); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + return f.forkRoutePutContactpoints(ctx, conf) +} + func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingService, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { + group.Delete( + toMacaronPath("/api/provisioning/contact-points/{ID}"), + api.authorize(http.MethodDelete, "/api/provisioning/contact-points/{ID}"), + metrics.Instrument( + http.MethodDelete, + "/api/provisioning/contact-points/{ID}", + srv.RouteDeleteContactpoints, + m, + ), + ) + group.Get( + toMacaronPath("/api/provisioning/contact-points"), + api.authorize(http.MethodGet, "/api/provisioning/contact-points"), + metrics.Instrument( + http.MethodGet, + "/api/provisioning/contact-points", + srv.RouteGetContactpoints, + m, + ), + ) group.Get( toMacaronPath("/api/provisioning/policies"), api.authorize(http.MethodGet, "/api/provisioning/policies"), @@ -48,6 +96,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Post( + toMacaronPath("/api/provisioning/contact-points"), + api.authorize(http.MethodPost, "/api/provisioning/contact-points"), + metrics.Instrument( + http.MethodPost, + "/api/provisioning/contact-points", + srv.RoutePostContactpoints, + m, + ), + ) group.Post( toMacaronPath("/api/provisioning/policies"), api.authorize(http.MethodPost, "/api/provisioning/policies"), @@ -58,5 +116,15 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Put( + toMacaronPath("/api/provisioning/contact-points"), + api.authorize(http.MethodPut, "/api/provisioning/contact-points"), + metrics.Instrument( + http.MethodPut, + "/api/provisioning/contact-points", + srv.RoutePutContactpoints, + m, + ), + ) }, middleware.ReqSignedIn) } diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go new file mode 100644 index 00000000000..3d86af76928 --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go @@ -0,0 +1,157 @@ +package definitions + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels" +) + +// swagger:route GET /api/provisioning/contact-points provisioning RouteGetContactpoints +// +// Get all the contact points. +// +// Responses: +// 200: Route +// 400: ValidationError + +// swagger:route POST /api/provisioning/contact-points provisioning RoutePostContactpoints +// +// Create a contact point. +// +// Consumes: +// - application/json +// +// Responses: +// 202: Accepted +// 400: ValidationError + +// swagger:route PUT /api/provisioning/contact-points provisioning RoutePutContactpoints +// +// Update an existing contact point. +// +// Consumes: +// - application/json +// +// Responses: +// 202: Accepted +// 400: ValidationError + +// swagger:route DELETE /api/provisioning/contact-points/{ID} provisioning RouteDeleteContactpoints +// +// Delete a contact point. +// +// Consumes: +// - application/json +// +// Responses: +// 202: Accepted +// 400: ValidationError + +// swagger:parameters RoutePostContactpoints RoutePutContactpoints +type ContactPointPayload struct { + // in:body + Body EmbeddedContactPoint +} + +// EmbeddedContactPoint is the contact point type that is used +// by grafanas embedded alertmanager implementation. +type EmbeddedContactPoint struct { + // UID is the unique identifier of the contact point. This will be + // automatically set be the Grafana. + UID string `json:"uid"` + // Name is used as grouping key in the UI. Contact points with the + // same name will be grouped in the UI. + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Settings *simplejson.Json `json:"settings" binding:"required"` + DisableResolveMessage bool `json:"disableResolveMessage"` + Provenance string `json:"provanance"` +} + +const RedactedValue = "[REDACTED]" + +func (e *EmbeddedContactPoint) Valid(decryptFunc channels.GetDecryptedValueFn) error { + if e.Type == "" { + return fmt.Errorf("type should not be an empty string") + } + if e.Settings == nil { + return fmt.Errorf("settings should not be empty") + } + factory, exists := channels.Factory(e.Type) + if !exists { + return fmt.Errorf("unknown type '%s'", e.Type) + } + cfg, _ := channels.NewFactoryConfig(&channels.NotificationChannelConfig{ + Settings: e.Settings, + Type: e.Type, + }, nil, decryptFunc, nil) + if _, err := factory(cfg); err != nil { + return err + } + return nil +} + +func (e *EmbeddedContactPoint) SecretKeys() ([]string, error) { + switch e.Type { + case "alertmanager": + return []string{"basicAuthPassword"}, nil + case "dingding": + return []string{}, nil + case "discord": + return []string{}, nil + case "email": + return []string{}, nil + case "googlechat": + return []string{}, nil + case "kafka": + return []string{}, nil + case "line": + return []string{"token"}, nil + case "opsgenie": + return []string{"apiKey"}, nil + case "pagerduty": + return []string{"integrationKey"}, nil + case "pushover": + return []string{"userKey", "apiToken"}, nil + case "sensugo": + return []string{"apiKey"}, nil + case "slack": + return []string{"url", "token"}, nil + case "teams": + return []string{}, nil + case "telegram": + return []string{"bottoken"}, nil + case "threema": + return []string{"api_secret"}, nil + case "victorops": + return []string{}, nil + case "webhook": + return []string{}, nil + case "wecom": + return []string{"url"}, nil + } + return nil, fmt.Errorf("no secrets configured for type '%s'", e.Type) +} + +func (e *EmbeddedContactPoint) ExtractSecrets() (map[string]string, error) { + secrets := map[string]string{} + secretKeys, err := e.SecretKeys() + if err != nil { + return nil, err + } + for _, secretKey := range secretKeys { + secretValue := e.Settings.Get(secretKey).MustString() + e.Settings.Del(secretKey) + secrets[secretKey] = secretValue + } + return secrets, nil +} + +func (e *EmbeddedContactPoint) ResourceID() string { + return e.UID +} + +func (e *EmbeddedContactPoint) ResourceType() string { + return "contactPoint" +} diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go similarity index 100% rename from pkg/services/ngalert/api/tooling/definitions/provisioning.go rename to pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index dd87745df0d..f9373171bf5 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -558,6 +558,36 @@ "type": "object", "x-go-package": "github.com/prometheus/alertmanager/config" }, + "EmbeddedContactPoint": { + "description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.", + "properties": { + "disableResolveMessage": { + "type": "boolean", + "x-go-name": "DisableResolveMessage" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "provanance": { + "type": "string", + "x-go-name": "Provenance" + }, + "settings": { + "$ref": "#/definitions/Json" + }, + "type": { + "type": "string", + "x-go-name": "Type" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + } + }, + "type": "object", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, "ErrorType": { "title": "ErrorType models the different API error types.", "type": "string", @@ -3034,6 +3064,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroup": { + "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -3055,17 +3086,14 @@ "labels", "receiver" ], - "type": "object", - "x-go-name": "AlertGroup", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, - "type": "array", - "x-go-name": "AlertGroups", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "array" }, "alertStatus": { "description": "AlertStatus alert status", @@ -3255,6 +3283,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -3306,17 +3335,14 @@ "status", "updatedAt" ], - "type": "object", - "x-go-name": "GettableSilence", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, - "type": "array", - "x-go-name": "GettableSilences", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "array" }, "labelSet": { "additionalProperties": { @@ -3445,7 +3471,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { - "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3485,7 +3510,9 @@ "matchers", "startsAt" ], - "type": "object" + "type": "object", + "x-go-name": "PostableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "receiver": { "properties": { @@ -4733,6 +4760,112 @@ ] } }, + "/api/provisioning/contact-points": { + "get": { + "operationId": "RouteGetContactpoints", + "responses": { + "200": { + "description": "Route", + "schema": { + "$ref": "#/definitions/Route" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Get all the contact points.", + "tags": [ + "provisioning" + ] + }, + "post": { + "consumes": [ + "application/json" + ], + "operationId": "RoutePostContactpoints", + "parameters": [ + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/EmbeddedContactPoint" + } + } + ], + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Create a contact point.", + "tags": [ + "provisioning" + ] + }, + "put": { + "consumes": [ + "application/json" + ], + "operationId": "RoutePutContactpoints", + "parameters": [ + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/EmbeddedContactPoint" + } + } + ], + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Update an existing contact point.", + "tags": [ + "provisioning" + ] + } + }, + "/api/provisioning/contact-points/{ID}": { + "delete": { + "consumes": [ + "application/json" + ], + "operationId": "RouteDeleteContactpoints", + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Delete a contact point.", + "tags": [ + "provisioning" + ] + } + }, "/api/provisioning/policies": { "get": { "operationId": "RouteGetPolicyTree", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index fff24ed4f08..188a5c4b885 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1136,6 +1136,112 @@ } } }, + "/api/provisioning/contact-points": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Get all the contact points.", + "operationId": "RouteGetContactpoints", + "responses": { + "200": { + "description": "Route", + "schema": { + "$ref": "#/definitions/Route" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning" + ], + "summary": "Update an existing contact point.", + "operationId": "RoutePutContactpoints", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/EmbeddedContactPoint" + } + } + ], + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning" + ], + "summary": "Create a contact point.", + "operationId": "RoutePostContactpoints", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/EmbeddedContactPoint" + } + } + ], + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, + "/api/provisioning/contact-points/{ID}": { + "delete": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning" + ], + "summary": "Delete a contact point.", + "operationId": "RouteDeleteContactpoints", + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, "/api/provisioning/policies": { "get": { "tags": [ @@ -2369,6 +2475,36 @@ }, "x-go-package": "github.com/prometheus/alertmanager/config" }, + "EmbeddedContactPoint": { + "description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.", + "type": "object", + "properties": { + "disableResolveMessage": { + "type": "boolean", + "x-go-name": "DisableResolveMessage" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "provanance": { + "type": "string", + "x-go-name": "Provenance" + }, + "settings": { + "$ref": "#/definitions/Json" + }, + "type": { + "type": "string", + "x-go-name": "Type" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, "ErrorType": { "type": "string", "title": "ErrorType models the different API error types.", @@ -4848,6 +4984,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroup": { + "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -4870,17 +5007,14 @@ "$ref": "#/definitions/receiver" } }, - "x-go-name": "AlertGroup", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/alertGroup" }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" }, - "x-go-name": "AlertGroups", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/alertGroups" }, "alertStatus": { @@ -5073,6 +5207,7 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -5125,17 +5260,14 @@ "x-go-name": "UpdatedAt" } }, - "x-go-name": "GettableSilence", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" }, - "x-go-name": "GettableSilences", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableSilences" }, "labelSet": { @@ -5265,7 +5397,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { - "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", @@ -5306,6 +5437,8 @@ "x-go-name": "StartsAt" } }, + "x-go-name": "PostableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/postableSilence" }, "receiver": { diff --git a/pkg/services/ngalert/models/provisioning.go b/pkg/services/ngalert/models/provisioning.go index b3ad9612ef5..c3655edfc5b 100644 --- a/pkg/services/ngalert/models/provisioning.go +++ b/pkg/services/ngalert/models/provisioning.go @@ -3,8 +3,10 @@ package models type Provenance string const ( + // ProvenanceNone reflects the provenance when no provenance is stored + // for the requested object in the database. ProvenanceNone Provenance = "" - ProvenanceApi Provenance = "api" + ProvenanceAPI Provenance = "api" ProvenanceFile Provenance = "file" ) diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index fe070adf47a..9d03ecfe9cd 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -139,6 +139,7 @@ func (ng *AlertNG) init() error { // Provisioning policyService := provisioning.NewNotificationPolicyService(store, store, store, ng.Log) + contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log) api := api.API{ Cfg: ng.Cfg, @@ -158,6 +159,7 @@ func (ng *AlertNG) init() error { StateManager: ng.stateManager, AccessControl: ng.accesscontrol, Policies: policyService, + ContactPointService: contactPointService, } api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) diff --git a/pkg/services/ngalert/provisioning/contactpoints.go b/pkg/services/ngalert/provisioning/contactpoints.go new file mode 100644 index 00000000000..00b1b60aba7 --- /dev/null +++ b/pkg/services/ngalert/provisioning/contactpoints.go @@ -0,0 +1,402 @@ +package provisioning + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "sort" + + "github.com/grafana/grafana/pkg/infra/log" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/util" + "github.com/prometheus/alertmanager/config" +) + +type ContactPointService struct { + amStore AMConfigStore + encryptionService secrets.Service + provenanceStore ProvisioningStore + xact TransactionManager + log log.Logger +} + +func NewContactPointService(store store.AlertingStore, encryptionService secrets.Service, + provenanceStore ProvisioningStore, xact TransactionManager, log log.Logger) *ContactPointService { + return &ContactPointService{ + amStore: store, + encryptionService: encryptionService, + provenanceStore: provenanceStore, + xact: xact, + log: log, + } +} + +func (ecp *ContactPointService) GetContactPoints(ctx context.Context, orgID int64) ([]apimodels.EmbeddedContactPoint, error) { + cfg, _, err := ecp.getCurrentConfig(ctx, orgID) + if err != nil { + return nil, err + } + provenances, err := ecp.provenanceStore.GetProvenances(ctx, orgID, "contactPoint") + if err != nil { + return nil, err + } + contactPoints := []apimodels.EmbeddedContactPoint{} + for _, contactPoint := range cfg.GetGrafanaReceiverMap() { + embeddedContactPoint := apimodels.EmbeddedContactPoint{ + UID: contactPoint.UID, + Type: contactPoint.Type, + Name: contactPoint.Name, + DisableResolveMessage: contactPoint.DisableResolveMessage, + Settings: contactPoint.Settings, + } + if val, exists := provenances[embeddedContactPoint.UID]; exists && val != "" { + embeddedContactPoint.Provenance = string(val) + } + for k, v := range contactPoint.SecureSettings { + decryptedValue, err := ecp.decryptValue(v) + if err != nil { + ecp.log.Warn("decrypting value failed", "err", err.Error()) + continue + } + if decryptedValue == "" { + continue + } + embeddedContactPoint.Settings.Set(k, apimodels.RedactedValue) + } + contactPoints = append(contactPoints, embeddedContactPoint) + } + sort.SliceStable(contactPoints, func(i, j int) bool { + return contactPoints[i].Name < contactPoints[j].Name + }) + return contactPoints, nil +} + +// internal only +func (ecp *ContactPointService) getContactPointDecrypted(ctx context.Context, orgID int64, uid string) (apimodels.EmbeddedContactPoint, error) { + cfg, _, err := ecp.getCurrentConfig(ctx, orgID) + if err != nil { + return apimodels.EmbeddedContactPoint{}, err + } + for _, receiver := range cfg.GetGrafanaReceiverMap() { + if receiver.UID != uid { + continue + } + embeddedContactPoint := apimodels.EmbeddedContactPoint{ + UID: receiver.UID, + Type: receiver.Type, + Name: receiver.Name, + DisableResolveMessage: receiver.DisableResolveMessage, + Settings: receiver.Settings, + } + for k, v := range receiver.SecureSettings { + decryptedValue, err := ecp.decryptValue(v) + if err != nil { + ecp.log.Warn("decrypting value failed", "err", err.Error()) + continue + } + if decryptedValue == "" { + continue + } + embeddedContactPoint.Settings.Set(k, decryptedValue) + } + return embeddedContactPoint, nil + } + return apimodels.EmbeddedContactPoint{}, fmt.Errorf("contact point with uid '%s' not found", uid) +} + +func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID int64, + contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) (apimodels.EmbeddedContactPoint, error) { + if err := contactPoint.Valid(ecp.encryptionService.GetDecryptedValue); err != nil { + return apimodels.EmbeddedContactPoint{}, fmt.Errorf("contact point is not valid: %w", err) + } + + cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID) + if err != nil { + return apimodels.EmbeddedContactPoint{}, err + } + + extractedSecrets, err := contactPoint.ExtractSecrets() + if err != nil { + return apimodels.EmbeddedContactPoint{}, err + } + + for k, v := range extractedSecrets { + encryptedValue, err := ecp.encryptValue(v) + if err != nil { + return apimodels.EmbeddedContactPoint{}, err + } + extractedSecrets[k] = encryptedValue + } + + contactPoint.UID = util.GenerateShortUID() + grafanaReceiver := &apimodels.PostableGrafanaReceiver{ + UID: contactPoint.UID, + Name: contactPoint.Name, + Type: contactPoint.Type, + DisableResolveMessage: contactPoint.DisableResolveMessage, + Settings: contactPoint.Settings, + SecureSettings: extractedSecrets, + } + + receiverFound := false + for _, receiver := range cfg.AlertmanagerConfig.Receivers { + if receiver.Name == contactPoint.Name { + receiver.PostableGrafanaReceivers.GrafanaManagedReceivers = append(receiver.PostableGrafanaReceivers.GrafanaManagedReceivers, grafanaReceiver) + receiverFound = true + } + } + + if !receiverFound { + cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers, &apimodels.PostableApiReceiver{ + Receiver: config.Receiver{ + Name: grafanaReceiver.Name, + }, + PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{grafanaReceiver}, + }, + }) + } + + data, err := json.Marshal(cfg) + if err != nil { + return apimodels.EmbeddedContactPoint{}, err + } + + err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error { + err = ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: string(data), + FetchedConfigurationHash: fetchedHash, + ConfigurationVersion: "v1", + Default: false, + OrgID: orgID, + }) + if err != nil { + return err + } + adapter := provenanceOrgAdapter{ + inner: &contactPoint, + orgID: orgID, + } + err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance) + if err != nil { + return err + } + contactPoint.Provenance = string(provenance) + return nil + }) + if err != nil { + return apimodels.EmbeddedContactPoint{}, err + } + for k := range extractedSecrets { + contactPoint.Settings.Set(k, apimodels.RedactedValue) + } + return contactPoint, nil +} + +func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) error { + // set all redacted values with the latest known value from the store + rawContactPoint, err := ecp.getContactPointDecrypted(ctx, orgID, contactPoint.UID) + if err != nil { + return err + } + secretKeys, err := contactPoint.SecretKeys() + if err != nil { + return err + } + for _, secretKey := range secretKeys { + secretValue := contactPoint.Settings.Get(secretKey).MustString() + if secretValue == apimodels.RedactedValue { + contactPoint.Settings.Set(secretKey, rawContactPoint.Settings.Get(secretKey).MustString()) + } + } + // validate merged values + if err := contactPoint.Valid(ecp.encryptionService.GetDecryptedValue); err != nil { + return err + } + // check that provenance is not changed in a invalid way + storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, provenanceOrgAdapter{ + inner: &contactPoint, + orgID: orgID, + }) + if err != nil { + return err + } + if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { + return fmt.Errorf("cannot changed provenance from '%s' to '%s'", storedProvenance, provenance) + } + // transform to internal model + extractedSecrets, err := contactPoint.ExtractSecrets() + if err != nil { + return err + } + for k, v := range extractedSecrets { + encryptedValue, err := ecp.encryptValue(v) + if err != nil { + return err + } + extractedSecrets[k] = encryptedValue + } + mergedReceiver := &apimodels.PostableGrafanaReceiver{ + UID: contactPoint.UID, + Name: contactPoint.Name, + Type: contactPoint.Type, + DisableResolveMessage: contactPoint.DisableResolveMessage, + Settings: contactPoint.Settings, + SecureSettings: extractedSecrets, + } + // save to store + cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID) + if err != nil { + return err + } + for _, receiver := range cfg.AlertmanagerConfig.Receivers { + if receiver.Name == contactPoint.Name { + receiverNotFound := true + for i, grafanaReceiver := range receiver.GrafanaManagedReceivers { + if grafanaReceiver.UID == mergedReceiver.UID { + receiverNotFound = false + receiver.GrafanaManagedReceivers[i] = mergedReceiver + break + } + } + if receiverNotFound { + return fmt.Errorf("contact point with uid '%s' not found", mergedReceiver.UID) + } + } + } + data, err := json.Marshal(cfg) + if err != nil { + return err + } + return ecp.xact.InTransaction(ctx, func(ctx context.Context) error { + err = ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: string(data), + FetchedConfigurationHash: fetchedHash, + ConfigurationVersion: "v1", + Default: false, + OrgID: orgID, + }) + if err != nil { + return err + } + adapter := provenanceOrgAdapter{ + inner: &contactPoint, + orgID: orgID, + } + err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance) + if err != nil { + return err + } + contactPoint.Provenance = string(provenance) + return nil + }) +} + +func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID int64, uid string) error { + cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID) + if err != nil { + return err + } + // Indicates if the full contact point is removed or just one of the + // configurations, as a contactpoint can consist of any number of + // configurations. + fullRemoval := false + // Name of the contact point that will be removed, might be used if a + // full removal is done to check if it's referenced in any route. + name := "" + for i, receiver := range cfg.AlertmanagerConfig.Receivers { + for j, grafanaReceiver := range receiver.GrafanaManagedReceivers { + if grafanaReceiver.UID == uid { + name = grafanaReceiver.Name + receiver.GrafanaManagedReceivers = append(receiver.GrafanaManagedReceivers[:j], receiver.GrafanaManagedReceivers[j+1:]...) + // if this was the last receiver we removed, we remove the whole receiver + if len(receiver.GrafanaManagedReceivers) == 0 { + fullRemoval = true + cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers[:i], cfg.AlertmanagerConfig.Receivers[i+1:]...) + } + break + } + } + } + if fullRemoval && isContactPointInUse(name, []*apimodels.Route{cfg.AlertmanagerConfig.Route}) { + return fmt.Errorf("contact point '%s' is currently used by a notification policy", name) + } + data, err := json.Marshal(cfg) + if err != nil { + return err + } + return ecp.xact.InTransaction(ctx, func(ctx context.Context) error { + err := ecp.provenanceStore.DeleteProvenance(ctx, provenanceOrgAdapter{ + inner: &apimodels.EmbeddedContactPoint{ + UID: uid, + }, + orgID: orgID, + }) + if err != nil { + return err + } + return ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: string(data), + FetchedConfigurationHash: fetchedHash, + ConfigurationVersion: "v1", + Default: false, + OrgID: orgID, + }) + }) +} + +func (ecp *ContactPointService) getCurrentConfig(ctx context.Context, orgID int64) (*apimodels.PostableUserConfig, string, error) { + query := &models.GetLatestAlertmanagerConfigurationQuery{ + OrgID: orgID, + } + err := ecp.amStore.GetLatestAlertmanagerConfiguration(ctx, query) + if err != nil { + return nil, "", err + } + cfg, err := DeserializeAlertmanagerConfig([]byte(query.Result.AlertmanagerConfiguration)) + if err != nil { + return nil, "", err + } + return cfg, query.Result.ConfigurationHash, nil +} + +func isContactPointInUse(name string, routes []*apimodels.Route) bool { + if len(routes) == 0 { + return false + } + for _, route := range routes { + if route.Receiver == name { + return true + } + if isContactPointInUse(name, route.Routes) { + return true + } + } + return false +} + +func (ecp *ContactPointService) decryptValue(value string) (string, error) { + decodeValue, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return "", err + } + + decryptedValue, err := ecp.encryptionService.Decrypt(context.Background(), decodeValue) + if err != nil { + return "", err + } + + return string(decryptedValue), nil +} + +func (ecp *ContactPointService) encryptValue(value string) (string, error) { + encryptedData, err := ecp.encryptionService.Encrypt(context.Background(), []byte(value), secrets.WithoutScope()) + if err != nil { + return "", fmt.Errorf("failed to encrypt secure settings: %w", err) + } + return base64.StdEncoding.EncodeToString(encryptedData), nil +} diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go new file mode 100644 index 00000000000..cb70fe05baa --- /dev/null +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -0,0 +1,195 @@ +package provisioning + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/secrets/database" + "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" +) + +func TestContactPointService(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) + t.Run("service gets contact points from AM config", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + + cps, err := sut.GetContactPoints(context.Background(), 1) + require.NoError(t, err) + + require.Len(t, cps, 1) + require.Equal(t, "email receiver", cps[0].Name) + }) + + t.Run("service stitches contact point into org's AM config", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + + _, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + require.NoError(t, err) + + cps, err := sut.GetContactPoints(context.Background(), 1) + require.NoError(t, err) + require.Len(t, cps, 2) + require.Equal(t, "test-contact-point", cps[1].Name) + require.Equal(t, "slack", cps[1].Type) + }) + + t.Run("default provenance of contact points is none", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + + cps, err := sut.GetContactPoints(context.Background(), 1) + require.NoError(t, err) + + require.Equal(t, models.ProvenanceNone, models.Provenance(cps[0].Provenance)) + }) + + t.Run("it's possible to update provenance from none to API", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + + newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceNone) + require.NoError(t, err) + + cps, err := sut.GetContactPoints(context.Background(), 1) + require.NoError(t, err) + require.Equal(t, newCp.UID, cps[1].UID) + require.Equal(t, models.ProvenanceNone, models.Provenance(cps[1].Provenance)) + + err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + require.NoError(t, err) + + cps, err = sut.GetContactPoints(context.Background(), 1) + require.NoError(t, err) + require.Equal(t, newCp.UID, cps[1].UID) + require.Equal(t, models.ProvenanceAPI, models.Provenance(cps[1].Provenance)) + }) + + t.Run("it's possible to update provenance from none to File", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + + newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceNone) + require.NoError(t, err) + + cps, err := sut.GetContactPoints(context.Background(), 1) + require.NoError(t, err) + require.Equal(t, newCp.UID, cps[1].UID) + require.Equal(t, models.ProvenanceNone, models.Provenance(cps[1].Provenance)) + + err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile) + require.NoError(t, err) + + cps, err = sut.GetContactPoints(context.Background(), 1) + require.NoError(t, err) + require.Equal(t, newCp.UID, cps[1].UID) + require.Equal(t, models.ProvenanceFile, models.Provenance(cps[1].Provenance)) + }) + + t.Run("it's not possible to update provenance from File to API", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + + newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile) + require.NoError(t, err) + + cps, err := sut.GetContactPoints(context.Background(), 1) + require.NoError(t, err) + require.Equal(t, newCp.UID, cps[1].UID) + require.Equal(t, models.ProvenanceFile, models.Provenance(cps[1].Provenance)) + + err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + require.Error(t, err) + }) + + t.Run("it's not possible to update provenance from API to File", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + + newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + require.NoError(t, err) + + cps, err := sut.GetContactPoints(context.Background(), 1) + require.NoError(t, err) + require.Equal(t, newCp.UID, cps[1].UID) + require.Equal(t, models.ProvenanceAPI, models.Provenance(cps[1].Provenance)) + + err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile) + require.Error(t, err) + }) + + t.Run("service respects concurrency token when updating", func(t *testing.T) { + sut := createContactPointServiceSut(secretsService) + newCp := createTestContactPoint() + q := models.GetLatestAlertmanagerConfigurationQuery{ + OrgID: 1, + } + err := sut.amStore.GetLatestAlertmanagerConfiguration(context.Background(), &q) + require.NoError(t, err) + expectedConcurrencyToken := q.Result.ConfigurationHash + + _, err = sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + require.NoError(t, err) + + fake := sut.amStore.(*fakeAMConfigStore) + intercepted := fake.lastSaveCommand + require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash) + }) +} + +func TestContactPointInUse(t *testing.T) { + result := isContactPointInUse("test", []*definitions.Route{ + { + Receiver: "not-test", + Routes: []*definitions.Route{ + { + Receiver: "not-test", + }, + { + Receiver: "test", + }, + }, + }, + }) + require.True(t, result) + result = isContactPointInUse("test", []*definitions.Route{ + { + Receiver: "not-test", + Routes: []*definitions.Route{ + { + Receiver: "not-test", + }, + { + Receiver: "not-test", + }, + }, + }, + }) + require.False(t, result) +} + +func createContactPointServiceSut(secretService secrets.Service) *ContactPointService { + return &ContactPointService{ + amStore: newFakeAMConfigStore(), + provenanceStore: newFakeProvisioningStore(), + xact: newNopTransactionManager(), + encryptionService: secretService, + log: log.NewNopLogger(), + } +} + +func createTestContactPoint() definitions.EmbeddedContactPoint { + settings, _ := simplejson.NewJson([]byte(`{"recipient":"value_recipient","token":"value_token"}`)) + return definitions.EmbeddedContactPoint{ + Name: "test-contact-point", + Type: "slack", + Settings: settings, + } +} diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index 3df52d4bc81..2a27b6b3554 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -45,12 +45,12 @@ func TestNotificationPolicyService(t *testing.T) { sut := createNotificationPolicyServiceSut() newRoute := createTestRoutingTree() - err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceApi) + err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceAPI) require.NoError(t, err) updated, err := sut.GetPolicyTree(context.Background(), 1) require.NoError(t, err) - require.Equal(t, models.ProvenanceApi, updated.Provenance) + require.Equal(t, models.ProvenanceAPI, updated.Provenance) }) t.Run("service respects concurrency token when updating", func(t *testing.T) { @@ -63,7 +63,7 @@ func TestNotificationPolicyService(t *testing.T) { require.NoError(t, err) expectedConcurrencyToken := q.Result.ConfigurationHash - err = sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceApi) + err = sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceAPI) require.NoError(t, err) fake := sut.GetAMConfigStore().(*fakeAMConfigStore) diff --git a/pkg/services/ngalert/provisioning/persist.go b/pkg/services/ngalert/provisioning/persist.go index 2a550db327f..eb428055a86 100644 --- a/pkg/services/ngalert/provisioning/persist.go +++ b/pkg/services/ngalert/provisioning/persist.go @@ -15,7 +15,9 @@ type AMConfigStore interface { // ProvisioningStore is a store of provisioning data for arbitrary objects. type ProvisioningStore interface { GetProvenance(ctx context.Context, o models.Provisionable) (models.Provenance, error) + GetProvenances(ctx context.Context, orgID int64, resourceType string) (map[string]models.Provenance, error) SetProvenance(ctx context.Context, o models.Provisionable, p models.Provenance) error + DeleteProvenance(ctx context.Context, o models.Provisionable) error } // TransactionManager represents the ability to issue and close transactions through contexts. diff --git a/pkg/services/ngalert/provisioning/testing.go b/pkg/services/ngalert/provisioning/testing.go index 12c54cc02c7..aba31dfd513 100644 --- a/pkg/services/ngalert/provisioning/testing.go +++ b/pkg/services/ngalert/provisioning/testing.go @@ -4,6 +4,7 @@ import ( "context" "crypto/md5" "fmt" + "strings" "github.com/grafana/grafana/pkg/services/ngalert/models" ) @@ -99,19 +100,39 @@ func newFakeProvisioningStore() *fakeProvisioningStore { func (f *fakeProvisioningStore) GetProvenance(ctx context.Context, o models.Provisionable) (models.Provenance, error) { if val, ok := f.records[o.ResourceOrgID()]; ok { - if prov, ok := val[o.ResourceID()]; ok { + if prov, ok := val[o.ResourceID()+o.ResourceType()]; ok { return prov, nil } } return models.ProvenanceNone, nil } +func (f *fakeProvisioningStore) GetProvenances(ctx context.Context, orgID int64, resourceType string) (map[string]models.Provenance, error) { + results := make(map[string]models.Provenance) + if val, ok := f.records[orgID]; ok { + for k, v := range val { + if strings.HasSuffix(k, resourceType) { + results[strings.TrimSuffix(k, resourceType)] = v + } + } + } + return results, nil +} + func (f *fakeProvisioningStore) SetProvenance(ctx context.Context, o models.Provisionable, p models.Provenance) error { orgID := o.ResourceOrgID() if _, ok := f.records[orgID]; !ok { f.records[orgID] = map[string]models.Provenance{} } - f.records[orgID][o.ResourceID()] = p + _ = f.DeleteProvenance(ctx, o) // delete old entries first + f.records[orgID][o.ResourceID()+o.ResourceType()] = p + return nil +} + +func (f *fakeProvisioningStore) DeleteProvenance(ctx context.Context, o models.Provisionable) error { + if val, ok := f.records[o.ResourceOrgID()]; ok { + delete(val, o.ResourceID()+o.ResourceType()) + } return nil } diff --git a/pkg/services/ngalert/store/provisioning_store.go b/pkg/services/ngalert/store/provisioning_store.go index 56c5d157606..6710b702a84 100644 --- a/pkg/services/ngalert/store/provisioning_store.go +++ b/pkg/services/ngalert/store/provisioning_store.go @@ -45,6 +45,23 @@ func (st DBstore) GetProvenance(ctx context.Context, o models.Provisionable) (mo return provenance, nil } +// GetProvenance gets the provenance status for a provisionable object. +func (st DBstore) GetProvenances(ctx context.Context, orgID int64, resourceType string) (map[string]models.Provenance, error) { + resultMap := make(map[string]models.Provenance) + err := st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + filter := "record_type = ? AND org_id = ?" + rawData, err := sess.Table(provenanceRecord{}).Where(filter, resourceType, orgID).Desc("id").Cols("record_key", "provenance").QueryString() + if err != nil { + return fmt.Errorf("failed to query for existing provenance status: %w", err) + } + for _, data := range rawData { + resultMap[data["record_key"]] = models.Provenance(data["provenance"]) + } + return nil + }) + return resultMap, err +} + // SetProvenance changes the provenance status for a provisionable object. func (st DBstore) SetProvenance(ctx context.Context, o models.Provisionable, p models.Provenance) error { recordType := o.ResourceType() @@ -76,3 +93,15 @@ func (st DBstore) SetProvenance(ctx context.Context, o models.Provisionable, p m return nil }) } + +// DeleteProvenance deletes the provenance record from the table +func (st DBstore) DeleteProvenance(ctx context.Context, o models.Provisionable) error { + return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + _, err := sess.Delete(provenanceRecord{ + RecordKey: o.ResourceID(), + RecordType: o.ResourceType(), + OrgID: o.ResourceOrgID(), + }) + return err + }) +} diff --git a/pkg/services/ngalert/store/provisioning_store_test.go b/pkg/services/ngalert/store/provisioning_store_test.go index 815b799f080..886ca7f6884 100644 --- a/pkg/services/ngalert/store/provisioning_store_test.go +++ b/pkg/services/ngalert/store/provisioning_store_test.go @@ -73,16 +73,58 @@ func TestProvisioningStore(t *testing.T) { err = store.SetProvenance(context.Background(), &ruleOrg3, models.ProvenanceFile) require.NoError(t, err) - err = store.SetProvenance(context.Background(), &ruleOrg2, models.ProvenanceApi) + err = store.SetProvenance(context.Background(), &ruleOrg2, models.ProvenanceAPI) require.NoError(t, err) p, err := store.GetProvenance(context.Background(), &ruleOrg2) require.NoError(t, err) - require.Equal(t, models.ProvenanceApi, p) + require.Equal(t, models.ProvenanceAPI, p) p, err = store.GetProvenance(context.Background(), &ruleOrg3) require.NoError(t, err) require.Equal(t, models.ProvenanceFile, p) }) + + t.Run("Store should return all provenances by type", func(t *testing.T) { + const orgID = 123 + ruleOrg1 := models.AlertRule{ + UID: "789", + OrgID: orgID, + } + ruleOrg2 := models.AlertRule{ + UID: "790", + OrgID: orgID, + } + err := store.SetProvenance(context.Background(), &ruleOrg1, models.ProvenanceFile) + require.NoError(t, err) + err = store.SetProvenance(context.Background(), &ruleOrg2, models.ProvenanceAPI) + require.NoError(t, err) + + p, err := store.GetProvenances(context.Background(), orgID, ruleOrg1.ResourceType()) + require.NoError(t, err) + require.Len(t, p, 2) + require.Equal(t, models.ProvenanceFile, p[ruleOrg1.UID]) + require.Equal(t, models.ProvenanceAPI, p[ruleOrg2.UID]) + }) + + t.Run("Store should delete provenance correctly", func(t *testing.T) { + const orgID = 1234 + ruleOrg := models.AlertRule{ + UID: "7834539", + OrgID: orgID, + } + err := store.SetProvenance(context.Background(), &ruleOrg, models.ProvenanceFile) + require.NoError(t, err) + p, err := store.GetProvenance(context.Background(), &ruleOrg) + require.NoError(t, err) + require.Equal(t, models.ProvenanceFile, p) + + err = store.DeleteProvenance(context.Background(), &ruleOrg) + require.NoError(t, err) + + p, err = store.GetProvenance(context.Background(), &ruleOrg) + require.NoError(t, err) + require.Equal(t, models.ProvenanceNone, p) + }) } func createProvisioningStoreSut(_ *ngalert.AlertNG, db *store.DBstore) provisioning.ProvisioningStore { diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index ad4e70e32b2..38428d00ca5 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -126,6 +126,98 @@ func TestProvisioning(t *testing.T) { require.Equal(t, 202, resp.StatusCode) }) + t.Run("admin POST should succeed", func(t *testing.T) { + req := createTestRequest("POST", url, "admin", body) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 202, resp.StatusCode) + }) + }) + t.Run("when provisioning contactpoints", func(t *testing.T) { + url := fmt.Sprintf("http://%s/api/provisioning/contact-points", grafanaListedAddr) + body := ` + { + "name": "my-contact-point", + "type": "slack", + "settings": { + "recipient": "value_recipient", + "token": "value_token" + } + }` + + t.Run("un-authenticated GET should 401", func(t *testing.T) { + req := createTestRequest("GET", url, "", "") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 401, resp.StatusCode) + }) + + t.Run("viewer GET should succeed", func(t *testing.T) { + req := createTestRequest("GET", url, "viewer", "") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 200, resp.StatusCode) + }) + + t.Run("editor GET should succeed", func(t *testing.T) { + req := createTestRequest("GET", url, "editor", "") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 200, resp.StatusCode) + }) + + t.Run("admin GET should succeed", func(t *testing.T) { + req := createTestRequest("GET", url, "admin", "") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 200, resp.StatusCode) + }) + + t.Run("un-authenticated POST should 401", func(t *testing.T) { + req := createTestRequest("POST", url, "", body) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 401, resp.StatusCode) + }) + + t.Run("viewer POST should 403", func(t *testing.T) { + req := createTestRequest("POST", url, "viewer", body) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 403, resp.StatusCode) + }) + + t.Run("editor POST should succeed", func(t *testing.T) { + req := createTestRequest("POST", url, "editor", body) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 202, resp.StatusCode) + }) + t.Run("admin POST should succeed", func(t *testing.T) { req := createTestRequest("POST", url, "admin", body) From 694a7e73dcc6ba0d0c36b7a4fa2a584f3d47856b Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 13 Apr 2022 17:47:53 -0300 Subject: [PATCH 081/121] notification template docs updated (#47744) --- .../alerting/old-alerting/add-notification-template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/alerting/old-alerting/add-notification-template.md b/docs/sources/alerting/old-alerting/add-notification-template.md index bd952bce72f..bafab9a4f9e 100644 --- a/docs/sources/alerting/old-alerting/add-notification-template.md +++ b/docs/sources/alerting/old-alerting/add-notification-template.md @@ -9,9 +9,9 @@ aliases = ["/docs/grafana/latest/alerting/add-notification-template/"] You can provide detailed information to alert notification recipients by injecting alert query data into an alert notification. This topic explains how you can use alert query labels in alert notifications. -Labels that exist from the evaluation of the alert query can be used in the alert rule name and in the alert notification message fields. The alert label data is injected into the notification fields when the alert is evaluated and the notification is about to be sent. When there are multiple unique values for the same label, the values are comma-separated. +You can use labels generated during an alerting query evaluation to create alert notification messages. For multiple unique values for the same label, the values are comma-separated. -When the alert is in `firing` state, the data used to parse the templates corresponds to the series that are violating the alert condition at the moment the notification is sent. For `resolved` alert notifications, all series are used since there are none that are violating the condition. +When an alert fires, the alerting data series indicates the violation. For resolved alerts, all data series are included in the resolved notification. This topic explains how you can use alert query labels in alert notifications. From c266a4ac8143c343953b620eab7449b24a4a4240 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Wed, 13 Apr 2022 19:31:57 -0500 Subject: [PATCH 082/121] Alerting: Remove mis-behaving fake and fix masked test failure in AM config API (#47747) * Remove misbehaving fake * Fix bug and inject logger --- pkg/services/ngalert/api/api_alertmanager.go | 6 +++-- .../ngalert/api/api_alertmanager_test.go | 13 ++++++---- pkg/services/ngalert/api/testing.go | 26 ------------------- 3 files changed, 12 insertions(+), 33 deletions(-) diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index 047a637c32c..c23cfc9985f 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -57,10 +57,12 @@ func (srv AlertmanagerSrv) loadSecureSettings(ctx context.Context, orgId int64, currentReceiverMap := make(map[string]*apimodels.PostableGrafanaReceiver) if query.Result != nil { currentConfig, err := notifier.Load([]byte(query.Result.AlertmanagerConfiguration)) + // If the current config is un-loadable, treat it as if it never existed. Providing a new, valid config should be able to "fix" this state. if err != nil { - return fmt.Errorf("failed to load latest configuration: %w", err) + srv.log.Warn("Last known alertmanager configuration was invalid. Overwriting...") + } else { + currentReceiverMap = currentConfig.GetGrafanaReceiverMap() } - currentReceiverMap = currentConfig.GetGrafanaReceiverMap() } // Copy the previously known secure settings diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index 13943adc9ed..75ee2176024 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -346,15 +346,18 @@ func createSut(t *testing.T, accessControl accesscontrol.AccessControl) Alertman t.Helper() mam := createMultiOrgAlertmanager(t) - store := newFakeAlertingStore(t) - store.Setup(1) - store.Setup(2) - store.Setup(3) + configs := map[int64]*ngmodels.AlertConfiguration{ + 1: {AlertmanagerConfiguration: validConfig, OrgID: 1}, + 2: {AlertmanagerConfiguration: validConfig, OrgID: 2}, + 3: {AlertmanagerConfiguration: brokenConfig, OrgID: 3}, + } + configStore := notifier.NewFakeConfigStore(t, configs) secrets := fakes.NewFakeSecretsService() if accessControl == nil { accessControl = acMock.New().WithDisabled() } - return AlertmanagerSrv{mam: mam, store: store, secrets: secrets, ac: accessControl} + log := log.NewNopLogger() + return AlertmanagerSrv{mam: mam, store: &configStore, secrets: secrets, ac: accessControl, log: log} } func createAmConfigRequest(t *testing.T) apimodels.PostableUserConfig { diff --git a/pkg/services/ngalert/api/testing.go b/pkg/services/ngalert/api/testing.go index 26344d66b64..68504135781 100644 --- a/pkg/services/ngalert/api/testing.go +++ b/pkg/services/ngalert/api/testing.go @@ -1,43 +1,17 @@ package api import ( - "context" "fmt" "sync" "testing" "time" "github.com/grafana/grafana/pkg/services/ngalert/eval" - "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" - "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana-plugin-sdk-go/data" ) -type FakeAlertingStore struct { - orgsWithConfig map[int64]bool -} - -func newFakeAlertingStore(t *testing.T) FakeAlertingStore { - t.Helper() - - return FakeAlertingStore{ - orgsWithConfig: map[int64]bool{}, - } -} - -func (f FakeAlertingStore) Setup(orgID int64) { - f.orgsWithConfig[orgID] = true -} - -func (f FakeAlertingStore) GetLatestAlertmanagerConfiguration(_ context.Context, query *models.GetLatestAlertmanagerConfigurationQuery) error { - if _, ok := f.orgsWithConfig[query.OrgID]; ok { - return nil - } - return store.ErrNoAlertmanagerConfiguration -} - type fakeAlertInstanceManager struct { mtx sync.Mutex // orgID -> RuleID -> States From 17e44c306cf24868c570d0d6d22da3caeaca2998 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 13 Apr 2022 20:45:30 -0700 Subject: [PATCH 083/121] Heatmap: hover legend regardless of tooltip state (#47600) --- .../features/transformers/calculateHeatmap/heatmap.ts | 4 +++- .../plugins/panel/heatmap-new/HeatmapHoverView.tsx | 10 ++++++---- public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx | 9 ++++----- public/app/plugins/panel/heatmap-new/fields.ts | 11 +++++++++++ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index bac9bda5086..a714be39029 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -220,7 +220,9 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa name: 'count', type: FieldType.number, values: new ArrayVector(heat2d.count), - config: {}, + config: { + unit: 'short', // always integer + }, }, ], }; diff --git a/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx index de0f990dd25..8f83d59879f 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import { Field, FieldType, formattedValueToString, LinkModel } from '@grafana/data'; +import { Field, FieldType, formattedValueToString, getFieldDisplayName, LinkModel } from '@grafana/data'; import { HeatmapHoverEvent } from './utils'; import { BucketLayout, HeatmapData } from './fields'; @@ -26,7 +26,7 @@ export const HeatmapHoverView = ({ data, hover, showHistogram }: Props) => { const dashboard = getDashboardSrv().getCurrent(); return dashboard?.formatDate(v, tooltipTimeFormat); } - return `${v}XX`; + return `${v}`; }; const xVals = xField?.values.toArray(); @@ -45,7 +45,7 @@ export const HeatmapHoverView = ({ data, hover, showHistogram }: Props) => { if (yField?.display) { return formattedValueToString(yField.display(v)); } - return `${v}YYY`; + return `${v}`; }; } @@ -170,7 +170,9 @@ export const HeatmapHoverView = ({ data, hover, showHistogram }: Props) => {
    Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)}
    -
    Count: {count}
    +
    + {getFieldDisplayName(countField!, data.heatmap)}: {count} +
    {links.length > 0 && ( diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx index ec2b798879f..12c0e7a2816 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { css } from '@emotion/css'; -import { formattedValueToString, GrafanaTheme2, PanelProps, reduceField, ReducerID, TimeRange } from '@grafana/data'; +import { GrafanaTheme2, PanelProps, reduceField, ReducerID, TimeRange } from '@grafana/data'; import { Portal, UPlotChart, @@ -82,7 +82,7 @@ export const HeatmapPanel: React.FC = ({ return prepConfig({ dataRef, theme, - onhover: options.tooltip.show ? onhover : null, + onhover: onhover, onclick: options.tooltip.show ? onclick : null, onzoom: (evt) => { onChangeTimeRange({ from: evt.xMin, to: evt.xMax }); @@ -104,7 +104,6 @@ export const HeatmapPanel: React.FC = ({ const field = info.heatmap.fields[2]; const { min, max } = reduceField({ field, reducers: [ReducerID.min, ReducerID.max] }); - const display = field.display ? (v: number) => formattedValueToString(field.display!(v)) : (v: number) => `${v}`; let hoverValue: number | undefined = undefined; if (hover && info.heatmap.fields) { @@ -114,7 +113,7 @@ export const HeatmapPanel: React.FC = ({ return ( - + ); }; @@ -133,7 +132,7 @@ export const HeatmapPanel: React.FC = ({ )} - {hover && ( + {hover && options.tooltip.show && ( string; + // Errors warning?: string; } @@ -84,6 +89,7 @@ const getHeatmapData = (frame: DataFrame, theme: GrafanaTheme2): HeatmapData => return { heatmap: frame }; } + // Y field values (display is used in the axis) if (!frame.fields[1].display) { frame.fields[1].display = getDisplayProcessor({ field: frame.fields[1], theme }); } @@ -105,14 +111,19 @@ const getHeatmapData = (frame: DataFrame, theme: GrafanaTheme2): HeatmapData => let yBinIncr = ys[1] - ys[0]; let xBinIncr = xs[yBinQty] - xs[0]; + // The "count" field + const disp = frame.fields[2].display ?? getValueFormat('short'); return { heatmap: frame, xBucketSize: xBinIncr, yBucketSize: yBinIncr, xBucketCount: xBinQty, yBucketCount: yBinQty, + // TODO: improve heuristic xLayout: frame.fields[0].name === 'xMax' ? BucketLayout.le : BucketLayout.ge, yLayout: frame.fields[1].name === 'yMax' ? BucketLayout.le : BucketLayout.ge, + + display: (v) => formattedValueToString(disp(v)), }; }; From aceedb3a32b00d88d16c7542c0828ad97bc389c6 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 14 Apr 2022 09:33:41 +0200 Subject: [PATCH 084/121] Query history: Add migration endpoint (#47551) * Add endpoint for migration * Check for createdAt * Query history: Remove returning of dtos * Query history: Fix CreatedAt * Refactor based on suggestions * Insert into table in batches --- pkg/services/queryhistory/api.go | 16 +++ pkg/services/queryhistory/database.go | 59 +++++++++ pkg/services/queryhistory/models.go | 18 +++ pkg/services/queryhistory/queryhistory.go | 5 + .../queryhistory/queryhistory_migrate_test.go | 120 ++++++++++++++++++ 5 files changed, 218 insertions(+) create mode 100644 pkg/services/queryhistory/queryhistory_migrate_test.go diff --git a/pkg/services/queryhistory/api.go b/pkg/services/queryhistory/api.go index dc2245407f4..fd719cf7b9e 100644 --- a/pkg/services/queryhistory/api.go +++ b/pkg/services/queryhistory/api.go @@ -19,6 +19,8 @@ func (s *QueryHistoryService) registerAPIEndpoints() { entities.Post("/star/:uid", middleware.ReqSignedIn, routing.Wrap(s.starHandler)) entities.Delete("/star/:uid", middleware.ReqSignedIn, routing.Wrap(s.unstarHandler)) entities.Patch("/:uid", middleware.ReqSignedIn, routing.Wrap(s.patchCommentHandler)) + // Remove migrate endpoint in Grafana v10 as breaking change + entities.Post("/migrate", middleware.ReqSignedIn, routing.Wrap(s.migrateHandler)) }) } @@ -117,3 +119,17 @@ func (s *QueryHistoryService) unstarHandler(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, QueryHistoryResponse{Result: query}) } + +func (s *QueryHistoryService) migrateHandler(c *models.ReqContext) response.Response { + cmd := MigrateQueriesToQueryHistoryCommand{} + if err := web.Bind(c.Req, &cmd); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + + totalCount, starredCount, err := s.MigrateQueriesToQueryHistory(c.Req.Context(), c.SignedInUser, cmd) + if err != nil { + return response.Error(http.StatusInternalServerError, "Failed to migrate query history", err) + } + + return response.JSON(http.StatusOK, QueryHistoryMigrationResponse{Message: "Query history successfully migrated", TotalCount: totalCount, StarredCount: starredCount}) +} diff --git a/pkg/services/queryhistory/database.go b/pkg/services/queryhistory/database.go index edb16de741f..3f718a6b69a 100644 --- a/pkg/services/queryhistory/database.go +++ b/pkg/services/queryhistory/database.go @@ -3,6 +3,7 @@ package queryhistory import ( "context" "errors" + "fmt" "time" "github.com/grafana/grafana/pkg/models" @@ -265,3 +266,61 @@ func (s QueryHistoryService) unstarQuery(ctx context.Context, user *models.Signe return dto, nil } + +func (s QueryHistoryService) migrateQueries(ctx context.Context, user *models.SignedInUser, cmd MigrateQueriesToQueryHistoryCommand) (int, int, error) { + queryHistories := make([]*QueryHistory, 0, len(cmd.Queries)) + starredQueries := make([]*QueryHistoryStar, 0) + + err := s.SQLStore.WithTransactionalDbSession(ctx, func(session *sqlstore.DBSession) error { + for _, query := range cmd.Queries { + uid := util.GenerateShortUID() + queryHistories = append(queryHistories, &QueryHistory{ + OrgID: user.OrgId, + UID: uid, + Queries: query.Queries, + DatasourceUID: query.DatasourceUID, + CreatedBy: user.UserId, + CreatedAt: query.CreatedAt, + Comment: query.Comment, + }) + + if query.Starred { + starredQueries = append(starredQueries, &QueryHistoryStar{ + UserID: user.UserId, + QueryUID: uid, + }) + } + } + + batchSize := 50 + var err error + for i := 0; i < len(queryHistories); i += batchSize { + j := i + batchSize + if j > len(queryHistories) { + j = len(queryHistories) + } + _, err = session.InsertMulti(queryHistories[i:j]) + if err != nil { + return err + } + } + + for i := 0; i < len(starredQueries); i += batchSize { + j := i + batchSize + if j > len(starredQueries) { + j = len(starredQueries) + } + _, err = session.InsertMulti(starredQueries[i:j]) + if err != nil { + return err + } + } + return err + }) + + if err != nil { + return 0, 0, fmt.Errorf("failed to migrate query history: %w", err) + } + + return len(queryHistories), len(starredQueries), nil +} diff --git a/pkg/services/queryhistory/models.go b/pkg/services/queryhistory/models.go index 7dfc9de69a0..19d7d77ad5c 100644 --- a/pkg/services/queryhistory/models.go +++ b/pkg/services/queryhistory/models.go @@ -78,3 +78,21 @@ type DeleteQueryFromQueryHistoryResponse struct { ID int64 `json:"id"` Message string `json:"message"` } + +type MigrateQueriesToQueryHistoryCommand struct { + Queries []QueryToMigrate `json:"queries"` +} + +type QueryToMigrate struct { + DatasourceUID string `json:"datasourceUid"` + Queries *simplejson.Json `json:"queries"` + CreatedAt int64 `json:"createdAt"` + Comment string `json:"comment"` + Starred bool `json:"starred"` +} + +type QueryHistoryMigrationResponse struct { + Message string `json:"message"` + TotalCount int `json:"totalCount"` + StarredCount int `json:"starredCount"` +} diff --git a/pkg/services/queryhistory/queryhistory.go b/pkg/services/queryhistory/queryhistory.go index bd8c88ab826..c3c1290cb00 100644 --- a/pkg/services/queryhistory/queryhistory.go +++ b/pkg/services/queryhistory/queryhistory.go @@ -33,6 +33,7 @@ type Service interface { PatchQueryCommentInQueryHistory(ctx context.Context, user *models.SignedInUser, UID string, cmd PatchQueryCommentInQueryHistoryCommand) (QueryHistoryDTO, error) StarQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, UID string) (QueryHistoryDTO, error) UnstarQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, UID string) (QueryHistoryDTO, error) + MigrateQueriesToQueryHistory(ctx context.Context, user *models.SignedInUser, cmd MigrateQueriesToQueryHistoryCommand) (int, int, error) } type QueryHistoryService struct { @@ -65,3 +66,7 @@ func (s QueryHistoryService) StarQueryInQueryHistory(ctx context.Context, user * func (s QueryHistoryService) UnstarQueryInQueryHistory(ctx context.Context, user *models.SignedInUser, UID string) (QueryHistoryDTO, error) { return s.unstarQuery(ctx, user, UID) } + +func (s QueryHistoryService) MigrateQueriesToQueryHistory(ctx context.Context, user *models.SignedInUser, cmd MigrateQueriesToQueryHistoryCommand) (int, int, error) { + return s.migrateQueries(ctx, user, cmd) +} diff --git a/pkg/services/queryhistory/queryhistory_migrate_test.go b/pkg/services/queryhistory/queryhistory_migrate_test.go new file mode 100644 index 00000000000..eb896dc9b33 --- /dev/null +++ b/pkg/services/queryhistory/queryhistory_migrate_test.go @@ -0,0 +1,120 @@ +//go:build integration +// +build integration + +package queryhistory + +import ( + "encoding/json" + "testing" + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/stretchr/testify/require" +) + +func TestMigrateQueriesToQueryHistory(t *testing.T) { + testScenario(t, "When users tries to migrate 1 query in query history it should succeed", + func(t *testing.T, sc scenarioContext) { + command := MigrateQueriesToQueryHistoryCommand{ + Queries: []QueryToMigrate{ + { + DatasourceUID: "NCzh67i", + Queries: simplejson.NewFromAny(map[string]interface{}{ + "expr": "test", + }), + Comment: "", + Starred: false, + CreatedAt: time.Now().Unix(), + }, + }, + } + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.migrateHandler(sc.reqContext) + var response QueryHistoryMigrationResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, "Query history successfully migrated", response.Message) + require.Equal(t, 1, response.TotalCount) + require.Equal(t, 0, response.StarredCount) + }) + + testScenario(t, "When users tries to migrate multiple queries in query history it should succeed", + func(t *testing.T, sc scenarioContext) { + command := MigrateQueriesToQueryHistoryCommand{ + Queries: []QueryToMigrate{ + { + DatasourceUID: "NCzh67i", + Queries: simplejson.NewFromAny(map[string]interface{}{ + "expr": "test1", + }), + Comment: "", + Starred: false, + CreatedAt: time.Now().Unix(), + }, + { + DatasourceUID: "NCzh67i", + Queries: simplejson.NewFromAny(map[string]interface{}{ + "expr": "test2", + }), + Comment: "", + Starred: false, + CreatedAt: time.Now().Unix() - int64(100), + }, + { + DatasourceUID: "ABch68f", + Queries: simplejson.NewFromAny(map[string]interface{}{ + "expr": "test3", + }), + Comment: "", + Starred: false, + CreatedAt: time.Now().Unix() - int64(1000), + }, + }, + } + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.migrateHandler(sc.reqContext) + var response QueryHistoryMigrationResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, "Query history successfully migrated", response.Message) + require.Equal(t, 3, response.TotalCount) + require.Equal(t, 0, response.StarredCount) + }) + + testScenario(t, "When users tries to migrate starred and not starred query in query history it should succeed", + func(t *testing.T, sc scenarioContext) { + command := MigrateQueriesToQueryHistoryCommand{ + Queries: []QueryToMigrate{ + { + DatasourceUID: "NCzh67i", + Queries: simplejson.NewFromAny(map[string]interface{}{ + "expr": "test1", + }), + Comment: "", + Starred: true, + CreatedAt: time.Now().Unix(), + }, + { + DatasourceUID: "NCzh67i", + Queries: simplejson.NewFromAny(map[string]interface{}{ + "expr": "test2", + }), + Comment: "", + Starred: false, + CreatedAt: time.Now().Unix() - int64(100), + }, + }, + } + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.migrateHandler(sc.reqContext) + var response QueryHistoryMigrationResponse + err := json.Unmarshal(resp.Body(), &response) + require.NoError(t, err) + require.Equal(t, 200, resp.Status()) + require.Equal(t, "Query history successfully migrated", response.Message) + require.Equal(t, 2, response.TotalCount) + require.Equal(t, 1, response.StarredCount) + }) +} From e3590e1a9ba181b541e4c4acc3544e154ef971a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Apr 2022 10:10:03 +0200 Subject: [PATCH 085/121] Panels: Use the No value option when showing the no data message (#47675) * Performance: Create separate div for portal root to limit reflow -> recalc style * refactoring * NoData: Use field config No value option as text when display no data message * Use PanelDataErrorView in TabelPanel * Add PanelDataErrorView to PieChart * Updated --- .../src/components/PanelDataErrorView.tsx | 3 +- .../grafana-ui/src/utils/standardEditors.tsx | 2 +- .../components/PanelDataErrorView.test.tsx | 50 +++++++++++++++++++ .../panel/components/PanelDataErrorView.tsx | 4 +- .../plugins/panel/barchart/BarChartPanel.tsx | 20 +++++++- .../panel/candlestick/CandlestickPanel.tsx | 10 +++- .../panel/heatmap-new/HeatmapPanel.tsx | 10 +++- public/app/plugins/panel/logs/LogsPanel.tsx | 3 +- .../plugins/panel/piechart/PieChartPanel.tsx | 9 ++-- public/app/plugins/panel/table/TablePanel.tsx | 5 +- .../panel/timeseries/TimeSeriesPanel.tsx | 10 +++- 11 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 public/app/features/panel/components/PanelDataErrorView.test.tsx diff --git a/packages/grafana-runtime/src/components/PanelDataErrorView.tsx b/packages/grafana-runtime/src/components/PanelDataErrorView.tsx index 25e577d6712..2e3907c9264 100644 --- a/packages/grafana-runtime/src/components/PanelDataErrorView.tsx +++ b/packages/grafana-runtime/src/components/PanelDataErrorView.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { PanelData } from '@grafana/data'; +import { FieldConfigSource, PanelData } from '@grafana/data'; /** * Describes the properties that can be passed to the PanelDataErrorView. @@ -10,6 +10,7 @@ export interface PanelDataErrorViewProps { message?: string; panelId: number; data: PanelData; + fieldConfig?: FieldConfigSource; needsTimeField?: boolean; needsNumberField?: boolean; needsStringField?: boolean; diff --git a/packages/grafana-ui/src/utils/standardEditors.tsx b/packages/grafana-ui/src/utils/standardEditors.tsx index 8ddbb3cf528..065c1812113 100644 --- a/packages/grafana-ui/src/utils/standardEditors.tsx +++ b/packages/grafana-ui/src/utils/standardEditors.tsx @@ -134,7 +134,7 @@ export const getStandardFieldConfigs = () => { const noValue: FieldConfigPropertyItem = { id: 'noValue', path: 'noValue', - name: 'No Value', + name: 'No value', description: 'What to show when there is no value', editor: standardEditorsRegistry.get('text').editor as any, diff --git a/public/app/features/panel/components/PanelDataErrorView.test.tsx b/public/app/features/panel/components/PanelDataErrorView.test.tsx new file mode 100644 index 00000000000..10525bf0751 --- /dev/null +++ b/public/app/features/panel/components/PanelDataErrorView.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { defaultsDeep } from 'lodash'; +import { PanelDataErrorView } from './PanelDataErrorView'; +import { PanelDataErrorViewProps } from '@grafana/runtime'; +import { getDefaultTimeRange, LoadingState } from '@grafana/data'; +import { Provider } from 'react-redux'; +import { configureStore } from 'app/store/configureStore'; + +describe('PanelDataErrorView', () => { + it('show No data when there is no data', () => { + renderWithProps(); + + expect(screen.getByText('No data')).toBeInTheDocument(); + }); + + it('show no value field config when there is no data', () => { + renderWithProps({ + fieldConfig: { + overrides: [], + defaults: { + noValue: 'Query returned nothing', + }, + }, + }); + + expect(screen.getByText('Query returned nothing')).toBeInTheDocument(); + }); +}); + +function renderWithProps(overrides?: Partial) { + const defaults: PanelDataErrorViewProps = { + panelId: 1, + data: { + state: LoadingState.Done, + series: [], + timeRange: getDefaultTimeRange(), + }, + }; + + const props = defaultsDeep(overrides ?? {}, defaults); + const store = configureStore(); + + const stuff = render( + + + + ); + return { ...stuff }; +} diff --git a/public/app/features/panel/components/PanelDataErrorView.tsx b/public/app/features/panel/components/PanelDataErrorView.tsx index 35baee77e1c..55bb7aa50ca 100644 --- a/public/app/features/panel/components/PanelDataErrorView.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.tsx @@ -57,7 +57,7 @@ export function PanelDataErrorView(props: PanelDataErrorViewProps) { } function getMessageFor( - { data, message, needsNumberField, needsTimeField, needsStringField }: PanelDataErrorViewProps, + { data, fieldConfig, message, needsNumberField, needsTimeField, needsStringField }: PanelDataErrorViewProps, dataSummary: PanelDataSummary ): string { if (message) { @@ -66,7 +66,7 @@ function getMessageFor( // In some cases there is a data frame but with no fields if (!data.series || data.series.length === 0 || (data.series.length === 1 && data.series[0].fields.length === 0)) { - return 'No data'; + return fieldConfig?.defaults.noValue ?? 'No data'; } if (needsStringField && !dataSummary.hasStringField) { diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 7a4e7dc5f2c..61515d561b7 100755 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -63,7 +63,15 @@ const propsToDiff: Array = [ interface Props extends PanelProps {} -export const BarChartPanel: React.FunctionComponent = ({ data, options, width, height, timeZone, id }) => { +export const BarChartPanel: React.FunctionComponent = ({ + data, + options, + fieldConfig, + width, + height, + timeZone, + id, +}) => { const theme = useTheme2(); const styles = useStyles2(getStyles); const { eventBus } = usePanelContext(); @@ -133,7 +141,15 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w }, [height, options.xTickLabelRotation, options.xTickLabelMaxLength]); if (!info.viz[0]?.fields.length) { - return ; + return ( + + ); } const renderTooltip = (alignedFrame: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => { diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index ddd24370c6b..fd691d5b84f 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -209,7 +209,15 @@ export const CandlestickPanel: React.FC = ({ }, [options, data.structureRev]); if (!info) { - return ; + return ( + + ); } const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx index 12c0e7a2816..a76ece81a90 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx @@ -119,7 +119,15 @@ export const HeatmapPanel: React.FC = ({ }; if (info.warning || !info.heatmap) { - return ; + return ( + + ); } return ( diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index d5cc9973979..72f742ab929 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -22,6 +22,7 @@ interface LogsPanelProps extends PanelProps {} export const LogsPanel: React.FunctionComponent = ({ data, timeZone, + fieldConfig, options: { showLabels, showTime, @@ -83,7 +84,7 @@ export const LogsPanel: React.FunctionComponent = ({ ); if (!data || logRows.length === 0) { - return ; + return ; } const renderCommonLabels = () => ( diff --git a/public/app/plugins/panel/piechart/PieChartPanel.tsx b/public/app/plugins/panel/piechart/PieChartPanel.tsx index a1ba2a08a3a..d8eeeb59efd 100644 --- a/public/app/plugins/panel/piechart/PieChartPanel.tsx +++ b/public/app/plugins/panel/piechart/PieChartPanel.tsx @@ -23,6 +23,7 @@ import { VizLegendItem, } from '@grafana/ui'; import { filterDisplayItems, sumDisplayItemsReducer } from './utils'; +import { PanelDataErrorView } from '@grafana/runtime'; const defaultLegendOptions: PieChartLegendOptions = { displayMode: LegendDisplayMode.List, @@ -37,7 +38,7 @@ interface Props extends PanelProps {} * @beta */ export function PieChartPanel(props: Props) { - const { data, timeZone, fieldConfig, replaceVariables, width, height, options } = props; + const { data, timeZone, fieldConfig, replaceVariables, width, height, options, id } = props; const theme = useTheme2(); const highlightedTitle = useSliceHighlightState(); @@ -51,11 +52,7 @@ export function PieChartPanel(props: Props) { }); if (!hasFrames(fieldDisplayValues)) { - return ( -
    -

    No data

    -
    - ); + return ; } return ( diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 852b0bbaeeb..a55e7e21e2e 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -17,6 +17,7 @@ import { applyFilterFromTable } from '../../../features/variables/adhoc/actions' import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv'; import { getFooterCells } from './footer'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { PanelDataErrorView } from '@grafana/runtime'; interface Props extends PanelProps {} @@ -118,14 +119,14 @@ export class TablePanel extends Component { } render() { - const { data, height, width, options } = this.props; + const { data, height, width, options, fieldConfig, id } = this.props; const frames = data.series; const count = frames?.length; const hasFields = frames[0]?.fields.length; if (!count || !hasFields) { - return
    No data
    ; + return ; } if (count > 1) { diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 64c4b6ea7df..5eb7e489b41 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -36,7 +36,15 @@ export const TimeSeriesPanel: React.FC = ({ const frames = useMemo(() => prepareGraphableFields(data.series, config.theme2), [data]); if (!frames) { - return ; + return ( + + ); } const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); From 4c99e681b1a5192631c92fee9f283b8f9b06269e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Apr 2022 09:46:58 +0100 Subject: [PATCH 086/121] Update dependency @betterer/cli to v5.3.0 (#47703) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 105 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 1647c9e05b0..ca82144b917 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "@babel/preset-react": "7.16.7", "@babel/preset-typescript": "7.16.7", "@betterer/betterer": "5.2.1", - "@betterer/cli": "5.1.7", + "@betterer/cli": "5.3.0", "@betterer/regexp": "5.1.7", "@emotion/eslint-plugin": "11.7.0", "@grafana/api-documenter": "7.11.2", diff --git a/yarn.lock b/yarn.lock index f79e96fbfa3..accc8a1958c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3293,13 +3293,40 @@ __metadata: languageName: node linkType: hard -"@betterer/cli@npm:5.1.7": - version: 5.1.7 - resolution: "@betterer/cli@npm:5.1.7" +"@betterer/betterer@npm:^5.3.0": + version: 5.3.0 + resolution: "@betterer/betterer@npm:5.3.0" dependencies: - "@betterer/betterer": ^5.1.7 - "@betterer/errors": ^5.0.0 - "@betterer/tasks": ^5.1.7 + "@betterer/constraints": ^5.3.0 + "@betterer/errors": ^5.3.0 + "@betterer/logger": ^5.3.0 + "@betterer/reporter": ^5.3.0 + "@phenomnomnominal/debug": ^0.2.5 + "@phenomnomnominal/worker-require": ^0.0.34 + chokidar: ^3.3.1 + djb2a: ^1.2.0 + fast-memoize: ^2.5.2 + lines-and-columns: ^2.0.3 + minimatch: ^5.0.1 + prettier: ^2.3.2 + simple-git: ^3.1.1 + ts-node: ^10.2.1 + tslib: ^2.3.1 + typescript: ">=2.7" + dependenciesMeta: + typescript: + optional: true + checksum: 4c948bde624d71ae1f5c970e2eec1aa4116d0b691bef848053d9ca577e19425011949068686b0630f20bd90c4784de9dcbe5c2a8bb98cea1b8282f71bbf00fbc + languageName: node + linkType: hard + +"@betterer/cli@npm:5.3.0": + version: 5.3.0 + resolution: "@betterer/cli@npm:5.3.0" + dependencies: + "@betterer/betterer": ^5.3.0 + "@betterer/errors": ^5.3.0 + "@betterer/tasks": ^5.3.0 "@phenomnomnominal/tsquery": ^4.1.1 "@phenomnomnominal/tstemplate": ^0.1.0 "@phenomnomnominal/worker-require": ^0.0.34 @@ -3314,7 +3341,7 @@ __metadata: tslib: ^2.3.1 bin: betterer: bin/betterer - checksum: cabfca31186e2d3ab76e0b25145fbc671079f68ab5737583395f6035ca019e61fd38a20bfa92ad9b488c5c86c70f8744c1676cf37215d109361ce496227840b3 + checksum: 1bb282b039461d866f6be5981054e30416040f739009ac9cc268f091482a050aa7be971203fbddae87485c14e04bfaff09908aa0be7d862761bdfbdb380c951d languageName: node linkType: hard @@ -3327,6 +3354,15 @@ __metadata: languageName: node linkType: hard +"@betterer/constraints@npm:^5.3.0": + version: 5.3.0 + resolution: "@betterer/constraints@npm:5.3.0" + dependencies: + tslib: ^2.3.1 + checksum: a3991bc4f138bfaff2ddd842790ea43c50edc6ea4b50546e44b8489231dfb5a7f9ef995d1c7d8695149fda5cd4b1aa79731c76482708a18cbfe398c39b9dcee8 + languageName: node + linkType: hard + "@betterer/errors@npm:^5.0.0": version: 5.0.0 resolution: "@betterer/errors@npm:5.0.0" @@ -3334,6 +3370,13 @@ __metadata: languageName: node linkType: hard +"@betterer/errors@npm:^5.3.0": + version: 5.3.0 + resolution: "@betterer/errors@npm:5.3.0" + checksum: 15d6679dca00a97ee1845225af662fdbcf84a684286d3ae654023ec5d1e2e61bb6c7a97ba5e6e50dfeebbeb5247f4bf5d5e4705a0468ea067bff30e22b7ccaf3 + languageName: node + linkType: hard + "@betterer/logger@npm:^5.1.5": version: 5.1.5 resolution: "@betterer/logger@npm:5.1.5" @@ -3362,6 +3405,20 @@ __metadata: languageName: node linkType: hard +"@betterer/logger@npm:^5.3.0": + version: 5.3.0 + resolution: "@betterer/logger@npm:5.3.0" + dependencies: + "@babel/code-frame": ^7.10.3 + jest-diff: ^27.0.6 + lines-and-columns: ^2.0.3 + react: ">=16.8.4 || >=17.0.0" + react-dom: ">=16.8.4 || >=17.0.0" + tslib: ^2.3.1 + checksum: 38bc7bbc84dc174fa39e8ab533a23a4163f0aca0168942ebdebd026e087242f5254e3f7ca363bd574c5bc9d28045de0b83cdf3c11d670f74e726a24410320323 + languageName: node + linkType: hard + "@betterer/regexp@npm:5.1.7": version: 5.1.7 resolution: "@betterer/regexp@npm:5.1.7" @@ -3407,6 +3464,23 @@ __metadata: languageName: node linkType: hard +"@betterer/reporter@npm:^5.3.0": + version: 5.3.0 + resolution: "@betterer/reporter@npm:5.3.0" + dependencies: + "@betterer/betterer": ^5.3.0 + "@betterer/errors": ^5.3.0 + "@betterer/logger": ^5.3.0 + "@betterer/tasks": ^5.3.0 + ink: ^3.0.9 + ink-text-input: ^4.0.1 + react: ">=16.8.4 || >=17.0.0" + react-dom: ">=16.8.4 || >=17.0.0" + tslib: ^2.3.1 + checksum: a303108200cd5a0b3de35093db17a0b208c1c2456f5edad8be346367f036052e88cb87f06f047a52f4671cb895158013b13545bfd4bc12aae2cfc7133c4d6a94 + languageName: node + linkType: hard + "@betterer/tasks@npm:^5.1.7": version: 5.1.7 resolution: "@betterer/tasks@npm:5.1.7" @@ -3437,6 +3511,21 @@ __metadata: languageName: node linkType: hard +"@betterer/tasks@npm:^5.3.0": + version: 5.3.0 + resolution: "@betterer/tasks@npm:5.3.0" + dependencies: + "@betterer/errors": ^5.3.0 + "@betterer/logger": ^5.3.0 + chalk: ^4.1.2 + ink: ^3.0.9 + react: ">=16.8.4 || >=17.0.0" + react-dom: ">=16.8.4 || >=17.0.0" + tslib: ^2.3.1 + checksum: 65ea5a76ee8a6e447a866876521f7b14255087537b6ea20069fd326015b31d39dd24e52799c095706faf21622a8365f8933e5ba0bc805bfac55e13634ab9df55 + languageName: node + linkType: hard + "@braintree/sanitize-url@npm:6.0.0": version: 6.0.0 resolution: "@braintree/sanitize-url@npm:6.0.0" @@ -20488,7 +20577,7 @@ __metadata: "@babel/preset-react": 7.16.7 "@babel/preset-typescript": 7.16.7 "@betterer/betterer": 5.2.1 - "@betterer/cli": 5.1.7 + "@betterer/cli": 5.3.0 "@betterer/regexp": 5.1.7 "@emotion/css": 11.7.1 "@emotion/eslint-plugin": 11.7.0 From 5df05e31bbda315817f1cc292db9619e2bbfc467 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 14 Apr 2022 10:59:39 +0200 Subject: [PATCH 087/121] Loki: Use single string expr as a state for the visual editor (#47566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Loki: Use expr as state for visual editor * Loki: Use query with line filter as default for visual editor * Refactor based on feedback * fix background for query text row Co-authored-by: Torkel Ödegaard --- .../components/LokiQueryBuilder.tsx | 6 -- .../LokiQueryBuilderContainer.test.tsx | 39 +++++++++ .../components/LokiQueryBuilderContainer.tsx | 82 +++++++++++++++++++ ...aind.tsx => LokiQueryBuilderExplained.tsx} | 9 +- .../LokiQueryEditorSelector.test.tsx | 7 -- .../components/LokiQueryEditorSelector.tsx | 47 ++++------- .../querybuilder/components/QueryPreview.tsx | 29 ++++--- .../datasource/loki/querybuilder/types.ts | 7 -- public/app/plugins/datasource/loki/types.ts | 3 - .../querybuilder/components/QueryPreview.tsx | 4 +- 10 files changed, 157 insertions(+), 76 deletions(-) create mode 100644 public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx create mode 100644 public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx rename public/app/plugins/datasource/loki/querybuilder/components/{LokiQueryBuilderExplaind.tsx => LokiQueryBuilderExplained.tsx} (79%) diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 69da0eb0828..9b84ad88348 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -7,7 +7,6 @@ import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/query import { lokiQueryModeller } from '../LokiQueryModeller'; import { DataSourceApi, SelectableValue } from '@grafana/data'; import { EditorRow } from '@grafana/experimental'; -import { QueryPreview } from './QueryPreview'; import { OperationsEditorRow } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationsEditorRow'; import { NestedQueryList } from './NestedQueryList'; @@ -84,11 +83,6 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, nested, {query.binaryQueries && query.binaryQueries.length > 0 && ( )} - {!nested && ( - - - - )} ); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx new file mode 100644 index 00000000000..80717a06fea --- /dev/null +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer'; +import { LokiDatasource } from '../../datasource'; +import { addOperation } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationList.testUtils'; + +describe('LokiQueryBuilderContainer', () => { + it('translates query between string and model', async () => { + const props = { + query: { + expr: '{job="testjob"}', + refId: 'A', + }, + datasource: new LokiDatasource( + { + id: 1, + uid: '', + type: 'loki', + name: 'loki-test', + access: 'proxy', + url: '', + jsonData: {}, + meta: {} as any, + }, + undefined, + undefined + ), + onChange: jest.fn(), + onRunQuery: () => {}, + }; + render(); + expect(screen.getByText('testjob')).toBeInTheDocument(); + addOperation('Range functions', 'Rate'); + expect(props.onChange).toBeCalledWith({ + expr: 'rate({job="testjob"} [$__interval])', + refId: 'A', + }); + }); +}); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx new file mode 100644 index 00000000000..3a428319d78 --- /dev/null +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx @@ -0,0 +1,82 @@ +import React, { useEffect, useReducer } from 'react'; +import { LokiDatasource } from '../../datasource'; +import { LokiQuery } from '../../types'; +import { buildVisualQueryFromString } from '../parsing'; +import { lokiQueryModeller } from '../LokiQueryModeller'; +import { LokiQueryBuilder } from './LokiQueryBuilder'; +import { QueryPreview } from './QueryPreview'; +import { LokiVisualQuery } from '../types'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface Props { + query: LokiQuery; + datasource: LokiDatasource; + onChange: (update: LokiQuery) => void; + onRunQuery: () => void; +} + +export interface State { + visQuery?: LokiVisualQuery; + expr: string; +} + +/** + * This component is here just to contain the translation logic between string query and the visual query builder model. + */ +export function LokiQueryBuilderContainer(props: Props) { + const { query, onChange, onRunQuery, datasource } = props; + const [state, dispatch] = useReducer(stateSlice.reducer, { + expr: '', + visQuery: { + labels: [], + operations: [{ id: '__line_contains', params: [''] }], + }, + }); + + // Only rebuild visual query if expr changes from outside + useEffect(() => { + dispatch(exprChanged(query.expr)); + }, [query.expr]); + + const onVisQueryChange = (visQuery: LokiVisualQuery) => { + const expr = lokiQueryModeller.renderQuery(visQuery); + dispatch(visualQueryChange({ visQuery, expr })); + onChange({ ...props.query, expr: expr }); + }; + + if (!state.visQuery) { + return null; + } + + return ( + <> + + + + ); +} + +const stateSlice = createSlice({ + name: 'prom-builder-container', + initialState: { expr: '' } as State, + reducers: { + visualQueryChange: (state, action: PayloadAction<{ visQuery: LokiVisualQuery; expr: string }>) => { + state.expr = action.payload.expr; + state.visQuery = action.payload.visQuery; + }, + exprChanged: (state, action: PayloadAction) => { + if (!state.visQuery || state.expr !== action.payload) { + state.expr = action.payload; + const parseResult = buildVisualQueryFromString(action.payload); + state.visQuery = parseResult.query; + } + }, + }, +}); + +const { visualQueryChange, exprChanged } = stateSlice.actions; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplaind.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx similarity index 79% rename from public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplaind.tsx rename to public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx index 69e6115a8e6..9b53334a453 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplaind.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx @@ -4,19 +4,22 @@ import { Stack } from '@grafana/experimental'; import { lokiQueryModeller } from '../LokiQueryModeller'; import { OperationListExplained } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained'; import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox'; +import { buildVisualQueryFromString } from '../parsing'; export interface Props { - query: LokiVisualQuery; + query: string; nested?: boolean; } export const LokiQueryBuilderExplained = React.memo(({ query, nested }) => { + const visQuery = buildVisualQueryFromString(query || '').query; + return ( - + Fetch all log lines matching label filters. - stepNumber={2} queryModeller={lokiQueryModeller} query={query} /> + stepNumber={2} queryModeller={lokiQueryModeller} query={visQuery} /> ); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx index 46e6fa8a22e..c655ffcac96 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx @@ -79,13 +79,6 @@ describe('LokiQueryEditorSelector', () => { expr: defaultQuery.expr, queryType: LokiQueryType.Range, editorMode: QueryEditorMode.Builder, - visualQuery: { - labels: [ - { label: 'label1', op: '=', value: 'foo' }, - { label: 'label2', op: '=', value: 'bar' }, - ], - operations: [], - }, }); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx index 3223069ada1..58f0de41fe9 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx @@ -6,13 +6,10 @@ import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybu import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; import React, { useCallback, useState } from 'react'; import { LokiQueryEditorProps } from '../../components/types'; -import { LokiQuery } from '../../types'; - import { lokiQueryModeller } from '../LokiQueryModeller'; import { getQueryWithDefaults } from '../state'; -import { getDefaultEmptyQuery, LokiVisualQuery } from '../types'; -import { LokiQueryBuilder } from './LokiQueryBuilder'; -import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplaind'; +import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer'; +import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplained'; import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions'; import { LokiQueryCodeEditor } from './LokiQueryCodeEditor'; import { buildVisualQueryFromString } from '../parsing'; @@ -21,43 +18,26 @@ export const LokiQueryEditorSelector = React.memo((props) const { onChange, onRunQuery, data } = props; const styles = useStyles2(getStyles); const query = getQueryWithDefaults(props.query); - const [visualQuery, setVisualQuery] = useState(query.visualQuery ?? getDefaultEmptyQuery()); const [parseModalOpen, setParseModalOpen] = useState(false); - const [pendingChange, setPendingChange] = useState(undefined); const onEditorModeChange = useCallback( (newMetricEditorMode: QueryEditorMode) => { const change = { ...query, editorMode: newMetricEditorMode }; if (newMetricEditorMode === QueryEditorMode.Builder) { - const result = buildVisualQueryFromString(query.expr); - change.visualQuery = result.query; + const result = buildVisualQueryFromString(query.expr || ''); // If there are errors, give user a chance to decide if they want to go to builder as that can loose some data. if (result.errors.length) { setParseModalOpen(true); - setPendingChange(change); return; } - setVisualQuery(change.visualQuery); } onChange(change); }, [onChange, query] ); - const onChangeViewModel = (updatedQuery: LokiVisualQuery) => { - setVisualQuery(updatedQuery); - - onChange({ - ...query, - expr: lokiQueryModeller.renderQuery(updatedQuery), - visualQuery: updatedQuery, - editorMode: QueryEditorMode.Builder, - }); - }; - // If no expr (ie new query) then default to builder const editorMode = query.editorMode ?? (query.expr ? QueryEditorMode.Code : QueryEditorMode.Builder); - return ( <> ((props) body="There were errors while trying to parse the query. Continuing to visual builder may loose some parts of the query." confirmText="Continue" onConfirm={() => { - setVisualQuery(pendingChange!.visualQuery!); - onChange(pendingChange!); + onChange({ ...query, editorMode: QueryEditorMode.Builder }); setParseModalOpen(false); }} onDismiss={() => setParseModalOpen(false)} @@ -90,27 +69,29 @@ export const LokiQueryEditorSelector = React.memo((props) placeholder="Query patterns" allowCustomValue onChange={({ value }) => { - onChangeViewModel({ - ...visualQuery, - operations: value?.operations!, + const result = buildVisualQueryFromString(query.expr || ''); + result.query.operations = value?.operations!; + onChange({ + ...query, + expr: lokiQueryModeller.renderQuery(result.query), }); }} options={lokiQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))} /> - + {editorMode === QueryEditorMode.Code && } {editorMode === QueryEditorMode.Builder && ( - )} - {editorMode === QueryEditorMode.Explain && } + {editorMode === QueryEditorMode.Explain && } {editorMode !== QueryEditorMode.Explain && ( )} diff --git a/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx b/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx index 55ef0adc1c6..4fd6592d8b6 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx @@ -1,39 +1,38 @@ import React from 'react'; -import { LokiVisualQuery } from '../types'; import { useTheme2 } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; import { css, cx } from '@emotion/css'; -import { EditorField, EditorFieldGroup } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; import Prism from 'prismjs'; import { lokiGrammar } from '../../syntax'; -import { lokiQueryModeller } from '../LokiQueryModeller'; export interface Props { - query: LokiVisualQuery; + query: string; } export function QueryPreview({ query }: Props) { const theme = useTheme2(); const styles = getStyles(theme); - const hightlighted = Prism.highlight(lokiQueryModeller.renderQuery(query), lokiGrammar, 'lokiql'); + const highlighted = Prism.highlight(query, lokiGrammar, 'lokiql'); return ( - - -
    - - + + + +
    + + + ); } const getStyles = (theme: GrafanaTheme2) => { return { editorField: css({ - padding: theme.spacing(0.25, 1), fontFamily: theme.typography.fontFamilyMonospace, fontSize: theme.typography.bodySmall.fontSize, }), diff --git a/public/app/plugins/datasource/loki/querybuilder/types.ts b/public/app/plugins/datasource/loki/querybuilder/types.ts index 36939dc783d..63aaa7f7481 100644 --- a/public/app/plugins/datasource/loki/querybuilder/types.ts +++ b/public/app/plugins/datasource/loki/querybuilder/types.ts @@ -90,10 +90,3 @@ export enum LokiOperationOrder { RangeVectorFunction = 5, Last = 6, } - -export function getDefaultEmptyQuery(): LokiVisualQuery { - return { - labels: [], - operations: [{ id: '__line_contains', params: [''] }], - }; -} diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index e8da6e417bb..f1e91c3b279 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -1,6 +1,5 @@ import { DataQuery, DataSourceJsonData, QueryResultMeta, ScopedVars } from '@grafana/data'; import { QueryEditorMode } from '../prometheus/querybuilder/shared/types'; -import { LokiVisualQuery } from './querybuilder/types'; export interface LokiInstantQueryRequest { query: string; @@ -43,8 +42,6 @@ export interface LokiQuery extends DataQuery { /* @deprecated now use queryType */ instant?: boolean; editorMode?: QueryEditorMode; - /** Temporary until we have a parser */ - visualQuery?: LokiVisualQuery; } export interface LokiOptions extends DataSourceJsonData { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx index bdc0c7718de..b95df791017 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx @@ -13,7 +13,7 @@ export interface Props { export function QueryPreview({ query }: Props) { const theme = useTheme2(); const styles = getStyles(theme); - const hightlighted = Prism.highlight(query, promqlGrammar, 'promql'); + const highlighted = Prism.highlight(query, promqlGrammar, 'promql'); return ( @@ -22,7 +22,7 @@ export function QueryPreview({ query }: Props) {
    From 86ca14f7e6bd6e6976f31b66d9e67185e401b98e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Apr 2022 11:01:04 +0200 Subject: [PATCH 088/121] Update dependency @betterer/betterer to v5.3.0 (#47757) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 91 ++++++---------------------------------------------- 2 files changed, 10 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index ca82144b917..dd390697e2d 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "@babel/preset-env": "7.16.11", "@babel/preset-react": "7.16.7", "@babel/preset-typescript": "7.16.7", - "@betterer/betterer": "5.2.1", + "@betterer/betterer": "5.3.0", "@betterer/cli": "5.3.0", "@betterer/regexp": "5.1.7", "@emotion/eslint-plugin": "11.7.0", diff --git a/yarn.lock b/yarn.lock index accc8a1958c..442f7166ddc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3239,14 +3239,14 @@ __metadata: languageName: node linkType: hard -"@betterer/betterer@npm:5.2.1, @betterer/betterer@npm:^5.2.1": - version: 5.2.1 - resolution: "@betterer/betterer@npm:5.2.1" +"@betterer/betterer@npm:5.3.0, @betterer/betterer@npm:^5.3.0": + version: 5.3.0 + resolution: "@betterer/betterer@npm:5.3.0" dependencies: - "@betterer/constraints": ^5.0.0 - "@betterer/errors": ^5.0.0 - "@betterer/logger": ^5.2.0 - "@betterer/reporter": ^5.2.1 + "@betterer/constraints": ^5.3.0 + "@betterer/errors": ^5.3.0 + "@betterer/logger": ^5.3.0 + "@betterer/reporter": ^5.3.0 "@phenomnomnominal/debug": ^0.2.5 "@phenomnomnominal/worker-require": ^0.0.34 chokidar: ^3.3.1 @@ -3262,7 +3262,7 @@ __metadata: dependenciesMeta: typescript: optional: true - checksum: 7af5030332d4161c966ab4ff0c1e4cd1cd02fa242b4cd5d9f8a52cba48b10c00235d3d280e806fca096537e4062a5a94fdf49e25988faa7e27ec561b142d11d6 + checksum: 4c948bde624d71ae1f5c970e2eec1aa4116d0b691bef848053d9ca577e19425011949068686b0630f20bd90c4784de9dcbe5c2a8bb98cea1b8282f71bbf00fbc languageName: node linkType: hard @@ -3293,33 +3293,6 @@ __metadata: languageName: node linkType: hard -"@betterer/betterer@npm:^5.3.0": - version: 5.3.0 - resolution: "@betterer/betterer@npm:5.3.0" - dependencies: - "@betterer/constraints": ^5.3.0 - "@betterer/errors": ^5.3.0 - "@betterer/logger": ^5.3.0 - "@betterer/reporter": ^5.3.0 - "@phenomnomnominal/debug": ^0.2.5 - "@phenomnomnominal/worker-require": ^0.0.34 - chokidar: ^3.3.1 - djb2a: ^1.2.0 - fast-memoize: ^2.5.2 - lines-and-columns: ^2.0.3 - minimatch: ^5.0.1 - prettier: ^2.3.2 - simple-git: ^3.1.1 - ts-node: ^10.2.1 - tslib: ^2.3.1 - typescript: ">=2.7" - dependenciesMeta: - typescript: - optional: true - checksum: 4c948bde624d71ae1f5c970e2eec1aa4116d0b691bef848053d9ca577e19425011949068686b0630f20bd90c4784de9dcbe5c2a8bb98cea1b8282f71bbf00fbc - languageName: node - linkType: hard - "@betterer/cli@npm:5.3.0": version: 5.3.0 resolution: "@betterer/cli@npm:5.3.0" @@ -3391,20 +3364,6 @@ __metadata: languageName: node linkType: hard -"@betterer/logger@npm:^5.2.0": - version: 5.2.0 - resolution: "@betterer/logger@npm:5.2.0" - dependencies: - "@babel/code-frame": ^7.10.3 - jest-diff: ^27.0.6 - lines-and-columns: ^2.0.3 - react: ">=16.8.4 || >=17.0.0" - react-dom: ">=16.8.4 || >=17.0.0" - tslib: ^2.3.1 - checksum: c9789050d3f95e39a5165a97dcafdc9da194b57bb351ed993b92a37e6a602c345a00825a8ad6e3a4037e4c428532c502f231d45dfa27a29c569cd76d6b504105 - languageName: node - linkType: hard - "@betterer/logger@npm:^5.3.0": version: 5.3.0 resolution: "@betterer/logger@npm:5.3.0" @@ -3447,23 +3406,6 @@ __metadata: languageName: node linkType: hard -"@betterer/reporter@npm:^5.2.1": - version: 5.2.1 - resolution: "@betterer/reporter@npm:5.2.1" - dependencies: - "@betterer/betterer": ^5.2.1 - "@betterer/errors": ^5.0.0 - "@betterer/logger": ^5.2.0 - "@betterer/tasks": ^5.2.0 - ink: ^3.0.9 - ink-text-input: ^4.0.1 - react: ">=16.8.4 || >=17.0.0" - react-dom: ">=16.8.4 || >=17.0.0" - tslib: ^2.3.1 - checksum: 53b58bfcbd3333480cb13e62d2526b70ace020768e6cccd147088b7b6ecf3d011292e529dced0967b6196cf029748458412ff4b2a16469e41fa75e4e962bc8e8 - languageName: node - linkType: hard - "@betterer/reporter@npm:^5.3.0": version: 5.3.0 resolution: "@betterer/reporter@npm:5.3.0" @@ -3496,21 +3438,6 @@ __metadata: languageName: node linkType: hard -"@betterer/tasks@npm:^5.2.0": - version: 5.2.0 - resolution: "@betterer/tasks@npm:5.2.0" - dependencies: - "@betterer/errors": ^5.0.0 - "@betterer/logger": ^5.2.0 - chalk: ^4.1.2 - ink: ^3.0.9 - react: ">=16.8.4 || >=17.0.0" - react-dom: ">=16.8.4 || >=17.0.0" - tslib: ^2.3.1 - checksum: aab9c9c76c21396b553aa1f0e86d8760864f3156f222b724d13f53ac0264aa2a67ee37b9ec515ddf56b819bbaf6c2fd0138d540ca7b5f0eba404b0207b07c1ec - languageName: node - linkType: hard - "@betterer/tasks@npm:^5.3.0": version: 5.3.0 resolution: "@betterer/tasks@npm:5.3.0" @@ -20576,7 +20503,7 @@ __metadata: "@babel/preset-env": 7.16.11 "@babel/preset-react": 7.16.7 "@babel/preset-typescript": 7.16.7 - "@betterer/betterer": 5.2.1 + "@betterer/betterer": 5.3.0 "@betterer/cli": 5.3.0 "@betterer/regexp": 5.1.7 "@emotion/css": 11.7.1 From 7916f3b6388da5cadd54bc8153a5951d5238a7a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Apr 2022 09:16:28 +0000 Subject: [PATCH 089/121] Update dependency @betterer/regexp to v5.3.0 (#47759) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 103 ++++----------------------------------------------- 2 files changed, 8 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index dd390697e2d..2706e103b50 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "@babel/preset-typescript": "7.16.7", "@betterer/betterer": "5.3.0", "@betterer/cli": "5.3.0", - "@betterer/regexp": "5.1.7", + "@betterer/regexp": "5.3.0", "@emotion/eslint-plugin": "11.7.0", "@grafana/api-documenter": "7.11.2", "@grafana/e2e": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 442f7166ddc..1cd157ebdc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3266,33 +3266,6 @@ __metadata: languageName: node linkType: hard -"@betterer/betterer@npm:^5.1.7": - version: 5.1.7 - resolution: "@betterer/betterer@npm:5.1.7" - dependencies: - "@betterer/constraints": ^5.0.0 - "@betterer/errors": ^5.0.0 - "@betterer/logger": ^5.1.5 - "@betterer/reporter": ^5.1.7 - "@phenomnomnominal/debug": ^0.2.5 - "@phenomnomnominal/worker-require": ^0.0.34 - chokidar: ^3.3.1 - djb2a: ^1.2.0 - fast-memoize: ^2.5.2 - lines-and-columns: ^1.1.6 - minimatch: ^3.0.4 - prettier: ^2.3.2 - simple-git: ^3.1.1 - ts-node: ^10.2.1 - tslib: ^2.3.1 - typescript: ">=2.7" - dependenciesMeta: - typescript: - optional: true - checksum: 3c8b980910de3078b6e44e9aaa741419d1985800ec2fb4e5c64fa1f1cd473aae03a07a93511dbde86647142c7277a3002b39c2c87fe4828f79412dac4c687faf - languageName: node - linkType: hard - "@betterer/cli@npm:5.3.0": version: 5.3.0 resolution: "@betterer/cli@npm:5.3.0" @@ -3318,15 +3291,6 @@ __metadata: languageName: node linkType: hard -"@betterer/constraints@npm:^5.0.0": - version: 5.0.0 - resolution: "@betterer/constraints@npm:5.0.0" - dependencies: - tslib: ^2.3.1 - checksum: 6b6560ea7f68f5909e40e444c0a394b299968b0c2b533aea69d8c0bd9e3239952507df1f6665952dde03a1fd90d9786ea4e18b207af37b3d6e331c2f369e696d - languageName: node - linkType: hard - "@betterer/constraints@npm:^5.3.0": version: 5.3.0 resolution: "@betterer/constraints@npm:5.3.0" @@ -3336,13 +3300,6 @@ __metadata: languageName: node linkType: hard -"@betterer/errors@npm:^5.0.0": - version: 5.0.0 - resolution: "@betterer/errors@npm:5.0.0" - checksum: d4245427d15305df047bd656820ea492f055237f3cf6e2e971b087f6df404fff1b8bc07ddc353e00d2ffe3aea99ae9edfa471436dccd5a187b93092d9e659217 - languageName: node - linkType: hard - "@betterer/errors@npm:^5.3.0": version: 5.3.0 resolution: "@betterer/errors@npm:5.3.0" @@ -3350,20 +3307,6 @@ __metadata: languageName: node linkType: hard -"@betterer/logger@npm:^5.1.5": - version: 5.1.5 - resolution: "@betterer/logger@npm:5.1.5" - dependencies: - "@babel/code-frame": ^7.10.3 - jest-diff: ^27.0.6 - lines-and-columns: ^1.1.6 - react: ">=16.8.4 || >=17.0.0" - react-dom: ">=16.8.4 || >=17.0.0" - tslib: ^2.3.1 - checksum: ce0e6847049e2132b527003b4732e4ee3e80464651c7882bf94c308f2a2a9f5495013837809743b2a16a11a844d534f8b84fc29cab22ec7fbe40c628c1759cec - languageName: node - linkType: hard - "@betterer/logger@npm:^5.3.0": version: 5.3.0 resolution: "@betterer/logger@npm:5.3.0" @@ -3378,31 +3321,14 @@ __metadata: languageName: node linkType: hard -"@betterer/regexp@npm:5.1.7": - version: 5.1.7 - resolution: "@betterer/regexp@npm:5.1.7" +"@betterer/regexp@npm:5.3.0": + version: 5.3.0 + resolution: "@betterer/regexp@npm:5.3.0" dependencies: - "@betterer/betterer": ^5.1.7 - "@betterer/errors": ^5.0.0 + "@betterer/betterer": ^5.3.0 + "@betterer/errors": ^5.3.0 tslib: ^2.3.1 - checksum: 17af07f3e8240ce45716b6d24cf5492aeca4ab2a78c57455ae2f26365dbbe5d1a1f93b02ee2cdaddd817b65a510de0a041e7cb8694663acedd7a090599bde547 - languageName: node - linkType: hard - -"@betterer/reporter@npm:^5.1.7": - version: 5.1.7 - resolution: "@betterer/reporter@npm:5.1.7" - dependencies: - "@betterer/betterer": ^5.1.7 - "@betterer/errors": ^5.0.0 - "@betterer/logger": ^5.1.5 - "@betterer/tasks": ^5.1.7 - ink: ^3.0.9 - ink-text-input: ^4.0.1 - react: ">=16.8.4 || >=17.0.0" - react-dom: ">=16.8.4 || >=17.0.0" - tslib: ^2.3.1 - checksum: 269f65fb76e539fbd714d75c53a02b941aecdfeb46599e3944f23e7932f1251a4e0c807ad21a610183e797529ce11deeb08b2f6e13b059bc1bbbbe03878ad115 + checksum: 9b69160783b147ef7c9c585ad8dc82303dee39b549903ae1a74a87d0e3bfaa9a2fc408389d39e655dfae516c82565397841efd04d7b5d051e80df292d21e0530 languageName: node linkType: hard @@ -3423,21 +3349,6 @@ __metadata: languageName: node linkType: hard -"@betterer/tasks@npm:^5.1.7": - version: 5.1.7 - resolution: "@betterer/tasks@npm:5.1.7" - dependencies: - "@betterer/errors": ^5.0.0 - "@betterer/logger": ^5.1.5 - chalk: ^4.1.2 - ink: ^3.0.9 - react: ">=16.8.4 || >=17.0.0" - react-dom: ">=16.8.4 || >=17.0.0" - tslib: ^2.3.1 - checksum: b46a019d101a2c3a143cf2e1405f40982ef493c144552a116871f0f037f92ed734618da657ca1beb517dcef370d41daa78be73c224afbb42fe5d7c6c9f45fc38 - languageName: node - linkType: hard - "@betterer/tasks@npm:^5.3.0": version: 5.3.0 resolution: "@betterer/tasks@npm:5.3.0" @@ -20505,7 +20416,7 @@ __metadata: "@babel/preset-typescript": 7.16.7 "@betterer/betterer": 5.3.0 "@betterer/cli": 5.3.0 - "@betterer/regexp": 5.1.7 + "@betterer/regexp": 5.3.0 "@emotion/css": 11.7.1 "@emotion/eslint-plugin": 11.7.0 "@emotion/react": 11.8.2 From c8189e4808f83019486edb9637e840cb0fe1e6aa Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Thu, 14 Apr 2022 13:28:13 +0300 Subject: [PATCH 090/121] API: enable proxying datasource calls using the datasource UID (#47634) * Introduce additional routes --- pkg/api/api.go | 2 + pkg/api/dataproxy.go | 4 ++ .../datasourceproxy/datasourceproxy.go | 46 ++++++++++++++----- .../datasourceproxy/datasourceproxy_test.go | 12 +++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index f54e5ee7a94..4836363dfaf 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -322,7 +322,9 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings) apiRoute.Any("/datasources/proxy/:id/*", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), hs.ProxyDataSourceRequest) + apiRoute.Any("/datasources/proxy/uid/:uid/*", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), hs.ProxyDataSourceRequestWithUID) apiRoute.Any("/datasources/proxy/:id", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), hs.ProxyDataSourceRequest) + apiRoute.Any("/datasources/proxy/uid/:uid", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), hs.ProxyDataSourceRequestWithUID) apiRoute.Any("/datasources/:id/resources", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), hs.CallDatasourceResource) apiRoute.Any("/datasources/:id/resources/*", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), hs.CallDatasourceResource) apiRoute.Any("/datasources/:id/health", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.CheckDatasourceHealth)) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 5a39872d20e..af006072019 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -5,3 +5,7 @@ import "github.com/grafana/grafana/pkg/models" func (hs *HTTPServer) ProxyDataSourceRequest(c *models.ReqContext) { hs.DataProxy.ProxyDataSourceRequest(c) } + +func (hs *HTTPServer) ProxyDataSourceRequestWithUID(c *models.ReqContext) { + hs.DataProxy.ProxyDatasourceRequestWithUID(c) +} diff --git a/pkg/services/datasourceproxy/datasourceproxy.go b/pkg/services/datasourceproxy/datasourceproxy.go index fc101016b87..75d5f506ea1 100644 --- a/pkg/services/datasourceproxy/datasourceproxy.go +++ b/pkg/services/datasourceproxy/datasourceproxy.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -59,24 +60,47 @@ func (p *DataSourceProxyService) ProxyDataSourceRequest(c *models.ReqContext) { p.ProxyDatasourceRequestWithID(c, id) } +func (p *DataSourceProxyService) ProxyDatasourceRequestWithUID(c *models.ReqContext) { + c.TimeRequest(metrics.MDataSourceProxyReqTimer) + + dsUID := web.Params(c.Req)[":uid"] + if !util.IsValidShortUID(dsUID) { + c.JsonApiErr(http.StatusBadRequest, "UID is invalid", nil) + return + } + + ds, err := p.DataSourceCache.GetDatasourceByUID(c.Req.Context(), dsUID, c.SignedInUser, c.SkipCache) + if err != nil { + toAPIError(c, err) + return + } + p.proxyDatasourceRequest(c, ds) +} + func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqContext, dsID int64) { c.TimeRequest(metrics.MDataSourceProxyReqTimer) ds, err := p.DataSourceCache.GetDatasource(c.Req.Context(), dsID, c.SignedInUser, c.SkipCache) if err != nil { - if errors.Is(err, models.ErrDataSourceAccessDenied) { - c.JsonApiErr(http.StatusForbidden, "Access denied to datasource", err) - return - } - if errors.Is(err, models.ErrDataSourceNotFound) { - c.JsonApiErr(http.StatusNotFound, "Unable to find datasource", err) - return - } - c.JsonApiErr(http.StatusInternalServerError, "Unable to load datasource meta data", err) + toAPIError(c, err) + } + p.proxyDatasourceRequest(c, ds) +} + +func toAPIError(c *models.ReqContext, err error) { + if errors.Is(err, models.ErrDataSourceAccessDenied) { + c.JsonApiErr(http.StatusForbidden, "Access denied to datasource", err) return } + if errors.Is(err, models.ErrDataSourceNotFound) { + c.JsonApiErr(http.StatusNotFound, "Unable to find datasource", err) + return + } + c.JsonApiErr(http.StatusInternalServerError, "Unable to load datasource meta data", err) +} - err = p.PluginRequestValidator.Validate(ds.Url, c.Req) +func (p *DataSourceProxyService) proxyDatasourceRequest(c *models.ReqContext, ds *models.DataSource) { + err := p.PluginRequestValidator.Validate(ds.Url, c.Req) if err != nil { c.JsonApiErr(http.StatusForbidden, "Access denied", err) return @@ -103,7 +127,7 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqConte proxy.HandleRequest() } -var proxyPathRegexp = regexp.MustCompile(`^\/api\/datasources\/proxy\/[\d]+\/?`) +var proxyPathRegexp = regexp.MustCompile(`^\/api\/datasources\/proxy\/([\d]+|uid\/[\w]+)\/?`) func extractProxyPath(originalRawPath string) string { return proxyPathRegexp.ReplaceAllString(originalRawPath, "") diff --git a/pkg/services/datasourceproxy/datasourceproxy_test.go b/pkg/services/datasourceproxy/datasourceproxy_test.go index 4533fbf74d9..6567ba579c5 100644 --- a/pkg/services/datasourceproxy/datasourceproxy_test.go +++ b/pkg/services/datasourceproxy/datasourceproxy_test.go @@ -24,6 +24,18 @@ func TestDataProxy(t *testing.T) { "/api/datasources/proxy/54/api/services/afsd%2Fafsd/operations", "api/services/afsd%2Fafsd/operations", }, + { + "/api/datasources/proxy/uid/26MI0wZ7k", + "", + }, + { + "/api/datasources/proxy/uid/26MI0wZ7k/some/thing", + "some/thing", + }, + { + "/api/datasources/proxy/uid/26MI0wZ7k/api/services/afsd%2Fafsd/operations", + "api/services/afsd%2Fafsd/operations", + }, } for _, tc := range testCases { t.Run("Given raw path, should extract expected proxy path", func(t *testing.T) { From 6f31a69bfd9cd3480fcd156da41a0e87f265a138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Apr 2022 12:57:56 +0200 Subject: [PATCH 091/121] QueryEditorRow: Show query errors next to query in a consistent way across Grafana (#47613) * Show query errors under each query row * Testing a more plain error box * Font size * Make it green * Nit UI * Slight simplification of condition * New design * Update Co-authored-by: Dominik Prokop --- packages/grafana-data/src/types/datasource.ts | 2 +- packages/grafana-ui/src/types/icon.ts | 1 + .../features/explore/ErrorContainer.test.tsx | 4 +- .../app/features/explore/ErrorContainer.tsx | 9 +--- .../explore/ResponseErrorContainer.test.tsx | 7 ++-- .../explore/ResponseErrorContainer.tsx | 6 ++- .../inspector/InspectErrorTab.test.tsx | 4 +- .../query/components/QueryEditorRow.test.ts | 15 +++++++ .../query/components/QueryEditorRow.tsx | 20 ++++----- .../query/components/QueryErrorAlert.tsx | 41 +++++++++++++++++++ .../app/plugins/datasource/loki/datasource.ts | 7 +++- 11 files changed, 86 insertions(+), 30 deletions(-) create mode 100644 public/app/features/query/components/QueryErrorAlert.tsx diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 69717854e2b..1396140c41e 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -468,7 +468,7 @@ export interface DataQueryError { error?: string; }; message?: string; - status?: string; + status?: number; statusText?: string; refId?: string; type?: DataQueryErrorType; diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index de315cb978e..870cf8b2d82 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -69,6 +69,7 @@ export const getAvailableIcons = () => 'envelope', 'exchange-alt', 'exclamation-triangle', + 'exclamation-circle', 'external-link-alt', 'eye', 'eye-slash', diff --git a/public/app/features/explore/ErrorContainer.test.tsx b/public/app/features/explore/ErrorContainer.test.tsx index 589cdba349c..accc19c1877 100644 --- a/public/app/features/explore/ErrorContainer.test.tsx +++ b/public/app/features/explore/ErrorContainer.test.tsx @@ -11,7 +11,7 @@ describe('ErrorContainer', () => { error: 'Error data content', }, message: 'Error message', - status: 'Error status', + status: 500, statusText: 'Error status text', refId: 'A', }, @@ -30,7 +30,7 @@ describe('ErrorContainer', () => { message: 'Error data message', error: 'Error data content', }, - status: 'Error status', + status: 500, statusText: 'Error status text', refId: 'A', }, diff --git a/public/app/features/explore/ErrorContainer.tsx b/public/app/features/explore/ErrorContainer.tsx index 675b2dcf6f7..fffa9c10313 100644 --- a/public/app/features/explore/ErrorContainer.tsx +++ b/public/app/features/explore/ErrorContainer.tsx @@ -1,8 +1,7 @@ import React, { FunctionComponent } from 'react'; import { DataQueryError } from '@grafana/data'; -import { Alert, useTheme2 } from '@grafana/ui'; +import { Alert } from '@grafana/ui'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; -import { css } from '@emotion/css'; export interface ErrorContainerProps { queryError?: DataQueryError; @@ -10,18 +9,14 @@ export interface ErrorContainerProps { export const ErrorContainer: FunctionComponent = (props) => { const { queryError } = props; - const theme = useTheme2(); const showError = queryError ? true : false; const duration = showError ? 100 : 10; const title = queryError ? 'Query error' : 'Unknown error'; const message = queryError?.message || queryError?.data?.message || null; - const alertWithTopMargin = css` - margin-top: ${theme.spacing(2)}; - `; return ( - + {message} diff --git a/public/app/features/explore/ResponseErrorContainer.test.tsx b/public/app/features/explore/ResponseErrorContainer.test.tsx index 75982c68a2f..387dc9ee937 100644 --- a/public/app/features/explore/ResponseErrorContainer.test.tsx +++ b/public/app/features/explore/ResponseErrorContainer.test.tsx @@ -19,15 +19,14 @@ describe('ResponseErrorContainer', () => { expect(errorEl).toHaveTextContent(errorMessage); }); - it('shows error if there is refID', async () => { + it('do not show error if there is a refId', async () => { const errorMessage = 'test error'; setup({ refId: 'someId', message: errorMessage, }); - const errorEl = screen.getByTestId(selectors.components.Alert.alertV2('error')); - expect(errorEl).toBeInTheDocument(); - expect(errorEl).toHaveTextContent(errorMessage); + const errorEl = screen.queryByTestId(selectors.components.Alert.alertV2('error')); + expect(errorEl).not.toBeInTheDocument(); }); it('shows error.data.message if error.message does not exist', async () => { diff --git a/public/app/features/explore/ResponseErrorContainer.tsx b/public/app/features/explore/ResponseErrorContainer.tsx index a23ad7fa2f4..5dd5f59f154 100644 --- a/public/app/features/explore/ResponseErrorContainer.tsx +++ b/public/app/features/explore/ResponseErrorContainer.tsx @@ -9,8 +9,12 @@ interface Props { } export function ResponseErrorContainer(props: Props) { const queryResponse = useSelector((state: StoreState) => state.explore[props.exploreId]?.queryResponse); - const queryError = queryResponse?.state === LoadingState.Error ? queryResponse?.error : undefined; + // Errors with ref ids are shown below the corresponding query + if (queryError?.refId) { + return null; + } + return ; } diff --git a/public/app/features/inspector/InspectErrorTab.test.tsx b/public/app/features/inspector/InspectErrorTab.test.tsx index b04e1283d35..07531dbb3e3 100644 --- a/public/app/features/inspector/InspectErrorTab.test.tsx +++ b/public/app/features/inspector/InspectErrorTab.test.tsx @@ -62,11 +62,11 @@ describe('InspectErrorTab', () => { it('should return a jsonFormatter object of error if it has no .data and no .message', () => { const error = { - status: '400', + status: 400, }; const { container } = render(); expect(container.childElementCount).toEqual(1); expect(screen.getByText('status:')).toBeInTheDocument(); - expect(screen.getByText('"400"')).toBeInTheDocument(); + expect(screen.getByText('400')).toBeInTheDocument(); }); }); diff --git a/public/app/features/query/components/QueryEditorRow.test.ts b/public/app/features/query/components/QueryEditorRow.test.ts index 6117923d66c..34953929ce2 100644 --- a/public/app/features/query/components/QueryEditorRow.test.ts +++ b/public/app/features/query/components/QueryEditorRow.test.ts @@ -75,6 +75,21 @@ describe('filterPanelDataToQuery', () => { expect(panelDataA?.state).toBe(LoadingState.Done); }); + it('should return error for query that returns no data, but another query does return data', () => { + const withError = { + ...data, + state: LoadingState.Error, + error: { + message: 'Sad', + refId: 'Q', + }, + }; + + const panelDataB = filterPanelDataToQuery(withError, 'Q'); + expect(panelDataB?.series.length).toBe(0); + expect(panelDataB?.error?.refId).toBe('Q'); + }); + it('should not set the state to done if the frame is loading and has no errors', () => { const loadingData: PanelData = { state: LoadingState.Loading, diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index fb7a32444c3..d7a6f156d2e 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -32,6 +32,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { RowActionComponents } from './QueryActionComponent'; +import { QueryErrorAlert } from './QueryErrorAlert'; interface Props { data: PanelData; @@ -382,7 +383,7 @@ export class QueryEditorRow extends PureComponent extends PureComponent + {data?.error && data.error.refId === query.refId && } {visualization}
    @@ -463,16 +465,12 @@ export interface AngularQueryComponentScope { export function filterPanelDataToQuery(data: PanelData, refId: string): PanelData | undefined { const series = data.series.filter((series) => series.refId === refId); - // No matching series - if (!series.length) { - // If there was an error with no data, pass it to the QueryEditors - if (data.error && !data.series.length) { - return { - ...data, - state: LoadingState.Error, - }; - } - return undefined; + // If there was an error with no data, pass it to the QueryEditors + if (data.error && !data.series.length) { + return { + ...data, + state: LoadingState.Error, + }; } // Only say this is an error if the error links to the query diff --git a/public/app/features/query/components/QueryErrorAlert.tsx b/public/app/features/query/components/QueryErrorAlert.tsx new file mode 100644 index 00000000000..c7cfd73909b --- /dev/null +++ b/public/app/features/query/components/QueryErrorAlert.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { DataQueryError, GrafanaTheme2 } from '@grafana/data'; +import { Icon, useStyles2 } from '@grafana/ui'; +import { css } from '@emotion/css'; + +export interface Props { + error: DataQueryError; +} + +export function QueryErrorAlert({ error }: Props) { + const styles = useStyles2(getStyles); + + const message = error?.message ?? error?.data?.message ?? 'Query error'; + + return ( +
    +
    + +
    +
    {message}
    +
    + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css({ + marginTop: theme.spacing(0.5), + background: theme.colors.background.secondary, + display: 'flex', + }), + icon: css({ + background: theme.colors.error.main, + color: theme.colors.error.contrastText, + padding: theme.spacing(1), + }), + message: css({ + fontSize: theme.typography.bodySmall.fontSize, + fontFamily: theme.typography.fontFamilyMonospace, + padding: theme.spacing(1), + }), +}); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 41a71d3709f..9de4a813dbc 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -735,10 +735,13 @@ export class LokiDatasource } processError(err: FetchError, target: LokiQuery) { - let error = cloneDeep(err); - if (err.data.message.includes('escape') && target.expr.includes('\\')) { + let error: DataQueryError = cloneDeep(err); + error.refId = target.refId; + + if (error.data && err.data.message.includes('escape') && target.expr.includes('\\')) { error.data.message = `Error: ${err.data.message}. Make sure that all special characters are escaped with \\. For more information on escaping of special characters visit LogQL documentation at https://grafana.com/docs/loki/latest/logql/.`; } + return error; } From e50bd5cac8e714f0ff6694ca9473c3c3b60e5291 Mon Sep 17 00:00:00 2001 From: Ieva Date: Thu, 14 Apr 2022 12:40:15 +0100 Subject: [PATCH 092/121] Access control: expose SA frontend to users with the right permissions (#47727) * expose frontend to users with permissions * cover the ui endpoints * fix permissions --- pkg/api/accesscontrol.go | 7 ++++ pkg/api/api.go | 7 ++-- pkg/api/index.go | 9 +++-- .../ServiceAccountsListItem.tsx | 37 +++++++++++-------- .../features/serviceaccounts/state/actions.ts | 18 ++++++--- public/app/routes/routes.tsx | 4 +- public/app/types/accessControl.ts | 3 ++ 7 files changed, 57 insertions(+), 28 deletions(-) diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 6f08cfd6382..dffe67e03a8 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -5,6 +5,7 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/setting" ) @@ -462,6 +463,12 @@ var teamsEditAccessEvaluator = ac.EvalAll( ), ) +// apiKeyAccessEvaluator is used to protect the "Configuration > API keys" page access +var apiKeyAccessEvaluator = ac.EvalPermission(ac.ActionAPIKeyRead) + +// serviceAccountAccessEvaluator is used to protect the "Configuration > Service accounts" page access +var serviceAccountAccessEvaluator = ac.EvalPermission(serviceaccounts.ActionRead) + // Metadata helpers // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, diff --git a/pkg/api/api.go b/pkg/api/api.go index 4836363dfaf..a7de6111fab 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/web" ) @@ -62,9 +63,9 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index) r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index) r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index) - r.Get("/org/serviceaccounts", middleware.ReqOrgAdmin, hs.Index) - r.Get("/org/serviceaccounts/:serviceAccountId", middleware.ReqOrgAdmin, hs.Index) - r.Get("/org/apikeys/", reqOrgAdmin, hs.Index) + r.Get("/org/serviceaccounts", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index) + r.Get("/org/serviceaccounts/:serviceAccountId", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index) + r.Get("/org/apikeys/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyRead)), hs.Index) r.Get("/dashboard/import/", reqSignedIn, hs.Index) r.Get("/configuration", reqGrafanaAdmin, hs.Index) r.Get("/admin", reqGrafanaAdmin, hs.Index) diff --git a/pkg/api/index.go b/pkg/api/index.go index 3167a3012e0..6f1bf97ee54 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -149,8 +149,11 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) } func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool { - return (c.OrgRole == models.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == models.ROLE_EDITOR)) && - hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) + if !hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) { + return false + } + hasAccess := ac.HasAccess(hs.AccessControl, c) + return hasAccess(ac.ReqOrgAdmin, serviceAccountAccessEvaluator) } func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool { @@ -291,7 +294,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto }) } - if c.OrgRole == models.ROLE_ADMIN { + if hasAccess(ac.ReqOrgAdmin, apiKeyAccessEvaluator) { configNodes = append(configNodes, &dtos.NavLink{ Text: "API keys", Id: "apikeys", diff --git a/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx b/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx index e5d875a9be6..d0de3b2e9e9 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx @@ -28,7 +28,10 @@ const ServiceAccountListItem = memo( const editUrl = `org/serviceaccounts/${serviceAccount.id}`; const styles = useStyles2(getStyles); const canUpdateRole = contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsWrite, serviceAccount); - const rolePickerDisabled = !canUpdateRole; + const displayRolePicker = + contextSrv.hasPermission(AccessControlAction.ActionRolesList) && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesList); + const enableRolePicker = contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate) && canUpdateRole; return ( @@ -61,26 +64,30 @@ const ServiceAccountListItem = memo( {serviceAccount.login} - - {contextSrv.licensedAccessControlEnabled() ? ( - onRoleChange(newRole, serviceAccount)} - roleOptions={roleOptions} - builtInRoles={builtInRoles} - disabled={rolePickerDisabled} - /> - ) : ( + {contextSrv.licensedAccessControlEnabled() ? ( + displayRolePicker && ( + + onRoleChange(newRole, serviceAccount)} + roleOptions={roleOptions} + builtInRoles={builtInRoles} + disabled={!enableRolePicker} + /> + + ) + ) : ( + onRoleChange(newRole, serviceAccount)} /> - )} - + + )} { return async (dispatch) => { try { - const options = await fetchRoleOptions(); - dispatch(acOptionsLoaded(options)); - const builtInRoles = await fetchBuiltinRoles(); - dispatch(builtInRolesLoaded(builtInRoles)); + if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { + const options = await fetchRoleOptions(); + dispatch(acOptionsLoaded(options)); + } + if ( + contextSrv.licensedAccessControlEnabled() && + contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList) + ) { + const builtInRoles = await fetchBuiltinRoles(); + dispatch(builtInRolesLoaded(builtInRoles)); + } } catch (error) { console.error(error); } diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index bb8964345ca..b749467036d 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -195,14 +195,14 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/org/apikeys', - roles: () => ['Editor', 'Admin'], + roles: () => contextSrv.evaluatePermission(() => ['Admin'], [AccessControlAction.ActionAPIKeysRead]), component: SafeDynamicImport( () => import(/* webpackChunkName: "ApiKeysPage" */ 'app/features/api-keys/ApiKeysPage') ), }, { path: '/org/serviceaccounts', - roles: () => ['Editor', 'Admin'], + roles: () => contextSrv.evaluatePermission(() => ['Admin'], [AccessControlAction.ServiceAccountsRead]), component: SafeDynamicImport( () => import(/* webpackChunkName: "ServiceAccountsPage" */ 'app/features/serviceaccounts/ServiceAccountsListPage') diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index b2fd9b0ca2f..969985ba21a 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -23,6 +23,7 @@ export enum AccessControlAction { UsersQuotasList = 'users.quotas:list', UsersQuotasUpdate = 'users.quotas:update', + ServiceAccountsRead = 'serviceaccounts:read', ServiceAccountsCreate = 'serviceaccounts:create', ServiceAccountsWrite = 'serviceaccounts:write', ServiceAccountsDelete = 'serviceaccounts:delete', @@ -107,6 +108,8 @@ export enum AccessControlAction { // External alerting notifications actions. AlertingNotificationsExternalWrite = 'alert.notifications.external:write', AlertingNotificationsExternalRead = 'alert.notifications.external:read', + + ActionAPIKeysRead = 'apikeys:read', } export interface Role { From 90892050d094dcaef81211ae54b1eebb2b6bc15d Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Thu, 14 Apr 2022 14:56:14 +0300 Subject: [PATCH 093/121] CI: Remove `initialize` step (#47622) * Remove initialize step from pr.star * Remove initialize step from main.star * Remove initialize step from release.star * Reorder dependencies * Re-add identify runner step * Add enterprise steps * Remove is_downstream variable from starlark logic (#47715) * Make init steps depend on init-enterprise for enterprise pipelines * Exclude shellcheck from enterprise pipelines * Add init steps to enterprise integration tests pipeline * Add dependency for windows enterprise step * Update grabpl version --- .drone.yml | 699 ++++++++++++++++----------- scripts/drone/pipelines/docs.star | 17 +- scripts/drone/pipelines/main.star | 52 +- scripts/drone/pipelines/pr.star | 20 +- scripts/drone/pipelines/release.star | 73 ++- scripts/drone/steps/lib.star | 363 +++++++------- 6 files changed, 680 insertions(+), 544 deletions(-) diff --git a/.drone.yml b/.drone.yml index 4fc307c5c50..5ef3316da98 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,22 +9,32 @@ platform: os: linux services: [] steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl - commands: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner - commands: - - make gen-go + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - make gen-go + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install - commands: - ./bin/grabpl verify-drone depends_on: @@ -41,20 +51,18 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - depends_on: - - initialize image: grafana/build-container:1.5.3 name: codespell - commands: - ./bin/grabpl shellcheck depends_on: - - initialize + - grabpl image: grafana/build-container:1.5.3 name: shellcheck - commands: - ./bin/grabpl lint-backend --edition oss depends_on: - - initialize + - wire-install environment: CGO_ENABLED: "1" image: grafana/build-container:1.5.3 @@ -65,7 +73,7 @@ steps: - yarn run i18n:compile - yarn run typecheck depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -73,19 +81,19 @@ steps: - commands: - ./bin/grabpl test-backend --edition oss depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend - commands: - ./bin/grabpl integration-tests --edition oss depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-integration - commands: - yarn run ci:test-frontend depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -112,34 +120,46 @@ platform: os: linux services: [] steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl - commands: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner - commands: - - make gen-go + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - make gen-go + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install - commands: - ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --variants linux-amd64,linux-amd64-musl,darwin-amd64,windows-amd64,armv6 depends_on: - - initialize + - gen-version + - wire-install environment: {} image: grafana/build-container:1.5.3 name: build-backend - commands: - ./bin/grabpl build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -147,7 +167,8 @@ steps: - commands: - ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -155,7 +176,8 @@ steps: - commands: - ./bin/grabpl build-plugins --jobs 8 --edition oss depends_on: - - initialize + - gen-version + - yarn-install environment: null image: grafana/build-container:1.5.3 name: build-plugins @@ -356,10 +378,14 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl +- commands: + - echo $DRONE_RUNNER_NAME + image: alpine:3.15 + name: identify-runner - commands: - apt-get update - apt-get install -yq postgresql-client @@ -420,7 +446,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -429,11 +455,17 @@ steps: image: alpine:3.15 name: identify-runner - commands: - - make gen-go - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install - commands: - |- echo -e "unknwon @@ -444,14 +476,12 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - depends_on: - - initialize image: grafana/build-container:1.5.3 name: codespell - commands: - yarn run prettier:checkDocs depends_on: - - initialize + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -459,7 +489,8 @@ steps: - commands: - ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -503,7 +534,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -512,11 +543,17 @@ steps: image: alpine:3.15 name: identify-runner - commands: - - make gen-go - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install - commands: - |- echo -e "unknwon @@ -527,14 +564,12 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - depends_on: - - initialize image: grafana/build-container:1.5.3 name: codespell - commands: - yarn run prettier:checkDocs depends_on: - - initialize + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -542,7 +577,8 @@ steps: - commands: - ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -581,22 +617,32 @@ platform: os: linux services: [] steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl - commands: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner - commands: - - make gen-go + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - make gen-go + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install - commands: - ./bin/grabpl verify-drone depends_on: @@ -613,20 +659,18 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - depends_on: - - initialize image: grafana/build-container:1.5.3 name: codespell - commands: - ./bin/grabpl shellcheck depends_on: - - initialize + - grabpl image: grafana/build-container:1.5.3 name: shellcheck - commands: - ./bin/grabpl lint-backend --edition oss depends_on: - - initialize + - wire-install environment: CGO_ENABLED: "1" image: grafana/build-container:1.5.3 @@ -637,7 +681,7 @@ steps: - yarn run i18n:compile - yarn run typecheck depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -645,19 +689,19 @@ steps: - commands: - ./bin/grabpl test-backend --edition oss depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend - commands: - ./bin/grabpl integration-tests --edition oss depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-integration - commands: - yarn run ci:test-frontend depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -682,22 +726,32 @@ platform: os: linux services: [] steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl - commands: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner - commands: - - make gen-go + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - make gen-go + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install - commands: - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" --depth=1 @@ -739,14 +793,16 @@ steps: - commands: - ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - wire-install environment: {} image: grafana/build-container:1.5.3 name: build-backend - commands: - ./bin/grabpl build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -754,7 +810,8 @@ steps: - commands: - ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -762,7 +819,8 @@ steps: - commands: - ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin depends_on: - - initialize + - gen-version + - yarn-install environment: GRAFANA_API_KEY: from_secret: grafana_api_key @@ -1109,10 +1167,14 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl +- commands: + - echo $DRONE_RUNNER_NAME + image: alpine:3.15 + name: identify-runner - commands: - apt-get update - apt-get install -yq postgresql-client @@ -1177,10 +1239,10 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 - name: initialize + name: windows-init - commands: - $$gcpKey = $$env:GCP_KEY - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) @@ -1196,7 +1258,7 @@ steps: - gsutil cp $$fname gs://grafana-downloads/oss/main/ - gsutil cp "$$fname.sha256" gs://grafana-downloads/oss/main/ depends_on: - - initialize + - windows-init environment: GCP_KEY: from_secret: gcp_key @@ -1264,7 +1326,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1272,10 +1334,6 @@ steps: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner -- commands: - - make gen-go - image: grafana/build-container:1.5.3 - name: initialize - commands: - ./bin/grabpl store-packages --edition oss --gcp-key /tmp/gcpkey.json --build-id ${DRONE_BUILD_NUMBER} @@ -1346,28 +1404,38 @@ platform: os: linux services: [] steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl - commands: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner - commands: - - make gen-go - - ./bin/grabpl verify-version ${DRONE_TAG} + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: - ./bin/grabpl gen-version ${DRONE_TAG} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - make gen-go + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install - commands: - ./bin/grabpl build-backend --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} ${DRONE_TAG} depends_on: - - initialize + - gen-version + - wire-install environment: GITHUB_TOKEN: from_secret: github_token @@ -1377,7 +1445,8 @@ steps: - ./bin/grabpl build-frontend --jobs 8 --github-token $${GITHUB_TOKEN} --edition oss ${DRONE_TAG} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -1386,7 +1455,8 @@ steps: - ./bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN} --edition oss ${DRONE_TAG} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -1394,7 +1464,8 @@ steps: - commands: - ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin depends_on: - - initialize + - gen-version + - yarn-install environment: GRAFANA_API_KEY: from_secret: grafana_api_key @@ -1648,23 +1719,38 @@ platform: os: linux services: [] steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl - commands: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner - commands: - - make gen-go - - ./bin/grabpl verify-version ${DRONE_TAG} + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: - ./bin/grabpl gen-version ${DRONE_TAG} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - make gen-go + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install +- commands: + - ./bin/grabpl shellcheck + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: shellcheck - commands: - |- echo -e "unknwon @@ -1675,20 +1761,12 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - depends_on: - - initialize image: grafana/build-container:1.5.3 name: codespell -- commands: - - ./bin/grabpl shellcheck - depends_on: - - initialize - image: grafana/build-container:1.5.3 - name: shellcheck - commands: - ./bin/grabpl lint-backend --edition oss depends_on: - - initialize + - wire-install environment: CGO_ENABLED: "1" image: grafana/build-container:1.5.3 @@ -1699,7 +1777,7 @@ steps: - yarn run i18n:compile - yarn run typecheck depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -1707,19 +1785,19 @@ steps: - commands: - ./bin/grabpl test-backend --edition oss depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend - commands: - ./bin/grabpl integration-tests --edition oss depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-integration - commands: - yarn run ci:test-frontend depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -1771,7 +1849,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1779,13 +1857,6 @@ steps: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner -- commands: - - make gen-go - - ./bin/grabpl verify-version ${DRONE_TAG} - - ./bin/grabpl gen-version ${DRONE_TAG} - - yarn install --immutable - image: grafana/build-container:1.5.3 - name: initialize - commands: - apt-get update - apt-get install -yq postgresql-client @@ -1855,10 +1926,10 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 - name: initialize + name: windows-init - commands: - $$gcpKey = $$env:GCP_KEY - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) @@ -1873,7 +1944,7 @@ steps: - gsutil cp $$fname gs://%PRERELEASE_BUCKET%/artifacts/downloads/${DRONE_TAG}/oss/release/ - gsutil cp "$$fname.sha256" gs://%PRERELEASE_BUCKET%/artifacts/downloads/${DRONE_TAG}/oss/release/ depends_on: - - initialize + - windows-init environment: GCP_KEY: from_secret: gcp_key @@ -1914,7 +1985,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1940,22 +2011,37 @@ steps: - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - mkdir bin - mv /tmp/grabpl bin/ - - make gen-go - - ./bin/grabpl verify-version ${DRONE_TAG} - - ./bin/grabpl gen-version ${DRONE_TAG} - - yarn install --immutable depends_on: - clone-enterprise environment: GITHUB_TOKEN: from_secret: github_token image: grafana/build-container:1.5.3 - name: initialize + name: init-enterprise +- commands: + - make gen-go + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: yarn-install +- commands: + - ./bin/grabpl gen-version ${DRONE_TAG} + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: gen-version - commands: - ./bin/grabpl build-backend --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN} ${DRONE_TAG} depends_on: - - initialize + - gen-version + - wire-install environment: GITHUB_TOKEN: from_secret: github_token @@ -1965,7 +2051,8 @@ steps: - ./bin/grabpl build-frontend --jobs 8 --github-token $${GITHUB_TOKEN} --edition enterprise ${DRONE_TAG} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -1974,7 +2061,8 @@ steps: - ./bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN} --edition enterprise ${DRONE_TAG} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -1982,7 +2070,8 @@ steps: - commands: - ./bin/grabpl build-plugins --jobs 8 --edition enterprise --sign --signing-admin depends_on: - - initialize + - gen-version + - yarn-install environment: GRAFANA_API_KEY: from_secret: grafana_api_key @@ -2011,7 +2100,8 @@ steps: - ./bin/grabpl build-backend --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN} ${DRONE_TAG} depends_on: - - initialize + - gen-version + - wire-install environment: GITHUB_TOKEN: from_secret: github_token @@ -2268,7 +2358,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2294,17 +2384,31 @@ steps: - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - mkdir bin - mv /tmp/grabpl bin/ - - make gen-go - - ./bin/grabpl verify-version ${DRONE_TAG} - - ./bin/grabpl gen-version ${DRONE_TAG} - - yarn install --immutable depends_on: - clone-enterprise environment: GITHUB_TOKEN: from_secret: github_token image: grafana/build-container:1.5.3 - name: initialize + name: init-enterprise +- commands: + - make gen-go + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: yarn-install +- commands: + - ./bin/grabpl gen-version ${DRONE_TAG} + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: gen-version - commands: - |- echo -e "unknwon @@ -2315,20 +2419,12 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - depends_on: - - initialize image: grafana/build-container:1.5.3 name: codespell -- commands: - - ./bin/grabpl shellcheck - depends_on: - - initialize - image: grafana/build-container:1.5.3 - name: shellcheck - commands: - ./bin/grabpl lint-backend --edition enterprise depends_on: - - initialize + - wire-install environment: CGO_ENABLED: "1" image: grafana/build-container:1.5.3 @@ -2339,7 +2435,7 @@ steps: - yarn run i18n:compile - yarn run typecheck depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -2347,19 +2443,19 @@ steps: - commands: - ./bin/grabpl test-backend --edition enterprise depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend - commands: - ./bin/grabpl integration-tests --edition enterprise depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-integration - commands: - yarn run ci:test-frontend depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -2367,7 +2463,7 @@ steps: - commands: - ./bin/grabpl lint-backend --edition enterprise2 depends_on: - - initialize + - wire-install environment: CGO_ENABLED: "1" image: grafana/build-container:1.5.3 @@ -2375,13 +2471,13 @@ steps: - commands: - ./bin/grabpl test-backend --edition enterprise2 depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-enterprise2 - commands: - ./bin/grabpl integration-tests --edition enterprise2 depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-integration-enterprise2 trigger: @@ -2441,7 +2537,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2467,17 +2563,13 @@ steps: - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - mkdir bin - mv /tmp/grabpl bin/ - - make gen-go - - ./bin/grabpl verify-version ${DRONE_TAG} - - ./bin/grabpl gen-version ${DRONE_TAG} - - yarn install --immutable depends_on: - clone-enterprise environment: GITHUB_TOKEN: from_secret: github_token image: grafana/build-container:1.5.3 - name: initialize + name: init-enterprise - commands: - apt-get update - apt-get install -yq postgresql-client @@ -2486,7 +2578,7 @@ steps: - go clean -testcache - ./bin/grabpl integration-tests --database postgres depends_on: - - initialize + - init-enterprise environment: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest @@ -2502,7 +2594,7 @@ steps: - go clean -testcache - ./bin/grabpl integration-tests --database mysql depends_on: - - initialize + - init-enterprise environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql @@ -2512,7 +2604,7 @@ steps: - dockerize -wait tcp://redis:6379/0 -timeout 120s - ./bin/grabpl integration-tests depends_on: - - initialize + - init-enterprise environment: REDIS_URL: redis://redis:6379/0 image: grafana/build-container:1.5.3 @@ -2521,7 +2613,7 @@ steps: - dockerize -wait tcp://memcached:11211 -timeout 120s - ./bin/grabpl integration-tests depends_on: - - initialize + - init-enterprise environment: MEMCACHED_HOSTS: memcached:11211 image: grafana/build-container:1.5.3 @@ -2569,7 +2661,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -2592,7 +2684,7 @@ steps: GITHUB_TOKEN: from_secret: github_token image: grafana/ci-wix:0.1.1 - name: initialize + name: windows-init - commands: - $$gcpKey = $$env:GCP_KEY - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) @@ -2607,7 +2699,7 @@ steps: - gsutil cp $$fname gs://%PRERELEASE_BUCKET%/artifacts/downloads/${DRONE_TAG}/enterprise/release/ - gsutil cp "$$fname.sha256" gs://%PRERELEASE_BUCKET%/artifacts/downloads/${DRONE_TAG}/enterprise/release/ depends_on: - - initialize + - windows-init environment: GCP_KEY: from_secret: gcp_key @@ -2644,7 +2736,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2722,7 +2814,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2783,7 +2875,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2862,7 +2954,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2924,7 +3016,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2962,7 +3054,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2998,27 +3090,16 @@ platform: os: linux services: [] steps: -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.15 - name: identify-runner -- commands: - - make gen-go - - ./bin/grabpl verify-version ${DRONE_TAG} - - ./bin/grabpl gen-version ${DRONE_TAG} - - yarn install --immutable - image: grafana/build-container:1.5.3 - name: initialize - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl - commands: - ./bin/grabpl artifacts npm retrieve --tag v${TAG} depends_on: - - initialize + - grabpl environment: GCP_KEY: from_secret: gcp_key @@ -3061,7 +3142,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3122,33 +3203,45 @@ platform: os: linux services: [] steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl - commands: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner - commands: - - make gen-go + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - make gen-go + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install - commands: - ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - wire-install environment: {} image: grafana/build-container:1.5.3 name: build-backend - commands: - ./bin/grabpl build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -3156,7 +3249,8 @@ steps: - commands: - ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -3164,7 +3258,8 @@ steps: - commands: - ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin depends_on: - - initialize + - gen-version + - yarn-install environment: GRAFANA_API_KEY: from_secret: grafana_api_key @@ -3387,22 +3482,38 @@ platform: os: linux services: [] steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl - commands: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner - commands: - - make gen-go + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable + depends_on: + - grabpl image: grafana/build-container:1.5.3 - name: initialize + name: gen-version +- commands: + - make gen-go + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: yarn-install +- commands: + - ./bin/grabpl shellcheck + depends_on: + - grabpl + image: grafana/build-container:1.5.3 + name: shellcheck - commands: - |- echo -e "unknwon @@ -3413,20 +3524,12 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - depends_on: - - initialize image: grafana/build-container:1.5.3 name: codespell -- commands: - - ./bin/grabpl shellcheck - depends_on: - - initialize - image: grafana/build-container:1.5.3 - name: shellcheck - commands: - ./bin/grabpl lint-backend --edition oss depends_on: - - initialize + - wire-install environment: CGO_ENABLED: "1" image: grafana/build-container:1.5.3 @@ -3437,7 +3540,7 @@ steps: - yarn run i18n:compile - yarn run typecheck depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -3445,19 +3548,19 @@ steps: - commands: - ./bin/grabpl test-backend --edition oss depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend - commands: - ./bin/grabpl integration-tests --edition oss depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-integration - commands: - yarn run ci:test-frontend depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -3503,7 +3606,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3511,12 +3614,6 @@ steps: - echo $DRONE_RUNNER_NAME image: alpine:3.15 name: identify-runner -- commands: - - make gen-go - - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable - image: grafana/build-container:1.5.3 - name: initialize - commands: - apt-get update - apt-get install -yq postgresql-client @@ -3580,10 +3677,10 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 - name: initialize + name: windows-init - commands: - $$gcpKey = $$env:GCP_KEY - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) @@ -3593,7 +3690,7 @@ steps: - rm gcpkey.json - cp C:\App\nssm-2.24.zip . depends_on: - - initialize + - windows-init environment: GCP_KEY: from_secret: gcp_key @@ -3628,7 +3725,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3653,25 +3750,42 @@ steps: - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - mkdir bin - mv /tmp/grabpl bin/ - - make gen-go - - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable depends_on: - clone-enterprise environment: {} image: grafana/build-container:1.5.3 - name: initialize + name: init-enterprise +- commands: + - make gen-go + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: yarn-install +- commands: + - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: gen-version - commands: - ./bin/grabpl build-backend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - wire-install environment: {} image: grafana/build-container:1.5.3 name: build-backend - commands: - ./bin/grabpl build-frontend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -3680,7 +3794,8 @@ steps: - ./bin/grabpl build-frontend-packages --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} depends_on: - - initialize + - gen-version + - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 image: grafana/build-container:1.5.3 @@ -3688,7 +3803,8 @@ steps: - commands: - ./bin/grabpl build-plugins --jobs 8 --edition enterprise --sign --signing-admin depends_on: - - initialize + - gen-version + - yarn-install environment: GRAFANA_API_KEY: from_secret: grafana_api_key @@ -3717,7 +3833,8 @@ steps: - ./bin/grabpl build-backend --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER} --variants linux-amd64 depends_on: - - initialize + - gen-version + - wire-install environment: {} image: grafana/build-container:1.5.3 name: build-backend-enterprise2 @@ -3969,7 +4086,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3994,14 +4111,29 @@ steps: - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - mkdir bin - mv /tmp/grabpl bin/ - - make gen-go - - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable depends_on: - clone-enterprise environment: {} image: grafana/build-container:1.5.3 - name: initialize + name: init-enterprise +- commands: + - make gen-go + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: wire-install +- commands: + - yarn install --immutable + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: yarn-install +- commands: + - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} + depends_on: + - init-enterprise + image: grafana/build-container:1.5.3 + name: gen-version - commands: - |- echo -e "unknwon @@ -4012,20 +4144,12 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - depends_on: - - initialize image: grafana/build-container:1.5.3 name: codespell -- commands: - - ./bin/grabpl shellcheck - depends_on: - - initialize - image: grafana/build-container:1.5.3 - name: shellcheck - commands: - ./bin/grabpl lint-backend --edition enterprise depends_on: - - initialize + - wire-install environment: CGO_ENABLED: "1" image: grafana/build-container:1.5.3 @@ -4036,7 +4160,7 @@ steps: - yarn run i18n:compile - yarn run typecheck depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -4044,19 +4168,19 @@ steps: - commands: - ./bin/grabpl test-backend --edition enterprise depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend - commands: - ./bin/grabpl integration-tests --edition enterprise depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-integration - commands: - yarn run ci:test-frontend depends_on: - - initialize + - yarn-install environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.5.3 @@ -4064,7 +4188,7 @@ steps: - commands: - ./bin/grabpl lint-backend --edition enterprise2 depends_on: - - initialize + - wire-install environment: CGO_ENABLED: "1" image: grafana/build-container:1.5.3 @@ -4072,13 +4196,13 @@ steps: - commands: - ./bin/grabpl test-backend --edition enterprise2 depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-enterprise2 - commands: - ./bin/grabpl integration-tests --edition enterprise2 depends_on: - - initialize + - wire-install image: grafana/build-container:1.5.3 name: test-backend-integration-enterprise2 trigger: @@ -4132,7 +4256,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4157,14 +4281,11 @@ steps: - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - mkdir bin - mv /tmp/grabpl bin/ - - make gen-go - - ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER} - - yarn install --immutable depends_on: - clone-enterprise environment: {} image: grafana/build-container:1.5.3 - name: initialize + name: init-enterprise - commands: - apt-get update - apt-get install -yq postgresql-client @@ -4173,7 +4294,7 @@ steps: - go clean -testcache - ./bin/grabpl integration-tests --database postgres depends_on: - - initialize + - init-enterprise environment: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest @@ -4189,7 +4310,7 @@ steps: - go clean -testcache - ./bin/grabpl integration-tests --database mysql depends_on: - - initialize + - init-enterprise environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql @@ -4199,7 +4320,7 @@ steps: - dockerize -wait tcp://redis:6379/0 -timeout 120s - ./bin/grabpl integration-tests depends_on: - - initialize + - init-enterprise environment: REDIS_URL: redis://redis:6379/0 image: grafana/build-container:1.5.3 @@ -4208,7 +4329,7 @@ steps: - dockerize -wait tcp://memcached:11211 -timeout 120s - ./bin/grabpl integration-tests depends_on: - - initialize + - init-enterprise environment: MEMCACHED_HOSTS: memcached:11211 image: grafana/build-container:1.5.3 @@ -4250,7 +4371,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.34/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -4273,7 +4394,7 @@ steps: GITHUB_TOKEN: from_secret: github_token image: grafana/ci-wix:0.1.1 - name: initialize + name: windows-init - commands: - $$gcpKey = $$env:GCP_KEY - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) @@ -4283,7 +4404,7 @@ steps: - rm gcpkey.json - cp C:\App\nssm-2.24.zip . depends_on: - - initialize + - windows-init environment: GCP_KEY: from_secret: gcp_key @@ -4447,6 +4568,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 48fc25b65552d02f3d75f3ad1e24a9854c0485bebac8d4e062cfa8605e0a75ba +hmac: 6b2cdb67f637bdd527ba527741fd7d0af7e61b0700487be912821b93d8d16ca1 ... diff --git a/scripts/drone/pipelines/docs.star b/scripts/drone/pipelines/docs.star index fc6ce20cfa7..ababa390740 100644 --- a/scripts/drone/pipelines/docs.star +++ b/scripts/drone/pipelines/docs.star @@ -1,7 +1,9 @@ load( 'scripts/drone/steps/lib.star', 'build_image', - 'initialize_step', + 'yarn_install_step', + 'identify_runner_step', + 'gen_version_step', 'download_grabpl_step', 'lint_frontend_step', 'codespell_step', @@ -25,16 +27,17 @@ load( def docs_pipelines(edition, ver_mode, trigger): - steps = [download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) - - # Insert remaining steps - steps.extend([ + steps = [ + download_grabpl_step(), + identify_runner_step(), + gen_version_step(ver_mode), + yarn_install_step(), codespell_step(), lint_docs(), build_frontend_package_step(edition=edition, ver_mode=ver_mode), build_frontend_docs_step(edition=edition), build_docs_website_step(), - ]) + ] return pipeline( name='{}-docs'.format(ver_mode), edition=edition, trigger=trigger, services=[], steps=steps, @@ -45,7 +48,7 @@ def lint_docs(): 'name': 'lint-docs', 'image': build_image, 'depends_on': [ - 'initialize', + 'yarn-install', ], 'environment': { 'NODE_OPTIONS': '--max_old_space_size=8192', diff --git a/scripts/drone/pipelines/main.star b/scripts/drone/pipelines/main.star index fb3f671cdcb..e86e337593f 100644 --- a/scripts/drone/pipelines/main.star +++ b/scripts/drone/pipelines/main.star @@ -2,7 +2,10 @@ load( 'scripts/drone/steps/lib.star', 'download_grabpl_step', 'build_image', - 'initialize_step', + 'identify_runner_step', + 'gen_version_step', + 'wire_install_step', + 'yarn_install_step', 'lint_drone_step', 'lint_backend_step', 'lint_frontend_step', @@ -67,10 +70,16 @@ load('scripts/drone/vault.star', 'from_secret') ver_mode = 'main' -def get_steps(edition, is_downstream=False): +def get_steps(edition): services = integration_test_services(edition) - publish = edition != 'enterprise' or is_downstream include_enterprise2 = edition == 'enterprise' + init_steps = [ + identify_runner_step(), + download_grabpl_step(), + gen_version_step(ver_mode), + wire_install_step(), + yarn_install_step(), + ] test_steps = [ lint_drone_step(), codespell_step(), @@ -84,9 +93,9 @@ def get_steps(edition, is_downstream=False): build_steps = [ trigger_test_release(), enterprise_downstream_step(edition=edition), - build_backend_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream), - build_frontend_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream), - build_frontend_package_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream), + build_backend_step(edition=edition, ver_mode=ver_mode), + build_frontend_step(edition=edition, ver_mode=ver_mode), + build_frontend_package_step(edition=edition, ver_mode=ver_mode), build_plugins_step(edition=edition, sign=True), validate_scuemata_step(), ensure_cuetsified_step(), @@ -106,12 +115,12 @@ def get_steps(edition, is_downstream=False): test_backend_integration_step(edition=edition2), ]) build_steps.extend([ - build_backend_step(edition=edition2, ver_mode=ver_mode, variants=['linux-amd64'], is_downstream=is_downstream), + build_backend_step(edition=edition2, ver_mode=ver_mode, variants=['linux-amd64']), ]) # Insert remaining steps build_steps.extend([ - package_step(edition=edition, ver_mode=ver_mode, include_enterprise2=include_enterprise2, is_downstream=is_downstream), + package_step(edition=edition, ver_mode=ver_mode, include_enterprise2=include_enterprise2), grafana_server_step(edition=edition), e2e_tests_step('dashboards-suite', edition=edition), e2e_tests_step('smoke-tests-suite', edition=edition), @@ -134,27 +143,27 @@ def get_steps(edition, is_downstream=False): build_steps.extend([ release_canary_npm_packages_step(edition, trigger=trigger_oss), - upload_packages_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream, trigger=trigger_oss), + upload_packages_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss), upload_cdn_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss) ]) if include_enterprise2: edition2 = 'enterprise2' build_steps.extend([ - package_step(edition=edition2, ver_mode=ver_mode, include_enterprise2=include_enterprise2, variants=['linux-amd64'], is_downstream=is_downstream), - upload_packages_step(edition=edition2, ver_mode=ver_mode, is_downstream=is_downstream), + package_step(edition=edition2, ver_mode=ver_mode, include_enterprise2=include_enterprise2, variants=['linux-amd64']), + upload_packages_step(edition=edition2, ver_mode=ver_mode), upload_cdn_step(edition=edition2, ver_mode=ver_mode) ]) - windows_steps = get_windows_steps(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream) - if edition == 'enterprise' and not is_downstream: + windows_steps = get_windows_steps(edition=edition, ver_mode=ver_mode) + if edition == 'enterprise': store_steps = [] else: store_steps = [ - store_packages_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream), + store_packages_step(edition=edition, ver_mode=ver_mode), ] - return test_steps, build_steps, integration_test_steps, windows_steps, store_steps + return init_steps, test_steps, build_steps, integration_test_steps, windows_steps, store_steps def trigger_test_release(): return { @@ -210,7 +219,7 @@ def main_pipelines(edition): ], }, } - test_steps, build_steps, integration_test_steps, windows_steps, store_steps = get_steps(edition=edition) + init_steps, test_steps, build_steps, integration_test_steps, windows_steps, store_steps = get_steps(edition=edition) if edition == 'enterprise': services.append(ldap_service()) @@ -218,27 +227,26 @@ def main_pipelines(edition): pipelines = [docs_pipelines(edition, ver_mode, trigger), pipeline( name='main-test', edition=edition, trigger=trigger, services=[], - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + test_steps, + steps=init_steps + test_steps, volumes=[], ), pipeline( name='main-build-e2e-publish', edition=edition, trigger=trigger, services=[], - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + build_steps, + steps=init_steps + build_steps, volumes=volumes, ), pipeline( name='main-integration-tests', edition=edition, trigger=trigger, services=services, - steps=[download_grabpl_step()] + integration_test_steps, + steps=[download_grabpl_step(), identify_runner_step(),] + integration_test_steps, volumes=volumes, ), pipeline( name='windows-main', edition=edition, trigger=dict(trigger, repo=['grafana/grafana']), - steps=initialize_step(edition, platform='windows', ver_mode=ver_mode) + windows_steps, + steps=[identify_runner_step('windows')] + windows_steps, depends_on=['main-test', 'main-build-e2e-publish', 'main-integration-tests'], platform='windows', ), notify_pipeline( name='notify-drone-changes', slack_channel='slack-webhooks-test', trigger=drone_change_trigger, template=drone_change_template, secret='drone-changes-webhook', ), pipeline( name='publish-main', edition=edition, trigger=dict(trigger, repo=['grafana/grafana']), - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode, - install_deps=False) + store_steps, + steps=[download_grabpl_step(), identify_runner_step(),] + store_steps, depends_on=['main-test', 'main-build-e2e-publish', 'main-integration-tests', 'windows-main', ], ), notify_pipeline( name='notify-main', slack_channel='grafana-ci-notifications', trigger=dict(trigger, status=['failure']), diff --git a/scripts/drone/pipelines/pr.star b/scripts/drone/pipelines/pr.star index 1f768f1f5a2..d14425cc84e 100644 --- a/scripts/drone/pipelines/pr.star +++ b/scripts/drone/pipelines/pr.star @@ -1,7 +1,10 @@ load( 'scripts/drone/steps/lib.star', 'download_grabpl_step', - 'initialize_step', + 'gen_version_step', + 'yarn_install_step', + 'wire_install_step', + 'identify_runner_step', 'lint_drone_step', 'lint_backend_step', 'lint_frontend_step', @@ -59,6 +62,13 @@ def pr_pipelines(edition): volumes = integration_test_services_volumes() variants = ['linux-amd64', 'linux-amd64-musl', 'darwin-amd64', 'windows-amd64', 'armv6',] include_enterprise2 = edition == 'enterprise' + init_steps = [ + identify_runner_step(), + download_grabpl_step(), + gen_version_step(ver_mode), + wire_install_step(), + yarn_install_step(), + ] test_steps = [ lint_drone_step(), codespell_step(), @@ -132,14 +142,12 @@ def pr_pipelines(edition): return [ pipeline( - name='pr-test', edition=edition, trigger=trigger, services=[], steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) - + test_steps, + name='pr-test', edition=edition, trigger=trigger, services=[], steps=init_steps + test_steps, ), pipeline( - name='pr-build-e2e', edition=edition, trigger=trigger, services=[], steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) - + build_steps, + name='pr-build-e2e', edition=edition, trigger=trigger, services=[], steps=init_steps + build_steps, ), pipeline( name='pr-integration-tests', edition=edition, trigger=trigger, services=services, - steps=[download_grabpl_step()] + integration_test_steps, + steps=[download_grabpl_step(), identify_runner_step(),] + integration_test_steps, volumes=volumes, ), docs_pipelines(edition, ver_mode, trigger_docs()) ] diff --git a/scripts/drone/pipelines/release.star b/scripts/drone/pipelines/release.star index 0e10c9892fa..cb7a8e9cb6b 100644 --- a/scripts/drone/pipelines/release.star +++ b/scripts/drone/pipelines/release.star @@ -1,10 +1,15 @@ load( 'scripts/drone/steps/lib.star', 'disable_tests', + 'clone_enterprise_step', 'download_grabpl_step', - 'initialize_step', + 'gen_version_step', + 'yarn_install_step', + 'wire_install_step', + 'init_enterprise_step', 'lint_drone_step', 'build_image', + 'identify_runner_step', 'publish_image', 'lint_backend_step', 'lint_frontend_step', @@ -77,7 +82,7 @@ def retrieve_npm_packages_step(): 'name': 'retrieve-npm-packages', 'image': publish_image, 'depends_on': [ - 'initialize', + 'grabpl', ], 'environment': { 'GCP_KEY': from_secret('gcp_key'), @@ -150,16 +155,26 @@ def get_steps(edition, ver_mode): should_upload = should_publish or ver_mode in ('release-branch',) include_enterprise2 = edition == 'enterprise' edition2 = 'enterprise2' + init_steps = [ + identify_runner_step(), + download_grabpl_step(), + gen_version_step(ver_mode), + wire_install_step(), + yarn_install_step(), + ] - test_steps = [ + test_steps = [] + if edition != 'enterprise': + test_steps.extend([shellcheck_step()]) + + test_steps.extend([ codespell_step(), - shellcheck_step(), lint_backend_step(edition=edition), lint_frontend_step(), test_backend_step(edition=edition), test_backend_integration_step(edition=edition), test_frontend_step(), - ] + ]) build_steps = [ build_backend_step(edition=edition, ver_mode=ver_mode), @@ -233,16 +248,16 @@ def get_steps(edition, ver_mode): if step: publish_steps.append(step) - return test_steps, build_steps, integration_test_steps, package_steps, windows_package_steps, publish_steps + return init_steps, test_steps, build_steps, integration_test_steps, package_steps, windows_package_steps, publish_steps def get_oss_pipelines(trigger, ver_mode): edition = 'oss' services = integration_test_services(edition=edition) volumes = integration_test_services_volumes() - test_steps, build_steps, integration_test_steps, package_steps, windows_package_steps, publish_steps = get_steps(edition=edition, ver_mode=ver_mode) + init_steps, test_steps, build_steps, integration_test_steps, package_steps, windows_package_steps, publish_steps = get_steps(edition=edition, ver_mode=ver_mode) windows_pipeline = pipeline( name='oss-windows-{}'.format(ver_mode), edition=edition, trigger=trigger, - steps=initialize_step(edition, platform='windows', ver_mode=ver_mode) + windows_package_steps, + steps=[identify_runner_step('windows')] + windows_package_steps, platform='windows', depends_on=[ 'oss-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), ], @@ -250,8 +265,7 @@ def get_oss_pipelines(trigger, ver_mode): pipelines = [ pipeline( name='oss-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), edition=edition, trigger=trigger, services=[], - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + - build_steps + package_steps + publish_steps, + steps=init_steps + build_steps + package_steps + publish_steps, volumes=volumes, ), ] @@ -259,14 +273,12 @@ def get_oss_pipelines(trigger, ver_mode): pipelines.extend([ pipeline( name='oss-test-{}'.format(ver_mode), edition=edition, trigger=trigger, services=[], - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + - test_steps, + steps=init_steps + test_steps, volumes=[], ), pipeline( name='oss-integration-tests-{}'.format(ver_mode), edition=edition, trigger=trigger, services=services, - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + - integration_test_steps, + steps=[download_grabpl_step(), identify_runner_step(),] + integration_test_steps, volumes=volumes, ) ]) @@ -286,10 +298,28 @@ def get_enterprise_pipelines(trigger, ver_mode): edition = 'enterprise' services = integration_test_services(edition=edition) volumes = integration_test_services_volumes() - test_steps, build_steps, integration_test_steps, package_steps, windows_package_steps, publish_steps = get_steps(edition=edition, ver_mode=ver_mode) + deps_on_clone_enterprise_step = { + 'depends_on': [ + 'init-enterprise', + ] + } + _, test_steps, build_steps, integration_test_steps, package_steps, windows_package_steps, publish_steps = get_steps(edition=edition, ver_mode=ver_mode) + init_steps = [ + download_grabpl_step(), + identify_runner_step(), + clone_enterprise_step(ver_mode), + init_enterprise_step(ver_mode) + ] + for step in [wire_install_step(), yarn_install_step(), gen_version_step(ver_mode)]: + step.update(deps_on_clone_enterprise_step) + init_steps.extend([step]) + + for step in integration_test_steps: + step.update(deps_on_clone_enterprise_step) + windows_pipeline = pipeline( name='enterprise-windows-{}'.format(ver_mode), edition=edition, trigger=trigger, - steps=initialize_step(edition, platform='windows', ver_mode=ver_mode) + windows_package_steps, + steps=[identify_runner_step('windows')] + windows_package_steps, platform='windows', depends_on=[ 'enterprise-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), ], @@ -297,8 +327,7 @@ def get_enterprise_pipelines(trigger, ver_mode): pipelines = [ pipeline( name='enterprise-build{}-publish-{}'.format(get_e2e_suffix(), ver_mode), edition=edition, trigger=trigger, services=[], - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + - build_steps + package_steps + publish_steps, + steps=init_steps + build_steps + package_steps + publish_steps, volumes=volumes, ), ] @@ -306,14 +335,12 @@ def get_enterprise_pipelines(trigger, ver_mode): pipelines.extend([ pipeline( name='enterprise-test-{}'.format(ver_mode), edition=edition, trigger=trigger, services=[], - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + - test_steps, + steps=init_steps + test_steps, volumes=[], ), pipeline( name='enterprise-integration-tests-{}'.format(ver_mode), edition=edition, trigger=trigger, services=services, - steps=[download_grabpl_step()] + initialize_step(edition, platform='linux', ver_mode=ver_mode) + - integration_test_steps, + steps=[download_grabpl_step(), identify_runner_step(), clone_enterprise_step(ver_mode), init_enterprise_step(ver_mode),] + integration_test_steps, volumes=volumes, ), ]) @@ -401,7 +428,7 @@ def publish_npm_pipelines(mode): ] return [pipeline( - name='publish-npm-packages-{}'.format(mode), trigger=trigger, steps = initialize_step(edition='oss', platform='linux', ver_mode='release') + steps, edition="all" + name='publish-npm-packages-{}'.format(mode), trigger=trigger, steps = steps, edition="all" )] def release_pipelines(ver_mode='release', trigger=None, environment=None): diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 3ba6b73a9d2..44e4c6ae5b7 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1,6 +1,6 @@ load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket') -grabpl_version = 'v2.9.34' +grabpl_version = 'v2.9.35' build_image = 'grafana/build-container:1.5.3' publish_image = 'grafana/grafana-ci-deploy:1.3.1' deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image' @@ -18,6 +18,7 @@ trigger_oss = { } } + def slack_step(channel, template, secret): return { 'name': 'slack', @@ -30,106 +31,49 @@ def slack_step(channel, template, secret): } -def initialize_step(edition, platform, ver_mode, is_downstream=False, install_deps=True): - if platform == 'windows': - return [ - { - 'name': 'identify-runner', - 'image': windows_image, - 'commands': [ - 'echo $env:DRONE_RUNNER_NAME', - ], - }, - ] - - common_cmds = [ - # Generate Go code, will install Wire - # TODO: Install Wire in Docker image instead - 'make gen-go', - ] - +def gen_version_step(ver_mode): if ver_mode == 'release': args = '${DRONE_TAG}' - common_cmds.append('./bin/grabpl verify-version ${DRONE_TAG}') else: - if not is_downstream: - build_no = '${DRONE_BUILD_NUMBER}' - else: - build_no = '$${SOURCE_BUILD_NUMBER}' + build_no = '${DRONE_BUILD_NUMBER}' args = '--build-id {}'.format(build_no) - - identify_runner = identify_runner_step(platform) - - if install_deps: - common_cmds.extend([ + return { + 'name': 'gen-version', + 'image': build_image, + 'depends_on': [ + 'grabpl', + ], + 'commands': [ './bin/grabpl gen-version {}'.format(args), + ], + } + + +def yarn_install_step(): + return { + 'name': 'yarn-install', + 'image': build_image, + 'commands': [ 'yarn install --immutable', - ]) - if edition in ('enterprise', 'enterprise2'): - source_commit = '' - if ver_mode == 'release': - committish = '${DRONE_TAG}' - source_commit = ' ${DRONE_TAG}' - environment = { - 'GITHUB_TOKEN': from_secret(github_token), - } - token = "--github-token $${GITHUB_TOKEN}" - elif ver_mode == 'release-branch': - committish = '${DRONE_BRANCH}' - environment = {} - token = "" - else: - environment = {} - if is_downstream: - source_commit = ' $${SOURCE_COMMIT}' - committish = '${DRONE_COMMIT}' - token = "" - steps = [ - identify_runner, - clone_enterprise(committish), - { - 'name': 'initialize', - 'image': build_image, - 'depends_on': [ - 'clone-enterprise', - ], - 'environment': environment, - 'commands': [ - 'mv bin/grabpl /tmp/', - 'rmdir bin', - 'mv grafana-enterprise /tmp/', - '/tmp/grabpl init-enterprise {} /tmp/grafana-enterprise{}'.format(token, source_commit), - 'mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json', - 'mkdir bin', - 'mv /tmp/grabpl bin/' - ] + common_cmds, - }, - ] - - return steps - - steps = [ - identify_runner, - { - 'name': 'initialize', - 'image': build_image, - 'commands': common_cmds, - }, - ] - - return steps + ], + 'depends_on': [ + 'grabpl', + ], + } -def identify_runner_step(platform): - if platform == 'linux': - return { - 'name': 'identify-runner', - 'image': alpine_image, - 'commands': [ - 'echo $DRONE_RUNNER_NAME', - ], - } - else: +def wire_install_step(): + return { + 'name': 'wire-install', + 'image': build_image, + 'commands': [ + 'make gen-go', + ], + } + + +def identify_runner_step(platform='linux'): + if platform == 'windows': return { 'name': 'identify-runner', 'image': windows_image, @@ -137,9 +81,23 @@ def identify_runner_step(platform): 'echo $env:DRONE_RUNNER_NAME', ], } + else: + return { + 'name': 'identify-runner', + 'image': alpine_image, + 'commands': [ + 'echo $DRONE_RUNNER_NAME', + ], + } -def clone_enterprise(committish): +def clone_enterprise_step(ver_mode): + if ver_mode == 'release': + committish = '${DRONE_TAG}' + elif ver_mode == 'release-branch': + committish = '${DRONE_BRANCH}' + else: + committish = '${DRONE_COMMIT}' return { 'name': 'clone-enterprise', 'image': build_image, @@ -153,8 +111,51 @@ def clone_enterprise(committish): ], } +def init_enterprise_step(ver_mode): + source_commit = '' + if ver_mode == 'release': + source_commit = ' ${DRONE_TAG}' + environment = { + 'GITHUB_TOKEN': from_secret(github_token), + } + token = "--github-token $${GITHUB_TOKEN}" + elif ver_mode == 'release-branch': + environment = {} + token = "" + else: + environment = {} + token = "" + return { + 'name': 'init-enterprise', + 'image': build_image, + 'depends_on': [ + 'clone-enterprise', + ], + 'environment': environment, + 'commands': [ + 'mv bin/grabpl /tmp/', + 'rmdir bin', + 'mv grafana-enterprise /tmp/', + '/tmp/grabpl init-enterprise {} /tmp/grafana-enterprise{}'.format(token, source_commit), + 'mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json', + 'mkdir bin', + 'mv /tmp/grabpl bin/' + ], + } + + +def download_grabpl_step(platform="linux"): + if platform == 'windows': + return { + 'name': 'grabpl', + 'image': wix_image, + 'commands': [ + '$$ProgressPreference = "SilentlyContinue"', + 'Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/{}/windows/grabpl.exe -OutFile grabpl.exe'.format( + grabpl_version), + ] + } -def download_grabpl_step(): return { 'name': 'grabpl', 'image': curl_image, @@ -211,7 +212,7 @@ def lint_backend_step(edition): 'CGO_ENABLED': '1', }, 'depends_on': [ - 'initialize', + 'wire-install', ], 'commands': [ # Don't use Make since it will re-download the linters @@ -224,9 +225,6 @@ def benchmark_ldap_step(): return { 'name': 'benchmark-ldap', 'image': build_image, - 'depends_on': [ - 'initialize', - ], 'environment': { 'LDAP_HOSTNAME': 'ldap', }, @@ -263,7 +261,6 @@ def store_storybook_step(edition, ver_mode, trigger=None): if edition in ('enterprise', 'enterprise2'): return None - commands = [] if ver_mode == 'release': commands.extend([ @@ -273,12 +270,12 @@ def store_storybook_step(edition, ver_mode, trigger=None): else: # main pipelines should deploy storybook to grafana-storybook/canary public bucket - commands = ['./bin/grabpl store-storybook --deployment canary --src-bucket grafana-storybook',] + commands = ['./bin/grabpl store-storybook --deployment canary --src-bucket grafana-storybook', ] step = { 'name': 'store-storybook', 'image': publish_image, - 'depends_on': ['build-storybook',] + end_to_end_tests_deps(edition), + 'depends_on': ['build-storybook', ] + end_to_end_tests_deps(edition), 'environment': { 'GCP_KEY': from_secret('gcp_key'), 'PRERELEASE_BUCKET': from_secret(prerelease_bucket) @@ -289,6 +286,7 @@ def store_storybook_step(edition, ver_mode, trigger=None): step.update(trigger) return step + def e2e_tests_artifacts(edition): return { 'name': 'e2e-tests-artifacts-upload' + enterprise2_suffix(edition), @@ -363,7 +361,7 @@ def upload_cdn_step(edition, ver_mode, trigger=None): return step -def build_backend_step(edition, ver_mode, variants=None, is_downstream=False): +def build_backend_step(edition, ver_mode, variants=None): variants_str = '' if variants: variants_str = ' --variants {}'.format(','.join(variants)) @@ -379,10 +377,7 @@ def build_backend_step(edition, ver_mode, variants=None, is_downstream=False): ), ] else: - if not is_downstream: - build_no = '${DRONE_BUILD_NUMBER}' - else: - build_no = '$${SOURCE_BUILD_NUMBER}' + build_no = '${DRONE_BUILD_NUMBER}' env = {} cmds = [ './bin/grabpl build-backend --jobs 8 --edition {} --build-id {}{}'.format( @@ -393,19 +388,17 @@ def build_backend_step(edition, ver_mode, variants=None, is_downstream=False): return { 'name': 'build-backend' + enterprise2_suffix(edition), 'image': build_image, - 'depends_on': [ - 'initialize', - ], 'environment': env, + 'depends_on': [ + 'gen-version', + 'wire-install', + ], 'commands': cmds, } -def build_frontend_step(edition, ver_mode, is_downstream=False): - if not is_downstream: - build_no = '${DRONE_BUILD_NUMBER}' - else: - build_no = '$${SOURCE_BUILD_NUMBER}' +def build_frontend_step(edition, ver_mode): + build_no = '${DRONE_BUILD_NUMBER}' # TODO: Use percentage for num jobs if ver_mode == 'release': @@ -422,42 +415,42 @@ def build_frontend_step(edition, ver_mode, is_downstream=False): return { 'name': 'build-frontend', 'image': build_image, - 'depends_on': [ - 'initialize', - ], 'environment': { 'NODE_OPTIONS': '--max_old_space_size=8192', }, + 'depends_on': [ + 'gen-version', + 'yarn-install', + ], 'commands': cmds, } -def build_frontend_package_step(edition, ver_mode, is_downstream=False): - if not is_downstream: - build_no = '${DRONE_BUILD_NUMBER}' - else: - build_no = '$${SOURCE_BUILD_NUMBER}' + +def build_frontend_package_step(edition, ver_mode): + build_no = '${DRONE_BUILD_NUMBER}' # TODO: Use percentage for num jobs if ver_mode == 'release': cmds = [ './bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN} ' + \ '--edition {} ${{DRONE_TAG}}'.format(edition), - ] + ] else: cmds = [ './bin/grabpl build-frontend-packages --jobs 8 --edition {} '.format(edition) + \ '--build-id {}'.format(build_no), - ] + ] return { 'name': 'build-frontend-packages', 'image': build_image, - 'depends_on': [ - 'initialize', - ], 'environment': { 'NODE_OPTIONS': '--max_old_space_size=8192', }, + 'depends_on': [ + 'gen-version', + 'yarn-install', + ], 'commands': cmds, } @@ -487,10 +480,11 @@ def build_plugins_step(edition, sign=False): return { 'name': 'build-plugins', 'image': build_image, - 'depends_on': [ - 'initialize', - ], 'environment': env, + 'depends_on': [ + 'gen-version', + 'yarn-install', + ], 'commands': [ # TODO: Use percentage for num jobs './bin/grabpl build-plugins --jobs 8 --edition {}{}'.format(edition, sign_args), @@ -503,7 +497,7 @@ def test_backend_step(edition): 'name': 'test-backend' + enterprise2_suffix(edition), 'image': build_image, 'depends_on': [ - 'initialize', + 'wire-install', ], 'commands': [ './bin/grabpl test-backend --edition {}'.format(edition), @@ -516,7 +510,7 @@ def test_backend_integration_step(edition): 'name': 'test-backend-integration' + enterprise2_suffix(edition), 'image': build_image, 'depends_on': [ - 'initialize', + 'wire-install', ], 'commands': [ './bin/grabpl integration-tests --edition {}'.format(edition), @@ -528,12 +522,12 @@ def test_frontend_step(): return { 'name': 'test-frontend', 'image': build_image, - 'depends_on': [ - 'initialize', - ], 'environment': { 'TEST_MAX_WORKERS': '50%', }, + 'depends_on': [ + 'yarn-install', + ], 'commands': [ 'yarn run ci:test-frontend', ], @@ -544,16 +538,16 @@ def lint_frontend_step(): return { 'name': 'lint-frontend', 'image': build_image, - 'depends_on': [ - 'initialize', - ], 'environment': { 'TEST_MAX_WORKERS': '50%', }, + 'depends_on': [ + 'yarn-install', + ], 'commands': [ 'yarn run prettier:check', 'yarn run lint', - 'yarn run i18n:compile', # TODO: right place for this? + 'yarn run i18n:compile', # TODO: right place for this? 'yarn run typecheck', ], } @@ -617,9 +611,6 @@ def codespell_step(): return { 'name': 'codespell', 'image': build_image, - 'depends_on': [ - 'initialize', - ], 'commands': [ # Important: all words have to be in lowercase, and separated by "\n". 'echo -e "unknwon\nreferer\nerrorstring\neror\niam\nwan" > words_to_ignore.txt', @@ -634,7 +625,7 @@ def shellcheck_step(): 'name': 'shellcheck', 'image': build_image, 'depends_on': [ - 'initialize', + 'grabpl', ], 'commands': [ './bin/grabpl shellcheck', @@ -642,7 +633,7 @@ def shellcheck_step(): } -def package_step(edition, ver_mode, include_enterprise2=False, variants=None, is_downstream=False): +def package_step(edition, ver_mode, include_enterprise2=False, variants=None): deps = [ 'build-plugins', 'build-backend', @@ -683,10 +674,7 @@ def package_step(edition, ver_mode, include_enterprise2=False, variants=None, is ), ] else: - if not is_downstream: - build_no = '${DRONE_BUILD_NUMBER}' - else: - build_no = '$${SOURCE_BUILD_NUMBER}' + build_no = '${DRONE_BUILD_NUMBER}' cmds = [ '{}./bin/grabpl package --jobs 8 --edition {} '.format(test_args, edition) + \ '--build-id {}{}{}'.format(build_no, variants_str, sign_args), @@ -731,6 +719,7 @@ def grafana_server_step(edition, port=3001): ], } + def e2e_tests_step(suite, edition, port=3001, tries=None): cmd = './bin/grabpl e2e-tests --port {} --suite {}'.format(port, suite) if tries: @@ -810,13 +799,15 @@ def build_docker_images_step(edition, ver_mode, archs=None, ubuntu=False, publis }, } + def publish_images_step(edition, ver_mode, mode, docker_repo, trigger=None): if mode == 'security': mode = '--{} '.format(mode) else: mode = '' - cmd = './bin/grabpl artifacts docker publish {}--dockerhub-repo {} --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7'.format(mode, docker_repo) + cmd = './bin/grabpl artifacts docker publish {}--dockerhub-repo {} --base alpine --base ubuntu --arch amd64 --arch arm64 --arch armv7'.format( + mode, docker_repo) if ver_mode == 'release': deps = ['fetch-images-{}'.format(edition)] @@ -847,10 +838,7 @@ def publish_images_step(edition, ver_mode, mode, docker_repo, trigger=None): def postgres_integration_tests_step(edition, ver_mode): deps = [] - if edition in ('enterprise', 'enterprise2') and ver_mode in ('release-branch', 'release'): - deps.extend(['initialize']) - else: - deps.extend(['grabpl']) + deps.extend(['grabpl']) return { 'name': 'postgres-integration-tests', 'image': build_image, @@ -875,10 +863,7 @@ def postgres_integration_tests_step(edition, ver_mode): def mysql_integration_tests_step(edition, ver_mode): deps = [] - if edition in ('enterprise', 'enterprise2') and ver_mode in ('release-branch', 'release'): - deps.extend(['initialize']) - else: - deps.extend(['grabpl']) + deps.extend(['grabpl']) return { 'name': 'mysql-integration-tests', 'image': build_image, @@ -901,10 +886,7 @@ def mysql_integration_tests_step(edition, ver_mode): def redis_integration_tests_step(edition, ver_mode): deps = [] - if edition in ('enterprise', 'enterprise2') and ver_mode in ('release-branch', 'release'): - deps.extend(['initialize']) - else: - deps.extend(['grabpl']) + deps.extend(['grabpl']) return { 'name': 'redis-integration-tests', 'image': build_image, @@ -921,10 +903,7 @@ def redis_integration_tests_step(edition, ver_mode): def memcached_integration_tests_step(edition, ver_mode): deps = [] - if edition in ('enterprise', 'enterprise2') and ver_mode in ('release-branch', 'release'): - deps.extend(['initialize']) - else: - deps.extend(['grabpl']) + deps.extend(['grabpl']) return { 'name': 'memcached-integration-tests', 'image': build_image, @@ -965,15 +944,16 @@ def enterprise2_suffix(edition): return '' -def upload_packages_step(edition, ver_mode, is_downstream=False, trigger=None): - if ver_mode == 'main' and edition in ('enterprise', 'enterprise2') and not is_downstream: +def upload_packages_step(edition, ver_mode, trigger=None): + if ver_mode == 'main' and edition in ('enterprise', 'enterprise2'): return None if ver_mode == 'release': packages_bucket = '$${{PRERELEASE_BUCKET}}/artifacts/downloads{}'.format(enterprise2_suffix(edition)) cmd = './bin/grabpl upload-packages --edition {} --packages-bucket {}'.format(edition, packages_bucket) elif edition == 'enterprise2': - cmd = './bin/grabpl upload-packages --edition {} --packages-bucket grafana-downloads-enterprise2'.format(edition) + cmd = './bin/grabpl upload-packages --edition {} --packages-bucket grafana-downloads-enterprise2'.format( + edition) else: cmd = './bin/grabpl upload-packages --edition {} --packages-bucket grafana-downloads'.format(edition) @@ -981,7 +961,7 @@ def upload_packages_step(edition, ver_mode, is_downstream=False, trigger=None): if edition in 'enterprise2' or not end_to_end_tests_deps(edition): deps.extend([ 'package' + enterprise2_suffix(edition), - ]) + ]) else: deps.extend(end_to_end_tests_deps(edition)) @@ -1000,16 +980,13 @@ def upload_packages_step(edition, ver_mode, is_downstream=False, trigger=None): return step -def store_packages_step(edition, ver_mode, is_downstream=False): +def store_packages_step(edition, ver_mode): if ver_mode == 'release': cmd = './bin/grabpl store-packages --edition {} --packages-bucket grafana-downloads --gcp-key /tmp/gcpkey.json ${{DRONE_TAG}}'.format( edition, ) elif ver_mode == 'main': - if not is_downstream: - build_no = '${DRONE_BUILD_NUMBER}' - else: - build_no = '$${SOURCE_BUILD_NUMBER}' + build_no = '${DRONE_BUILD_NUMBER}' cmd = './bin/grabpl store-packages --edition {} --gcp-key /tmp/gcpkey.json --build-id {}'.format( edition, build_no, ) @@ -1035,12 +1012,7 @@ def store_packages_step(edition, ver_mode, is_downstream=False): } -def get_windows_steps(edition, ver_mode, is_downstream=False): - if not is_downstream: - source_commit = '' - else: - source_commit = ' $$env:SOURCE_COMMIT' - +def get_windows_steps(edition, ver_mode): init_cmds = [] sfx = '' if edition in ('enterprise', 'enterprise2'): @@ -1053,12 +1025,12 @@ def get_windows_steps(edition, ver_mode, is_downstream=False): ]) steps = [ { - 'name': 'initialize', + 'name': 'windows-init', 'image': wix_image, 'commands': init_cmds, }, ] - if (ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2') or is_downstream)) or ver_mode in ( + if (ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2'))) or ver_mode in ( 'release', 'release-branch', ): bucket_part = '' @@ -1070,10 +1042,7 @@ def get_windows_steps(edition, ver_mode, is_downstream=False): dir = 'main' bucket = 'grafana-downloads' bucket_part = ' --packages-bucket {}'.format(bucket) - if not is_downstream: - build_no = 'DRONE_BUILD_NUMBER' - else: - build_no = 'SOURCE_BUILD_NUMBER' + build_no = 'DRONE_BUILD_NUMBER' ver_part = '--build-id $$env:{}'.format(build_no) installer_commands = [ '$$gcpKey = $$env:GCP_KEY', @@ -1084,7 +1053,7 @@ def get_windows_steps(edition, ver_mode, is_downstream=False): 'rm gcpkey.json', 'cp C:\\App\\nssm-2.24.zip .', ] - if (ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2') or is_downstream)) or ver_mode in ( + if (ver_mode == 'main' and (edition not in ('enterprise', 'enterprise2'))) or ver_mode in ( 'release', ): installer_commands.extend([ @@ -1105,15 +1074,15 @@ def get_windows_steps(edition, ver_mode, is_downstream=False): steps.append({ 'name': 'build-windows-installer', 'image': wix_image, + 'depends_on': [ + 'windows-init', + ], 'environment': { 'GCP_KEY': from_secret('gcp_key'), 'PRERELEASE_BUCKET': from_secret(prerelease_bucket), 'GITHUB_TOKEN': from_secret('github_token') }, 'commands': installer_commands, - 'depends_on': [ - 'initialize', - ], }) if edition in ('enterprise', 'enterprise2'): @@ -1132,11 +1101,10 @@ def get_windows_steps(edition, ver_mode, is_downstream=False): clone_cmds = [ 'git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"', ] - if not is_downstream: - clone_cmds.extend([ - 'cd grafana-enterprise', - 'git checkout {}'.format(committish), - ]) + clone_cmds.extend([ + 'cd grafana-enterprise', + 'git checkout {}'.format(committish), + ]) steps.insert(0, { 'name': 'clone', 'image': wix_image, @@ -1154,7 +1122,7 @@ def get_windows_steps(edition, ver_mode, is_downstream=False): 'rm -r -force grafana-enterprise', 'cp grabpl.exe C:\\App\\grabpl.exe', 'rm -force grabpl.exe', - 'C:\\App\\grabpl.exe init-enterprise --github-token $$env:GITHUB_TOKEN C:\\App\\grafana-enterprise{}'.format(source_commit), + 'C:\\App\\grabpl.exe init-enterprise --github-token $$env:GITHUB_TOKEN C:\\App\\grafana-enterprise', 'cp C:\\App\\grabpl.exe grabpl.exe', ]) if 'environment' in steps[1]: @@ -1194,6 +1162,7 @@ def ensure_cuetsified_step(): ], } + def end_to_end_tests_deps(edition): if disable_tests: return [] From c63086822d3be2bd1b8d97ffa7de80a058e979e8 Mon Sep 17 00:00:00 2001 From: Nathan Rodman Date: Thu, 14 Apr 2022 05:19:15 -0700 Subject: [PATCH 094/121] Alerting: Add FGAC to alerts tab in panel editor (#47732) * add FGAC for alert panel tab * add test for panel tabs * Refactor condition Co-authored-by: Konrad Lalik --- .../alerting/unified/PanelAlertTabContent.tsx | 8 +++- .../PanelEditor/state/selectors.test.ts | 39 +++++++++++++++++++ .../components/PanelEditor/state/selectors.ts | 15 +++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.tsx index 7460af80219..6832a62b200 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.tsx @@ -7,6 +7,8 @@ import { NewRuleFromPanelButton } from './components/panel-alerts-tab/NewRuleFro import { RulesTable } from './components/rules/RulesTable'; import { usePanelCombinedRules } from './hooks/usePanelCombinedRules'; import { selectors } from '@grafana/e2e-selectors'; +import { getRulesPermissions } from './utils/access-control'; +import { contextSrv } from 'app/core/services/context_srv'; interface Props { dashboard: DashboardModel; @@ -20,6 +22,8 @@ export const PanelAlertTabContent: FC = ({ dashboard, panel }) => { panel, poll: true, }); + const permissions = getRulesPermissions('grafana'); + const canCreateRules = contextSrv.hasPermission(permissions.create); const alert = errors.length ? ( @@ -44,7 +48,7 @@ export const PanelAlertTabContent: FC = ({ dashboard, panel }) => {
    {alert} - {!!dashboard.meta.canSave && ( + {!!dashboard.meta.canSave && canCreateRules && ( )}
    @@ -58,7 +62,7 @@ export const PanelAlertTabContent: FC = ({ dashboard, panel }) => { {!!dashboard.uid && ( <>

    There are no alert rules linked to this panel.

    - {!!dashboard.meta.canSave && } + {!!dashboard.meta.canSave && canCreateRules && } )} {!dashboard.uid && !!dashboard.meta.canSave && ( diff --git a/public/app/features/dashboard/components/PanelEditor/state/selectors.test.ts b/public/app/features/dashboard/components/PanelEditor/state/selectors.test.ts index 5768728b784..0bab17d32f9 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/selectors.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/selectors.test.ts @@ -2,6 +2,13 @@ import { getPanelEditorTabs } from './selectors'; import { PanelPlugin } from '@grafana/data'; import { PanelEditorTabId } from '../types'; import { updateConfig } from '../../../../../core/config'; +import { contextSrv } from 'app/core/services/context_srv'; + +jest.mock('app/core/services/context_srv'); + +const mocks = { + contextSrv: jest.mocked(contextSrv), +}; describe('getPanelEditorTabs selector', () => { it('return no tabs when no plugin provided', () => { @@ -79,5 +86,37 @@ describe('getPanelEditorTabs selector', () => { expect(tabs[1].id).toEqual(PanelEditorTabId.Transform); }); }); + + describe('with unified alerting enabled', () => { + beforeAll(() => { + updateConfig({ unifiedAlertingEnabled: true }); + }); + + it('shows the alert tab for users with read permission', () => { + mocks.contextSrv.hasPermission.mockReturnValue(true); + + const tabs = getPanelEditorTabs(undefined, { + meta: { + id: 'graph', + }, + } as PanelPlugin); + + expect(tabs.length).toEqual(3); + expect(tabs[2].id).toEqual(PanelEditorTabId.Alert); + }); + + it('hides the alert tab for users with read permission', () => { + mocks.contextSrv.hasPermission.mockReturnValue(false); + + const tabs = getPanelEditorTabs(undefined, { + meta: { + id: 'graph', + }, + } as PanelPlugin); + + expect(tabs.length).toEqual(2); + expect(tabs[1].id).toEqual(PanelEditorTabId.Transform); + }); + }); }); }); diff --git a/public/app/features/dashboard/components/PanelEditor/state/selectors.ts b/public/app/features/dashboard/components/PanelEditor/state/selectors.ts index 64ce72c531f..007945700da 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/selectors.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/selectors.ts @@ -2,6 +2,9 @@ import memoizeOne from 'memoize-one'; import { PanelPlugin } from '@grafana/data'; import { PanelEditorTab, PanelEditorTabId } from '../types'; import { getConfig } from 'app/core/config'; +import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control'; +import { contextSrv } from 'app/core/services/context_srv'; +import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; export const getPanelEditorTabs = memoizeOne((tab?: string, plugin?: PanelPlugin) => { const tabs: PanelEditorTab[] = []; @@ -34,10 +37,14 @@ export const getPanelEditorTabs = memoizeOne((tab?: string, plugin?: PanelPlugin }); } - if ( - ((getConfig().alertingEnabled || getConfig().unifiedAlertingEnabled) && plugin.meta.id === 'graph') || - plugin.meta.id === 'timeseries' - ) { + const { alertingEnabled, unifiedAlertingEnabled } = getConfig(); + const hasRuleReadPermissions = contextSrv.hasPermission(getRulesPermissions(GRAFANA_RULES_SOURCE_NAME).read); + const isAlertingAvailable = alertingEnabled || (unifiedAlertingEnabled && hasRuleReadPermissions); + + const isGraph = plugin.meta.id === 'graph'; + const isTimeseries = plugin.meta.id === 'timeseries'; + + if ((isAlertingAvailable && isGraph) || isTimeseries) { tabs.push({ id: PanelEditorTabId.Alert, text: 'Alert', From 060ccacbf97f36de7548dd33d9f75e42c9fa6188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 14 Apr 2022 14:21:36 +0200 Subject: [PATCH 095/121] Alerting: unwrap upsert into insert and update function (#47731) * Alerting: unwrap upsert into insert and update function * add changelog entry * remove changelog entry * rename upsertrule to updaterule * use directly alertrule model for inserts * add test for updating a rule with a conflicting name --- pkg/services/ngalert/CHANGELOG.md | 2 +- pkg/services/ngalert/api/api_ruler.go | 18 +-- pkg/services/ngalert/store/alert_rule.go | 123 ++++++++++++------- pkg/services/ngalert/store/testing.go | 12 +- pkg/services/ngalert/tests/util.go | 41 ++++--- pkg/tests/api/alerting/api_ruler_test.go | 146 +++++++++++++++++------ 6 files changed, 231 insertions(+), 111 deletions(-) diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md index c5650250638..15d9aadcf60 100644 --- a/pkg/services/ngalert/CHANGELOG.md +++ b/pkg/services/ngalert/CHANGELOG.md @@ -48,4 +48,4 @@ Scopes must have an order to ensure consistency and ease of search, this helps u - [CHANGE] Prometheus Compatible API: Use float-like values for `api/prometheus/grafana/api/v1/alerts` and `api/prometheus/grafana/api/v1/rules` instead of the evaluation string #47216 - [BUGFIX] (Legacy) Templates: Parse notification templates using all the matches of the alert rule when going from `Alerting` to `OK` in legacy alerting #47355 - [BUGFIX] Scheduler: Fix state manager to support OK option of `AlertRule.ExecErrState` #47670 -- [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971 \ No newline at end of file +- [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971 diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index dfefb87c961..963597e7e9c 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -355,23 +355,25 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod logger.Debug("updating database with the authorized changes", "add", len(authorizedChanges.New), "update", len(authorizedChanges.New), "delete", len(authorizedChanges.Delete)) if len(authorizedChanges.Update) > 0 || len(authorizedChanges.New) > 0 { - upsert := make([]store.UpsertRule, 0, len(authorizedChanges.Update)+len(authorizedChanges.New)) + updates := make([]store.UpdateRule, 0, len(authorizedChanges.Update)) + inserts := make([]ngmodels.AlertRule, 0, len(authorizedChanges.New)) for _, update := range authorizedChanges.Update { logger.Debug("updating rule", "rule_uid", update.New.UID, "diff", update.Diff.String()) - upsert = append(upsert, store.UpsertRule{ + updates = append(updates, store.UpdateRule{ Existing: update.Existing, New: *update.New, }) } for _, rule := range authorizedChanges.New { - upsert = append(upsert, store.UpsertRule{ - Existing: nil, - New: *rule, - }) + inserts = append(inserts, *rule) } - err = srv.store.UpsertAlertRules(tranCtx, upsert) + err = srv.store.InsertAlertRules(tranCtx, inserts) if err != nil { - return fmt.Errorf("failed to add or update rules: %w", err) + return fmt.Errorf("failed to add rules: %w", err) + } + err = srv.store.UpdateAlertRules(tranCtx, updates) + if err != nil { + return fmt.Errorf("failed to update rules: %w", err) } } diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index bbb026e3b41..ff1df1709ce 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -27,7 +27,7 @@ type UpdateRuleGroupCmd struct { RuleGroupConfig apimodels.PostableRuleGroupConfig } -type UpsertRule struct { +type UpdateRule struct { Existing *ngmodels.AlertRule New ngmodels.AlertRule } @@ -42,7 +42,8 @@ type RuleStore interface { GetAlertRules(ctx context.Context, query *ngmodels.GetAlertRulesQuery) error GetUserVisibleNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error) GetNamespaceByTitle(context.Context, string, int64, *models.SignedInUser, bool) (*models.Folder, error) - UpsertAlertRules(ctx context.Context, rule []UpsertRule) error + InsertAlertRules(ctx context.Context, rule []ngmodels.AlertRule) error + UpdateAlertRules(ctx context.Context, rule []UpdateRule) error } func getAlertRuleByUID(sess *sqlstore.DBSession, alertRuleUID string, orgID int64) (*ngmodels.AlertRule, error) { @@ -107,52 +108,87 @@ func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAler }) } -// UpsertAlertRules is a handler for creating/updating alert rules. -func (st DBstore) UpsertAlertRules(ctx context.Context, rules []UpsertRule) error { +// InsertAlertRules is a handler for creating/updating alert rules. +func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRule) error { + return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + newRules := make([]ngmodels.AlertRule, 0, len(rules)) + ruleVersions := make([]ngmodels.AlertRuleVersion, 0, len(rules)) + for i := range rules { + r := rules[i] + uid, err := GenerateNewAlertRuleUID(sess, r.OrgID, r.Title) + if err != nil { + return fmt.Errorf("failed to generate UID for alert rule %q: %w", r.Title, err) + } + r.UID = uid + r.Version = 1 + if err := st.validateAlertRule(r); err != nil { + return err + } + if err := (&r).PreSave(TimeNow); err != nil { + return err + } + newRules = append(newRules, r) + ruleVersions = append(ruleVersions, ngmodels.AlertRuleVersion{ + RuleOrgID: r.OrgID, + RuleUID: r.UID, + RuleNamespaceUID: r.NamespaceUID, + RuleGroup: r.RuleGroup, + ParentVersion: 0, + Version: r.Version, + Created: r.Updated, + Condition: r.Condition, + Title: r.Title, + Data: r.Data, + IntervalSeconds: r.IntervalSeconds, + NoDataState: r.NoDataState, + ExecErrState: r.ExecErrState, + For: r.For, + Annotations: r.Annotations, + Labels: r.Labels, + }) + } + if len(newRules) > 0 { + if _, err := sess.Insert(&newRules); err != nil { + if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return ngmodels.ErrAlertRuleUniqueConstraintViolation + } + return fmt.Errorf("failed to create new rules: %w", err) + } + } + + if len(ruleVersions) > 0 { + if _, err := sess.Insert(&ruleVersions); err != nil { + return fmt.Errorf("failed to create new rule versions: %w", err) + } + } + + return nil + }) +} + +// UpdateAlertRules is a handler for creating/updating alert rules. +func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { newRules := make([]ngmodels.AlertRule, 0, len(rules)) ruleVersions := make([]ngmodels.AlertRuleVersion, 0, len(rules)) for _, r := range rules { var parentVersion int64 - switch r.Existing { - case nil: // new rule - uid, err := GenerateNewAlertRuleUID(sess, r.New.OrgID, r.New.Title) - if err != nil { - return fmt.Errorf("failed to generate UID for alert rule %q: %w", r.New.Title, err) - } - r.New.UID = uid - r.New.Version = 1 - - if err := st.validateAlertRule(r.New); err != nil { - return err - } - - if err := (&r.New).PreSave(TimeNow); err != nil { - return err - } - newRules = append(newRules, r.New) - default: - r.New.ID = r.Existing.ID - r.New.Version = r.Existing.Version + 1 - - if err := st.validateAlertRule(r.New); err != nil { - return err - } - - if err := (&r.New).PreSave(TimeNow); err != nil { - return err - } - - // no way to update multiple rules at once - if _, err := sess.ID(r.Existing.ID).AllCols().Update(r.New); err != nil { - if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { - return ngmodels.ErrAlertRuleUniqueConstraintViolation - } - return fmt.Errorf("failed to update rule [%s] %s: %w", r.New.UID, r.New.Title, err) - } - parentVersion = r.Existing.Version + r.New.ID = r.Existing.ID + r.New.Version = r.Existing.Version + 1 + if err := st.validateAlertRule(r.New); err != nil { + return err } - + if err := (&r.New).PreSave(TimeNow); err != nil { + return err + } + // no way to update multiple rules at once + if _, err := sess.ID(r.Existing.ID).AllCols().Update(r.New); err != nil { + if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return ngmodels.ErrAlertRuleUniqueConstraintViolation + } + return fmt.Errorf("failed to update rule [%s] %s: %w", r.New.UID, r.New.Title, err) + } + parentVersion = r.Existing.Version ruleVersions = append(ruleVersions, ngmodels.AlertRuleVersion{ RuleOrgID: r.New.OrgID, RuleUID: r.New.UID, @@ -172,7 +208,6 @@ func (st DBstore) UpsertAlertRules(ctx context.Context, rules []UpsertRule) erro Labels: r.New.Labels, }) } - if len(newRules) > 0 { if _, err := sess.Insert(&newRules); err != nil { if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { @@ -181,13 +216,11 @@ func (st DBstore) UpsertAlertRules(ctx context.Context, rules []UpsertRule) erro return fmt.Errorf("failed to create new rules: %w", err) } } - if len(ruleVersions) > 0 { if _, err := sess.Insert(&ruleVersions); err != nil { return fmt.Errorf("failed to create new rule versions: %w", err) } } - return nil }) } diff --git a/pkg/services/ngalert/store/testing.go b/pkg/services/ngalert/store/testing.go index 396bb4431a6..0c69fd5f522 100644 --- a/pkg/services/ngalert/store/testing.go +++ b/pkg/services/ngalert/store/testing.go @@ -228,7 +228,17 @@ func (f *FakeRuleStore) GetNamespaceByTitle(_ context.Context, title string, org return nil, fmt.Errorf("not found") } -func (f *FakeRuleStore) UpsertAlertRules(_ context.Context, q []UpsertRule) error { +func (f *FakeRuleStore) UpdateAlertRules(_ context.Context, q []UpdateRule) error { + f.mtx.Lock() + defer f.mtx.Unlock() + f.RecordedOps = append(f.RecordedOps, q) + if err := f.Hook(q); err != nil { + return err + } + return nil +} + +func (f *FakeRuleStore) InsertAlertRules(_ context.Context, q []models.AlertRule) error { f.mtx.Lock() defer f.mtx.Unlock() f.RecordedOps = append(f.RecordedOps, q) diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 35ef0930872..19de8f046a2 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -81,35 +81,34 @@ func CreateTestAlertRule(t *testing.T, ctx context.Context, dbstore *store.DBsto func CreateTestAlertRuleWithLabels(t *testing.T, ctx context.Context, dbstore *store.DBstore, intervalSeconds int64, orgID int64, labels map[string]string) *models.AlertRule { ruleGroup := fmt.Sprintf("ruleGroup-%s", util.GenerateShortUID()) - err := dbstore.UpsertAlertRules(ctx, []store.UpsertRule{ + err := dbstore.InsertAlertRules(ctx, []models.AlertRule{ { - New: models.AlertRule{ - ID: 0, - OrgID: orgID, - Title: fmt.Sprintf("an alert definition %s", util.GenerateShortUID()), - Condition: "A", - Data: []models.AlertQuery{ - { - Model: json.RawMessage(`{ + + ID: 0, + OrgID: orgID, + Title: fmt.Sprintf("an alert definition %s", util.GenerateShortUID()), + Condition: "A", + Data: []models.AlertQuery{ + { + Model: json.RawMessage(`{ "datasourceUid": "-100", "type":"math", "expression":"2 + 2 > 1" }`), - RelativeTimeRange: models.RelativeTimeRange{ - From: models.Duration(5 * time.Hour), - To: models.Duration(3 * time.Hour), - }, - RefID: "A", + RelativeTimeRange: models.RelativeTimeRange{ + From: models.Duration(5 * time.Hour), + To: models.Duration(3 * time.Hour), }, + RefID: "A", }, - Labels: labels, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, - IntervalSeconds: intervalSeconds, - NamespaceUID: "namespace", - RuleGroup: ruleGroup, - NoDataState: models.NoData, - ExecErrState: models.AlertingErrState, }, + Labels: labels, + Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + IntervalSeconds: intervalSeconds, + NamespaceUID: "namespace", + RuleGroup: ruleGroup, + NoDataState: models.NoData, + ExecErrState: models.AlertingErrState, }, }) require.NoError(t, err) diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 7af16c013d0..734d1bffbab 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -356,41 +356,8 @@ func TestAlertRuleConflictingTitle(t *testing.T) { Login: "admin", }) - interval, err := model.ParseDuration("1m") - require.NoError(t, err) + rules := newTestingRuleConfig(t) - rules := apimodels.PostableRuleGroupConfig{ - Name: "arulegroup", - Rules: []apimodels.PostableExtendedRuleNode{ - { - ApiRuleNode: &apimodels.ApiRuleNode{ - For: interval, - Labels: map[string]string{"label1": "val1"}, - Annotations: map[string]string{"annotation1": "val1"}, - }, - // this rule does not explicitly set no data and error states - // therefore it should get the default values - GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ - Title: "AlwaysFiring", - Condition: "A", - Data: []ngmodels.AlertQuery{ - { - RefID: "A", - RelativeTimeRange: ngmodels.RelativeTimeRange{ - From: ngmodels.Duration(time.Duration(5) * time.Hour), - To: ngmodels.Duration(time.Duration(3) * time.Hour), - }, - DatasourceUID: "-100", - Model: json.RawMessage(`{ - "type": "math", - "expression": "2 + 3 > 1" - }`), - }, - }, - }, - }, - }, - } buf := bytes.Buffer{} enc := json.NewEncoder(&buf) err = enc.Encode(&rules) @@ -410,7 +377,22 @@ func TestAlertRuleConflictingTitle(t *testing.T) { assert.Equal(t, http.StatusAccepted, resp.StatusCode) require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b)) + // fetch the created rules, so we can get the uid's and trigger + // and update by reusing the uid's + resp, err = http.Get(u + "/" + rules.Name) + require.NoError(t, err) + + var createdRuleGroup apimodels.GettableRuleGroupConfig + data, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + err = json.Unmarshal(data, &createdRuleGroup) + require.NoError(t, err) + require.Len(t, createdRuleGroup.Rules, 2) + t.Run("trying to create alert with same title under same folder should fail", func(t *testing.T) { + rules := newTestingRuleConfig(t) + buf := bytes.Buffer{} enc := json.NewEncoder(&buf) err = enc.Encode(&rules) @@ -428,10 +410,36 @@ func TestAlertRuleConflictingTitle(t *testing.T) { require.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - require.JSONEq(t, `{"message": "failed to update rule group: failed to add or update rules: a conflicting alert rule is found: rule title under the same organisation and folder should be unique"}`, string(b)) + require.JSONEq(t, `{"message": "failed to update rule group: failed to add rules: a conflicting alert rule is found: rule title under the same organisation and folder should be unique"}`, string(b)) + }) + + t.Run("trying to update an alert to the title of an existing alert in the same folder should fail", func(t *testing.T) { + rules := newTestingRuleConfig(t) + rules.Rules[0].GrafanaManagedAlert.UID = createdRuleGroup.Rules[0].GrafanaManagedAlert.UID + rules.Rules[1].GrafanaManagedAlert.UID = createdRuleGroup.Rules[1].GrafanaManagedAlert.UID + rules.Rules[1].GrafanaManagedAlert.Title = "AlwaysFiring" + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + err = enc.Encode(&rules) + require.NoError(t, err) + + u := fmt.Sprintf("http://admin:admin@%s/api/ruler/grafana/api/v1/rules/folder1", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(u, "application/json", &buf) + require.NoError(t, err) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + require.JSONEq(t, `{"message": "failed to update rule group: failed to update rules: a conflicting alert rule is found: rule title under the same organisation and folder should be unique"}`, string(b)) }) t.Run("trying to create alert with same title under another folder should succeed", func(t *testing.T) { + rules := newTestingRuleConfig(t) buf := bytes.Buffer{} enc := json.NewEncoder(&buf) err = enc.Encode(&rules) @@ -800,3 +808,71 @@ func TestRulerRulesFilterByDashboard(t *testing.T) { require.JSONEq(t, `{"message": "panel_id must be set with dashboard_uid"}`, string(b)) } } + +func newTestingRuleConfig(t *testing.T) apimodels.PostableRuleGroupConfig { + interval, err := model.ParseDuration("1m") + require.NoError(t, err) + + firstRule := apimodels.PostableExtendedRuleNode{ + ApiRuleNode: &apimodels.ApiRuleNode{ + For: interval, + Labels: map[string]string{"label1": "val1"}, + Annotations: map[string]string{"annotation1": "val1"}, + }, + // this rule does not explicitly set no data and error states + // therefore it should get the default values + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + Title: "AlwaysFiring", + Condition: "A", + Data: []ngmodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: ngmodels.RelativeTimeRange{ + From: ngmodels.Duration(time.Duration(5) * time.Hour), + To: ngmodels.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: "-100", + Model: json.RawMessage(`{ + "type": "math", + "expression": "2 + 3 > 1" + }`), + }, + }, + }, + } + secondRule := apimodels.PostableExtendedRuleNode{ + ApiRuleNode: &apimodels.ApiRuleNode{ + For: interval, + Labels: map[string]string{"label1": "val1"}, + Annotations: map[string]string{"annotation1": "val1"}, + }, + // this rule does not explicitly set no data and error states + // therefore it should get the default values + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + Title: "AlwaysFiring2", + Condition: "A", + Data: []ngmodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: ngmodels.RelativeTimeRange{ + From: ngmodels.Duration(time.Duration(5) * time.Hour), + To: ngmodels.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: "-100", + Model: json.RawMessage(`{ + "type": "math", + "expression": "2 + 3 > 1" + }`), + }, + }, + }, + } + + return apimodels.PostableRuleGroupConfig{ + Name: "arulegroup", + Rules: []apimodels.PostableExtendedRuleNode{ + firstRule, + secondRule, + }, + } +} From ecd6cd4a929f026df589f15cb3ef51e101d3d400 Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Thu, 14 Apr 2022 14:22:00 +0200 Subject: [PATCH 096/121] Split preference store (#46843) * Split preference store * Chore: Add tests to pref * Fix preference in wire * Rename and adjust * Add pref service test * Rename methods, add tests * Rename Preferences to Preference, names IDs correctly * Fix lint * Refactor Save * Refactor upsert Add new logic for QueryHistory Rename some fields according to go naming conventions Refactore tests * Roll back ID that breaks tests * Rename Id to ID in UpdatePreferenceQuery * Use preference as a model to modify store * Move pref store fakes to pref test file * Add integration tag for store tests * Adjust test Co-authored-by: yangkb09 --- pkg/server/wire.go | 3 +- pkg/services/preference/model.go | 100 ++++ pkg/services/preference/pref.go | 13 + pkg/services/preference/prefimpl/pref.go | 203 ++++++++ pkg/services/preference/prefimpl/pref_test.go | 446 ++++++++++++++++++ pkg/services/preference/prefimpl/store.go | 88 ++++ .../preference/prefimpl/store_test.go | 170 +++++++ pkg/services/preference/preftest/fake.go | 32 ++ 8 files changed, 1054 insertions(+), 1 deletion(-) create mode 100644 pkg/services/preference/model.go create mode 100644 pkg/services/preference/pref.go create mode 100644 pkg/services/preference/prefimpl/pref.go create mode 100644 pkg/services/preference/prefimpl/pref_test.go create mode 100644 pkg/services/preference/prefimpl/store.go create mode 100644 pkg/services/preference/prefimpl/store_test.go create mode 100644 pkg/services/preference/preftest/fake.go diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 61a10765e42..74418497d83 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -65,13 +65,13 @@ import ( plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service" "github.com/grafana/grafana/pkg/services/pluginsettings" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" + "github.com/grafana/grafana/pkg/services/preference/prefimpl" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/queryhistory" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/schemaloader" "github.com/grafana/grafana/pkg/services/search" - "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/secrets" secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database" @@ -235,6 +235,7 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)), comments.ProvideService, guardian.ProvideService, + prefimpl.ProvideService, avatar.ProvideAvatarCacheServer, authproxy.ProvideAuthProxy, statscollector.ProvideService, diff --git a/pkg/services/preference/model.go b/pkg/services/preference/model.go new file mode 100644 index 00000000000..ae8b5ca665b --- /dev/null +++ b/pkg/services/preference/model.go @@ -0,0 +1,100 @@ +package pref + +import ( + "bytes" + "encoding/json" + "errors" + "time" +) + +var ErrPrefNotFound = errors.New("preference not found") + +type Preference struct { + ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` + UserID int64 `xorm:"user_id"` + TeamID int64 `xorm:"team_id"` + Teams []int64 `xorm:"extends"` + Version int + HomeDashboardID int64 `xorm:"home_dashboard_id"` + Timezone string + WeekStart string + Theme string + Created time.Time + Updated time.Time + JSONData *PreferenceJSONData `xorm:"json_data"` +} + +type GetPreferenceWithDefaultsQuery struct { + Teams []int64 + OrgID int64 + UserID int64 +} + +type GetPreferenceQuery struct { + OrgID int64 + UserID int64 + TeamID int64 +} + +type SavePreferenceCommand struct { + UserID int64 + OrgID int64 + TeamID int64 + + HomeDashboardID int64 `json:"homeDashboardId,omitempty"` + Timezone string `json:"timezone,omitempty"` + WeekStart string `json:"weekStart,omitempty"` + Theme string `json:"theme,omitempty"` + Navbar *NavbarPreference `json:"navbar,omitempty"` + QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"` +} + +type PatchPreferenceCommand struct { + UserID int64 + OrgID int64 + TeamID int64 + + HomeDashboardID *int64 `json:"homeDashboardId,omitempty"` + Timezone *string `json:"timezone,omitempty"` + WeekStart *string `json:"weekStart,omitempty"` + Theme *string `json:"theme,omitempty"` + Navbar *NavbarPreference `json:"navbar,omitempty"` + QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"` +} + +type NavLink struct { + ID string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Url string `json:"url,omitempty"` + Target string `json:"target,omitempty"` +} + +type NavbarPreference struct { + SavedItems []NavLink `json:"savedItems"` +} + +type PreferenceJSONData struct { + Navbar NavbarPreference `json:"navbar"` + QueryHistory QueryHistoryPreference `json:"queryHistory"` +} + +type QueryHistoryPreference struct { + HomeTab string `json:"homeTab"` +} + +func (j *PreferenceJSONData) FromDB(data []byte) error { + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.UseNumber() + return dec.Decode(j) +} + +func (j *PreferenceJSONData) ToDB() ([]byte, error) { + if j == nil { + return nil, nil + } + + return json.Marshal(j) +} + +func (p Preference) TableName() string { return "preferences" } diff --git a/pkg/services/preference/pref.go b/pkg/services/preference/pref.go new file mode 100644 index 00000000000..5cf17789a25 --- /dev/null +++ b/pkg/services/preference/pref.go @@ -0,0 +1,13 @@ +package pref + +import ( + "context" +) + +type Service interface { + GetWithDefaults(context.Context, *GetPreferenceWithDefaultsQuery) (*Preference, error) + Get(context.Context, *GetPreferenceQuery) (*Preference, error) + Save(context.Context, *SavePreferenceCommand) error + Patch(ctx context.Context, cmd *PatchPreferenceCommand) error + GetDefaults() *Preference +} diff --git a/pkg/services/preference/prefimpl/pref.go b/pkg/services/preference/prefimpl/pref.go new file mode 100644 index 00000000000..c43fb7d98c5 --- /dev/null +++ b/pkg/services/preference/prefimpl/pref.go @@ -0,0 +1,203 @@ +package prefimpl + +import ( + "context" + "errors" + "time" + + pref "github.com/grafana/grafana/pkg/services/preference" + "github.com/grafana/grafana/pkg/services/sqlstore/db" + "github.com/grafana/grafana/pkg/setting" +) + +type Service struct { + store store + cfg *setting.Cfg +} + +func ProvideService(db db.DB, cfg *setting.Cfg) *Service { + return &Service{ + store: &sqlStore{ + db: db, + }, + cfg: cfg, + } +} + +func (s *Service) GetWithDefaults(ctx context.Context, query *pref.GetPreferenceWithDefaultsQuery) (*pref.Preference, error) { + listQuery := &pref.Preference{ + Teams: query.Teams, + OrgID: query.OrgID, + UserID: query.UserID, + } + prefs, err := s.store.List(ctx, listQuery) + if err != nil { + return nil, err + } + + res := s.GetDefaults() + for _, p := range prefs { + if p.Theme != "" { + res.Theme = p.Theme + } + if p.Timezone != "" { + res.Timezone = p.Timezone + } + if p.WeekStart != "" { + res.WeekStart = p.WeekStart + } + if p.HomeDashboardID != 0 { + res.HomeDashboardID = p.HomeDashboardID + } + if p.JSONData != nil { + res.JSONData = p.JSONData + } + } + + return res, err +} + +func (s *Service) Get(ctx context.Context, query *pref.GetPreferenceQuery) (*pref.Preference, error) { + getPref := &pref.Preference{ + OrgID: query.OrgID, + UserID: query.UserID, + TeamID: query.TeamID, + } + prefs, err := s.store.Get(ctx, getPref) + if err != nil && !errors.Is(err, pref.ErrPrefNotFound) { + return nil, err + } + return prefs, nil +} + +func (s *Service) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) error { + preference, err := s.store.Get(ctx, &pref.Preference{ + OrgID: cmd.OrgID, + UserID: cmd.UserID, + TeamID: cmd.TeamID, + }) + if err != nil { + if errors.Is(err, pref.ErrPrefNotFound) { + preference := &pref.Preference{ + UserID: cmd.UserID, + OrgID: cmd.OrgID, + TeamID: cmd.TeamID, + HomeDashboardID: cmd.HomeDashboardID, + Timezone: cmd.Timezone, + WeekStart: cmd.WeekStart, + Theme: cmd.Theme, + Created: time.Now(), + Updated: time.Now(), + } + _, err = s.store.Insert(ctx, preference) + if err != nil { + return err + } + } + return err + } + preference.Timezone = cmd.Timezone + preference.WeekStart = cmd.WeekStart + preference.Theme = cmd.Theme + preference.Updated = time.Now() + preference.Version += 1 + preference.JSONData = &pref.PreferenceJSONData{} + + if cmd.Navbar != nil { + preference.JSONData.Navbar = *cmd.Navbar + } + if cmd.QueryHistory != nil { + preference.JSONData.QueryHistory = *cmd.QueryHistory + } + return s.store.Update(ctx, preference) +} + +func (s *Service) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) error { + var exists bool + preference, err := s.store.Get(ctx, &pref.Preference{ + OrgID: cmd.OrgID, + UserID: cmd.UserID, + TeamID: cmd.TeamID, + }) + if err != nil && !errors.Is(err, pref.ErrPrefNotFound) { + return err + } + + if errors.Is(err, pref.ErrPrefNotFound) { + preference = &pref.Preference{ + UserID: cmd.UserID, + OrgID: cmd.OrgID, + TeamID: cmd.TeamID, + Created: time.Now(), + JSONData: &pref.PreferenceJSONData{}, + } + } else { + exists = true + } + + if cmd.Navbar != nil { + if preference.JSONData == nil { + preference.JSONData = &pref.PreferenceJSONData{} + } + if cmd.Navbar.SavedItems != nil { + preference.JSONData.Navbar.SavedItems = cmd.Navbar.SavedItems + } + } + + if cmd.QueryHistory != nil { + if preference.JSONData == nil { + preference.JSONData = &pref.PreferenceJSONData{} + } + if cmd.QueryHistory.HomeTab != "" { + preference.JSONData.QueryHistory.HomeTab = cmd.QueryHistory.HomeTab + } + } + + if cmd.HomeDashboardID != nil { + preference.HomeDashboardID = *cmd.HomeDashboardID + } + + if cmd.Timezone != nil { + preference.Timezone = *cmd.Timezone + } + + if cmd.WeekStart != nil { + preference.WeekStart = *cmd.WeekStart + } + + if cmd.Theme != nil { + preference.Theme = *cmd.Theme + } + + preference.Updated = time.Now() + preference.Version += 1 + + // Wrap this in an if statement to maintain backwards compatibility + if cmd.Navbar != nil { + if preference.JSONData == nil { + preference.JSONData = &pref.PreferenceJSONData{} + } + if cmd.Navbar.SavedItems != nil { + preference.JSONData.Navbar.SavedItems = cmd.Navbar.SavedItems + } + } + + if exists { + err = s.store.Update(ctx, preference) + } else { + _, err = s.store.Insert(ctx, preference) + } + return err +} + +func (s *Service) GetDefaults() *pref.Preference { + defaults := &pref.Preference{ + Theme: s.cfg.DefaultTheme, + Timezone: s.cfg.DateFormats.DefaultTimezone, + WeekStart: s.cfg.DateFormats.DefaultWeekStart, + HomeDashboardID: 0, + JSONData: &pref.PreferenceJSONData{}, + } + + return defaults +} diff --git a/pkg/services/preference/prefimpl/pref_test.go b/pkg/services/preference/prefimpl/pref_test.go new file mode 100644 index 00000000000..b1e439b6830 --- /dev/null +++ b/pkg/services/preference/prefimpl/pref_test.go @@ -0,0 +1,446 @@ +package prefimpl + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + pref "github.com/grafana/grafana/pkg/services/preference" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" +) + +func TestPreferencesService(t *testing.T) { + prefStoreFake := newPreferenceStoreFake() + prefService := &Service{ + store: prefStoreFake, + } + + emptyNavbarPreferences := pref.NavbarPreference{} + userNavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "explore", + Text: "Explore", + Url: "/explore", + }}, + } + orgNavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "alerting", + Text: "Alerting", + Url: "/alerting", + }}, + } + team1NavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "dashboards", + Text: "Dashboards", + Url: "/dashboards", + }}, + } + team2NavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "home", + Text: "Home", + Url: "/home", + }}, + } + + emptyQueryPreference := pref.QueryHistoryPreference{} + + queryPreference := pref.QueryHistoryPreference{ + HomeTab: "hometab", + } + + queryPreference2 := pref.QueryHistoryPreference{ + HomeTab: "hometab", + } + + emptyPreferencesJsonData := pref.PreferenceJSONData{ + Navbar: emptyNavbarPreferences, + } + userPreferencesJsonData := pref.PreferenceJSONData{ + Navbar: userNavbarPreferences, + QueryHistory: queryPreference, + } + orgPreferencesJsonData := pref.PreferenceJSONData{ + Navbar: orgNavbarPreferences, + } + team2PreferencesJsonData := pref.PreferenceJSONData{ + Navbar: team2NavbarPreferences, + } + team1PreferencesJsonData := pref.PreferenceJSONData{ + Navbar: team1NavbarPreferences, + } + + t.Run("GetDefaults should return defaults", func(t *testing.T) { + prefService.cfg = setting.NewCfg() + prefService.cfg.DefaultTheme = "light" + prefService.cfg.DateFormats.DefaultTimezone = "UTC" + + preferences := prefService.GetDefaults() + expected := &pref.Preference{ + Theme: "light", + Timezone: "UTC", + HomeDashboardID: 0, + JSONData: &pref.PreferenceJSONData{}, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("GetDefaults with no saved preferences should return defaults", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + Theme: "light", + Timezone: "UTC", + } + query := &pref.GetPreferenceWithDefaultsQuery{} + preferences, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + Theme: "light", + Timezone: "UTC", + HomeDashboardID: 0, + JSONData: &emptyPreferencesJsonData, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("GetWithDefaults with saved org and user home dashboard should return user home dashboard", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{} + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + HomeDashboardID: 1, + Theme: "dark", + Timezone: "UTC", + }, + { + OrgID: 1, + UserID: 1, + HomeDashboardID: 4, + Theme: "light", + WeekStart: "1", + }, + } + query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 1} + preferences, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + Theme: "light", + Timezone: "UTC", + WeekStart: "1", + HomeDashboardID: 4, + JSONData: &pref.PreferenceJSONData{}, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("GetWithDefaults with saved org and other user home dashboard should return org home dashboard", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{} + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + HomeDashboardID: 1, + Theme: "dark", + Timezone: "UTC", + WeekStart: "1", + }, + { + OrgID: 1, + UserID: 1, + HomeDashboardID: 4, + Theme: "light", + Timezone: "browser", + WeekStart: "2", + }, + } + prefService.GetDefaults().HomeDashboardID = 1 + query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 2} + preferences, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + Theme: "light", + Timezone: "browser", + WeekStart: "2", + HomeDashboardID: 4, + JSONData: &pref.PreferenceJSONData{}, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("GetPreferencesWithDefaults with saved org and user json data should return user json data", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{} + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + JSONData: &orgPreferencesJsonData, + }, + { + OrgID: 1, + UserID: 1, + JSONData: &userPreferencesJsonData, + }, + } + query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 1} + preference, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + require.Equal(t, &pref.Preference{ + Theme: "light", + JSONData: &userPreferencesJsonData, + Timezone: "UTC", + }, preference) + }) + + t.Run("GetWithDefaults with saved org and teams json data should return last team json data", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{} + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + JSONData: &orgPreferencesJsonData, + }, + { + OrgID: 1, + TeamID: 2, + JSONData: &team1PreferencesJsonData, + }, + { + OrgID: 1, + TeamID: 3, + JSONData: &team2PreferencesJsonData, + }, + } + query := &pref.GetPreferenceWithDefaultsQuery{ + OrgID: 1, Teams: []int64{2, 3}, + } + preference, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + require.Equal(t, &pref.Preference{ + Timezone: "UTC", + Theme: "light", + JSONData: &team2PreferencesJsonData, + }, preference) + }) + + t.Run("GetWithDefaults with saved org and teams home dashboard should return last team home dashboard", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + Theme: "dark", + Timezone: "UTC", + } + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + HomeDashboardID: 1, + Theme: "light", + Timezone: "browser", + WeekStart: "1", + }, + { + OrgID: 1, + UserID: 1, + HomeDashboardID: 4, + Theme: "light", + Timezone: "browser", + WeekStart: "2", + }, + } + + query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, Teams: []int64{2, 3}} + preferences, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + Theme: "light", + Timezone: "browser", + WeekStart: "2", + HomeDashboardID: 4, + JSONData: &pref.PreferenceJSONData{}, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("SavePreferences for a user should store correct values", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + ID: 1, + OrgID: 1, + UserID: 3, + TeamID: 6, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + } + err := prefService.Save(context.Background(), + &pref.SavePreferenceCommand{ + Theme: "dark", + Timezone: "browser", + HomeDashboardID: 5, + WeekStart: "1"}, + ) + require.NoError(t, err) + }) + + t.Run("SavePreferences for a user should store correct values, when preference not found", func(t *testing.T) { + prefStoreFake.ExpectedGetError = pref.ErrPrefNotFound + prefStoreFake.ExpectedPreference = &pref.Preference{ + ID: 1, + OrgID: 1, + UserID: 3, + TeamID: 6, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + } + err := prefService.Save(context.Background(), + &pref.SavePreferenceCommand{ + Theme: "dark", + Timezone: "browser", + HomeDashboardID: 5, + WeekStart: "1", + }, + ) + require.NoError(t, err) + prefStoreFake.ExpectedGetError = nil + }) + + t.Run("SavePreferences for a user should store correct values with nav and query history", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + ID: 1, + OrgID: 1, + UserID: 3, + TeamID: 6, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + JSONData: &userPreferencesJsonData, + } + err := prefService.Save(context.Background(), + &pref.SavePreferenceCommand{ + Theme: "dark", + Timezone: "browser", + HomeDashboardID: 5, + WeekStart: "1", + Navbar: &userNavbarPreferences, + QueryHistory: &emptyQueryPreference, + }, + ) + require.NoError(t, err) + }) + + t.Run("Get for a user should store correct values", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + } + preference, err := prefService.Get(context.Background(), &pref.GetPreferenceQuery{}) + require.NoError(t, err) + + expected := &pref.Preference{ + ID: preference.ID, + Version: preference.Version, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + Created: preference.Created, + Updated: preference.Updated, + } + if diff := cmp.Diff(expected, preference); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("Patch for a user should store correct values", func(t *testing.T) { + darkTheme := "dark" + prefStoreFake.ExpectedPreference = &pref.Preference{ + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + JSONData: &userPreferencesJsonData, + } + err := prefService.Patch(context.Background(), + &pref.PatchPreferenceCommand{ + Theme: &darkTheme, + Navbar: &userNavbarPreferences, + QueryHistory: &queryPreference2, + }) + require.NoError(t, err) + }) + + t.Run("Patch for a user should store correct values, without navbar and query history", func(t *testing.T) { + darkTheme := "dark" + prefStoreFake.ExpectedPreference = &pref.Preference{ + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + } + err := prefService.Patch(context.Background(), + &pref.PatchPreferenceCommand{ + Theme: &darkTheme, + Navbar: &userNavbarPreferences, + QueryHistory: &queryPreference2, + }) + require.NoError(t, err) + }) + + t.Run("Patch for a user should store correct values, when preference not found", func(t *testing.T) { + timezone := "browser" + weekStart := "1" + homeDashboardID := int64(5) + prefStoreFake.ExpectedGetError = pref.ErrPrefNotFound + prefStoreFake.ExpectedPreference = nil + + err := prefService.Patch(context.Background(), + &pref.PatchPreferenceCommand{ + HomeDashboardID: &homeDashboardID, + Timezone: &timezone, + WeekStart: &weekStart, + Navbar: &emptyNavbarPreferences, + QueryHistory: &emptyQueryPreference, + }) + require.NoError(t, err) + prefStoreFake.ExpectedGetError = nil + }) +} + +type FakePreferenceStore struct { + ExpectedPreference *pref.Preference + ExpectedListPreferences []*pref.Preference + ExpectedID int64 + ExpectedError error + ExpectedGetError error +} + +func newPreferenceStoreFake() *FakePreferenceStore { + return &FakePreferenceStore{} +} + +func (f *FakePreferenceStore) List(ctx context.Context, query *pref.Preference) ([]*pref.Preference, error) { + return f.ExpectedListPreferences, f.ExpectedError +} + +func (f *FakePreferenceStore) Get(ctx context.Context, query *pref.Preference) (*pref.Preference, error) { + return f.ExpectedPreference, f.ExpectedGetError +} + +func (f *FakePreferenceStore) Insert(ctx context.Context, cmd *pref.Preference) (int64, error) { + return f.ExpectedID, f.ExpectedError +} + +func (f *FakePreferenceStore) Update(ctx context.Context, cmd *pref.Preference) error { + return f.ExpectedError +} diff --git a/pkg/services/preference/prefimpl/store.go b/pkg/services/preference/prefimpl/store.go new file mode 100644 index 00000000000..139b723363b --- /dev/null +++ b/pkg/services/preference/prefimpl/store.go @@ -0,0 +1,88 @@ +package prefimpl + +import ( + "context" + "strings" + + pref "github.com/grafana/grafana/pkg/services/preference" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/db" +) + +type store interface { + Get(context.Context, *pref.Preference) (*pref.Preference, error) + List(context.Context, *pref.Preference) ([]*pref.Preference, error) + Insert(context.Context, *pref.Preference) (int64, error) + Update(context.Context, *pref.Preference) error +} + +type sqlStore struct { + db db.DB +} + +func (s *sqlStore) Get(ctx context.Context, query *pref.Preference) (*pref.Preference, error) { + var prefs pref.Preference + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + exist, err := sess.Where("org_id=? AND user_id=? AND team_id=?", query.OrgID, query.UserID, query.TeamID).Get(&prefs) + if err != nil { + return err + } + if !exist { + return pref.ErrPrefNotFound + } + return nil + }) + if err != nil { + return nil, err + } + return &prefs, nil +} + +func (s *sqlStore) List(ctx context.Context, query *pref.Preference) ([]*pref.Preference, error) { + prefs := make([]*pref.Preference, 0) + params := make([]interface{}, 0) + filter := "" + + if len(query.Teams) > 0 { + filter = "(org_id=? AND team_id IN (?" + strings.Repeat(",?", len(query.Teams)-1) + ")) OR " + params = append(params, query.OrgID) + for _, v := range query.Teams { + params = append(params, v) + } + } + + filter += "(org_id=? AND user_id=? AND team_id=0) OR (org_id=? AND team_id=0 AND user_id=0)" + params = append(params, query.OrgID) + params = append(params, query.UserID) + params = append(params, query.OrgID) + + err := s.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + err := dbSession.Where(filter, params...). + OrderBy("user_id ASC, team_id ASC"). + Find(&prefs) + + if err != nil { + return err + } + + return nil + }) + return prefs, err +} + +func (s *sqlStore) Update(ctx context.Context, cmd *pref.Preference) error { + return s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + _, err := sess.ID(cmd.ID).AllCols().Update(cmd) + return err + }) +} + +func (s *sqlStore) Insert(ctx context.Context, cmd *pref.Preference) (int64, error) { + var ID int64 + var err error + err = s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + ID, err = sess.Insert(cmd) + return err + }) + return ID, err +} diff --git a/pkg/services/preference/prefimpl/store_test.go b/pkg/services/preference/prefimpl/store_test.go new file mode 100644 index 00000000000..773b6dea57b --- /dev/null +++ b/pkg/services/preference/prefimpl/store_test.go @@ -0,0 +1,170 @@ +//go:build integration +// +build integration + +package prefimpl + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana/pkg/models" + pref "github.com/grafana/grafana/pkg/services/preference" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" +) + +func TestPreferencesDataAccess(t *testing.T) { + ss := sqlstore.InitTestDB(t) + prefStore := sqlStore{db: ss} + orgNavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "alerting", + Text: "Alerting", + Url: "/alerting", + }}, + } + + t.Run("Get with saved org and user home dashboard returns not found", func(t *testing.T) { + query := &pref.Preference{OrgID: 1, UserID: 1, TeamID: 2} + prefs, err := prefStore.Get(context.Background(), query) + require.EqualError(t, err, pref.ErrPrefNotFound.Error()) + require.Nil(t, prefs) + }) + + t.Run("Get with saved org and user home dashboard should return user home dashboard", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), + &pref.Preference{ + OrgID: 1, + UserID: 1, + HomeDashboardID: 4, + TeamID: 2, + Created: time.Now(), + Updated: time.Now(), + }) + require.NoError(t, err) + + query := &pref.Preference{OrgID: 1, UserID: 1, TeamID: 2} + prefs, err := prefStore.Get(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(4), prefs.HomeDashboardID) + }) + + t.Run("List with saved org and user home dashboard should return user home dashboard", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), + &pref.Preference{ + OrgID: 1, + UserID: 1, + TeamID: 3, + HomeDashboardID: 1, + Created: time.Now(), + Updated: time.Now(), + }) + require.NoError(t, err) + + query := &pref.Preference{OrgID: 1, UserID: 1, Teams: []int64{2}} + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(4), prefs[0].HomeDashboardID) + }) + + t.Run("List with saved org and other user home dashboard should return org home dashboard", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), + &pref.Preference{ + OrgID: 1, + UserID: 2, + TeamID: 3, + HomeDashboardID: 1, + Created: time.Now(), + Updated: time.Now(), + }) + require.NoError(t, err) + + query := &pref.Preference{OrgID: 1, UserID: 1, Teams: []int64{3}} + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(1), prefs[0].HomeDashboardID) + require.Equal(t, int64(1), prefs[1].HomeDashboardID) + }) + + t.Run("List with saved org and teams home dashboard should return last team home dashboard", func(t *testing.T) { + query := &pref.Preference{ + OrgID: 1, Teams: []int64{2, 3}, + } + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(4), prefs[0].HomeDashboardID) + require.Equal(t, int64(1), prefs[1].HomeDashboardID) + require.Equal(t, int64(1), prefs[2].HomeDashboardID) + }) + + t.Run("List with saved org and other teams home dashboard should return org home dashboard", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), &pref.Preference{OrgID: 1, HomeDashboardID: 1, Created: time.Now(), Updated: time.Now()}) + require.NoError(t, err) + _, err = prefStore.Insert(context.Background(), &pref.Preference{OrgID: 1, TeamID: 2, HomeDashboardID: 2, Created: time.Now(), Updated: time.Now()}) + require.NoError(t, err) + _, err = prefStore.Insert(context.Background(), &pref.Preference{OrgID: 1, TeamID: 3, HomeDashboardID: 3, Created: time.Now(), Updated: time.Now()}) + require.NoError(t, err) + + query := &pref.Preference{OrgID: 1} + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(1), prefs[0].HomeDashboardID) + }) + + t.Run("Update for a user should only modify a single value", func(t *testing.T) { + ss := sqlstore.InitTestDB(t) + prefStore := sqlStore{db: ss} + id, err := prefStore.Insert(context.Background(), &pref.Preference{ + UserID: models.SignedInUser{}.UserId, + Theme: "dark", + Timezone: "browser", + HomeDashboardID: 5, + WeekStart: "1", + JSONData: &pref.PreferenceJSONData{Navbar: orgNavbarPreferences}, + Created: time.Now(), + Updated: time.Now(), + }) + require.NoError(t, err) + + err = prefStore.Update(context.Background(), &pref.Preference{ + ID: id, + Theme: "dark", + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Created: time.Now(), + Updated: time.Now(), + JSONData: &pref.PreferenceJSONData{}, + }) + require.NoError(t, err) + query := &pref.Preference{} + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + ID: prefs[0].ID, + Version: prefs[0].Version, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + JSONData: prefs[0].JSONData, + Created: prefs[0].Created, + Updated: prefs[0].Updated, + } + if diff := cmp.Diff(expected, prefs[0]); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + t.Run("insert preference that does not exist", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), + &pref.Preference{ + UserID: models.SignedInUser{}.UserId, + Created: time.Now(), + Updated: time.Now(), + JSONData: &pref.PreferenceJSONData{}, + }) + require.NoError(t, err) + }) +} diff --git a/pkg/services/preference/preftest/fake.go b/pkg/services/preference/preftest/fake.go new file mode 100644 index 00000000000..f52acea2047 --- /dev/null +++ b/pkg/services/preference/preftest/fake.go @@ -0,0 +1,32 @@ +package preftest + +import ( + "context" + + pref "github.com/grafana/grafana/pkg/services/preference" +) + +type FakePreferenceService struct { + ExpectedPreference *pref.Preference + ExpectedError error +} + +func NewPreferenceServiceFake() *FakePreferenceService { + return &FakePreferenceService{} +} + +func (f *FakePreferenceService) GetWithDefaults(ctx context.Context, query *pref.GetPreferenceWithDefaultsQuery) (*pref.Preference, error) { + return f.ExpectedPreference, f.ExpectedError +} + +func (f *FakePreferenceService) Get(ctx context.Context, query *pref.GetPreferenceQuery) (*pref.Preference, error) { + return f.ExpectedPreference, f.ExpectedError +} + +func (f *FakePreferenceService) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) error { + return f.ExpectedError +} + +func (f *FakePreferenceService) GetDefaults() *pref.Preference { + return f.ExpectedPreference +} From f79e0c68cc171474a66fc1bc642cc902ed47b563 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Thu, 14 Apr 2022 13:53:40 +0100 Subject: [PATCH 097/121] Docs: remove outdated section from Explore docs (#47768) --- docs/sources/explore/_index.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/sources/explore/_index.md b/docs/sources/explore/_index.md index 207f5f7b7b1..b9552d5bc3b 100644 --- a/docs/sources/explore/_index.md +++ b/docs/sources/explore/_index.md @@ -55,14 +55,6 @@ In split view, timepickers for both panels can be linked (if you change one, the To close the newly created query, click on the Close Split button. -## Navigate between Explore and a dashboard - -To help accelerate workflows that involve regularly switching from Explore to a dashboard and vice-versa, Grafana provides you with the ability to return to the origin dashboard after navigating to Explore from the panel's dropdown. - -After you've navigated to Explore, you should notice a "Back" button in the Explore toolbar. Simply click it to return to the origin dashboard. To bring changes you make in Explore back to the dashboard, click the arrow next to the button to reveal a "Return to panel with changes" menu item. - -{{< figure src="/static/img/docs/explore/explore_return_dropdown-7-4.png" class="docs-image--no-shadow" max-width= "400px" caption="Screenshot of the expanded explore return dropdown" >}} - ## Share shortened link > **Note:** Available in Grafana 7.3 and later versions. From 782ec05d8c86424920d44ff0548ccb4859d8da7b Mon Sep 17 00:00:00 2001 From: Vardan Torosyan Date: Thu, 14 Apr 2022 15:09:55 +0200 Subject: [PATCH 098/121] Create fixed roles for reading API Keys and service accounts and fix listing of service account tokens (#47767) * Create fixed roles for reading API Keys and service accounts * Handle PR comments and fix the listing of token --- pkg/api/accesscontrol.go | 19 ++++++++++++++- .../serviceaccounts/database/database.go | 2 +- pkg/services/serviceaccounts/manager/roles.go | 24 ++++++++++++++++--- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index dffe67e03a8..e1c3eed6acd 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -150,6 +150,23 @@ func (hs *HTTPServer) declareFixedRoles() error { Grants: []string{string(models.ROLE_VIEWER)}, } + apikeyReaderRole := ac.RoleRegistration{ + Role: ac.RoleDTO{ + Version: 1, + Name: "fixed:apikeys:reader", + DisplayName: "APIKeys reader", + Description: "Gives access to read api keys.", + Group: "API Keys", + Permissions: []ac.Permission{ + { + Action: ac.ActionAPIKeyRead, + Scope: ac.ScopeAPIKeysAll, + }, + }, + }, + Grants: []string{string(models.ROLE_ADMIN)}, + } + apikeyWriterRole := ac.RoleRegistration{ Role: ac.RoleDTO{ Version: 1, @@ -411,7 +428,7 @@ func (hs *HTTPServer) declareFixedRoles() error { orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole, annotationsReaderRole, dashboardAnnotationsWriterRole, annotationsWriterRole, dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole, - foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyWriterRole, + foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyReaderRole, apikeyWriterRole, ) } diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index bd27655fc10..aa11773c867 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -157,7 +157,7 @@ func (s *ServiceAccountsStoreImpl) ListTokens(ctx context.Context, orgID int64, sess = dbSession. Join("inner", quotedUser, quotedUser+".id = api_key.service_account_id"). Where(quotedUser+".org_id=? AND "+quotedUser+".id=?", orgID, serviceAccountID). - Asc("name") + Asc("api_key.name") return sess.Find(&result) }) diff --git a/pkg/services/serviceaccounts/manager/roles.go b/pkg/services/serviceaccounts/manager/roles.go index 656f27881ec..7c323043a95 100644 --- a/pkg/services/serviceaccounts/manager/roles.go +++ b/pkg/services/serviceaccounts/manager/roles.go @@ -1,12 +1,30 @@ package manager import ( + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/serviceaccounts" ) func RegisterRoles(ac accesscontrol.AccessControl) error { - role := accesscontrol.RoleRegistration{ + saReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Version: 1, + Name: "fixed:serviceaccounts:reader", + DisplayName: "Service accounts reader", + Description: "Read service accounts and service account tokens.", + Group: "Service accounts", + Permissions: []accesscontrol.Permission{ + { + Action: serviceaccounts.ActionRead, + Scope: serviceaccounts.ScopeAll, + }, + }, + }, + Grants: []string{string(models.ROLE_ADMIN)}, + } + + saWriter := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ Version: 4, Name: "fixed:serviceaccounts:writer", @@ -31,10 +49,10 @@ func RegisterRoles(ac accesscontrol.AccessControl) error { }, }, }, - Grants: []string{"Admin"}, + Grants: []string{string(models.ROLE_ADMIN)}, } - if err := ac.DeclareFixedRoles(role); err != nil { + if err := ac.DeclareFixedRoles(saReader, saWriter); err != nil { return err } From 057ff5bcf53e5e89965913f2195483979502419a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Apr 2022 15:18:03 +0200 Subject: [PATCH 099/121] Prometheus: Query builder UX tweaks and feedback link (#47655) * Prometheus: Query builder UX tweaks and feedback link * Remove . * Fixed link * added option to hide feedback links * feedback link setting name change * move config check * fixed ts issue --- conf/defaults.ini | 3 ++ conf/sample.ini | 3 ++ docs/sources/administration/configuration.md | 4 ++ packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/frontendsettings.go | 1 + pkg/setting/setting.go | 5 +++ .../components/PromQueryBuilderContainer.tsx | 2 +- .../PromQueryEditorSelector.test.tsx | 10 ++--- .../components/PromQueryEditorSelector.tsx | 33 +++++++------- .../querybuilder/components/QueryPreview.tsx | 2 +- .../querybuilder/shared/FeedbackLink.tsx | 44 +++++++++++++++++++ .../plugins/datasource/prometheus/types.ts | 4 +- 13 files changed, 87 insertions(+), 26 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/shared/FeedbackLink.tsx diff --git a/conf/defaults.ini b/conf/defaults.ini index 09696c5f8dc..904719d5620 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -230,6 +230,9 @@ application_insights_connection_string = # Optional. Specifies an Application Insights endpoint URL where the endpoint string is wrapped in backticks ``. application_insights_endpoint_url = +# Controls if the UI contains any links to user feedback forms +feedback_links_enabled = true + #################################### Security ############################ [security] # disable creation of admin user on first start of grafana diff --git a/conf/sample.ini b/conf/sample.ini index f20d03f7461..fbe7b6b7f8d 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -230,6 +230,9 @@ # Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config ;rudderstack_config_url = +# Controls if the UI contains any links to user feedback forms +;feedback_links_enabled = true + #################################### Security #################################### [security] # disable creation of admin user on first start of grafana diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index 58cdcc98e3b..4a9106482d3 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -509,6 +509,10 @@ If you want to track Grafana usage via Azure Application Insights, then specify
    +### enable_feedback_links + +If set to false will remove all feedback links from the UI. Defaults to true. + ## [security] ### disable_initial_admin_creation diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index bb003374d92..8231bb4c252 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -180,4 +180,5 @@ export interface GrafanaConfig { geomapDisableCustomBaseLayer?: boolean; unifiedAlertingEnabled: boolean; angularSupportEnabled: boolean; + feedbackLinksEnabled: boolean; } diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index f2599a42894..d5076e5ae6a 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -36,6 +36,7 @@ export class GrafanaBootConfig implements GrafanaConfig { externalUserMngLinkName = ''; externalUserMngInfo = ''; allowOrgCreate = false; + feedbackLinksEnabled = true; disableLoginForm = false; defaultDatasource = ''; // UID alertingEnabled = false; diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index cc609bb2dda..cc23170a9ed 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -113,6 +113,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "rudderstackDataPlaneUrl": setting.RudderstackDataPlaneUrl, "rudderstackSdkUrl": setting.RudderstackSdkUrl, "rudderstackConfigUrl": setting.RudderstackConfigUrl, + "feedbackLinksEnabled": hs.Cfg.FeedbackLinksEnabled, "applicationInsightsConnectionString": hs.Cfg.ApplicationInsightsConnectionString, "applicationInsightsEndpointUrl": hs.Cfg.ApplicationInsightsEndpointUrl, "disableLoginForm": setting.DisableLoginForm, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 10834bf4556..58d695f41fc 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -393,6 +393,7 @@ type Cfg struct { ReportingEnabled bool ApplicationInsightsConnectionString string ApplicationInsightsEndpointUrl string + FeedbackLinksEnabled bool // LDAP LDAPEnabled bool @@ -938,13 +939,17 @@ func (cfg *Cfg) Load(args CommandLineArgs) error { RudderstackDataPlaneUrl = analytics.Key("rudderstack_data_plane_url").String() RudderstackSdkUrl = analytics.Key("rudderstack_sdk_url").String() RudderstackConfigUrl = analytics.Key("rudderstack_config_url").String() + cfg.ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) cfg.ReportingDistributor = analytics.Key("reporting_distributor").MustString("grafana-labs") + if len(cfg.ReportingDistributor) >= 100 { cfg.ReportingDistributor = cfg.ReportingDistributor[:100] } + cfg.ApplicationInsightsConnectionString = analytics.Key("application_insights_connection_string").String() cfg.ApplicationInsightsEndpointUrl = analytics.Key("application_insights_endpoint_url").String() + cfg.FeedbackLinksEnabled = analytics.Key("feedback_links_enabled").MustBool(true) if err := readAlertingSettings(iniFile); err != nil { return err diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx index 6d9bbe328d5..5a46feaa0ac 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx @@ -54,7 +54,7 @@ export function PromQueryBuilderContainer(props: Props) { onRunQuery={onRunQuery} data={data} /> - {query.editorPreview && } + {query.rawQuery && } ); } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx index bb92ec241d0..b1283286882 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx @@ -90,24 +90,24 @@ describe('PromQueryEditorSelector', () => { }); }); - it('Can enable preview', async () => { + it('Can enable raw query', async () => { const { onChange } = renderWithMode(QueryEditorMode.Builder); expect(screen.queryByLabelText('selector')).not.toBeInTheDocument(); - screen.getByLabelText('Preview').click(); + screen.getByLabelText('Raw query').click(); expect(onChange).toBeCalledWith({ refId: 'A', expr: defaultQuery.expr, range: true, editorMode: QueryEditorMode.Builder, - editorPreview: true, + rawQuery: true, }); }); - it('Should show preview', async () => { + it('Should show raw query', async () => { renderWithProps({ - editorPreview: true, + rawQuery: true, editorMode: QueryEditorMode.Builder, expr: 'my_metric', }); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx index bfcd143c9f1..9bd94e3761d 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx @@ -14,6 +14,7 @@ import { PromQueryBuilderContainer } from './PromQueryBuilderContainer'; import { PromQueryBuilderOptions } from './PromQueryBuilderOptions'; import { changeEditorMode, getQueryWithDefaults } from '../state'; import { PromQuery } from '../../types'; +import { FeedbackLink } from '../shared/FeedbackLink'; export const PromQueryEditorSelector = React.memo((props) => { const { onChange, onRunQuery, data } = props; @@ -44,7 +45,7 @@ export const PromQueryEditorSelector = React.memo((props) const onQueryPreviewChange = (event: SyntheticEvent) => { const isEnabled = event.currentTarget.checked; - onChange({ ...query, editorPreview: isEnabled }); + onChange({ ...query, rawQuery: isEnabled }); onRunQuery(); }; @@ -67,16 +68,6 @@ export const PromQueryEditorSelector = React.memo((props) onDismiss={() => setParseModalOpen(false)} /> - - {editorMode === QueryEditorMode.Builder && ( <> ((props) }} options={promQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))} /> + )} - + {editorMode === QueryEditorMode.Builder && ( + + )} + + diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx index b95df791017..cd7a267e542 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx @@ -18,7 +18,7 @@ export function QueryPreview({ query }: Props) { return ( - +
    + + Give feedback + + + ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + link: css({ + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + ':hover': { + color: theme.colors.text.link, + }, + }), + }; +} diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index 629028914ca..f2c4b494c5e 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -19,8 +19,8 @@ export interface PromQuery extends DataQuery { showingTable?: boolean; /** Code, Builder or Explain */ editorMode?: QueryEditorMode; - /** Controls if the query preview is shown */ - editorPreview?: boolean; + /** Controls if the raw query text is shown */ + rawQuery?: boolean; } export interface PromOptions extends DataSourceJsonData { From 388b3bf9084a2491e479a0baec8476a7ee28eba2 Mon Sep 17 00:00:00 2001 From: Andy Lindberg Date: Thu, 14 Apr 2022 08:49:56 -0600 Subject: [PATCH 100/121] Grafana/UI: Bundle raw SVG icon strings (#47665) * Build: bundle raw svg icon strings in grafana/ui * remove @rollup/plugin-image --- packages/grafana-ui/package.json | 2 +- packages/grafana-ui/rollup.config.ts | 4 +-- yarn.lock | 44 ++++++++++++++-------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index a9485cc8168..d73b877eb70 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -98,7 +98,6 @@ "@grafana/tsconfig": "^1.2.0-rc1", "@mdx-js/react": "1.6.22", "@rollup/plugin-commonjs": "21.0.2", - "@rollup/plugin-image": "2.1.1", "@rollup/plugin-node-resolve": "13.1.3", "@storybook/addon-a11y": "6.4.21", "@storybook/addon-actions": "6.4.21", @@ -168,6 +167,7 @@ "rimraf": "3.0.2", "rollup": "2.70.1", "rollup-plugin-sourcemaps": "0.6.3", + "rollup-plugin-svg-import": "^1.6.0", "rollup-plugin-terser": "7.0.2", "sass-loader": "12.6.0", "storybook-dark-mode": "1.0.9", diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts index d40c5776084..71f712df14f 100644 --- a/packages/grafana-ui/rollup.config.ts +++ b/packages/grafana-ui/rollup.config.ts @@ -1,6 +1,6 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; -import image from '@rollup/plugin-image'; +import svg from 'rollup-plugin-svg-import'; import { terser } from 'rollup-plugin-terser'; const pkg = require('./package.json'); @@ -41,7 +41,7 @@ const buildCjsPackage = ({ env }) => { include: /node_modules/, }), resolve(), - image(), + svg({ stringify: true }), env === 'production' && terser(), ], }; diff --git a/yarn.lock b/yarn.lock index 1cd157ebdc3..5273232fb8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4366,7 +4366,6 @@ __metadata: "@react-aria/overlays": 3.8.1 "@react-stately/menu": 3.2.6 "@rollup/plugin-commonjs": 21.0.2 - "@rollup/plugin-image": 2.1.1 "@rollup/plugin-node-resolve": 13.1.3 "@sentry/browser": 6.19.1 "@storybook/addon-a11y": 6.4.21 @@ -4476,6 +4475,7 @@ __metadata: rimraf: 3.0.2 rollup: 2.70.1 rollup-plugin-sourcemaps: 0.6.3 + rollup-plugin-svg-import: ^1.6.0 rollup-plugin-terser: 7.0.2 rxjs: 7.5.5 sass-loader: 12.6.0 @@ -7256,18 +7256,6 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-image@npm:2.1.1": - version: 2.1.1 - resolution: "@rollup/plugin-image@npm:2.1.1" - dependencies: - "@rollup/pluginutils": ^3.1.0 - mini-svg-data-uri: ^1.2.3 - peerDependencies: - rollup: ^1.20.0 || ^2.0.0 - checksum: a629c8f22233ca159c23655fdbc3449dab3c939372178ed4462fc9c525cc4ecd8b11fae359eb94be4f769d26f48b85fb18eb16ce1fbc33ed16b6a7c1f84391f6 - languageName: node - linkType: hard - "@rollup/plugin-json@npm:4.1.0": version: 4.1.0 resolution: "@rollup/plugin-json@npm:4.1.0" @@ -7308,6 +7296,16 @@ __metadata: languageName: node linkType: hard +"@rollup/pluginutils@npm:^4.1.1": + version: 4.2.0 + resolution: "@rollup/pluginutils@npm:4.2.0" + dependencies: + estree-walker: ^2.0.1 + picomatch: ^2.2.2 + checksum: 2e86d9bfb95919727bcba0bbbdbedc98e25a1e51fe3047f18ec6d85e0743d1c73e1c0de3f9fdbd2ff6b90c32f30d4b2706c9e794f3c2e7a80156980081558e2e + languageName: node + linkType: hard + "@rtsao/plugin-proposal-class-properties@npm:7.0.1-patch.1": version: 7.0.1-patch.1 resolution: "@rtsao/plugin-proposal-class-properties@npm:7.0.1-patch.1" @@ -25960,15 +25958,6 @@ __metadata: languageName: node linkType: hard -"mini-svg-data-uri@npm:^1.2.3": - version: 1.4.3 - resolution: "mini-svg-data-uri@npm:1.4.3" - bin: - mini-svg-data-uri: cli.js - checksum: bd1789c34907a36ae9157897b7c7f6ad700e71c8cb6d725bb2810ce6211abcaa3717333ca17408f6f8b2a44055e571cd5b78d478688e1864513cb7f8e023ae3f - languageName: node - linkType: hard - "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -32390,6 +32379,17 @@ __metadata: languageName: node linkType: hard +"rollup-plugin-svg-import@npm:^1.6.0": + version: 1.6.0 + resolution: "rollup-plugin-svg-import@npm:1.6.0" + dependencies: + "@rollup/pluginutils": ^4.1.1 + peerDependencies: + rollup: ">=1.29.0 <3.0.0" + checksum: 453862c39d2301563d9d07f6647c295377ff66cf3174d2a0612389fda4bfd9fa72718d90279feb782d3525f1d6f9710e1dc78641cd9d4044360a0179f88054b0 + languageName: node + linkType: hard + "rollup-plugin-terser@npm:7.0.2": version: 7.0.2 resolution: "rollup-plugin-terser@npm:7.0.2" From ab4c7f14aaf70db9591b48de833dee0cae299c5d Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 14 Apr 2022 10:22:09 -0500 Subject: [PATCH 101/121] Explore: Follow up NoData work (#47745) * Use theme styles and add basic test for rendering * Use util function for response --- public/app/features/explore/Explore.test.tsx | 125 +++++++++++++++++++ public/app/features/explore/NoData.tsx | 7 +- 2 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 public/app/features/explore/Explore.test.tsx diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx new file mode 100644 index 00000000000..b9ce6c9f5f7 --- /dev/null +++ b/public/app/features/explore/Explore.test.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { DataSourceApi, LoadingState, CoreApp, createTheme } from '@grafana/data'; +import { render, screen } from '@testing-library/react'; +import { ExploreId } from 'app/types/explore'; +import { Explore, Props } from './Explore'; +import { scanStopAction } from './state/query'; +import { Provider } from 'react-redux'; +import { configureStore } from 'app/store/configureStore'; +import { AutoSizerProps } from 'react-virtualized-auto-sizer'; +import { createEmptyQueryResponse } from './state/utils'; + +const makeEmptyQueryResponse = (loadingState: LoadingState) => { + const baseEmptyResponse = createEmptyQueryResponse(); + + baseEmptyResponse.request = { + requestId: '1', + intervalMs: 0, + interval: '1s', + dashboardId: 0, + panelId: 1, + range: baseEmptyResponse.timeRange, + scopedVars: { + apps: { + value: 'value', + text: 'text', + }, + }, + targets: [ + { + refId: 'A', + }, + ], + timezone: 'UTC', + app: CoreApp.Explore, + startTime: 0, + }; + + baseEmptyResponse.state = loadingState; + + return baseEmptyResponse; +}; + +const dummyProps: Props = { + logsResult: undefined, + changeSize: jest.fn(), + datasourceInstance: { + meta: { + metrics: true, + logs: true, + }, + components: { + QueryEditorHelp: {}, + }, + } as DataSourceApi, + datasourceMissing: false, + exploreId: ExploreId.left, + loading: false, + modifyQueries: jest.fn(), + scanStart: jest.fn(), + scanStopAction: scanStopAction, + setQueries: jest.fn(), + queryKeys: [], + isLive: false, + syncedTimes: false, + updateTimeRange: jest.fn(), + makeAbsoluteTime: jest.fn(), + graphResult: [], + absoluteRange: { + from: 0, + to: 0, + }, + timeZone: 'UTC', + queryResponse: makeEmptyQueryResponse(LoadingState.NotStarted), + addQueryRow: jest.fn(), + theme: createTheme(), + showMetrics: true, + showLogs: true, + showTable: true, + showTrace: true, + showNodeGraph: true, + splitOpen: (() => {}) as any, + logsVolumeData: undefined, + loadLogsVolumeData: () => {}, + changeGraphStyle: () => {}, + graphStyle: 'lines', +}; + +jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { + return { + getDataSourceSrv: () => ({ + get: () => Promise.resolve({}), + getList: () => [], + getInstanceSettings: () => {}, + }), + }; +}); + +// for the AutoSizer component to have a width +jest.mock('react-virtualized-auto-sizer', () => { + return ({ children }: AutoSizerProps) => children({ height: 1, width: 1 }); +}); + +const setup = (overrideProps?: Partial) => { + const store = configureStore(); + const exploreProps = { ...dummyProps, ...overrideProps }; + + return render( + + + + ); +}; + +describe('Explore', () => { + it('should not render no data with not started loading state', () => { + setup(); + expect(screen.queryByTestId('explore-no-data')).not.toBeInTheDocument(); + }); + + it('should render no data with done loading state', async () => { + const queryResp = makeEmptyQueryResponse(LoadingState.Done); + setup({ queryResponse: queryResp }); + expect(screen.getByTestId('explore-no-data')).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/explore/NoData.tsx b/public/app/features/explore/NoData.tsx index 7b9c8df5f0d..d50330747f6 100644 --- a/public/app/features/explore/NoData.tsx +++ b/public/app/features/explore/NoData.tsx @@ -9,7 +9,7 @@ export const NoData = () => { const css = useStyles2(getStyles); return ( <> -
    +
    {'No data'}
    @@ -29,9 +29,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ flex-grow: 1; `, message: css` - margin-bottom: ${theme.spacing(3)}; - font-size: 2em; - padding: 6em 1em; + font-size: ${theme.typography.h2.fontSize}; + padding: ${theme.spacing(4)}; color: ${theme.colors.text.disabled}; `, }); From 41012af997083f612a3167b269f04fe9b869b74f Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Thu, 14 Apr 2022 17:54:49 +0200 Subject: [PATCH 102/121] Tracing: Use common traceID context value for opentracing and opentelemetry (#46411) * use common traceID context value for opentracing and opentelemetry * support sampled trace IDs as well * inject traceID into NormalResponse on errors * Finally the test passed * fix the test * fix linter * change the function parameter Co-authored-by: Ying WANG --- pkg/api/ldap_debug_test.go | 26 +++++------ pkg/api/metrics_test.go | 41 +++++++---------- pkg/api/response/response.go | 11 ++++- pkg/infra/tracing/opentelemetry_tracing.go | 1 + pkg/infra/tracing/tracing.go | 19 ++++++++ pkg/middleware/logger.go | 6 +-- pkg/middleware/request_metrics.go | 4 +- pkg/models/context.go | 5 +- pkg/services/contexthandler/contexthandler.go | 5 +- pkg/services/sqlstore/database_wrapper.go | 3 +- .../alerting/api_admin_configuration_test.go | 5 +- .../api_alertmanager_configuration_test.go | 14 ++++-- .../api/alerting/api_alertmanager_test.go | 46 +++++++++++++------ .../alerting/api_notification_channel_test.go | 2 +- pkg/tests/api/alerting/api_prometheus_test.go | 12 +++-- pkg/tests/api/alerting/api_ruler_test.go | 18 ++++++-- 16 files changed, 140 insertions(+), 78 deletions(-) diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index f9d796018bc..f7ed3473906 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "errors" "net/http" "net/http/httptest" @@ -146,13 +147,11 @@ func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) { require.Equal(t, http.StatusBadRequest, sc.resp.Code) - expected := ` - { - "error": "unable to find organization with ID '2'", - "message": "An organization was not found - Please verify your LDAP configuration" - } - ` - assert.JSONEq(t, expected, sc.resp.Body.String()) + var res map[string]interface{} + err := json.Unmarshal(sc.resp.Body.Bytes(), &res) + assert.NoError(t, err) + assert.Equal(t, "unable to find organization with ID '2'", res["error"]) + assert.Equal(t, "An organization was not found - Please verify your LDAP configuration", res["message"]) } func TestGetUserFromLDAPAPIEndpoint(t *testing.T) { @@ -470,14 +469,11 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) { }, &sqlstoremock) assert.Equal(t, http.StatusBadRequest, sc.resp.Code) - expected := ` - { - "error": "did not find a user", - "message": "Refusing to sync grafana super admin \"ldap-daniel\" - it would be disabled" - } - ` - - assert.JSONEq(t, expected, sc.resp.Body.String()) + var res map[string]interface{} + err := json.Unmarshal(sc.resp.Body.Bytes(), &res) + assert.NoError(t, err) + assert.Equal(t, "did not find a user", res["error"]) + assert.Equal(t, "Refusing to sync grafana super admin \"ldap-daniel\" - it would be disabled", res["message"]) } func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) { diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index 634ce0ee74e..5a32d5cfbba 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -2,6 +2,7 @@ package api import ( "context" + "encoding/json" "fmt" "net/http" "strconv" @@ -241,15 +242,14 @@ func TestAPIEndpoint_Metrics_QueryMetricsFromDashboard(t *testing.T) { strings.NewReader(queryDatasourceInput), t, ) + assert.Equal(t, http.StatusBadRequest, response.Code) - assert.JSONEq( - t, - fmt.Sprintf( - "{\"error\":\"%[1]s\",\"message\":\"%[1]s\"}", - models.ErrDashboardOrPanelIdentifierNotSet, - ), - response.Body.String(), - ) + + var res map[string]interface{} + err := json.Unmarshal(response.Body.Bytes(), &res) + assert.NoError(t, err) + assert.Equal(t, models.ErrDashboardOrPanelIdentifierNotSet.Error(), res["error"]) + assert.Equal(t, models.ErrDashboardOrPanelIdentifierNotSet.Error(), res["message"]) }) t.Run("Cannot query without a valid orgid", func(t *testing.T) { @@ -261,14 +261,10 @@ func TestAPIEndpoint_Metrics_QueryMetricsFromDashboard(t *testing.T) { t, ) assert.Equal(t, http.StatusBadRequest, response.Code) - assert.JSONEq( - t, - fmt.Sprintf( - "{\"error\":\"%[1]s\",\"message\":\"%[1]s\"}", - models.ErrDashboardOrPanelIdentifierNotSet, - ), - response.Body.String(), - ) + var res map[string]interface{} + assert.NoError(t, json.Unmarshal(response.Body.Bytes(), &res)) + assert.Equal(t, models.ErrDashboardOrPanelIdentifierNotSet.Error(), res["error"]) + assert.Equal(t, models.ErrDashboardOrPanelIdentifierNotSet.Error(), res["message"]) }) t.Run("Cannot query without a valid dashboard or panel ID", func(t *testing.T) { @@ -280,14 +276,11 @@ func TestAPIEndpoint_Metrics_QueryMetricsFromDashboard(t *testing.T) { t, ) assert.Equal(t, http.StatusBadRequest, response.Code) - assert.JSONEq( - t, - fmt.Sprintf( - "{\"error\":\"%[1]s\",\"message\":\"%[1]s\"}", - models.ErrDashboardOrPanelIdentifierNotSet, - ), - response.Body.String(), - ) + + var res map[string]interface{} + assert.NoError(t, json.Unmarshal(response.Body.Bytes(), &res)) + assert.Equal(t, models.ErrDashboardOrPanelIdentifierNotSet.Error(), res["error"]) + assert.Equal(t, models.ErrDashboardOrPanelIdentifierNotSet.Error(), res["message"]) }) t.Run("Cannot query when ValidatedQueries is disabled", func(t *testing.T) { diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index 57d751a407c..58de6282652 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" jsoniter "github.com/json-iterator/go" @@ -73,7 +74,15 @@ func (r *NormalResponse) ErrMessage() string { func (r *NormalResponse) WriteTo(ctx *models.ReqContext) { if r.err != nil { - ctx.Logger.Error(r.errMessage, "error", r.err, "remote_addr", ctx.RemoteAddr()) + v := map[string]interface{}{} + traceID := tracing.TraceIDFromContext(ctx.Req.Context(), false) + if err := json.Unmarshal(r.body.Bytes(), &v); err == nil { + v["traceID"] = traceID + if b, err := json.Marshal(v); err == nil { + r.body = bytes.NewBuffer(b) + } + } + ctx.Logger.Error(r.errMessage, "error", r.err, "remote_addr", ctx.RemoteAddr(), "traceID", traceID) } header := ctx.Resp.Header() diff --git a/pkg/infra/tracing/opentelemetry_tracing.go b/pkg/infra/tracing/opentelemetry_tracing.go index abe7fcb2fe2..61b62a850e0 100644 --- a/pkg/infra/tracing/opentelemetry_tracing.go +++ b/pkg/infra/tracing/opentelemetry_tracing.go @@ -126,6 +126,7 @@ func (ots *Opentelemetry) Start(ctx context.Context, spanName string, opts ...tr opentelemetrySpan := OpentelemetrySpan{ span: span, } + ctx = context.WithValue(ctx, traceKey{}, traceValue{span.SpanContext().TraceID().String(), span.SpanContext().IsSampled()}) return ctx, opentelemetrySpan } diff --git a/pkg/infra/tracing/tracing.go b/pkg/infra/tracing/tracing.go index 95df49b4727..c13e353e94e 100644 --- a/pkg/infra/tracing/tracing.go +++ b/pkg/infra/tracing/tracing.go @@ -14,6 +14,7 @@ import ( opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" ol "github.com/opentracing/opentracing-go/log" + "github.com/uber/jaeger-client-go" jaegercfg "github.com/uber/jaeger-client-go/config" "github.com/uber/jaeger-client-go/zipkin" "go.opentelemetry.io/otel/attribute" @@ -52,6 +53,21 @@ func ProvideService(cfg *setting.Cfg) (Tracer, error) { return ots, ots.initOpentelemetryTracer() } +type traceKey struct{} +type traceValue struct { + ID string + IsSampled bool +} + +func TraceIDFromContext(c context.Context, requireSampled bool) string { + v := c.Value(traceKey{}) + // Return traceID if a) it is present and b) it is sampled when requireSampled param is true + if trace, ok := v.(traceValue); ok && (!requireSampled || trace.IsSampled) { + return trace.ID + } + return "" +} + type Opentracing struct { enabled bool address string @@ -172,6 +188,9 @@ func (ts *Opentracing) Run(ctx context.Context) error { func (ts *Opentracing) Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, Span) { span, ctx := opentracing.StartSpanFromContext(ctx, spanName) opentracingSpan := OpentracingSpan{span: span} + if sctx, ok := span.Context().(jaeger.SpanContext); ok { + ctx = context.WithValue(ctx, traceKey{}, traceValue{sctx.TraceID().String(), sctx.IsSampled()}) + } return ctx, opentracingSpan } diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index 15174880807..c2f127319cb 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -19,10 +19,10 @@ import ( "net/http" "time" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - cw "github.com/weaveworks/common/tracing" ) func Logger(cfg *setting.Cfg) web.Handler { @@ -58,8 +58,8 @@ func Logger(cfg *setting.Cfg) web.Handler { "referer", req.Referer(), } - traceID, exist := cw.ExtractTraceID(ctx.Req.Context()) - if exist { + traceID := tracing.TraceIDFromContext(ctx.Req.Context(), false) + if traceID != "" { logParams = append(logParams, "traceID", traceID) } diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go index d4923f233a6..6f9b2618993 100644 --- a/pkg/middleware/request_metrics.go +++ b/pkg/middleware/request_metrics.go @@ -7,10 +7,10 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" "github.com/prometheus/client_golang/prometheus" - cw "github.com/weaveworks/common/tracing" ) var ( @@ -80,7 +80,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler { // since they dont make much sense. We should remove them later. histogram := httpRequestDurationHistogram. WithLabelValues(handler, code, req.Method) - if traceID, ok := cw.ExtractSampledTraceID(c.Req.Context()); ok { + if traceID := tracing.TraceIDFromContext(c.Req.Context(), true); traceID != "" { // Need to type-convert the Observer to an // ExemplarObserver. This will always work for a // HistogramVec. diff --git a/pkg/models/context.go b/pkg/models/context.go index dca3b2e5310..f31ea1197d6 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" "github.com/prometheus/client_golang/prometheus" @@ -51,9 +52,11 @@ func (ctx *ReqContext) IsApiRequest() bool { func (ctx *ReqContext) JsonApiErr(status int, message string, err error) { resp := make(map[string]interface{}) + traceID := tracing.TraceIDFromContext(ctx.Req.Context(), false) if err != nil { - ctx.Logger.Error(message, "error", err) + resp["traceID"] = traceID + ctx.Logger.Error(message, "error", err, "traceID", traceID) if setting.Env != setting.Prod { resp["error"] = err.Error() } diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 386ea1ea1b4..d339ea5f09b 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -25,7 +25,6 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" - cw "github.com/weaveworks/common/tracing" ) const ( @@ -98,8 +97,8 @@ func (h *ContextHandler) Middleware(mContext *web.Context) { mContext.Req = mContext.Req.WithContext(context.WithValue(mContext.Req.Context(), reqContextKey{}, reqContext)) mContext.Map(mContext.Req) - traceID, exists := cw.ExtractTraceID(mContext.Req.Context()) - if exists { + traceID := tracing.TraceIDFromContext(mContext.Req.Context(), false) + if traceID != "" { reqContext.Logger = reqContext.Logger.New("traceID", traceID) } diff --git a/pkg/services/sqlstore/database_wrapper.go b/pkg/services/sqlstore/database_wrapper.go index 81079412cd3..400f823a55e 100644 --- a/pkg/services/sqlstore/database_wrapper.go +++ b/pkg/services/sqlstore/database_wrapper.go @@ -16,7 +16,6 @@ import ( "github.com/lib/pq" "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus" - cw "github.com/weaveworks/common/tracing" "xorm.io/core" ) @@ -83,7 +82,7 @@ func (h *databaseQueryWrapper) instrument(ctx context.Context, status string, qu elapsed := time.Since(begin) histogram := databaseQueryHistogram.WithLabelValues(status) - if traceID, ok := cw.ExtractSampledTraceID(ctx); ok { + if traceID := tracing.TraceIDFromContext(ctx, true); traceID != "" { // Need to type-convert the Observer to an // ExemplarObserver. This will always work for a // HistogramVec. diff --git a/pkg/tests/api/alerting/api_admin_configuration_test.go b/pkg/tests/api/alerting/api_admin_configuration_test.go index c114d046494..3dca58e0d16 100644 --- a/pkg/tests/api/alerting/api_admin_configuration_test.go +++ b/pkg/tests/api/alerting/api_admin_configuration_test.go @@ -66,7 +66,10 @@ func TestAdminConfiguration_SendingToExternalAlertmanagers(t *testing.T) { resp := getRequest(t, alertsURL, http.StatusNotFound) // nolint b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) - require.JSONEq(t, `{"message": "no admin configuration available"}`, string(b)) + var res map[string]interface{} + err = json.Unmarshal(b, &res) + require.NoError(t, err) + require.Equal(t, "no admin configuration available", res["message"]) } // An invalid alertmanager choice should return an error. diff --git a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go index dd6e1fcbbc3..7405c32083c 100644 --- a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go @@ -3,6 +3,7 @@ package alerting import ( "encoding/json" "fmt" + "io/ioutil" "net/http" "regexp" "testing" @@ -89,9 +90,13 @@ func TestAlertmanagerConfigurationIsTransactional(t *testing.T) { } ` resp := postRequest(t, alertConfigURL, payload, http.StatusBadRequest) // nolint - require.JSONEq(t, `{"message": "failed to save and apply Alertmanager configuration: failed to build integration map: the receiver is invalid: failed to validate receiver \"slack.receiver\" of type \"slack\": token must be specified when using the Slack chat API"}`, getBody(t, resp.Body)) - + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, `failed to save and apply Alertmanager configuration: failed to build integration map: the receiver is invalid: failed to validate receiver "slack.receiver" of type "slack": token must be specified when using the Slack chat API`, res["message"]) resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint + require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body)) } @@ -210,7 +215,10 @@ func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) { ` resp := postRequest(t, alertConfigURL, payload, http.StatusBadRequest) // nolint - require.JSONEq(t, `{"message": "unknown receiver: invalid"}`, getBody(t, resp.Body)) + s := getBody(t, resp.Body) + var res map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(s), &res)) + require.Equal(t, "unknown receiver: invalid", res["message"]) } // The secure settings must be present diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index 0d1a6e86db1..1ed1cde55c8 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -437,7 +437,10 @@ func TestAlertAndGroupsQuery(t *testing.T) { b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, http.StatusUnauthorized, resp.StatusCode) - require.JSONEq(t, `{"message": "invalid username or password"}`, string(b)) + + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, "invalid username or password", res["message"]) } // When there are no alerts available, it returns an empty list. @@ -897,7 +900,7 @@ func TestAlertRuleCRUD(t *testing.T) { Data: []ngmodels.AlertQuery{}, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: invalid alert rule: no queries or expressions are found"}`, + expectedResponse: `{"message": "invalid rule specification at index [0]: invalid alert rule: no queries or expressions are found", "traceID":"00000000000000000000000000000000"}`, }, { desc: "alert rule with empty title", @@ -927,7 +930,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: alert rule title cannot be empty"}`, + expectedResponse: `{"message": "invalid rule specification at index [0]: alert rule title cannot be empty", "traceID":"00000000000000000000000000000000"}`, }, { desc: "alert rule with too long name", @@ -957,7 +960,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: alert rule title is too long. Max length is 190"}`, + expectedResponse: `{"message": "invalid rule specification at index [0]: alert rule title is too long. Max length is 190", "traceID":"00000000000000000000000000000000"}`, }, { desc: "alert rule with too long rulegroup", @@ -987,7 +990,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "rule group name is too long. Max length is 190"}`, + expectedResponse: `{"message": "rule group name is too long. Max length is 190", "traceID":"00000000000000000000000000000000"}`, }, { desc: "alert rule with invalid interval", @@ -1018,7 +1021,8 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "rule evaluation interval (1 second) should be positive number that is multiple of the base interval of 10 seconds"}`, + expectedResponse: `{"message": "rule evaluation interval (1 second) should be positive ` + + `number that is multiple of the base interval of 10 seconds", "traceID":"00000000000000000000000000000000"}`, }, { desc: "alert rule with unknown datasource", @@ -1048,7 +1052,8 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: failed to validate condition of alert rule AlwaysFiring: invalid query A: data source not found: unknown"}`, + expectedResponse: `{"message": "invalid rule specification at index [0]: failed to validate condition of alert rule AlwaysFiring:` + + ` invalid query A: data source not found: unknown", "traceID":"00000000000000000000000000000000"}`, }, { desc: "alert rule with invalid condition", @@ -1078,7 +1083,8 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: failed to validate condition of alert rule AlwaysFiring: condition B not found in any query or expression: it should be one of: [A]"}`, + expectedResponse: `{"message": "invalid rule specification at index [0]: failed to validate condition of alert rule AlwaysFiring: ` + + `condition B not found in any query or expression: it should be one of: [A]", "traceID":"00000000000000000000000000000000"}`, }, } @@ -1368,7 +1374,9 @@ func TestAlertRuleCRUD(t *testing.T) { require.NoError(t, err) assert.Equal(t, http.StatusNotFound, resp.StatusCode) - require.JSONEq(t, `{"message": "failed to update rule group: failed to update rule with UID unknown because could not find alert rule"}`, string(b)) + var res map[string]interface{} + assert.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, "failed to update rule group: failed to update rule with UID unknown because could not find alert rule", res["message"]) // let's make sure that rule definitions are not affected by the failed POST request. u = fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) @@ -1487,7 +1495,9 @@ func TestAlertRuleCRUD(t *testing.T) { require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - require.JSONEq(t, fmt.Sprintf(`{"message": "rule [1] has UID %s that is already assigned to another rule at index 0"}`, ruleUID), string(b)) + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, fmt.Sprintf("rule [1] has UID %s that is already assigned to another rule at index 0", ruleUID), res["message"]) // let's make sure that rule definitions are not affected by the failed POST request. u = fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr) @@ -1893,7 +1903,9 @@ func TestAlertRuleCRUD(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusAccepted, resp.StatusCode) - require.JSONEq(t, `{"message":"rules deleted"}`, string(b)) + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, "rules deleted", res["message"]) }) t.Run("succeed if the rule group name does exist", func(t *testing.T) { @@ -2103,7 +2115,9 @@ func TestQuota(t *testing.T) { b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, http.StatusForbidden, resp.StatusCode) - require.JSONEq(t, `{"message": "quota has been exceeded"}`, string(b)) + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, "quota has been exceeded", res["message"]) }) t.Run("when quota limit exceed updating existing rule should succeed", func(t *testing.T) { @@ -2400,7 +2414,8 @@ func TestEval(t *testing.T) { } `, expectedStatusCode: http.StatusBadRequest, - expectedResponse: `{"message": "invalid condition: condition B not found in any query or expression: it should be one of: [A]"}`, + expectedResponse: `{"message": "invalid condition: condition B not found in any query or expression: it should be one of: [A]",` + + `"traceID": "00000000000000000000000000000000"}`, }, { desc: "unknown query datasource", @@ -2425,7 +2440,7 @@ func TestEval(t *testing.T) { } `, expectedStatusCode: http.StatusBadRequest, - expectedResponse: `{"message": "invalid condition: invalid query A: data source not found: unknown"}`, + expectedResponse: `{"message": "invalid condition: invalid query A: data source not found: unknown", "traceID": "00000000000000000000000000000000"}`, }, } @@ -2581,7 +2596,8 @@ func TestEval(t *testing.T) { } `, expectedStatusCode: http.StatusBadRequest, - expectedResponse: `{"message": "invalid queries or expressions: invalid query A: data source not found: unknown"}`, + expectedResponse: `{"message": "invalid queries or expressions: invalid query A: data source not found: unknown",` + + `"traceID": "00000000000000000000000000000000"}`, }, } diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 20cd1b0eac5..495656e115e 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -61,7 +61,7 @@ func TestTestReceivers(t *testing.T) { b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) - require.JSONEq(t, `{}`, string(b)) + require.JSONEq(t, `{"traceID":"00000000000000000000000000000000"}`, string(b)) }) t.Run("assert working receiver returns OK", func(t *testing.T) { diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index 95b059be794..28a4aec99b5 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -206,7 +206,9 @@ func TestPrometheusRules(t *testing.T) { require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) - require.JSONEq(t, `{"message": "invalid rule specification at index [0]: both annotations __dashboardUid__ and __panelId__ must be specified"}`, string(b)) + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, "invalid rule specification at index [0]: both annotations __dashboardUid__ and __panelId__ must be specified", res["message"]) } // Now, let's see how this looks like. @@ -587,7 +589,9 @@ func TestPrometheusRulesFilterByDashboard(t *testing.T) { require.Equal(t, http.StatusBadRequest, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) - require.JSONEq(t, `{"message": "invalid panel_id: strconv.ParseInt: parsing \"invalid\": invalid syntax"}`, string(b)) + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, `invalid panel_id: strconv.ParseInt: parsing "invalid": invalid syntax`, res["message"]) } // Now, let's check a panel_id without dashboard_uid returns a 400 Bad Request response @@ -603,7 +607,9 @@ func TestPrometheusRulesFilterByDashboard(t *testing.T) { require.Equal(t, http.StatusBadRequest, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) - require.JSONEq(t, `{"message": "panel_id must be set with dashboard_uid"}`, string(b)) + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, "panel_id must be set with dashboard_uid", res["message"]) } } diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 734d1bffbab..b9556e26d2e 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -410,7 +410,10 @@ func TestAlertRuleConflictingTitle(t *testing.T) { require.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - require.JSONEq(t, `{"message": "failed to update rule group: failed to add rules: a conflicting alert rule is found: rule title under the same organisation and folder should be unique"}`, string(b)) + + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, "failed to update rule group: failed to add rules: a conflicting alert rule is found: rule title under the same organisation and folder should be unique", res["message"]) }) t.Run("trying to update an alert to the title of an existing alert in the same folder should fail", func(t *testing.T) { @@ -435,7 +438,10 @@ func TestAlertRuleConflictingTitle(t *testing.T) { require.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - require.JSONEq(t, `{"message": "failed to update rule group: failed to update rules: a conflicting alert rule is found: rule title under the same organisation and folder should be unique"}`, string(b)) + + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, "failed to update rule group: failed to update rules: a conflicting alert rule is found: rule title under the same organisation and folder should be unique", res["message"]) }) t.Run("trying to create alert with same title under another folder should succeed", func(t *testing.T) { @@ -789,7 +795,9 @@ func TestRulerRulesFilterByDashboard(t *testing.T) { require.Equal(t, http.StatusBadRequest, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) - require.JSONEq(t, `{"message": "invalid panel_id: strconv.ParseInt: parsing \"invalid\": invalid syntax"}`, string(b)) + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, `invalid panel_id: strconv.ParseInt: parsing "invalid": invalid syntax`, res["message"]) } // Now, let's check a panel_id without dashboard_uid returns a 400 Bad Request response @@ -805,7 +813,9 @@ func TestRulerRulesFilterByDashboard(t *testing.T) { require.Equal(t, http.StatusBadRequest, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) - require.JSONEq(t, `{"message": "panel_id must be set with dashboard_uid"}`, string(b)) + var res map[string]interface{} + require.NoError(t, json.Unmarshal(b, &res)) + require.Equal(t, "panel_id must be set with dashboard_uid", res["message"]) } } From 6a8f9b7ebc6afeb288b84bdc6475c5c05546827a Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 14 Apr 2022 18:35:37 +0200 Subject: [PATCH 103/121] Table panel: Fix horizontal scrolling when pagination is enabled (#47776) * Table panel: Fix horizontal scrolling when pagination is enabled * Fix failing test --- .../panel-table/table_pagination.json | 2683 +++++++++++++++++ .../grafana-ui/src/components/Table/Table.tsx | 2 +- .../grafana-ui/src/components/Table/styles.ts | 18 +- 3 files changed, 2697 insertions(+), 6 deletions(-) create mode 100644 devenv/dev-dashboards/panel-table/table_pagination.json diff --git a/devenv/dev-dashboards/panel-table/table_pagination.json b/devenv/dev-dashboards/panel-table/table_pagination.json new file mode 100644 index 00000000000..da1765cd937 --- /dev/null +++ b/devenv/dev-dashboards/panel-table/table_pagination.json @@ -0,0 +1,2683 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1110, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [ + { + "options": { + "rent": { + "color": "yellow", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "property_type" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "store": { + "color": "blue", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "custom.width", + "value": 205 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Number of rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 66 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Description" + }, + "properties": [ + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "URL" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Show details", + "url": "${__value.raw}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Surface" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background-solid" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "orange", + "value": 100 + }, + { + "color": "yellow", + "value": 200 + }, + { + "color": "green", + "value": 300 + }, + { + "color": "#EAB839", + "value": 500 + }, + { + "color": "#6ED0E0", + "value": 1000 + }, + { + "color": "#EF843C", + "value": 2000 + } + ] + } + }, + { + "id": "unit", + "value": "areaM2" + }, + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 94 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "City" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 77 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Price" + }, + "properties": [ + { + "id": "unit", + "value": "currencyUSD" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "footer": { + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Surface" + } + ] + }, + "pluginVersion": "9.0.0-pre", + "targets": [ + { + "csvContent": "\"id\",\"created_on\",\"operation\",\"property_type\",\"place_name\",\"place_with_parent_names\",\"country_name\",\"state_name\",\"geonames_id\",\"lat_lon\",\"lat\",\"lon\",\"price\",\"currency\",\"price_aprox_local_currency\",\"price_aprox_usd\",\"surface_total_in_m2\",\"surface_covered_in_m2\",\"price_usd_per_m2\",\"price_per_m2\",\"floor\",\"rooms\",\"expenses\",\"properati_url\",\"description\",\"title\",\"image_thumbnail\"\nf73c373e0588bf568164a057dd94203ac87d47b1,2014-08-07,rent,store,Catu,|Brasil|Bahia|Catu|,Brasil,Bahia,,,,,0,,0,0,,,,,,,,http://www.properati.com.br/mfye_alugar_outros_catu,\"Otimo Imovel com o melhor valor da regiao, confira as fotos - \",Exclusividade! - Loja Em Catu,https://thumbs4.properati.com/3/jA8isZw8Xju7z3-V2I-ED8x5D2o=/trim/198x0/smart/filters:strip_icc()/galeriadefotos.net.br/backup/upload/15989/1598966287bc7/zd533ba5e829b0543093a45e1d8398bd7.jpg\nd6d377927ab49671acdd38aa6f661a460601a88b,2015-01-20,rent,apartment,Pernambuco,|Brasil|Pernambuco|,Brasil,Pernambuco,,\"-8.126271,-34.903793\",-8.126271,-34.903793,1200,BRL,1184.32,383.55,,63,6.088095238095239,19.047619047619047,1,2,,http://www.properati.com.br/w71k_alugar_apartamentos_pernambuco,\"CONTATOS: Claro: 81./ Tim: 81./ Oi: 81./ Vivo: 81.Apartamento de 2 quartos mais Dependência completa de empregada, 1.º andar de edifício localizado na Rua Dr. Gaspar Regueira da Costa, 99, Boa Viagem.Este apartamento é constituído por: 1 Sala, 2 Quartos, 2 Banheiros, Cozinha, Área de serviço, Dependência completa de empregada e 1 lugar de estacionamento.O Valor do aluguer já inclui taxas.Agende já a sua visita.Nota: O Edifício irá ser pintado exteriormente, em breve.ISI-120-T2 #ref:ISI-120-T2 - ISI-120-T2 - idalécio Soares\",\"Apartamento de 2 quartos, DCE, 1.º Andar, 63 m2, Boa Viagem - Recife\",https://thumbs4.properati.com/0/B9BCwP1HK7NvZIYINGkFNnNfOZ4=/trim/198x0/smart/filters:strip_icc()/feedmedia.egorealestate.com/Zfeed/S5/C4123/P2270979/Tphoto/ID03a72200-0000-0500-0000-00000141a7a5.JPG\nc6826a9508436920fd59b46a202354f3e4823d22,2014-10-29,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.1546253,-45.7906242\",-23.1546253,-45.7906242,0,BRL,0,0,,3800,,,,,,http://www.properati.com.br/s41f_alugar_comercial_cacapava,\"Condomínio com 22 Módulos de Galpões; 3.800m2 a 12.600m2; 12m pé-direito médio; Piso 6 TON/m2; Ã�rea total de Terreno : 324.000 m2; Ã�rea Total construída: 133.200 m2; 293 vagas para automóveis; 103 vagas para caminhões; Refeitório para 256 pessoas por turno;Benefícios Fiscais no município de Caçapava conforme Lei Lei 5055/211; Isenção de Taxas;Isenção de IPTU; Redução de ISS; Reversão de ICMS.Valores locação = R$ 15,00 o m2Tx Cond. = R$ 1,50 o m2Valores poderão ser alterados sem prévio aviso.A Heron Imóveis oferece este(a) Comercial em Caçapava, no bairro Caçapava. Gostou desse(a) Comercial em Caçapava? Então veja mais informações! Acesse o nosso site clicando no botão Contate o anunciante ou ligue - ou ainda entre em contato conosco pelo email contato@ Obrigado!\",\"Comercial em Caçapava, no bairro Caçapava\",https://thumbs4.properati.com/1/J2n8nXVlvduy6EJjyLwPneEoc28=/trim/198x0/smart/filters:strip_icc()/www.heronimoveis.com.br/fotos/F00/35/23500-DSC06177.JPG\nba957ad3cb2d12d55d1f0466eeffe788a35a6f4f,2014-08-04,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7780086,-49.3450562\",-20.7780086,-49.3450562,950,BRL,751.1,243.25,,,,,,1,,http://www.properati.com.br/lxp9_alugar_casas_sao-jose-do-rio-preto,\"Ref.: CA95876 Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Garden Village II..:Casa Condomnio Garden Village II, Casa Condominio, Dormitrios: 03, Tipo Apto.: 01, Wc. Social: 01, Sala de Visita: 01, Copa: 01, Cozinha: 01, Cozinha Planejada, Despensa, rea de Servio, Ar Condicionado, Alarme, Acabamento Em Gesso, Sanca, Campainha, Cerca Eletrica, Forro de Laje, Piso Dormitrios: Frio, Piso Salas: Frio, M2/ Terreno: 145, M2 rea Construida: 70, Garagem Descoberta: 01, Salo de Festa, Detalhes: R$ 950,00 + Iptu + Condomnio\", Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Garden Village II,https://thumbs4.properati.com/9/xcrfHjSnXdhGcOK1m1wg1jHJlsw=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/803/8860a8e8-6d62-49e6-a992-54ab8f360f35.jpg\n0999b33908c71af3b8be282ab35ab61b38a46353,2014-07-16,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,4300,BRL,4078.98,1321,,319,4.141065830721003,13.47962382445141,,4,,http://www.properati.com.br/ifuj_alugar_apartamentos_vila-olimpia,\"Apartamento na Vila Olí­mpia, com 4 quartos, sendo 2 suí­tes,Ampla sala com terraí§o, piso de madeira corrida, 1 dos quartos pode ser revertido para sala de tv ou escritório.Quartos com carpete de madeira, as suí­tes com armí¡rios embutidos, uma delas com ar condicionado.Armí¡rio corredor.Cozinha com armí¡rio, Piso de granito, na cozinha e lavanderia.  Referíªncia: AP34\",\"Apartamento no(a) Vila Olímpia , São Paulo - SP\",https://thumbs4.properati.com/8/nxrbzTDipvKnqdx1wDABEyIbSes=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/34/c789b808-bf30-460d-9376-16299ce5abbb.jpg\n011d5618639ce229b012022a32b22eff9abd213e,2014-11-19,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.9648026,-46.3855723\",-23.9648026,-46.3855723,1300,BRL,1323.12,428.5,,60,7.141666666666667,21.666666666666668,4,1,,http://www.properati.com.br/tl4s_alugar_apartamentos_sao-paulo,\"PACOTE R$ 1.300,00 OTIMO APARTAMENTO MOBILIADO COM TERRAÇO PROXIMO SHOPPING , PRAIA E COMERCIOS ACEITA FIADOR / SEG. FIANÇA A 3 Milenio oferece este(a) Apartamento em Sao Vicente, no bairro Gonzaguinha - 1 dormitórios. Gostou desse(a) Apartamento em Sao Vicente? Então veja mais informações! Acesse o nosso site clicando no botão Contate o anunciante ou ligue - ou ainda entre em contato conosco pelo email vendas@ Obrigado!\",\"Apartamento em Sao Vicente, no bairro Gonzaguinha - 1 dormitórios\",https://thumbs4.properati.com/4/x8BqTbd304ntTBY5pTf5Yi_nNMw=/trim/198x0/smart/filters:strip_icc()/www.imobiliariasdesantos.com.br/fotos/F00/74/757400-FOTOS%20HELANE%20001.jpg\n6f274a2d23ffa89cb0301f3277585beda7723ce8,2014-09-01,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8343086,-49.3805559\",-20.8343086,-49.3805559,5000,BRL,3953.34,1280.31,,,,,,,,http://www.properati.com.br/o23s_alugar_comercial_sao-jose-do-rio-preto_rua-nuno-alvares-pereira,\"Ref.: SA95668Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Pq. EstorilM2 rea Construida: 170, Detalhes: Salo de 170 M, Com Mezanino. J Com Acessibilidade. Tem Uma Linda Fachada e Estacionamento Para Clientes.\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Pq. Estoril,https://thumbs4.properati.com/8/7VqZVGV6Hl3paOrhYXv_ksowyEw=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1875/TG5yXGj9EJPaa5o0dWgL.jpg\ndb6f890847bcacc6ec3e78a0e6613132174e4c70,2014-12-05,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8178411,-49.387605\",-20.8178411,-49.387605,10000,BRL,7906.68,2560.62,,,,,,1,,http://www.properati.com.br/ue07_alugar_casas_sao-jose-do-rio-preto_rua-cila,\"Ref.: CO95026Tipo: Casa Comercial, Predio ComercialCidade: So Jos do Rio Preto - SPBairro: RedentoraSobrado, Dormitrios: 4, Tipo Apto.: 4, C/ A/e: 4, A/e C/ Interior: 4, Wc. Social: 1, Copa/cozinha: 1, Cozinha C/ A/e: 1, Despensa, Salas : 1, Sala P/ Ambientes : 4, M2 rea Construida: 330, Entrada Lateral: 1, Piso Dormitrios: Frio\",\"Ref.: CO95026 Tipo: Casa Comercial, Predio Comercial Cidade: So Jos do Rio Preto - SP Bairro: Redentora\",https://thumbs4.properati.com/2/DNREjVLgpGPzTlv8aIvhpd7KZkI=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2461/02ea33d3-3ebd-4139-a380-b60eb84c87d1.jpg\nf7d3243419c85cf439189481819f13cb22ab0498,2014-10-01,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8126181,-49.3404534\",-20.8126181,-49.3404534,3800,BRL,3004.52,973.03,,,,,,,,http://www.properati.com.br/qz8t_alugar_casas_sao-jose-do-rio-preto_avenida-miguel-damha,\"Ref.: CA95902Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Damha III..: Casa Condomnio Damha III, Casa Condominio, Dormitrios: 03, Suite: 01, Closet , Tipo Apto.: 02, C/ A/e: 02, Home, Copa/cozinha: 01, Cozinha Planejada, Piscina, Varanda, rea de Servio, Wc. de Empregada/servio, Forro de Laje, M2/ Terreno: 450, M2 rea Construida: 240, rea de Lazer, Garagem Descoberta: 02, Garagem Coberta: 02, Churrasqueira, Valor Condominio R$-: 420,00\",Ref.: CA95902 Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Damha III,https://thumbs4.properati.com/8/cqu5q3tBJOddQ526fWf2puVwINI=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2060/6064cf7e-f8ed-44f3-8a80-b6576de27d62.jpg\ncbfc4c495b3578c995363825a85a821217fab4ec,2014-10-18,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,14000,BRL,11069.36,3584.87,,,,,,,,http://www.properati.com.br/ri3p_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95918Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Mini Dist. Industrial Adail Vetorasso..: Salo Comercial No Mini Distrito Industrial Adail Vetorasso, Salo Comercial, Wcs.: 02, Cozinha: 01, Escritrio, M2/ Terreno: 2.100, M2 rea Construida: 1.350, Detalhes: Cobertura Metlica.p Direito Com 7 Metros.corredor Lateral.\",Ref.: SA95918 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Mini Dist. Industrial Adail Vetorasso,https://thumbs4.properati.com/9/INPTa4YGuWfsekPfC8ez-rqd3PI=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2285/Mhd9hMU2ZwU9YXAC9Hzi.jpg\nb4dab15f9d4411d0847d1871e0a62e107b492fdc,2014-08-02,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.8773135,-46.7592613\",-23.8773135,-46.7592613,28000,BRL,22138.72,7169.74,,,,,,,,http://www.properati.com.br/lud3_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95869Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Eng. Schimidt..: Salo Comercial Em Engenheiro Schimidt, Salo Comercial, Obs: Salo Ideal Para Transportadora Com Frente Para 03 Marginal, Esquina Com Rua Projetada 01 e Rodovia W. Luiz, M2/ Terreno: 5.500, M2 rea Construida: 2.200\",Ref.: SA95869 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Eng. Schimidt,https://thumbs4.properati.com/3/oJm3M_UtBn2PVRsZ2O4n48IvhuA=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/780/60d090bc-4a05-4929-88ae-aca5b9873a76.jpg\nd3769eb1b41c780c83f2d176adae8ad9a81b7ba3,2014-10-08,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.818728,-49.4212608\",-20.818728,-49.4212608,16000,BRL,12650.68,4096.99,,,,,,,,http://www.properati.com.br/r6d9_alugar_comercial_sao-jose-do-rio-preto_avenida-joao-batista-vetorasso,\"Ref.: SA95810Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Dist. Industrial Waldemar VerdiM2/ Terreno: 5.000, M2 rea Construida: 1.400, Detalhes: Com Recepo e 8 Salas. P Direito Com 7 M.\",Salo,https://thumbs4.properati.com/6/GFkI3LbwZQAhB8USv5fTltHxz0E=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2179/QEWBOD1T8GCOjBi0H6WI.jpg\nf51aebdb554e9b5dfa16c3538d3376040f5f2b5c,2014-08-20,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-22.1423284,-51.3750141\",-22.1423284,-51.3750141,16000,BRL,12650.68,4096.99,,,,,,,,http://www.properati.com.br/np97_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95723Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Moyss Miguel HaddadM2 rea Construida: 322, Obs: Salo Ideal Para Loja, 217m Terreo e 105m Mezanino. Estacionamento Na Frente.imvel Novo.\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Moyss Miguel Haddad,https://thumbs4.properati.com/2/viy2bV22HhxBkY_449PiH9sLQPw=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1548/9242514f-75f0-4bdc-a5b1-68a958cbda17.jpg\n7266f270625a52c84adaf69ac08a53b397617247,2014-09-01,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,6000,BRL,4744,1536.37,,,,,,,,http://www.properati.com.br/o20x_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95742Tipo: Salo IndustrialCidade: So Jos do Rio Preto - SPBairro: Quinta das PaineirasWcs.: 02, Cozinha: 01, M2/ Terreno: 720, M2 rea Construida: 458,90, Obs: Salo Industrial Grande Com 458m, Com Boa Acessibilidade e tima Localizao, 02 Quadras da Avenida Potirendaba, Fcil Acesso e Entrada Para Caminhes.\",Tipo: Salo Industrial Cidade: So Jos do Rio Preto - SP Bairro: Quinta das Paineiras,https://thumbs4.properati.com/3/0rnMsn1G6rWTjCQ9lnrLn6WR7JA=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1771/KET9NdQ0WNrtQlhGEVEq.jpg\nc1e10a8afe111ba5caa5034df1ecb56b25b772f9,2014-08-19,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-22.1423284,-51.3750141\",-22.1423284,-51.3750141,12000,BRL,9488,3072.74,,,,,,,,http://www.properati.com.br/muvz_alugar_casas_sao-jose-do-rio-preto,\"Ref.: CA95766Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Moyss Miguel HaddadWc. Social: 04, M2/ Terreno: 980, M2 rea Construida: 500, Obs: Entrada Lateral Com Acesso Para Caminho, Com 04 Wcs. Sendo 02 Wc P/ Deficiente.\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Moyss Miguel Haddad,https://thumbs4.properati.com/3/GvRImi73Q9koPTqVulTG5pg1J2s=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1519/T181YBWZd62P9tMfgs1F.jpg\nd156f7cc95141ef1fca4de81d6dc65b143c9988f,2014-08-19,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-22.1423284,-51.3750141\",-22.1423284,-51.3750141,12000,BRL,9488,3072.74,,,,,,,,http://www.properati.com.br/muwd_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95749Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Moyss Miguel HaddadM2 rea Construida: 280, Obs: Salo Ideal Para Loja, 217mterreo + 68m Mezanino.estacionamento Na Frente,\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Moyss Miguel Haddad,https://thumbs4.properati.com/5/A5TB5m6yLugDjIdQVsXy9br70YQ=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1533/eeec7b18-daa6-422b-aac1-82894f366e60.jpg\n90ed612641d0db18967ee9c07edc8b0647c97976,2014-08-19,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-22.1423284,-51.3750141\",-22.1423284,-51.3750141,12000,BRL,9488,3072.74,,,,,,,,http://www.properati.com.br/muwc_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95725Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Moyss Miguel HaddadM2 rea Construida: 280, Obs: Salo Ideal Para Loja, 217m Terreo, + 68m Mezanino.estacionamento Na Frente, Imvel Novo\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Moyss Miguel Haddad,https://thumbs4.properati.com/2/8rbyOC1iGXRWRZqh5Z6pcOsdsfQ=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1532/a142f7a0-ff97-4794-84d8-4144253429c3.jpg\nc69e1ca4e7cd3346bc510240893bf2c2c7437e8e,2014-08-19,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-22.1423284,-51.3750141\",-22.1423284,-51.3750141,12000,BRL,9488,3072.74,,,,,,,,http://www.properati.com.br/muvh_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95724Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Moyss Miguel HaddadM2/ Terreno: 280, Obs: Salo Ideal Para Loja, 217m Terreo e + 68m Mezanino.Estacionamento Na Frente,imvel Novo.\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Moyss Miguel Haddad,https://thumbs4.properati.com/1/3gyCoSFzrSme_NcQhKWIEnuoeaY=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1501/CeAfrlKKrQIRE9uzNxoa.jpg\n0c5e391bac4d007585efb02466681c760a758423,2014-09-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8379345,-49.3370139\",-20.8379345,-49.3370139,8000,BRL,6325.32,2048.49,,,,,,,,http://www.properati.com.br/q4jv_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95899Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Ch. Estancia Jockey Clube..: Salo Comercial , Salo Comercial, Wcs.: 04, Wcs. C/ A/e, Cozinha: 01, Obs: P Direito 9 Metros, M2 rea Construida: 1.000, Detalhes: Salo Novo, Sendo Os Wcs Piso Porcelanato, Cozinha Porcelanato e Piso Liso, Com Telhado Vulcanizado.\",Ref.: SA95899 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Ch. Estancia Jockey Clube,https://thumbs4.properati.com/8/OM-MkRbrVgdcQa163wlgXjWDHqU=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2007/8541c9f9-4477-448d-a193-d2b00dc5eed4.jpg\n01bdf823f7827d745574bb7a180a39e4db359463,2014-08-19,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,8000,BRL,6325.32,2048.49,,,,,,,,http://www.properati.com.br/muux_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95894Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Ch. Jockey Clube..: Salo Comercial Na Chcara Jockey Clube, Salo Comercial, M2/ Terreno: 5.000, M2 rea Construida: 1.150, Detalhes: Galpo Com Piso Cimento Polido, Estrutura Metlica, Entrada Lateral Para Caminho.p Direito Alto Com 6,/2 M. \",Ref.: SA95894 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Ch. Jockey Clube,https://thumbs4.properati.com/1/HZjM3Xdq2xWgpQ5gSrCVIASVnJA=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1481/Tz9VYOVWXC1ZVU5fUHwL.jpg\n050a20b55332403b962575877b81d609a5988a15,2014-09-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8379345,-49.3370139\",-20.8379345,-49.3370139,8000,BRL,6325.32,2048.49,,,,,,,,http://www.properati.com.br/q4ju_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95900Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Ch. Estancia Jockey Clube..: Salo Comercial , Salo Comercial, Wcs.: 04, Wcs. C/ A/e, Cozinha: 01, Obs: P Direito 9 Metros, M2 rea Construida: 1.000, Detalhes: Salo Novo, Sendo Os Wcs Piso Porcelanato, Cozinha Porcelanato e Piso Liso, Com Telhado Vulcanizado.\",Ref.: SA95900 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Ch. Estancia Jockey Clube,https://thumbs4.properati.com/3/ZL10NlMLKOM5C49dt-MjpOTaaMo=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2006/9fbe5c67-00ba-4e7b-9904-08316f433dd4.jpg\nfeb178373d5eab7df3b6c546a5d1e6a192156c7f,2014-08-19,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8457948,-49.353464\",-20.8457948,-49.353464,8000,BRL,6325.32,2048.49,,,,,,,,http://www.properati.com.br/muuv_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95892Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Ch. Jockey Clube..: Salo Comercial Na Chcara Jockey Clube, Salo Comercial, M2/ Terreno: 5.000, M2 rea Construida: 1.150, Detalhes: Galpo Com Piso Cimento Polido, Estrutura Metlica, Entrada Lateral Para Caminho.p Direito Alto Com 6,/2 M.\",Ref.: SA95892 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Ch. Jockey Clube ,https://thumbs4.properati.com/5/xwV26hopilS3ZK6xLE-O3iy4rE4=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1479/vT0mPYs7U2dAsti8LWGQ.jpg\n882a8542b44e551782d3126a1d43d907cdba854b,2014-08-18,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8442587,-49.3291753\",-20.8442587,-49.3291753,7000,BRL,5534.66,1792.43,,,,,,,,http://www.properati.com.br/mt5y_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95817Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Marginal Eng. SchimidtWc. Social: 02, Cozinha, Mesanino, Escritrio, M2/ Terreno: 2.000, M2 rea Construida: 1.100, Pintura Nova, Piso Dormitrios: Cimentado;, Piso Salas: Polido;, Detalhes: Salo Novo, Com Estrutura Metlica e Banheiro C/ Acessibilidade.\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Marginal Eng. Schimidt,https://thumbs4.properati.com/4/FPT3HYl7BCrZMA5Okj6iNxUgPRA=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1432/MaEwnQ1cL6HLcrscPFb8.jpg\n201e0ccc9fc63a78f68391bdd74ac5407e07da9b,2014-08-04,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,1000,BRL,790.66,256.06,,,,,,1,,http://www.properati.com.br/lxpd_alugar_casas_sao-jose-do-rio-preto,\"Ref.: CA95884Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Parque Liberdade III..:Casa Condomnio Parque da Liberdade III, Casa Condominio, Dormitrios: 03, C/ A/e: 02, Wc. Social: 01, Wcs. C/ A/e, Sala de Visita: 01, Cozinha: 01, Cozinha Planejada, Interfone, Acabamento Em Gesso, Sanca, Campainha, Forro de Laje, Obs: Imvel Semi Novo, Com Projeto da Arquiteta Kenia Miranda, o Imvel Tem Papel de Parede, Cortinas Nos Dormitrios e Sala, Luminrias Na Cozinha., M2/ Terreno: 143, Garagem Coberta: 02, Churrasqueira, Detalhes: R$ 1.000,00 + Iptu + Condominio\",Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Parque Liberdade III,https://thumbs4.properati.com/2/v319TrEqOc-5Hy9xmTQXh1KL2UE=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/807/7331be4a-3945-41cd-b761-4322696192f5.jpg\n05cf0728d4ba6016469b6b4a864542a1824dc25c,2014-08-15,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.7688712,-46.7100661\",-23.7688712,-46.7100661,2000,BRL,1581.32,512.12,,,,,,,,http://www.properati.com.br/mqtt_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95463Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Jardim So MarcosWc. Social: 2, Cozinha: 1, Mesanino, Recepo, M2/ Terreno: 406, M2 rea Construida: 110, Pintura Nova, Piso Salas: Piso Frio\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Jardim So Marcos,https://thumbs4.properati.com/9/EMUy3dbQiGwd8LmfHH92qsmCQ7Q=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1375/srbDQgpudCyI2lewxvjS.jpg\nf5942694fe76bfe705275acfe74148c588367f86,2014-09-01,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.7688712,-46.7100661\",-23.7688712,-46.7100661,4000,BRL,3162.64,1024.24,,,,,,8,,http://www.properati.com.br/o215_alugar_casas_sao-jose-do-rio-preto_rua-antonio-dias,\"Ref.: CO95681Tipo: Casa ComercialCidade: So Jos do Rio Preto - SPBairro: Jardim So MarcosWc. Social: 04, Sala(s): 08, M2/ Terreno: 280, Garagem Coberta: 02, Forro de Laje, Piso Salas: Frio, Iptu Valor: 58,00, Detalhes: timo Local Para Clincas.\",Tipo: Casa Comercial Cidade: So Jos do Rio Preto - SP Bairro: Jardim So Marcos,https://thumbs4.properati.com/4/F58YgAYlsu22NFL9RPPH5RURZJ8=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1779/sr0aS0NcXQGZ7HsOtyan.jpg\n1b093694ca05135b213162c0bd499e9adb708309,2014-12-10,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.6302071,-46.5818983\",-23.6302071,-46.5818983,4000,BRL,3162.64,1024.24,,,,,,,,http://www.properati.com.br/ufsq_alugar_comercial_sao-jose-do-rio-preto_avenida-marginal,\"Ref.: SA95942Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Pq. Res. Manor Daud..: Salo Comercial No Parque Residencial Manor Daud, Salo Comercial, Detalhes: Salo Com 340m, Entrada Para Caminho.frente Para Br153\",Ref.: SA95942 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Pq. Res. Manor Daud,https://thumbs4.properati.com/4/4e8bVPqS0g1ezrFKP9fcdzvSn0I=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2493/rJ3aboE1bZnwuqJHy4Lr.jpg\n50bd70c45a9a9b087c404cac6b67c491f64c211e,2014-10-18,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,2500,BRL,1976.65,640.15,,,,,,,,http://www.properati.com.br/ri3l_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95916Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Res. Caetano..: Salo Comercial No Residencial Caetano, Salo Comercial, Wcs.: 02, Cozinha: 01, M2 rea Construida: 350, Detalhes: Cozinha2 Wc Com Acessibilidade;1 Porta Na Frente1 Porta Na Lateral;barraco Novo.cobertura Com Estrutura Metlica. P Direito de 6 Metros.\",\"Ref.: SA95916 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Res. Caetano ..: Salo Comercial No Residencial Caetano, Salo Comercial, Wcs.: 02, Cozinha: 01, M2 rea Construida: 350, Detalhes: Cozinha 2 Wc Com Acessibilidade\",https://thumbs4.properati.com/9/l325GXwN3fD9xS0dglH5hmaB83A=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2281/FjKJBqBJFxhvJ4atJVuO.jpg\n50a4a96d3ec01b7b1032e5eec4a3496b38f35c4f,2014-10-09,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7780086,-49.3450562\",-20.7780086,-49.3450562,2500,BRL,1976.65,640.15,,,,,,,,http://www.properati.com.br/r9j7_alugar_casas_sao-jose-do-rio-preto_avenida-belvedere,\"Ref.: CA95904Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Village Damha Rio Preto I..: Casa Em Condomnio Village Damha Rio Preto I, Casa Condominio, Dormitrios: 03, Tipo Apto.: 01, C/ A/e: 03, Cozinha: 01, Cozinha Planejada, Varanda, rea de Servio, Wc. de Empregada/servio, Garagem Coberta: 02, Churrasqueira\",Ref.: CA95904 Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Village Damha Rio Preto I,https://thumbs4.properati.com/1/CVM0xMvGlg_DX1BluR3vWpRkhMU=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2206/7f2a5d99-2465-48fb-859c-a52311ba0c18.jpg\nf5be311dcee4a66dcb26f463f76c49b6084de8ab,2014-11-11,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,1600,BRL,1411.74,457.2,,,,,2,1,,http://www.properati.com.br/t8nu_alugar_apartamentos_tatuape_rua-serra-de-braganca,\"APTO DE 40M/2 COM 01 DORMITRIO, SALA AMPLA COM SACADA, COZINHA PLANEJADA, WC SOCIAL, REA DE SERVIO, 01 VAGA DE GARAGEM LIVRE, LAZER COMPLETO COM PISCINA, PLAYGROUD, SALO DE FESTA, CHURRASQUEIRA, SAUNA, SALA DE FITINESS, SALA DE LEITURA, SALO DE JOGOS, EXCELENTE LOCALIZAO. O APTO ESTA COM FOGO, GELADEIRA, MICROONDAS, SUGGAR, BALCO ESTILO COZINHA AMERICANA, 02 BANQUETAS, 02 MESINHAS DE CANTO, 01 HOME-OFFICER, CAMA BOX COM COLCHO, VENTILADOR DE TETO, ARMRIO NO DORMITRIO.\",TOLEDO CONSULTORIA JURIDICA E IMOBILIARIA,https://thumbs4.properati.com/6/-9lTZIOGIGHwHaddpPoJu6s92hY=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/toledo/photos/2/6c5e21eb-1c7b-41d6-9257-b6e7994ea93c.jpg\na031b9b0ccce57678aab134025713cdb4ad7ed19,2014-10-18,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7630911,-49.3810839\",-20.7630911,-49.3810839,1500,BRL,1185.99,384.09,,,,,,,,http://www.properati.com.br/ri3h_alugar_comercial_sao-jose-do-rio-preto_rua-gumercindo-tomas-de-aquino,\"Ref.: SA95914Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Dist. Industrial Centenrio da Emancipao..: Salo Comercial No Distrito Industrial Centenrio da Emancipao, Salo Comercial, Wcs.: 02, Cozinha: 01, M2/ Terreno: 200, M2 rea Construida: 100, Detalhes: Salo Em Construo, Com 2 Wc Com Acessibilidade e Cozinha.porta Frente e Lateral.estrutura Metlica, P Direito 5 Metros.\",Ref.: SA95914 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Dist. Industrial Centenrio da Emancipao,https://thumbs4.properati.com/5/cuTILpWk120n1EtpTTP7CuHqFTI=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2277/xOFbBdd0MAPLxuFIMMMc.jpg\n8198126233f30e45a7ec723640ecd09fc933f35f,2014-10-18,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7630911,-49.3810839\",-20.7630911,-49.3810839,1500,BRL,1185.99,384.09,,,,,,,,http://www.properati.com.br/ri3j_alugar_comercial_sao-jose-do-rio-preto_rua-gumercindo-tomas-de-aquino,\"Ref.: SA95915Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Dist. Industrial Centenrio da Emancipao..: Salo Comercial No Distrito Industrial Centenrio da Emancipao, Salo Comercial, Wcs.: 02, Cozinha: 01, M2/ Terreno: 200, M2 rea Construida: 100, Detalhes: Salo Em Construo, Com 2 Wc Com Acessibilidade e Cozinha.porta Frente e Lateral.estrutura Metlica, P Direito 5 Metros.\",Ref.: SA95915 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Dist. Industrial Centenrio da Emancipao,https://thumbs4.properati.com/0/LWklCEKjfuTI94hwh7U-tpkiQxw=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2279/YBRlngu70KNBBuf3pXju.jpg\ncfe8aedbfe7496306c9ee598fdb0799d1a4d43a7,2014-08-14,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8178411,-49.387605\",-20.8178411,-49.387605,3000,BRL,2371.98,768.18,,,,,,,,http://www.properati.com.br/mpsw_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95860Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: RedentoraSalo Comercial, Terrea, Acabamento Em Gesso, Forro de Laje, Piso Salas: Frio, M2 rea Construida: 160\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Redentora,https://thumbs4.properati.com/1/w-odT7rMEUYcr3TVS4fo2ausAAI=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1278/Kp7YsojHwZdQDjK4JvMz.jpg\n9002dc7d32f4f097a4f9c789ad93b7f5304396cf,2014-08-14,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8178411,-49.387605\",-20.8178411,-49.387605,3000,BRL,2371.98,768.18,,,,,,,,http://www.properati.com.br/mpsz_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95862Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: RedentoraM2 rea Construida: 190, Garagem Descoberta: 02, Forro de Laje, Piso Salas: Frio\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Redentora,https://thumbs4.properati.com/7/xW33i6TXrwRgMFrpyW_syKKKlPs=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1281/UKC5P4P7lZPyQSY9l8xC.jpg\n2128d47314a494ddecda6b6e981c46afbd3fae51,2014-10-30,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,3000,BRL,2371.98,768.18,,,,,,,,http://www.properati.com.br/s9we_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95921Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Redentora..: Salo Comercial Na Redentora, Salo Comercial, M2/ Terreno: 150, Detalhes: tima Localizao!\",Ref.: SA95921 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Redentora,https://thumbs4.properati.com/6/HVrSVpwKgIJpnnFxGcMzyHIeUx4=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2347/abTB8PxHKedvADO9yyhF.jpg\n0329fa7a5536c99eee743612010897cafed7f340,2014-10-15,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8030634,-49.4324803\",-20.8030634,-49.4324803,3000,BRL,2371.98,768.18,,,,,,,,http://www.properati.com.br/re15_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95910Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Res. Palestra..: Salo Comercial No Residencial Palestra, Salo Comercial, Wc. Social: 02, Forro de Laje, Obs: timo Salo, Novo., M2/ Terreno: 250, M2 rea Construida: 200\",Ref.: SA95910 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Res. Palestra,https://thumbs4.properati.com/8/MBuRdEvx5x-V-zpeycnxFNg-n70=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2238/NOqLX7BtA7KEH0SUs5rV.jpg\nc822a8819d8874916877cc6b3c91194cbf5968ca,2014-10-09,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7780086,-49.3450562\",-20.7780086,-49.3450562,3000,BRL,2371.98,768.18,,,,,,,,http://www.properati.com.br/r9j6_alugar_casas_sao-jose-do-rio-preto_avenida-belvedere,\"Ref.: CA95903Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Village Damha Rio Preto I..: Casa Condomnio Village Damha Rio Preto I, Casa Condominio, Dormitrios: 03, Tipo Apto.: 03, C/ A/e: 03, Cozinha: 01, Cozinha Planejada, Fogo, Lavabo, Salas : 01, Sala P/ Ambientes : 02, Piscina, Ducha Externa, Varanda, rea de Servio, Wc. de Empregada/servio, Garagem Coberta: 02, Churrasqueira\",Ref.: CA95903 Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Village Damha Rio Preto I,https://thumbs4.properati.com/2/595nD1yJ6OYSKSxIr0D1iMhNilQ=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2205/060de194-4e66-4f1e-b0cc-dd05e276539f.jpg\n2568b8aa05b9d3cb3e9e8ef74c4461c34d2d57b5,2014-10-05,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8350772,-49.369111\",-20.8350772,-49.369111,3000,BRL,2371.98,768.18,,,,,,,,http://www.properati.com.br/r1q8_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95905Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Quinta das Paineiras..: Salo Comercial No Quinta das Paineiras, Salo Comercial, Terrea, Cozinha: 01, Escritrio, M2 rea Construida: 225\",Ref.: SA95905 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Quinta das Paineiras,https://thumbs4.properati.com/6/2tu7W_u475v3n8Ubll7avmjLXPI=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2130/0y7ijJHaZqFbrC0LSLFy.jpg\n0dff14f277324eb9f04d59b6c26758ccdb744e27,2014-12-05,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8126181,-49.3404534\",-20.8126181,-49.3404534,3700,BRL,2925.47,947.43,,,,,,,,http://www.properati.com.br/udzy_alugar_casas_sao-jose-do-rio-preto_avenida-miguel-damha,\"Ref.: CA95938Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Damha III..: Linda Casa Tipo Sobrado No Condomnio Damha Iii, Casa Condominio, Sobrado, Dormitrios: 03, Suite Master: 01, C/ Hidro, Tipo Apto.: 02, C/ A/e, Ducha Dupla, Lavatrio Duplo, Cozinha: 01, Cozinha Planejada, Despensa, Escritrio, Lavabo, Salas : 01, Sala P/ Ambientes : 02, Varanda, rea de Servio, Lavanderia, Quarto de Empregada, Wc. de Empregada/servio, Forro de Laje, Garagem Descoberta: 02, Garagem Coberta: 02, Churrasqueira\",Ref.: CA95938 Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Damha III,https://thumbs4.properati.com/0/FGZdTzG9TG6qqioIMRxZ7WLDQWY=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2452/bVU2c6naoVF7KCpphJqL.jpg\n6b3bff7cc70a79dfed41fc0ae514a7ad16a328b8,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,7000,BRL,6640.22,2150.47,,88,24.437159090909088,79.54545454545455,,,,http://www.properati.com.br/ifvc_alugar_comercial_vila-olimpia,\"\tEdifício comercial de excelente padrão, localizado em ponto nobre da Vila Olímpia, próximo da Av. dos Bandeirantes e Shopping Vila Olímpia.\t Prédio conta com 3 elevadores de alta velocidade, sistema de proteção contra incêndio, ar condicionado central, próximo a bancos. Referíªncia: CO65\",\"Comercio no(a) Vila Olímpia , São Paulo - SP\",\nc26e111d703af6f65be91df94c274bbd41585a40,2014-08-21,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.6357809,-46.5324419\",-23.6357809,-46.5324419,50000,BRL,39533.44,12803.11,,,,,,,,http://www.properati.com.br/nqbl_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95307Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Rodovia Assis ChateaubriandWcs.: 4, Escritrio, M2/ Terreno: 4.000, Detalhes: Barraco Com 4.000 M\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Rodovia Assis Chateaubriand,https://thumbs4.properati.com/0/lJ2ffPr7gtS2vlQuGTg_zDX6844=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1592/WQmB3JTS7Y2N72aUI5Ud.jpg\n2e8ffab1b55b5245e917b3597009338cb92b6586,2014-10-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7807632,-49.3985157\",-20.7807632,-49.3985157,50000,BRL,39533.44,12803.11,,,,,,,,http://www.properati.com.br/rgyw_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95804Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Dist. Industrial Waldemar VerdiM2/ Terreno: 6.000, M2 rea Construida: 3.200\",Salo,https://thumbs4.properati.com/3/X38EXdJIkvCXPQCdWURfrgMMMWQ=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2270/OTptGk75zRPMzZ1pL1on.jpg\nc522d5f57ea8e14d18fa3ba746cb581cd379330a,2014-08-20,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,100000,BRL,79066.88,25606.22,,,,,,,,http://www.properati.com.br/np98_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95721Tipo: Salo IndustrialCidade: So Jos do Rio Preto - SPBairro: Dist. Industrial IWcs.: Vrios, M2/ Terreno: 13.000, M2 rea Construida: 8.500, Detalhes: tima Localizao\",Tipo: Salo Industrial Cidade: So Jos do Rio Preto - SP Bairro: Dist. Industrial I,https://thumbs4.properati.com/6/jARKWDWV0rGe-HzGUtUeE0X3knw=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1549/ZaCxToSHCaeCL7GxZ08g.jpg\n07383fafc72a9aa9f0286b841b782ca718485de8,2014-09-01,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8178198,-49.3468294\",-20.8178198,-49.3468294,3500,BRL,2767.31,896.21,,,,,,1,,http://www.properati.com.br/o246_alugar_casas_sao-jose-do-rio-preto_avenida-nadima-damha,\"Ref.: CA95827Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Damha II (Res. Jardins)Dormitrios: 03, Suite: 03, C/ A/e: 03, Sacada, Sala de Visita, Copa, Cozinha, Cozinha Planejada, Despensa, Piscina, rea de Servio, Lavanderia, Wc. de Empregada/servio, Garagem Descoberta: 02, Garagem Coberta: 02, Churrasqueira, Acabamento Em Gesso, Sanca, Forro de Laje, Piso Salas: Frio;, Documentao: Em Dia.\",Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Damha II (Res. Jardins),https://thumbs4.properati.com/6/Bq9TbjreWNnjd2VrvJ5oieGegeQ=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1889/db5caa0b-ec86-44f4-9c0f-ab26a6816b62.jpg\n179f70a47a9ce50e161ea483ffee741a8e1c7fb2,2014-08-14,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8178411,-49.387605\",-20.8178411,-49.387605,3500,BRL,2767.31,896.21,,,,,,,,http://www.properati.com.br/mpsx_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95861Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Redentora TerreaM2 rea Construida: 190, Acabamento Em Gesso, Forro de Laje, Pintura Nova, Piso Salas: Frio\",Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Redentora Terrea,https://thumbs4.properati.com/1/VQPzYdCL1X2L1IkzU17WHDuej9s=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1279/nGCLTL2QhwMJSHGtxR7y.jpg\ne48c41f6ad3dba93e0ac6038a5aa5cda39d651d0,2014-09-01,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-22.1423284,-51.3750141\",-22.1423284,-51.3750141,3500,BRL,2767.31,896.21,,,,,,,,http://www.properati.com.br/o2m6_alugar_casas_sao-jose-do-rio-preto,\"Ref.: CA95898Tipo: Casa Comercial, Casa ResidencialCidade: So Jos do Rio Preto - SPBairro: Moyss Miguel Haddad..: Casa Residencial No Moyss Miguel Haddad, Casa Residencial, Dormitrios: 03, Suite: 03, C/ Hidro: 01, C/ A/e: 03, Sacada, Wc. Social: 01, Wcs. C/ A/e, Sala de Visita: 01, Copa: 01, Cozinha: 01, Cozinha Planejada, Despensa, Escritrio, Salas : 01, Sala P/ Ambientes : 03, Piscina, Varanda, rea de Servio, Lavanderia, Quarto de Empregada, Wc. de Empregada/servio, Ar Split, Ventilador, Interfone, Acabamento Em Gesso, Sanca, Tv Cabo, Campainha, Quintal, Grade, Murado, Forro de Laje, Piso Salas: Frio, Obs: Imvel Prximo Ao Shopping Iguatemi, M2 rea Construida: 450, Garagem Descoberta: 04, Garagem Coberta: 02, Churrasqueira, Jardim de Inverno, Jardim Interno, Iptu Valor: 199,00\",\"Ref.: CA95898 Tipo: Casa Comercial, Casa Residencial Cidade: So Jos do Rio Preto - SP Bairro: Moyss Miguel Haddad\",https://thumbs4.properati.com/5/fZwdpPACH02ObP9aj7-UFvVm0lE=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1937/a7WmbhKqWo7JqFgAHcOD.jpg\n9ea36b0e269fbf2581eef1510f0148594919b330,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,3150,BRL,2988.09,967.71,,45,21.50466666666667,70,,,,http://www.properati.com.br/ifxf_alugar_comercial_chacara-santo-antonio,\"\tNovAmérica Inside Park, o novo marco imobiliário de São Paulo. É um novo espaço, um novo bairro, uma nova opção de vida. É a expansão urbana mais consciente e mais urbana. Um mundo para morar, trabalhar e viver em plena harmonia. Uma região onde a vida social e a vida familiar se equilibram. Um lugar que concretiza o prazer de viver em uma grande metrópole. 1 torre comercial, escritórios de vila e lojas, 5 condomínios residenciais independentes e 2 parques em mais de 70.000 m² de terreno.\t\tNovAmérica Office Park é um conceito totalmente novo de local de trabalho, que valoriza eficiência e a qualidade de vida de seus ocupantes. EDSA em parceria com Neusa Nakata tiveram a missão de conceber um produto inusitado, diferenciado e particular. O resultado é um cenário privilegiado para que você e seus clientes tenham bem-estar em horário comercial.\t\tA região do NovAmérica reúne as principais empresas nacionais e multinacionais, que chegaram por aqui atraídas pela infraestrutura e pelas importantes e fáceis vias de acesso. O que confirma que a região é o novo pólo econômico de São Paulo.\t\tConheça os diferenciais do NovAmérica Office Park:\t\tÚnico na Nações Unidas a integrar um complexo formado por 1 torre comercial, escritórios de vila, lojas e 5 condomínios residenciais independentes.\tConceito consagrado de bem-estar em horário comercial.\tAcesso ao parque com mais de 10 mil m² destinado à cidade.\tPotencial de clientes vindos dos 5 condomínios residenciais.\tA oportunidade de morar e trabalhar de forma integrada e independente.\tServiços by Facilities Office.\tArquitetura e decoração inspirados nas ultimas tendências mundiais.\tProjeto paisagístico assinado por um dos mais conceituados escritórios do mundo, a EDSA. Referíªncia: CO150\",\"Comercio no(a) Chácara Santo Antônio , São Paulo - SP\",https://thumbs4.properati.com/2/2S3OsO4umzIVXFHbHKTrb7dk-x8=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/150/c0d9b8fc-3da8-4a27-ac1f-4e1684d35312.jpg\na5c14fe842664d12fe4bdfad313d5af251121d89,2014-10-01,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.5882535,-46.6167776\",-23.5882535,-46.6167776,1400,BRL,1106.91,358.48,,,,,,,,http://www.properati.com.br/qz98_alugar_comercial_sao-jose-do-rio-preto_rua-da-imprensa,Ref.: SA95836Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Vila DinizDetalhes: Salo Comercial Com Aproximadamente 50 M e 02 Banheiros Com Acessibilidade.,Ref.: SA95836 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Vila Diniz,https://thumbs4.properati.com/9/CRgjdJjcRp1Lz-pPZChI7eL8ZIY=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2075/fOuQlJsX61PBHGGwhPSh.jpg\nb64a9bd212d9e91821e5e892eeff464bbc0202cd,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,9600,BRL,9106.57,2949.21,,120,24.57675,80,,,,http://www.properati.com.br/ifwi_alugar_comercial_vila-olimpia,\"\tConjunto Comercial com 120m² de área útil, 3 vagas, 3 banheiros, 1 copa. Referíªncia: CO112\",\"Comercio no(a) Vila Olímpia , São Paulo - SP\",https://thumbs4.properati.com/0/3pwYR5bzXi_fuQfUVbzkPbFkst4=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/112/93f2b4dc-86f7-4544-803f-62dfec9dc967.jpg\n59a2898d29cb6860a70355c1b2028b9ab38d89d9,2015-01-13,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.5456177,-46.563645\",-23.5456177,-46.563645,1801,BRL,1459.35,472.62,,,,,,2,,http://www.properati.com.br/vyso_alugar_apartamentos_tatuape,\"APTO DE 75M/2, COM 03 DORMITRIOS, SALA AMPLA COM SACADA, COZINHA PLANEJADA, WC SOCIAL, REA DE SERVIO COM WC, REPLETOS DE ARMRIOS, 01 VAGA DE GARAGEM FIXA, EXCELENTE ESTRUTURA DE LAZER COM PISCINA, PLAYGROUD, SALO DE FESTA, CHURRASQUEIRA, SALA DE GINASTICA, EXCELENTE LOCALIZAO, A 100 METROS DO METR.\",LOCAO TATUAPE CONDOMINIO ILHAS GREGAS,https://thumbs4.properati.com/6/-xzzzVWk3lxFITvW285d3YmP00c=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/toledo/photos/13/e7c88a20-dcac-49b2-ae42-e782447d3f07.jpg\n1925426e5575ecd98f1e63422a187081754e0f5c,2015-01-13,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.5444872,-46.5657875\",-23.5444872,-46.5657875,1499,BRL,1214.64,393.37,,,,,2,1,,http://www.properati.com.br/vysp_alugar_apartamentos_tatuape_rua-serra-de-braganca,\"APTO DE 40M/2, COM 01 DORMITRIO, SALA AMPLA COM SACADA, COZINHA, WC SOCIAL, SEMI-MOBILIADO COM FOGO, SUGGAR, GELADEIRA, MICRO-ONDAS, ARMRIOS PLANEJADOS NA COZINHA, CAMA BOX, GUARDA ROUPA, VENTILADOR DE TETO, RACK DE TV, 02 BANQUETAS, 02 MESINHAS DE CANTO, EXCELENTE ESTRUTURA DE LAZER COM PISCINA, PLAYGROUD, SALO DE FESTA, CHURRASQUEIRA, SAUNA, SALA DE FITINESS, BRINQUEDOTECA, SALO DE JOGOS, SALA DE LEITURA. EXCELENTE LOCALIZAO, PRXIMO DO SUPERMERCADO PO DE ACAR, FCIL ACESSO PARA O METR.\",NEW CONCEPT 22 A,https://thumbs4.properati.com/5/SXF7T5Zfxal6lcQifCYOufBKjYA=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/toledo/photos/14/0522b83e-c37e-4736-ac81-ca8540d2d806.jpg\nfda45d95b6fe8daf9215dcc80ad057531879eda4,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,4575,BRL,4339.84,1405.48,,120,11.712333333333333,38.125,,,,http://www.properati.com.br/ifxz_alugar_comercial_vila-olimpia,\"\tConjunto Comercial na Vila Olimpia, com 61m², podendo aumentar a área para 117m². Referíªncia: CO179\",\"Comercio no(a) Vila Olímpia , São Paulo - SP\",https://thumbs4.properati.com/5/cZY_8L_8bfKOV9ay-F-6DsnRZ-c=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/179/0df8ffa9-49a9-49c0-9968-37c13719d904.jpg\n7eb9fb8ad21ff1da490b75fd04643939fd72edf1,2015-01-13,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.5508578,-46.5679013\",-23.5508578,-46.5679013,1550,BRL,1255.99,406.76,,,,,11,1,,http://www.properati.com.br/vysn_alugar_apartamentos_tatuape,\"APTO DE 57M/2, COM 02 DORMITRIOS, SALA AMPLA COM SACADA, WC SOCIAL, COZINHA PLANEJADA, AMPLA REA DE SERVIO COM WC, 01 VAGA DE GARAGEM, SALO DE FESTA, CHURRASQUEIRA, PLAYGROUD, QUADRA DE FUTEBOL, EXCELENTE LOCALIZAO, PRXIMO AO SHOPP/METR BOULEVAR TATUAP.\",Apartamento,https://thumbs4.properati.com/4/mwisK4iz0H0viKmBVOGr8B40c1o=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/toledo/photos/12/612ab301-0ac4-4c4c-9691-7278200c2b62.jpg\n59a2934bc5a4c3530613fae471a210ae63498d80,2014-09-02,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.826094,-49.376396\",-20.826094,-49.376396,1200,BRL,960.76,311.15,,,,,,1,,http://www.properati.com.br/ob16_alugar_apartamentos_sao-jose-do-rio-preto,\"Apartamento mobiliado, pronto para morar e com excelente localizao, na zona sul.2 dormitrios sendo 1 tipo apto todos com armrio, ventilador de teto e ar-condicionadosala com sacada para 2 ambientes com sofa e estantecozinha planejada com mesa com 4 cadeiras e fogo2 banheiros com box blindex, gabinete e chuveirorea de servio com armrio, varal e mquina de lavar1 vaga de garagem pilotisCondomnio: Porto eletrnico, portaria 24h, elevador, churrasqueira e piscina.\",Apartamento com 2 dormitrios em Rio Preto,https://thumbs4.properati.com/5/VXcYDJRN6VtNxvgVw-mxJOlL_sA=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/premier/photos/111/91c65753-86b6-40fd-8673-7f8f107a8459.jpg\nd08581768058f5286ca4f209222d59a3f10af84d,2015-01-13,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-22.8877586,-47.081265\",-22.8877586,-47.081265,2401,BRL,1945.56,630.08,,,,,19,2,,http://www.properati.com.br/vysm_alugar_apartamentos_tatuape,\"APTO DE 93M/2, COM 03 DORMITRIOS, SENDO UMA SUTE, SALA AMPLA COM SACADA E DOIS AMBIENTES, WC SOCIAL, COZINHA, AMPLA REA DE SERVIO, REPLETO DE ARMRIOS, 02 VAGAS DE GARAGEM, LAZER COMPLETO COM PISCINA, PLAYGROUD, SALO DE FESTA, SALO DE JOGOS, SAUNA, CHURRASQUEIRA, EXCELENTE LOCALIZAO, PRXIMO DA PRAA SILVIO ROMERO.\",APTO LOCAO TATUAP,https://thumbs4.properati.com/3/d45ivnQIryA-xDQvwZrOc1pBPlE=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/toledo/photos/11/85e5aee8-90fa-4295-bcb5-dce1ed7e789c.jpg\n7739326a3915df1654866f5c27d42e40f0038365,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,2200,BRL,2086.92,675.86,,102,6.626078431372549,21.568627450980394,,,,http://www.properati.com.br/ifwp_alugar_comercial_vila-olimpia,\"\tConjunto Comercial com 46m², 2 banheiros, 3 salas, 1 vaga. Referíªncia: CO122\",\"Comercio no(a) Vila Olímpia , São Paulo - SP\",https://thumbs4.properati.com/6/QhZrO4ITAg_PJagrwPKR0ywe4GI=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/122/80e20053-aaba-4f3b-a8f7-9dc2cdcb44a7.jpg\n4d949c2fcb78e76cf0cb2376ae3a3acc3b27994a,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,2200,BRL,2086.92,675.86,,46,14.692608695652174,47.82608695652174,,,,http://www.properati.com.br/ifut_alugar_comercial_vila-olimpia,\"Conjunto Comercial na Rua Helena, com 46m², 1 vaga. Referíªncia: CO44\",\"Comercio no(a) Vila Olímpia , São Paulo - SP\",https://thumbs4.properati.com/7/rah-1WLiSRF_nrLl-U2mEY27Gus=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/44/3d3a55f5-dd0c-4969-bb57-19f9a554c1f2.jpg\nc98dc531eb6cb68bcec0e60d7dc042bd663caa4d,2014-08-02,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.815191,-49.389435\",-20.815191,-49.389435,5000,BRL,4003.24,1296.47,,,,,,,,http://www.properati.com.br/ludd_alugar_casas_sao-jose-do-rio-preto,\"Imvel com excelente localizao, com recepo, 1 sala com banheiro privativo, 3 salas amplas + 1 interna, todos os ambientes com ar condicionado, cozinha, despensa, garagem interna para vrios veculos. IMVEL SER ENTREGUE REFORMADO E COM AS ACESSIBILIDADES EXIGIDAS!\",Casa Comercial,https://thumbs4.properati.com/8/3TeTIOIgyjtraKZz08buZLIU8W8=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/premier/photos/80/6a0fcb3e-3e78-434f-9624-a5621798b2b2.jpg\ne85c67a2bc55f4834be8d4cf1fd96340ef4ce129,2014-08-04,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8379877,-49.385659\",-20.8379877,-49.385659,800,BRL,632.5,204.84,,,,,,1,,http://www.properati.com.br/lxpc_alugar_casas_higienopolis,\"Ref.: AP95874 Tipo: ApartamentoCidade: So Jos do Rio Preto - SPBairro: Higienpolis..: Apartamento Novo No Jardim Higienopolis, Apartamento, rea Util M2: 50, Dormitrios: 01, C/ A/e, Sacada, Wc. Social: 01, Cozinha C/ A/e, Lavanderia, Elevador, Piso Dormitrios: Porcelanato, Piso Salas: Porcelanato, Obs: Prdio Novo.apto Nunca Habitado., rea de Lazer, Garagem Coberta: 01, Churrasqueira, Espao Gourmet\",Tipo: Apartamento Cidade: So Jos do Rio Preto - SP Bairro: Higienpolis,https://thumbs4.properati.com/6/8tmxgf5PTdg11vI4rVdPwXy39gw=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/806/2077f853-c360-4cd7-b5c8-a33f76415c40.jpg\n33b22b30a043e5a868df61862bb97698b453df89,2014-12-05,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7910821,-49.3781115\",-20.7910821,-49.3781115,800,BRL,632.5,204.84,,,,,,1,,http://www.properati.com.br/ue05_alugar_apartamentos_sao-jose-do-rio-preto_rua-pedro-mulati,\"Ref.: AP95816Tipo: ApartamentoCidade: So Jos do Rio Preto - SPBairro: Jardim ConceioDormitrios: 02, C/ A/e, Wc. Social, Box Wcs., Sala de Estar, Cozinha, Piscina, rea de Servio, Garagem Coberta: 01, rea Util M2: 60, Condominio C/ rea de Lazer, Piscina Adulto, Playground, Valor Condominio R$-: 190,00\",Ref.: AP95816 Tipo: Apartamento Cidade: So Jos do Rio Preto - SP Bairro: Jardim Conceio,https://thumbs4.properati.com/2/4RA-EyT8LswOeQ6lcBbkzLlKjII=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2459/jWyxMNGlETMMxgj2uW9o.jpg\ne2235d97414fcc1a31865b1abe71c1cf85ae01fa,2014-11-17,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7780086,-49.3450562\",-20.7780086,-49.3450562,2700,BRL,2134.78,691.36,,,,,,,,http://www.properati.com.br/tdi5_alugar_casas_sao-jose-do-rio-preto_avenida-belvedere,\"Ref.: CA95926Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Village Damha Rio Preto I..: Casa Condomnio Village Damha Rio Preto, Casa Condominio, Terrea, Dormitrios: 3, Suite: 1, Tipo Apto.: 2, C/ A/e, Lavatrio Duplo: 1, Cozinha C/ A/e: 1, Cozinha Planejada, Escritrio, Lavabo, Salas : 1, Sala P/ Ambientes : 2, Varanda, rea de Servio, Wc. de Empregada/servio, Obs: Espao Para Colocar Piscina, M2 rea Construida: 180, Garagem Descoberta: 2, Garagem Coberta: 2, Churrasqueira, Detalhes: Com Armrios .\",Ref.: CA95926 Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Village Damha Rio Preto I ,https://thumbs4.properati.com/3/ZZ3MVCiFvwDaZxqgfl5oQJ1Vp6c=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2390/5d2ae653-6549-4772-b486-cd9360af6ebd.jpg\n1334a26f10895f5a21b6db0adf96e2ff1dc83799,2014-09-16,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7780086,-49.3450562\",-20.7780086,-49.3450562,1250,BRL,988.31,320.07,,,,,,,,http://www.properati.com.br/q4jz_alugar_casas_sao-jose-do-rio-preto_avenida-belvedere,\"Ref.: CA95901Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Maria Stella..: Casa Condomnio Maria Stella, Casa Condominio, Terrea, Dormitrios: 3, Tipo Apto.: 1, Wc. Social: 1, Cozinha: 1, Cozinha Planejada, Salas : 01, Sala P/ Ambientes : 02, Varanda, rea de Servio, Lavanderia, Garagem Coberta: 02, Salo de Festa, Playground, Piscina Adulto, Quadras Poliesportivas: 1, Campo de Futebol: 1, Valor Condominio R$-: 260,00\",Ref.: CA95901 Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Maria Stella,https://thumbs4.properati.com/3/nKZAghWN-90yQywuniXpPPLO9sk=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2011/e55aad60-d657-4c71-b732-96032067b76f.jpg\n40c34e74b932ff6e10ff49774a8f7fb6e982c1c2,2014-11-30,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8081017,-49.3505771\",-20.8081017,-49.3505771,4500,BRL,3558.01,1152.28,,,,,,,,http://www.properati.com.br/tz24_alugar_comercial_sao-jose-do-rio-preto_rua-valdomiro-nadotti,\"Ref.: SA95928Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Tres Maria..: Salo Comercial No Bairro Trs Maria, Salo Comercial, Detalhes: 160 Metros Mais 40 Metros de Mesanino.\",Ref.: SA95928 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Tres Maria,https://thumbs4.properati.com/3/Yg9eVpfZKwd2s12w6w1yQ4YYVhY=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2402/MTVaVFERWIxvf0IcXQaQ.jpg\n716428a7c9f46f98fd78e36a72109b0c0ec43753,2014-11-30,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8081017,-49.3505771\",-20.8081017,-49.3505771,4500,BRL,3558.01,1152.28,,,,,,,,http://www.properati.com.br/tz25_alugar_comercial_sao-jose-do-rio-preto_rua-valdomiro-nadotti,\"Ref.: SA95929Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Tres Maria..: Salo Comercial No Bairro Trs Maria, Salo Comercial, M2/ Terreno: 200, Detalhes: 140 Metros Mais 40 Metros de Mesanino.\",Ref.: SA95929 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Tres Maria,https://thumbs4.properati.com/9/a_rVYKLimoBl774T612e8o5Icfo=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2403/fhkihJbLNgNCmHo7eBRG.jpg\n2102860e4c561ee2afec1c15fb71f7c6cfafc28d,2014-08-19,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,9000,BRL,7116.02,2304.56,,,,,,,,http://www.properati.com.br/muuy_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95893Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Ch. Jockey Clube..: Salo Comercial Na Chcara Jockey Clube, Salo Comercial, M2/ Terreno: 5.000, M2 rea Construida: 1.200, Detalhes: Galpo Com Piso Cimento Polido, Estrutura Metlica, Escritrio, Cozinha, 02 Wc, Entrada Pela Frente e Lateral Para Caminho.p Direito Alto Com 6,/2 M.\",Ref.: SA95893 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Ch. Jockey Clube,https://thumbs4.properati.com/3/H-b0Ol05_NX0A5soGXBvD_dA3wk=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1482/gCB4KEDxOoIlG9HlGbCW.jpg\ndb16c4572d4f19875153b041d1dcb6b82115e6a7,2014-11-03,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8174527,-49.4306981\",-20.8174527,-49.4306981,18000,BRL,14232.04,4609.12,,,,,,,,http://www.properati.com.br/sjyy_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95923Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Parque Industrial..: Salo Comercial No Parque Industrial, Salo Comercial, M2/ Terreno: 3.000, M2 rea Construida: 1.780, Detalhes: 1.000m de Patio.escritrio Com Elevado Vrias Salas, Cozinha, Wc, Vestirio Mas/fem;04 Salas de Descanso Para Motorista Com Vestirio, 02 Chuveiros e 04 Wc;cozinha Externa Com Quiosque + 02 Wc Mas/fem;parte Operacional Com P Direito de 7 Metros, e A Lateral Toda Com Embarque e Desembarque;ptio Com Espao Para Carretas, Caminhes e Carros;portaria Na Entrada do Ptio.\",Ref.: SA95923 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Parque Industrial,https://thumbs4.properati.com/0/wtHJ8Xcu4pqyV7YSP06g_N1yrfg=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2356/rO0uXvqSM9ilLshNDz0f.jpg\nc8c81389391a237255bade4a0608212fa62e13b1,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,4200,BRL,3984.12,1290.28,,107,12.058691588785047,39.25233644859813,,,,http://www.properati.com.br/ifxy_alugar_comercial_vila-olimpia,\"\tConjunto Comercial na Vila Olimpia, com 56m² de área útil, excelente localização.  Referíªncia: CO178\",\"Comercio no(a) Vila Olímpia , São Paulo - SP\",https://thumbs4.properati.com/7/YjqSlFDSnOcayBrp2UtD_X2xaJM=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/178/fa4df6b6-bb36-40a4-a46b-7b1438c77d03.jpg\ne1f129a3f1296a7e7a3b60d5b96d61c49159900c,2014-11-19,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.9295411,-46.3418105\",-23.9295411,-46.3418105,1600,BRL,1628.47,527.39,,32,16.4809375,50,,1,,http://www.properati.com.br/tkvf_alugar_apartamentos_sao-paulo,\"A 3 Milenio oferece este(a) Apartamento em Sao Vicente, no bairro Morro dos Barbosas - 1 dormitórios. Gostou desse(a) Apartamento em Sao Vicente? Então veja mais informações! Acesse o nosso site clicando no botão Contate o anunciante ou ligue - ou ainda entre em contato conosco pelo email vendas@ Obrigado!\",\"Apartamento em Sao Vicente, no bairro Morro dos Barbosas - 1 dormitórios\",https://thumbs4.properati.com/5/FvlCdG4KWDtvmzsszDUttj3AuAY=/trim/198x0/smart/filters:strip_icc()/www.imobiliariasdesantos.com.br/fotos/F00/72/807200-DSC03573.JPG\na8821d1c24cccd45d37ef69cf2b29ef3be744fad,2014-11-17,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8456698,-49.4030718\",-20.8456698,-49.4030718,3200,BRL,2530.11,819.39,,,,,,1,,http://www.properati.com.br/tdi2_alugar_apartamentos_sao-jose-do-rio-preto_avenida-anisio-haddad,\"Ref.: AP95925Tipo: ApartamentoCidade: So Jos do Rio Preto - SPBairro: Cond. Green Fields..: Apartamento No Condomnio Green Fields, Apartamento, rea Util M2: 152, Dormitrios: 03, Suite Master: 01, Closet , Tipo Apto.: 02, C/ A/e: 02, Wc. Social: 02, Sala de Visita: 01, Cozinha Planejada, Garagem Coberta: 02\",Ref.: AP95925 Tipo: Apartamento Cidade: So Jos do Rio Preto - SP Bairro: Cond. Green Fields,https://thumbs4.properati.com/4/oRIx16Fpw4LPkRsB59wlBS_mxVo=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2387/ae2a6d87-5760-44ff-a47f-d237a5321c3c.jpg\n08918f4a93c54f4505b783a8e31ef11842c55e56,2014-10-01,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.5882535,-46.6167776\",-23.5882535,-46.6167776,80000,BRL,63253.52,20484.98,,,,,,,,http://www.properati.com.br/qz9a_alugar_comercial_sao-jose-do-rio-preto_rua-da-imprensa,\"Ref.: SA95835Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Vila DinizM2/ Terreno: 12.000, Detalhes: - Salo Com 5.000 M e Bastante rea Para Estacionamento Aberto.Ideal Para Atacadista, Hipermercado e Distribuidora, Entre Outros.\",Ref.: SA95835 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Vila Diniz,https://thumbs4.properati.com/1/PkKXX6cNiwu9krqBtdWDinLG89A=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2077/vSsX8s9IzXdbNCXPId0W.jpg\n1ce358d0b5c35f146d70dff4089a8b3d287cb7b9,2014-10-08,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8085627,-49.3452813\",-20.8085627,-49.3452813,5500,BRL,4348.67,1408.34,,,,,,,,http://www.properati.com.br/r6de_alugar_comercial_sao-jose-do-rio-preto_avenida-sebastiao-tavares-da-silva,\"Ref.: SA95809Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Vista AlegreM2/ Terreno: 360, M2 rea Construida: 360, Detalhes: Salo Com Vrias Divisrias.\",Salo,https://thumbs4.properati.com/8/dhtnMWfIFZ8mMCFVDpxpvjoF0LA=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2184/l63oynNjEecn8u1Dzf3W.jpg\nb6ed51b61bb57831cc00d904741b4f544cafc28f,2014-08-04,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8178198,-49.3468294\",-20.8178198,-49.3468294,5500,BRL,4348.67,1408.34,,,,,,1,,http://www.properati.com.br/lxoz_alugar_casas_sao-jose-do-rio-preto,\"Ref.: CA95886Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Damha II(Res. Jardins)..: Casa Condomnio Damha II, Casa Condominio, Sobrado, Dormitrios: 03, Suite: 01, C/ Hidro, Tipo Apto.: 02, C/ A/e: 03, Roupeiro, Home, Cozinha: 01, Cozinha Planejada, Despensa, Escritrio, Lavabo, Salas : 01, Sala P/ Ambientes : 03, Piscina, Varanda, rea de Servio, Quarto de Empregada, Wc. de Empregada/servio, Aquecedor Solar, M2 rea Construida: 350, rea de Lazer, Garagem Descoberta: 03, Garagem Coberta: 03, Churrasqueira\", Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Damha II,https://thumbs4.properati.com/1/gEw1FIkmFLn2_zGBjqnuUzcifYU=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/793/e9d2d588-8d92-4070-b7d6-5b55e4484038.jpg\n1273c64fb8b9402dfad98d3984dec2998d8bdb68,2014-08-15,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8126181,-49.3404534\",-20.8126181,-49.3404534,5500,BRL,4348.67,1408.34,,,,,,1,,http://www.properati.com.br/mqtl_alugar_casas_sao-jose-do-rio-preto_avenida-miguel-damha,\"Ref.: CA95731Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Damha IVDormitrios: 03, Suite: 01, Closet : 01, C/ Hidro, Tipo Apto.: 02, C/ A/e: 02, Wcs.: 05, Wcs. C/ A/e: 04, Box Wcs.: 04, Copa: 01, Cozinha: 01, Cozinha C/ A/e, Despensa, Escritrio, Lavabo, Salas : 01, Sala P/ Ambientes : 02, Piscina, Varanda, rea de Servio, Wc. de Empregada/servio, M2/ Terreno: 420, M2 rea Construida: 380, rea de Lazer, Garagem Descoberta: 02, Garagem Coberta: 02, Churrasqueira, Piso Dormitrios: Porcelanatoq, Piso Salas: Porcelanato, Obs: Imvel Muito Bem Conservado, timo Acabamento., Valor Condominio R$-: 300,00, Detalhes: Toda Reformada.\",Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Damha IV,https://thumbs4.properati.com/6/H5vuIYMzk8pMuWJ-OjcdH4GItq8=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1367/9717e95d-fe6f-4cbc-bb5d-21c2fe92d294.jpg\n6aa6bf8a204f123441efb9f97699ee24cf8f9932,2014-08-15,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8178198,-49.3468294\",-20.8178198,-49.3468294,5500,BRL,4348.67,1408.34,,,,,,1,,http://www.properati.com.br/mqtn_alugar_casas_sao-jose-do-rio-preto_avenida-nadima-damha,\"Ref.: CA95797Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Damha ISobrado, Dormitrios: 03, Suite: 01, Tipo Apto.: 02, Wcs.: 01, Cozinha Planejada, Escritrio, Lavabo, Salas : 01, Sala P/ Ambientes : 03, Piscina, rea de Servio, Wc. de Empregada/servio, rea de Lazer, Garagem Coberta: 03, Churrasqueira, Ar Condicionado, Ventilador, Interfone, Piso Dormitrios: Taco, Piso Salas: Frio\",Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Damha I,https://thumbs4.properati.com/0/NAjAu7cPHQwzwT2j3k1G4X7-A3U=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1369/7b6ca8dd-648a-4e18-b9f5-272073bfbf69.jpg\n8a977c0c4b9ef700024cb2ee3e59de53bcf599c8,2014-07-16,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,2000,BRL,1897.2,614.42,,70,8.77742857142857,28.571428571428573,,2,,http://www.properati.com.br/ifzl_alugar_apartamentos_vila-olimpia,\"\tApartamento na Vila Olimpia, sala para 2 ambientes, 2 dormitórios com armários embutidos, cozinha, lavanderia, quarto na área de serviço, 2 banheiros, 1 vaga. Referíªncia: AP237\",\"Apartamento no(a) Vila Olímpia , São Paulo - SP\",https://thumbs4.properati.com/9/waPGo7zWMk2XTzN-br6B0h4Nrgo=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/237/75daca8f-ee37-4813-97af-b4fa00920d4f.jpg\nb48b18eca01fc2732d6afe024904eb1e900cc52a,2014-07-16,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,2000,BRL,1897.2,614.42,,54,11.378148148148147,37.03703703703704,,2,,http://www.properati.com.br/ifzn_alugar_apartamentos_saude,\"\tÓtimo apartamento proximo ao Metrê Praça da Arvore, sala para 2 ambientes, 2 dormitórios com armários embutidos, cozinha com armários, banheiro social e de serviço, 1 vaga na garagem, quadra, piscina e play.\tCom pintura nova, pronto para morar!! Referíªncia: AP239\",\"Apartamento no(a) Saúde , São Paulo - SP\",https://thumbs4.properati.com/0/Zof1-heqqvAgC3xdyYRYiQHIX2I=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/239/450071da-5288-4f26-bbb4-66bcacaef572.jpg\n636cbd951ef34773973cfdef37746b911d07a24c,2014-07-16,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,4000,BRL,3794.41,1228.84,,200,6.1442,20,,4,,http://www.properati.com.br/ifxa_alugar_casas_saude,\"\tAluga sobrado em zona mista, residencial e ou serviços, a uma quadra da Av. do Cursino e a 1000m, do Shopping Plaza Sul, 4 dormitórios, sendo uma suite, ampla sala, mais sala de jantar, cozinha, edicula com 3 quartos, mais lavanderia, garagem para 3 veiculos, casa de esquina, com portões automaticos.  Referíªncia: CA145\",\"Casa no(a) Saúde , São Paulo - SP\",https://thumbs4.properati.com/3/0sie3N08lMquoZ655zNy1CeDUbU=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/145/9fef5234-2fa4-4974-b24c-2b3cf6c4c542.jpg\n0812a419c00dcbaaaa926ad4e8de47da7959f9d9,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,5700,BRL,5407.01,1751.09,,127,13.788110236220472,44.881889763779526,,,,http://www.properati.com.br/ifzp_alugar_comercial_saude,\"\tConjunto Comercial proximo do Metrô São Judas, com 109m² de área útil, dividido em 5 salas, com possibilidade de vão livre, 3 banheiro, 1 copa, 3 vagas, prédio 24 horas.  Referíªncia: CO241\",\"Comercio no(a) Saúde , São Paulo - SP\",https://thumbs4.properati.com/1/0kl3BRCM4riJX3LhCObIu4wuBxQ=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/241/25ac4ee5-f867-4808-b226-837aefd7ba2c.jpg\n570fece17687d05345022d1567c79dd80bd1dcb0,2014-12-19,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8312527,-49.360102\",-20.8312527,-49.360102,15000,BRL,11860.02,3840.93,,,,,,,,http://www.properati.com.br/viin_alugar_comercial_sao-jose-do-rio-preto_avenida-marginal-lineu-alcantara-gil,\"Ref.: SA95950Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Dist. Industrial Campo Verde..: Salo Comercial No Distrito Industrial Campo Verde, Salo Comercial, M2/ Terreno: 5.000, M2 rea Construida: 3.000, Detalhes: tima Localizao Com Asfalto Para Rua, Estacionamento Para 60 Carros.barraco 5.000m2c/ Escritrio Isolado Ao Lado do Galpo C/ 6 Salas Grandes Von Livre Cond. Port.24h.\",Ref.: SA95950 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Dist. Industrial Campo Verde,https://thumbs4.properati.com/6/7lTVZ92lzm8ap5amDGM_eb1EHOU=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2528/yS1CaXFW8XYwJdPuhgGn.jpg\n2ee8e3818d61fdc4e01a5d89be181974d74e4380,2014-08-15,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8424141,-49.3549387\",-20.8424141,-49.3549387,15000,BRL,11860.02,3840.93,,,,,,,,http://www.properati.com.br/mqtc_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95109Tipo: Salo Comercial, Salo IndustrialCidade: So Jos do Rio Preto - SPBairro: Dist. Industrial Tancredo Neves .Terrea M2/ Terreno: 2.100, M2 rea Construida: 936, Detalhes: Estacionamento Na Frente,entrada Lateral Para Carga e Descarga,prox. do Carrefour\",\"Tipo: Salo Comercial, Salo Industrial Cidade: So Jos do Rio Preto - SP Bairro: Dist. Industrial Tancredo Neves .\",https://thumbs4.properati.com/9/KVlFV9eR-3HhzpzA5EtDKJICEgY=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1358/k6zhNAyNgH9P1excuuTM.jpg\n0f0577616ba85404705aaead7f35ff6e8b4a91da,2014-08-18,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.5733308,-46.544242\",-23.5733308,-46.544242,15000,BRL,11860.02,3840.93,,,,,,,,http://www.properati.com.br/mt62_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: CO95825 Tipo: Predio Comercial, Salo Comercial, Salo Industrial Cidade: So Jos do Rio Preto - SP Bairro: Pq. IndustrialWc. Social: 02, Salas : 06, M2/ Terreno: 1.113, Documentao: Em Dia., Detalhes: Banheiro Feminino e Masculino, 06 Salas Amplas, Sendo de 420 M a Maior.o Salo Possui Ambientes Para Reunies, Sala de Refeitrio de 93 M e Armrio Embutido, Alarme, Campainha, Interfone, Cerca Eltrica e Estacionamento de 258 M Com 25 Vagas Cobertas.\",\"Tipo: Predio Comercial, Salo Comercial, Salo Industrial Cidade: So Jos do Rio Preto - SP Bairro: Pq. Industrial\",https://thumbs4.properati.com/5/ug_ZifuqqZGKLp9_hQAm2sSvkgE=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/1436/Q14vgp5W0wZIU71vOSui.jpg\n8d6a13266a268bd8dd46f19df78ef71e3509ee5a,2014-10-18,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7700326,-49.3956468\",-20.7700326,-49.3956468,6500,BRL,5139.33,1664.4,,,,,,,,http://www.properati.com.br/ri3n_alugar_comercial_sao-jose-do-rio-preto_avenida-ada-marzochi-polachini,\"Ref.: SA95917Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Mini Dist. Industrial Adail Vetorasso..: Salo Comercial No Mini Destrito Industrial Adail Vetorasso, Salo Comercial, Wcs.: 02, Cozinha: 01, Escritrio, M2/ Terreno: 1.050, M2 rea Construida: 680, Detalhes: Barraco Novo, Com Pilar No Centro;porta Na Frente e Lateral;cobertura Metlica;p Direito 6 Metros.\",Ref.: SA95917 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Mini Dist. Industrial Adail Vetorasso,https://thumbs4.properati.com/2/3gLCbr6emuqSINA3fAYUW1zaIUM=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2283/grmYrDDM8HnJtcT4RzOD.jpg\ndac06563fc851c6596496ceaff68cbf177bb6dde,2014-10-01,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7863702,-49.3690389\",-20.7863702,-49.3690389,6500,BRL,5139.33,1664.4,,,,,,,,http://www.properati.com.br/qz9p_alugar_comercial_sao-jose-do-rio-preto_avenida-solon-da-silva-varginha,\"Ref.: SA95834Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Vila NovaesWc. Social: 03, Mesanino, M2/ Terreno: 530, M2 rea Construida: 480, Detalhes: Salo Comercial Com P Direito de 6m e Mezanino de 55 M.possui Aparelhagem de Ar Condicionado, Som Ambiente, Cmeras de Segurana e Estacionamento Para 18 Carros.local Ideal Para Lojas, Mercados, Igreja, Depsito, Entre Outros.\",Ref.: SA95834 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Vila Novaes,https://thumbs4.properati.com/8/qD9kCsVbiYyB0WGFw5WesI2Ny6s=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2092/d62f4d68-2ea5-4f83-bde9-fdc53bb504f2.jpg\n1ccba6f23edef69215ca9cfe74214a5276597714,2014-10-18,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,6500,BRL,5139.33,1664.4,,,,,,,,http://www.properati.com.br/ri3q_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: SA95919Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Dist. Industrial Centenrio da Emancipao..: Salo Comercial No Distrito Industrial Centenrio da Emancipao., Salo Comercial, Cozinha: 02, Mesanino, Escritrio, M2 rea Construida: 1.015, Detalhes: Escritrio, Cozinha, 02 Wc, Mezanino, 2 Salas, Entrada Lateral.salo 700 Metrosdiviso 315 Metros.\",Ref.: SA95919 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Dist. Industrial Centenrio da Emancipao,https://thumbs4.properati.com/0/FlEAiRaerT0pRZEeW7AhhCuJjJc=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2286/v22UnUeHl8YbhJaP8IaU.jpg\na9d04cd869b56cac7bcb3a54a27147996a973a3d,2015-01-20,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.5418835,-46.5583192\",-23.5418835,-46.5583192,1900,BRL,1539.57,498.6,,,,,16,1,,http://www.properati.com.br/w730_alugar_apartamentos_tatuape,\"APTO DE 58M/2, COM 02 DORMITRIOS, SENDO UMA SUTE, SALA AMPLA COM SACADA, COZINHA, WC SOCIAL, REA DE SERVIO, COM ALGUMAS MOBILIAS, TUDO DE PRIMEIRA, 02 VAGAS DE GARAGEM, LAZER COMPLETE, EXCELENTE LOCALIZAO, A UMA QUADRA DO METR.\",MOBILIADO EXCLUSIVE,https://thumbs4.properati.com/2/KuQXATs4BGrSaZvfCzOSMN2pqWs=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/toledo/photos/19/30ad0c15-c9ef-48f7-a06e-2a0201bb7433.jpg\n5de8d0e3c8872b0af03bdc517a4a12021c48da59,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,24700,BRL,23430.5,7588.09,,289,26.25636678200692,85.46712802768167,,,,http://www.properati.com.br/ifvd_alugar_comercial_vila-olimpia,\"EDIFí�CIO COMERCIAL DE ALTí�SSIMO PADRí�O,  LOCALIZADO NO PONTO MAIS NOBRE DA REGIí�O, EM FRENTE AOSHOPPING VILA OLí�MPIA, ENTRE AV. DOS BANDEIRANTES E JUSCELINO KUBITSCHEK. OPRí�DIO CONTA COM 6 ELEVADORES DE ALTA VELOCIDADE, SENDO 1 PANORí�MICO, ARCONDICIONADO CENTRAL, FACHADA EM CORTINA DE VIDRO. EDIFí�CIO INTELIGENTE COMSOFISTICADO SISTEMA DE CONTROLE DE INCí�NDIO, CONSUMO DE ENERGIA. Referíªncia: CO66\",\"Comercio no(a) Vila Olímpia , São Paulo - SP\",\n705fdcf676aca2c9f51fcc0a332fcff39ee1706c,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,19500,BRL,18497.74,5990.59,,260,23.04073076923077,75,,,,http://www.properati.com.br/ifyk_alugar_comercial_vila-olimpia,\"\tConjunto Comercial com 260m² de área útil, 6 vagas, Ar condicionado Central. Referíªncia: CO200\",\"Comercio no(a) Vila Olímpia , São Paulo - SP\",https://thumbs4.properati.com/0/-wzyHwNVYSdVIvzDSj7dz-RtyfY=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/200/f2ef8797-b051-4b62-b4fe-e8fdb7523a26.jpg\n179ce00d44c6fafbac93e139f052ab4ce304100d,2014-08-04,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,4200,BRL,3320.8,1075.46,,,,,,,,http://www.properati.com.br/lxpa_alugar_casas_sao-jose-do-rio-preto,\"Ref.: CA95877 Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SPBairro: Cond. Village La MontangneDormitrios: 3, Suite: 1, C/ Hidro: 1, Tipo Apto.: 2, C/ A/e: 3, Cozinha Planejada, Escritrio, Lavabo, Salas : 1, Sala P/ Ambientes : 3, Piscina, rea de Servio, M2/ Terreno: 360, M2 rea Construida: 250, Garagem Descoberta: 2, Garagem Coberta: 2\",Tipo: Casa Condominio Cidade: So Jos do Rio Preto - SP Bairro: Cond. Village La Montangne,https://thumbs4.properati.com/8/XTvYImGZcecKNC8xwzjnrrqWibg=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/804/ce485ed8-97e3-4af6-bc5e-3d347e06b31d.jpg\n868d3ff8ea898557e9819638705d2f823431d000,2015-01-13,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.521407,-46.5769202\",-23.521407,-46.5769202,1500,BRL,1215.45,393.63,,,,,13,1,,http://www.properati.com.br/vysr_alugar_apartamentos_tatuape,\"APTO DE 60M/2, COM 02 DORMITRIO, SALA AMPLA COM SACADA, WC SOCIAL, COZINHA, REA DE SERVIO, 01 VAGA DE GARAGEM, EXCELENTE ESTRUTURA DE LAZER COM PISCINA, PLAYGROUD, SALO DE FESTA, CHURRASQUEIRA, EXCELENTE LOCALIZAO, FCIL ACESSO AO SHOPPING METR/TATUAP.\",131 VITORIA,https://thumbs4.properati.com/0/yyioUci-ExAIuKlfIudXKNKaMAs=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/toledo/photos/16/ff90ae98-dbde-47dc-9f04-32cdb2d2d0d3.jpg\nb9de26b4b13ad0f58469677b72b5171c2b04ae1f,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,13500,BRL,12806.12,4147.33,,183,22.663005464480875,73.77049180327869,,,,http://www.properati.com.br/ifv7_alugar_comercial_vila-olimpia,CONJUNTO COMERCIAL - RECEPCAO - HD - 4 SALAS - 2 WC - COPA  Referíªncia: CO60,\"Comercio no(a) Vila Olímpia , São Paulo - SP\",\n416be9056b69e83819532b65a04c6d1f7135f1ca,2014-12-11,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,2400,BRL,1897.57,614.54,,,,,,,,http://www.properati.com.br/uict_alugar_comercial_jardim-maracana,\"Ref.: SA95945Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Jardim Maracan..: Salo Comercial No Jardim Maracan, Salo Comercial, Wc. Social: 02, Cozinha: 01, Forro de Laje, Piso Salas: Frio, M2/ Terreno: 225, M2 rea Construida: 130\",Ref.: SA95945 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Jardim Maracan,https://thumbs4.properati.com/5/3964ltfTag6YXQlyxEM19akYy1s=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2500/f25a8d90-6759-426c-a4ed-d8aefc6b19a4.jpg\nc1aff8edd38c3254c5775927802a8b8fae45a1d8,2014-08-05,rent,house,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,2400,BRL,1897.57,614.54,,,,,,1,,http://www.properati.com.br/mel5_alugar_casas_bosque-da-saude,\"Ref.: CA95883Tipo: Casa ResidencialCidade: So Jos do Rio Preto - SPBairro: Bosque Da Saude..: Casa Residencial No Bosque da Saude, Casa Residencial, Dormitrios: 03, Tipo Apto.: 01, C/ A/e: 03, Wc. Social: 01, Copa: 01, Cozinha: 01, Despensa, Salas : 02, Varanda, Garagem Descoberta: 02, Garagem Coberta: 02\",Tipo: Casa Residencial Cidade: So Jos do Rio Preto - SP Bairro: Bosque Da Saude,https://thumbs4.properati.com/0/aAdm951DKitDcqkiJ8p68O6xdNA=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/848/083f9a71-b22a-4897-bf14-72fbe585dea5.jpg\ne4d569301fb8a21047d67f193929ecf360e9405e,2015-01-11,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,750,BRL,600.6,194.51,,,,,,1,,http://www.properati.com.br/vxnv_alugar_apartamentos_jardim-satelite,\"Excelente apartamento em frente ao condomnio Quinta das Flores totalmente reformado com 2 dormitrios, sala, cozinha planejada, rea de servio, lavabo com gabinete, wc com box blindex e 1 vaga de garagem.Ideal para casal sem filhos e sem animais\",Excelente apartamento no Jardim Satlite,https://thumbs4.properati.com/1/RhUtbG6YnK8FCa_yOduiEf3yyj0=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/tyrreno/photos/1074/71219c81-463f-4710-a46d-5e7e2fee9362.jpg\n6b59e27767f7570db49b2175077b1c059fa20b88,2014-10-01,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-23.5715766,-46.6697673\",-23.5715766,-46.6697673,3100,BRL,2451.06,793.79,,,,,,,,http://www.properati.com.br/qz9r_alugar_comercial_sao-jose-do-rio-preto_avenida-brasil,\"Ref.: SA95828Tipo: Salo ComercialCidade: So Jos do Rio Preto - SPBairro: Jardim MugnaniWc. Social: 02, Cozinha, M2/ Terreno: 350, M2 rea Construida: 210, Detalhes: - Salo Novo!\", Ref.: SA95828 Tipo: Salo Comercial Cidade: So Jos do Rio Preto - SP Bairro: Jardim Mugnani,https://thumbs4.properati.com/5/ShU0-2rvOmZQkWG3Ml57SSATz_s=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2094/VmmbZZ9w1FykW0ldwpGM.jpg\n3931096ab81c0a74496858654375447e859df8f2,2014-12-05,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.7910821,-49.3781115\",-20.7910821,-49.3781115,880,BRL,695.77,225.33,,,,,,1,,http://www.properati.com.br/ue04_alugar_apartamentos_sao-jose-do-rio-preto_rua-pedro-mulati,\"Ref.: AP95815Tipo: ApartamentoCidade: So Jos do Rio Preto - SPBairro: Jardim ConceioCobertura, Dormitrios: 02, C/ A/e, Wc. Social, Box Wcs., Sala de Estar, Cozinha, Piscina, rea de Servio, rea de Lazer, Garagem Coberta: 01, rea Util M2: 65, Playground\",Ref.: AP95815 Tipo: Apartamento Cidade: So Jos do Rio Preto - SP Bairro: Jardim Conceio,https://thumbs4.properati.com/3/0zqbkQ73LUnQeQMp0yntvzoZrq8=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2458/Dv0976z6fjHNbJf7oAX0.jpg\nc15e079e557ec6ad9d86baede07324e678686764,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,3800,BRL,3604.66,1167.39,,88,13.265795454545456,43.18181818181818,,,,http://www.properati.com.br/ifzr_alugar_comercial_saude,\"\tConjunto Comercial proximo do Metrô São Judas, com 76m², 2 banheiros, 2 vagas, 1 copa, predio 24 horas. Referíªncia: CO243\",\"Comercio no(a) Saúde , São Paulo - SP\",https://thumbs4.properati.com/4/h5_FesPCfkGnmz5ac8c3DvXy_cE=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/243/68c5314b-39b1-4ebd-af49-694d915540b0.jpg\n62cbd166c6e120af08202b4db367db83240ed7e1,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,8500,BRL,8063.11,2611.28,,300,8.704266666666667,28.333333333333332,,,,http://www.properati.com.br/ifw2_alugar_comercial_vila-olimpia,\"\t\t\tLindo(a) Comercio de 300 metros quadrados no bairro Vila Olímpia na cidade de São Paulo - SP.\t\t\t, 2 banheiro(s), 6 vagas(s) de garagem.\t \t\tCondições de Pagamento: \t \t \t \t\tCaracteristicas: Referíªncia: CO93\",\"Comercio no(a) Vila Olímpia , São Paulo - SP\",https://thumbs4.properati.com/1/gH6VwXVOHsfAy5JV8elaJg59lGA=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/93/4276fcba-994a-4296-9a35-65c580711457.jpg\n5a5bfb194fdbec1a633f5894a66c8bf45c8ae463,2015-01-06,rent,apartment,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,1100,BRL,869.7,281.66,,,,,,,,http://www.properati.com.br/vv2s_alugar_apartamentos_sao-jose-do-rio-preto,\"Ref.: AP95935Tipo: ApartamentoCidade: So Jos do Rio Preto - SPBairro: Vila Itlia..: Apartamento Residencial Na Vila Itlia, Apartamento, rea Util M2: 63, Dormitrios: 02, Closet , Tipo Apto.: 01, Wc. Social: 01, Sala de Tv, Cozinha: 01, Cozinha C/ A/e, Garagem Coberta: 01, Detalhes: Prdio Novo.\",Ref.: AP95935 Tipo: Apartamento Cidade: So Jos do Rio Preto - SP Bairro: Vila Itlia,https://thumbs4.properati.com/5/XPnfndqmnEKY04smTHRwdeanGd8=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2535/atPPaTZcVegFbIYjH45d.jpg\n8f0adde258e0b9ad5897ba55288b52005ca749a8,2014-07-16,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,,,,3200,BRL,3035.52,983.07,,100,9.8307,32,,,,http://www.properati.com.br/ifxp_alugar_comercial_republica,\"\tLoja/Galpão com mezanino, pé direito duplo, localizada na Av. Amaral Guegel, proximo da Consolação, 2 banheiro e 2 vagas demarcadas. Referíªncia: CO169\",\"Comercio no(a) República , São Paulo - SP\",https://thumbs4.properati.com/6/XJyKobKfgKlfwiO0SB2Tt2i0Y9w=/trim/198x0/smart/filters:strip_icc()/souzapinto.imobfusion01.com.br/accounts/souzapinto/data/photo/169/50b1ac29-6a0b-4488-aaa4-07bc131dd969.jpg\n480229dcb811d916f90a02ffd9c5b62e8a72ccbe,2014-10-15,rent,store,São Paulo,|Brasil|São Paulo|,Brasil,São Paulo,,\"-20.8168432,-49.3930989\",-20.8168432,-49.3930989,17000,BRL,13441.34,4353.05,,,,,,,,http://www.properati.com.br/re16_alugar_comercial_sao-jose-do-rio-preto,\"Ref.: CO95289Tipo: Predio ComercialCidade: So Jos do Rio Preto - SPBairro: Vila dos BancariosM2 rea Construida: 800, Garagem Descoberta: 20, Detalhes: Predio Comercial Com 10m Salas de 80 M Cada, Fica Em Um Pavimento Com Elevador, 20 Vagas Para Estacionar .\",Loja,https://thumbs4.properati.com/4/Z-A_sLSPQOFzDbPb8MkkcY9IwMU=/trim/198x0/smart/filters:strip_icc()/static.if2.com.br/acc/interplan/photos/2239/bsfsEYepcv9Cu4EHDvjU.jpg\n\n", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "No pagination, no footer", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "country_name": true, + "created_on": true, + "expenses": true, + "floor": true, + "geonames_id": true, + "id": true, + "image_thumbnail": true, + "lat": true, + "lat_lon": true, + "lon": true, + "operation": true, + "place_with_parent_names": true, + "price_aprox_local_currency": true, + "price_aprox_usd": true, + "state_name": true, + "surface_total_in_m2": true + }, + "indexByName": { + "country_name": 13, + "created_on": 9, + "currency": 19, + "description": 5, + "expenses": 24, + "floor": 7, + "geonames_id": 15, + "id": 0, + "image_thumbnail": 26, + "lat": 17, + "lat_lon": 16, + "lon": 18, + "operation": 8, + "place_name": 4, + "place_with_parent_names": 12, + "price": 11, + "price_aprox_local_currency": 20, + "price_aprox_usd": 21, + "price_per_m2": 23, + "price_usd_per_m2": 22, + "properati_url": 25, + "property_type": 2, + "rooms": 6, + "state_name": 14, + "surface_covered_in_m2": 3, + "surface_total_in_m2": 10, + "title": 1 + }, + "renameByName": { + "description": "Description", + "place_name": "City", + "price": "Price", + "properati_url": "URL", + "property_type": "Type", + "rooms": "Rooms", + "surface_covered_in_m2": "Surface", + "title": "Property name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [ + { + "options": { + "rent": { + "color": "yellow", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "property_type" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "store": { + "color": "blue", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "custom.width", + "value": 205 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Number of rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 66 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Description" + }, + "properties": [ + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "URL" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Show details", + "url": "${__value.raw}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Surface" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background-solid" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "orange", + "value": 100 + }, + { + "color": "yellow", + "value": 200 + }, + { + "color": "green", + "value": 300 + }, + { + "color": "#EAB839", + "value": 500 + }, + { + "color": "#6ED0E0", + "value": 1000 + }, + { + "color": "#EF843C", + "value": 2000 + } + ] + } + }, + { + "id": "unit", + "value": "areaM2" + }, + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 94 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "City" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 77 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Price" + }, + "properties": [ + { + "id": "unit", + "value": "currencyUSD" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 6, + "options": { + "footer": { + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Surface" + } + ] + }, + "pluginVersion": "9.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "No pagination, footer", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "country_name": true, + "created_on": true, + "expenses": true, + "floor": true, + "geonames_id": true, + "id": true, + "image_thumbnail": true, + "lat": true, + "lat_lon": true, + "lon": true, + "operation": true, + "place_with_parent_names": true, + "price_aprox_local_currency": true, + "price_aprox_usd": true, + "state_name": true, + "surface_total_in_m2": true + }, + "indexByName": { + "country_name": 13, + "created_on": 9, + "currency": 19, + "description": 5, + "expenses": 24, + "floor": 7, + "geonames_id": 15, + "id": 0, + "image_thumbnail": 26, + "lat": 17, + "lat_lon": 16, + "lon": 18, + "operation": 8, + "place_name": 4, + "place_with_parent_names": 12, + "price": 11, + "price_aprox_local_currency": 20, + "price_aprox_usd": 21, + "price_per_m2": 23, + "price_usd_per_m2": 22, + "properati_url": 25, + "property_type": 2, + "rooms": 6, + "state_name": 14, + "surface_covered_in_m2": 3, + "surface_total_in_m2": 10, + "title": 1 + }, + "renameByName": { + "description": "Description", + "place_name": "City", + "price": "Price", + "properati_url": "URL", + "property_type": "Type", + "rooms": "Rooms", + "surface_covered_in_m2": "Surface", + "title": "Property name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [ + { + "options": { + "rent": { + "color": "yellow", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "property_type" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "store": { + "color": "blue", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "custom.width", + "value": 205 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Number of rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 66 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Description" + }, + "properties": [ + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "URL" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Show details", + "url": "${__value.raw}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Surface" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background-solid" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "orange", + "value": 100 + }, + { + "color": "yellow", + "value": 200 + }, + { + "color": "green", + "value": 300 + }, + { + "color": "#EAB839", + "value": 500 + }, + { + "color": "#6ED0E0", + "value": 1000 + }, + { + "color": "#EF843C", + "value": 2000 + } + ] + } + }, + { + "id": "unit", + "value": "areaM2" + }, + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 94 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "City" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 77 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Price" + }, + "properties": [ + { + "id": "unit", + "value": "currencyUSD" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 9, + "options": { + "footer": { + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": false, + "sortBy": [ + { + "desc": true, + "displayName": "Surface" + } + ] + }, + "pluginVersion": "9.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "country_name": true, + "created_on": true, + "expenses": true, + "floor": true, + "geonames_id": true, + "id": true, + "image_thumbnail": true, + "lat": true, + "lat_lon": true, + "lon": true, + "operation": true, + "place_with_parent_names": true, + "price_aprox_local_currency": true, + "price_aprox_usd": true, + "state_name": true, + "surface_total_in_m2": true + }, + "indexByName": { + "country_name": 13, + "created_on": 9, + "currency": 19, + "description": 5, + "expenses": 24, + "floor": 7, + "geonames_id": 15, + "id": 0, + "image_thumbnail": 26, + "lat": 17, + "lat_lon": 16, + "lon": 18, + "operation": 8, + "place_name": 4, + "place_with_parent_names": 12, + "price": 11, + "price_aprox_local_currency": 20, + "price_aprox_usd": 21, + "price_per_m2": 23, + "price_usd_per_m2": 22, + "properati_url": 25, + "property_type": 2, + "rooms": 6, + "state_name": 14, + "surface_covered_in_m2": 3, + "surface_total_in_m2": 10, + "title": 1 + }, + "renameByName": { + "description": "Description", + "place_name": "City", + "price": "Price", + "properati_url": "URL", + "property_type": "Type", + "rooms": "Rooms", + "surface_covered_in_m2": "Surface", + "title": "Property name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [ + { + "options": { + "rent": { + "color": "yellow", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "property_type" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "store": { + "color": "blue", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "custom.width", + "value": 205 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Number of rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 66 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Description" + }, + "properties": [ + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "URL" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Show details", + "url": "${__value.raw}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Surface" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background-solid" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "orange", + "value": 100 + }, + { + "color": "yellow", + "value": 200 + }, + { + "color": "green", + "value": 300 + }, + { + "color": "#EAB839", + "value": 500 + }, + { + "color": "#6ED0E0", + "value": 1000 + }, + { + "color": "#EF843C", + "value": 2000 + } + ] + } + }, + { + "id": "unit", + "value": "areaM2" + }, + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 94 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "City" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 77 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Price" + }, + "properties": [ + { + "id": "unit", + "value": "currencyUSD" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 7, + "options": { + "footer": { + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Surface" + } + ] + }, + "pluginVersion": "9.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "Pagination, no footer", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "country_name": true, + "created_on": true, + "expenses": true, + "floor": true, + "geonames_id": true, + "id": true, + "image_thumbnail": true, + "lat": true, + "lat_lon": true, + "lon": true, + "operation": true, + "place_with_parent_names": true, + "price_aprox_local_currency": true, + "price_aprox_usd": true, + "state_name": true, + "surface_total_in_m2": true + }, + "indexByName": { + "country_name": 13, + "created_on": 9, + "currency": 19, + "description": 5, + "expenses": 24, + "floor": 7, + "geonames_id": 15, + "id": 0, + "image_thumbnail": 26, + "lat": 17, + "lat_lon": 16, + "lon": 18, + "operation": 8, + "place_name": 4, + "place_with_parent_names": 12, + "price": 11, + "price_aprox_local_currency": 20, + "price_aprox_usd": 21, + "price_per_m2": 23, + "price_usd_per_m2": 22, + "properati_url": 25, + "property_type": 2, + "rooms": 6, + "state_name": 14, + "surface_covered_in_m2": 3, + "surface_total_in_m2": 10, + "title": 1 + }, + "renameByName": { + "description": "Description", + "place_name": "City", + "price": "Price", + "properati_url": "URL", + "property_type": "Type", + "rooms": "Rooms", + "surface_covered_in_m2": "Surface", + "title": "Property name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [ + { + "options": { + "rent": { + "color": "yellow", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "property_type" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "store": { + "color": "blue", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "custom.width", + "value": 205 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Number of rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 66 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Description" + }, + "properties": [ + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "URL" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Show details", + "url": "${__value.raw}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Surface" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background-solid" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "orange", + "value": 100 + }, + { + "color": "yellow", + "value": 200 + }, + { + "color": "green", + "value": 300 + }, + { + "color": "#EAB839", + "value": 500 + }, + { + "color": "#6ED0E0", + "value": 1000 + }, + { + "color": "#EF843C", + "value": 2000 + } + ] + } + }, + { + "id": "unit", + "value": "areaM2" + }, + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 94 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "City" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 77 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Price" + }, + "properties": [ + { + "id": "unit", + "value": "currencyUSD" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 8, + "options": { + "footer": { + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Surface" + } + ] + }, + "pluginVersion": "9.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "Pagination and footer", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "country_name": true, + "created_on": true, + "expenses": true, + "floor": true, + "geonames_id": true, + "id": true, + "image_thumbnail": true, + "lat": true, + "lat_lon": true, + "lon": true, + "operation": true, + "place_with_parent_names": true, + "price_aprox_local_currency": true, + "price_aprox_usd": true, + "state_name": true, + "surface_total_in_m2": true + }, + "indexByName": { + "country_name": 13, + "created_on": 9, + "currency": 19, + "description": 5, + "expenses": 24, + "floor": 7, + "geonames_id": 15, + "id": 0, + "image_thumbnail": 26, + "lat": 17, + "lat_lon": 16, + "lon": 18, + "operation": 8, + "place_name": 4, + "place_with_parent_names": 12, + "price": 11, + "price_aprox_local_currency": 20, + "price_aprox_usd": 21, + "price_per_m2": 23, + "price_usd_per_m2": 22, + "properati_url": 25, + "property_type": 2, + "rooms": 6, + "state_name": 14, + "surface_covered_in_m2": 3, + "surface_total_in_m2": 10, + "title": 1 + }, + "renameByName": { + "description": "Description", + "place_name": "City", + "price": "Price", + "properati_url": "URL", + "property_type": "Type", + "rooms": "Rooms", + "surface_covered_in_m2": "Surface", + "title": "Property name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [ + { + "options": { + "rent": { + "color": "yellow", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "property_type" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "store": { + "color": "blue", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "custom.width", + "value": 205 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Number of rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 66 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Description" + }, + "properties": [ + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "URL" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Show details", + "url": "${__value.raw}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Surface" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background-solid" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "orange", + "value": 100 + }, + { + "color": "yellow", + "value": 200 + }, + { + "color": "green", + "value": 300 + }, + { + "color": "#EAB839", + "value": 500 + }, + { + "color": "#6ED0E0", + "value": 1000 + }, + { + "color": "#EF843C", + "value": 2000 + } + ] + } + }, + { + "id": "unit", + "value": "areaM2" + }, + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 94 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "City" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 77 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Price" + }, + "properties": [ + { + "id": "unit", + "value": "currencyUSD" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 10, + "options": { + "footer": { + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": false, + "sortBy": [ + { + "desc": true, + "displayName": "Surface" + } + ] + }, + "pluginVersion": "9.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "Pagination, no footer, no header", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "country_name": true, + "created_on": true, + "expenses": true, + "floor": true, + "geonames_id": true, + "id": true, + "image_thumbnail": true, + "lat": true, + "lat_lon": true, + "lon": true, + "operation": true, + "place_with_parent_names": true, + "price_aprox_local_currency": true, + "price_aprox_usd": true, + "state_name": true, + "surface_total_in_m2": true + }, + "indexByName": { + "country_name": 13, + "created_on": 9, + "currency": 19, + "description": 5, + "expenses": 24, + "floor": 7, + "geonames_id": 15, + "id": 0, + "image_thumbnail": 26, + "lat": 17, + "lat_lon": 16, + "lon": 18, + "operation": 8, + "place_name": 4, + "place_with_parent_names": 12, + "price": 11, + "price_aprox_local_currency": 20, + "price_aprox_usd": 21, + "price_per_m2": 23, + "price_usd_per_m2": 22, + "properati_url": 25, + "property_type": 2, + "rooms": 6, + "state_name": 14, + "surface_covered_in_m2": 3, + "surface_total_in_m2": 10, + "title": 1 + }, + "renameByName": { + "description": "Description", + "place_name": "City", + "price": "Price", + "properati_url": "URL", + "property_type": "Type", + "rooms": "Rooms", + "surface_covered_in_m2": "Surface", + "title": "Property name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [ + { + "options": { + "rent": { + "color": "yellow", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "property_type" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "store": { + "color": "blue", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "custom.width", + "value": 205 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Number of rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 66 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Description" + }, + "properties": [ + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "URL" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Show details", + "url": "${__value.raw}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Surface" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background-solid" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "orange", + "value": 100 + }, + { + "color": "yellow", + "value": 200 + }, + { + "color": "green", + "value": 300 + }, + { + "color": "#EAB839", + "value": 500 + }, + { + "color": "#6ED0E0", + "value": 1000 + }, + { + "color": "#EF843C", + "value": 2000 + } + ] + } + }, + { + "id": "unit", + "value": "areaM2" + }, + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 94 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "City" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 77 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Price" + }, + "properties": [ + { + "id": "unit", + "value": "currencyUSD" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 11, + "options": { + "footer": { + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": false, + "sortBy": [ + { + "desc": true, + "displayName": "Surface" + } + ] + }, + "pluginVersion": "9.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "Pagination, footer, no header", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "country_name": true, + "created_on": true, + "expenses": true, + "floor": true, + "geonames_id": true, + "id": true, + "image_thumbnail": true, + "lat": true, + "lat_lon": true, + "lon": true, + "operation": true, + "place_with_parent_names": true, + "price_aprox_local_currency": true, + "price_aprox_usd": true, + "state_name": true, + "surface_total_in_m2": true + }, + "indexByName": { + "country_name": 13, + "created_on": 9, + "currency": 19, + "description": 5, + "expenses": 24, + "floor": 7, + "geonames_id": 15, + "id": 0, + "image_thumbnail": 26, + "lat": 17, + "lat_lon": 16, + "lon": 18, + "operation": 8, + "place_name": 4, + "place_with_parent_names": 12, + "price": 11, + "price_aprox_local_currency": 20, + "price_aprox_usd": 21, + "price_per_m2": 23, + "price_usd_per_m2": 22, + "properati_url": 25, + "property_type": 2, + "rooms": 6, + "state_name": 14, + "surface_covered_in_m2": 3, + "surface_total_in_m2": 10, + "title": 1 + }, + "renameByName": { + "description": "Description", + "place_name": "City", + "price": "Price", + "properati_url": "URL", + "property_type": "Type", + "rooms": "Rooms", + "surface_covered_in_m2": "Surface", + "title": "Property name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [ + { + "options": { + "rent": { + "color": "yellow", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "property_type" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "store": { + "color": "blue", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "title" + }, + "properties": [ + { + "id": "custom.width", + "value": 205 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Number of rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 66 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Description" + }, + "properties": [ + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "URL" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Show details", + "url": "${__value.raw}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Surface" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-background-solid" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "orange", + "value": 100 + }, + { + "color": "yellow", + "value": 200 + }, + { + "color": "green", + "value": 300 + }, + { + "color": "#EAB839", + "value": 500 + }, + { + "color": "#6ED0E0", + "value": 1000 + }, + { + "color": "#EF843C", + "value": 2000 + } + ] + } + }, + { + "id": "unit", + "value": "areaM2" + }, + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 94 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "City" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Rooms" + }, + "properties": [ + { + "id": "custom.width", + "value": 77 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Price" + }, + "properties": [ + { + "id": "unit", + "value": "currencyUSD" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 12, + "options": { + "footer": { + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": false, + "sortBy": [ + { + "desc": true, + "displayName": "Surface" + } + ] + }, + "pluginVersion": "9.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "country_name": true, + "created_on": true, + "expenses": true, + "floor": true, + "geonames_id": true, + "id": true, + "image_thumbnail": true, + "lat": true, + "lat_lon": true, + "lon": true, + "operation": true, + "place_with_parent_names": true, + "price_aprox_local_currency": true, + "price_aprox_usd": true, + "state_name": true, + "surface_total_in_m2": true + }, + "indexByName": { + "country_name": 13, + "created_on": 9, + "currency": 19, + "description": 5, + "expenses": 24, + "floor": 7, + "geonames_id": 15, + "id": 0, + "image_thumbnail": 26, + "lat": 17, + "lat_lon": 16, + "lon": 18, + "operation": 8, + "place_name": 4, + "place_with_parent_names": 12, + "price": 11, + "price_aprox_local_currency": 20, + "price_aprox_usd": 21, + "price_per_m2": 23, + "price_usd_per_m2": 22, + "properati_url": 25, + "property_type": 2, + "rooms": 6, + "state_name": 14, + "surface_covered_in_m2": 3, + "surface_total_in_m2": 10, + "title": 1 + }, + "renameByName": { + "description": "Description", + "place_name": "City", + "price": "Price", + "properati_url": "URL", + "property_type": "Type", + "rooms": "Rooms", + "surface_covered_in_m2": "Surface", + "title": "Property name" + } + } + } + ], + "type": "table" + } + ], + "schemaVersion": 32, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Table panel - Pagination", + "uid": "1YGJxPUnz", + "version": 2, + "weekStart": "" +} \ No newline at end of file diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 1366744fd91..1097fad630f 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -304,9 +304,9 @@ export const Table: FC = memo((props: Props) => { totalColumnsWidth={totalColumnsWidth} /> )} - {paginationEl}
    + {paginationEl}
    ); }); diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index c70f0c9e199..3682b5da17e 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -184,6 +184,9 @@ export const getTableStyles = (theme: GrafanaTheme2) => { div:not(:only-child):first-child { flex-grow: 0.6; } + position: absolute; + bottom: 0; + left: 0; `, paginationSummary: css` color: ${theme.colors.text.secondary}; @@ -191,11 +194,16 @@ export const getTableStyles = (theme: GrafanaTheme2) => { margin-left: auto; `, - tableContentWrapper: (totalColumnsWidth: number) => css` - width: ${totalColumnsWidth ?? '100%'}; - display: flex; - flex-direction: column; - `, + tableContentWrapper: (totalColumnsWidth: number) => { + const width = totalColumnsWidth !== undefined ? `${totalColumnsWidth}px` : '100%'; + + return css` + label: tableContentWrapper; + width: ${width}; + display: flex; + flex-direction: column; + `; + }, row: css` label: row; border-bottom: 1px solid ${borderColor}; From a126c07e541733462c6b69970bec829f93a4c68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Apr 2022 19:58:08 +0200 Subject: [PATCH 104/121] DashboardPage: Remember scroll position when coming back panel edit / view panel (#47639) * DashboardPage: Remember scroll position when coming back panel edit / view panel * Use scollElement callback * Fixed ts issue --- .../dashboard/containers/DashboardPage.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 860e645f6bb..c73c0c6fb16 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -4,7 +4,6 @@ import { connect, ConnectedProps } from 'react-redux'; import { locationService } from '@grafana/runtime'; import { selectors } from '@grafana/e2e-selectors'; import { CustomScrollbar, stylesFactory, Themeable2, withTheme2 } from '@grafana/ui'; - import { createErrorNotification } from 'app/core/copy/appNotification'; import { Branding } from 'app/core/components/Branding/Branding'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; @@ -78,6 +77,7 @@ export interface State { showLoadingState: boolean; panelNotFound: boolean; editPanelAccessDenied: boolean; + scrollElement?: HTMLDivElement; } export class UnthemedDashboardPage extends PureComponent { @@ -224,14 +224,14 @@ export class UnthemedDashboardPage extends PureComponent { } if (dashboard.canEditPanel(panel)) { - return { ...state, editPanel: panel }; + return { ...state, editPanel: panel, rememberScrollTop: state.scrollElement?.scrollTop }; } else { return { ...state, editPanelAccessDenied: true }; } } // Leaving edit mode else if (state.editPanel && !urlEditPanelId) { - return { ...state, editPanel: null }; + return { ...state, editPanel: null, updateScrollTop: state.rememberScrollTop }; } // Entering view mode @@ -245,11 +245,7 @@ export class UnthemedDashboardPage extends PureComponent { // Should move this state out of dashboard in the future dashboard.initViewPanel(panel); - return { - ...state, - viewPanel: panel, - updateScrollTop: 0, - }; + return { ...state, viewPanel: panel, rememberScrollTop: state.scrollElement?.scrollTop, updateScrollTop: 0 }; } // Leaving view mode else if (state.viewPanel && !urlViewPanelId) { @@ -290,6 +286,10 @@ export class UnthemedDashboardPage extends PureComponent { this.setState({ updateScrollTop: 0 }); }; + setScrollRef = (scrollElement: HTMLDivElement): void => { + this.setState({ scrollElement }); + }; + getInspectPanel() { const { dashboard, queryParams } = this.props; @@ -346,6 +346,7 @@ export class UnthemedDashboardPage extends PureComponent {
    Date: Thu, 14 Apr 2022 13:06:21 -0500 Subject: [PATCH 105/121] Alerting: Refactor GET/POST alerting config routes to be more extensible (#47229) * Refactor GET am config to be extensible * Extract post config route * Fix tests * Remove temporary duplication * Fix broken test due to layer shift * Fix duplicated error message * Properly return 400 on config rejection * Revert weird half method extraction * Move things to notifier package and avoid redundant interface * Simplify documentation * Split encryption service and depend on minimal abstractions * Properly initialize things all the way up to the composition root * Encryption -> Crypto * Address misc feedback * Missing docstring * Few more simple polish improvements * Unify on MultiOrgAlertmanager. Discover bug in existing test * Fix rebase conflicts * Misc feedback, renames, docs * Access crypto hanging off MultiOrgAlertmanager rather than having a separate API to initialize --- pkg/services/ngalert/api/api.go | 2 +- pkg/services/ngalert/api/api_alertmanager.go | 196 +++--------------- .../ngalert/api/api_alertmanager_test.go | 5 +- .../api/tooling/definitions/alertmanager.go | 3 + pkg/services/ngalert/ngalert.go | 2 +- .../ngalert/notifier/alertmanager_config.go | 119 +++++++++++ pkg/services/ngalert/notifier/crypto.go | 118 +++++++++++ .../ngalert/notifier/multiorg_alertmanager.go | 7 +- .../notifier/multiorg_alertmanager_test.go | 6 +- .../ngalert/schedule/schedule_unit_test.go | 2 +- 10 files changed, 283 insertions(+), 177 deletions(-) create mode 100644 pkg/services/ngalert/notifier/alertmanager_config.go create mode 100644 pkg/services/ngalert/notifier/crypto.go diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index be3a4623a7a..0b6123b6603 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -91,7 +91,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { api.RegisterAlertmanagerApiEndpoints(NewForkedAM( api.DatasourceCache, NewLotexAM(proxy, logger), - &AlertmanagerSrv{store: api.AlertingStore, mam: api.MultiOrgAlertmanager, secrets: api.SecretsService, log: logger, ac: api.AccessControl}, + &AlertmanagerSrv{crypto: api.MultiOrgAlertmanager.Crypto, log: logger, ac: api.AccessControl, mam: api.MultiOrgAlertmanager}, ), m) // Register endpoints for proxying to Prometheus-compatible backends. api.RegisterPrometheusApiEndpoints(NewForkedProm( diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index c23cfc9985f..ee22a4145ab 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -2,7 +2,6 @@ package api import ( "context" - "encoding/base64" "errors" "fmt" "net/http" @@ -15,10 +14,8 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/store" - "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -29,11 +26,10 @@ const ( ) type AlertmanagerSrv struct { - mam *notifier.MultiOrgAlertmanager - secrets secrets.Service - store AlertingStore - log log.Logger - ac accesscontrol.AccessControl + log log.Logger + ac accesscontrol.AccessControl + mam *notifier.MultiOrgAlertmanager + crypto notifier.Crypto } type UnknownReceiverError struct { @@ -44,81 +40,6 @@ func (e UnknownReceiverError) Error() string { return fmt.Sprintf("unknown receiver: %s", e.UID) } -func (srv AlertmanagerSrv) loadSecureSettings(ctx context.Context, orgId int64, receivers []*apimodels.PostableApiReceiver) error { - // Get the last known working configuration - query := ngmodels.GetLatestAlertmanagerConfigurationQuery{OrgID: orgId} - if err := srv.store.GetLatestAlertmanagerConfiguration(ctx, &query); err != nil { - // If we don't have a configuration there's nothing for us to know and we should just continue saving the new one - if !errors.Is(err, store.ErrNoAlertmanagerConfiguration) { - return fmt.Errorf("failed to get latest configuration: %w", err) - } - } - - currentReceiverMap := make(map[string]*apimodels.PostableGrafanaReceiver) - if query.Result != nil { - currentConfig, err := notifier.Load([]byte(query.Result.AlertmanagerConfiguration)) - // If the current config is un-loadable, treat it as if it never existed. Providing a new, valid config should be able to "fix" this state. - if err != nil { - srv.log.Warn("Last known alertmanager configuration was invalid. Overwriting...") - } else { - currentReceiverMap = currentConfig.GetGrafanaReceiverMap() - } - } - - // Copy the previously known secure settings - for i, r := range receivers { - for j, gr := range r.PostableGrafanaReceivers.GrafanaManagedReceivers { - if gr.UID == "" { // new receiver - continue - } - - cgmr, ok := currentReceiverMap[gr.UID] - if !ok { - // it tries to update a receiver that didn't previously exist - return UnknownReceiverError{UID: gr.UID} - } - - // frontend sends only the secure settings that have to be updated - // therefore we have to copy from the last configuration only those secure settings not included in the request - for key := range cgmr.SecureSettings { - _, ok := gr.SecureSettings[key] - if !ok { - decryptedValue, err := srv.getDecryptedSecret(cgmr, key) - if err != nil { - return fmt.Errorf("failed to decrypt stored secure setting: %s: %w", key, err) - } - - if receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings == nil { - receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings = make(map[string]string, len(cgmr.SecureSettings)) - } - - receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings[key] = decryptedValue - } - } - } - } - return nil -} - -func (srv AlertmanagerSrv) getDecryptedSecret(r *apimodels.PostableGrafanaReceiver, key string) (string, error) { - storedValue, ok := r.SecureSettings[key] - if !ok { - return "", nil - } - - decodeValue, err := base64.StdEncoding.DecodeString(storedValue) - if err != nil { - return "", err - } - - decryptedValue, err := srv.secrets.Decrypt(context.Background(), decodeValue) - if err != nil { - return "", err - } - - return string(decryptedValue), nil -} - func (srv AlertmanagerSrv) RouteGetAMStatus(c *models.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgId) if errResp != nil { @@ -192,59 +113,14 @@ func (srv AlertmanagerSrv) RouteDeleteSilence(c *models.ReqContext) response.Res } func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response.Response { - query := ngmodels.GetLatestAlertmanagerConfigurationQuery{OrgID: c.OrgId} - if err := srv.store.GetLatestAlertmanagerConfiguration(c.Req.Context(), &query); err != nil { + config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.OrgId) + if err != nil { if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") } - return ErrResp(http.StatusInternalServerError, err, "failed to get latest configuration") + return ErrResp(http.StatusInternalServerError, err, err.Error()) } - - cfg, err := notifier.Load([]byte(query.Result.AlertmanagerConfiguration)) - if err != nil { - return ErrResp(http.StatusInternalServerError, err, "failed to unmarshal alertmanager configuration") - } - - result := apimodels.GettableUserConfig{ - TemplateFiles: cfg.TemplateFiles, - AlertmanagerConfig: apimodels.GettableApiAlertingConfig{ - Config: cfg.AlertmanagerConfig.Config, - }, - } - for _, recv := range cfg.AlertmanagerConfig.Receivers { - receivers := make([]*apimodels.GettableGrafanaReceiver, 0, len(recv.PostableGrafanaReceivers.GrafanaManagedReceivers)) - for _, pr := range recv.PostableGrafanaReceivers.GrafanaManagedReceivers { - secureFields := make(map[string]bool, len(pr.SecureSettings)) - for k := range pr.SecureSettings { - decryptedValue, err := srv.getDecryptedSecret(pr, k) - if err != nil { - return ErrResp(http.StatusInternalServerError, err, "failed to decrypt stored secure setting: %s", k) - } - if decryptedValue == "" { - continue - } - secureFields[k] = true - } - gr := apimodels.GettableGrafanaReceiver{ - UID: pr.UID, - Name: pr.Name, - Type: pr.Type, - DisableResolveMessage: pr.DisableResolveMessage, - Settings: pr.Settings, - SecureFields: secureFields, - } - receivers = append(receivers, &gr) - } - gettableApiReceiver := apimodels.GettableApiReceiver{ - GettableGrafanaReceivers: apimodels.GettableGrafanaReceivers{ - GrafanaManagedReceivers: receivers, - }, - } - gettableApiReceiver.Name = recv.Name - result.AlertmanagerConfig.Receivers = append(result.AlertmanagerConfig.Receivers, &gettableApiReceiver) - } - - return response.JSON(http.StatusOK, result) + return response.JSON(http.StatusOK, config) } func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *models.ReqContext) response.Response { @@ -334,41 +210,26 @@ func (srv AlertmanagerSrv) RouteGetSilences(c *models.ReqContext) response.Respo } func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body apimodels.PostableUserConfig) response.Response { - // Get the last known working configuration - query := ngmodels.GetLatestAlertmanagerConfigurationQuery{OrgID: c.OrgId} - if err := srv.store.GetLatestAlertmanagerConfiguration(c.Req.Context(), &query); err != nil { - // If we don't have a configuration there's nothing for us to know and we should just continue saving the new one - if !errors.Is(err, store.ErrNoAlertmanagerConfiguration) { - return ErrResp(http.StatusInternalServerError, err, "failed to get latest configuration") - } + err := srv.mam.ApplyAlertmanagerConfiguration(c.Req.Context(), c.OrgId, body) + if err == nil { + return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"}) + } + var unknownReceiverError notifier.UnknownReceiverError + if errors.As(err, &unknownReceiverError) { + return ErrResp(http.StatusBadRequest, unknownReceiverError, "") + } + var configRejectedError notifier.AlertmanagerConfigRejectedError + if errors.As(err, &configRejectedError) { + return ErrResp(http.StatusBadRequest, configRejectedError, "") + } + if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) { + return response.Error(http.StatusNotFound, err.Error(), err) + } + if errors.Is(err, notifier.ErrAlertmanagerNotReady) { + return response.Error(http.StatusConflict, err.Error(), err) } - if err := srv.loadSecureSettings(c.Req.Context(), c.OrgId, body.AlertmanagerConfig.Receivers); err != nil { - var unknownReceiverError UnknownReceiverError - if errors.As(err, &unknownReceiverError) { - return ErrResp(http.StatusBadRequest, err, "") - } - return ErrResp(http.StatusInternalServerError, err, "") - } - - if err := body.ProcessConfig(srv.secrets.Encrypt); err != nil { - return ErrResp(http.StatusInternalServerError, err, "failed to post process Alertmanager configuration") - } - - am, errResp := srv.AlertmanagerFor(c.OrgId) - if errResp != nil { - // It's okay if the alertmanager isn't ready yet, we're changing its config anyway. - if !errors.Is(errResp.Err(), notifier.ErrAlertmanagerNotReady) { - return errResp - } - } - - if err := am.SaveAndApplyConfig(c.Req.Context(), &body); err != nil { - srv.log.Error("unable to save and apply alertmanager configuration", "err", err) - return ErrResp(http.StatusBadRequest, err, "failed to save and apply Alertmanager configuration") - } - - return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"}) + return ErrResp(http.StatusInternalServerError, err, "") } func (srv AlertmanagerSrv) RoutePostAMAlerts(_ *models.ReqContext, _ apimodels.PostableAlerts) response.Response { @@ -376,7 +237,7 @@ func (srv AlertmanagerSrv) RoutePostAMAlerts(_ *models.ReqContext, _ apimodels.P } func (srv AlertmanagerSrv) RoutePostTestReceivers(c *models.ReqContext, body apimodels.TestReceiversConfigBodyParams) response.Response { - if err := srv.loadSecureSettings(c.Req.Context(), c.OrgId, body.Receivers); err != nil { + if err := srv.crypto.LoadSecureSettings(c.Req.Context(), c.OrgId, body.Receivers); err != nil { var unknownReceiverError UnknownReceiverError if errors.As(err, &unknownReceiverError) { return ErrResp(http.StatusBadRequest, err, "") @@ -384,7 +245,7 @@ func (srv AlertmanagerSrv) RoutePostTestReceivers(c *models.ReqContext, body api return ErrResp(http.StatusInternalServerError, err, "") } - if err := body.ProcessConfig(srv.secrets.Encrypt); err != nil { + if err := body.ProcessConfig(srv.crypto.Encrypt); err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to post process Alertmanager configuration") } @@ -514,7 +375,6 @@ func (srv AlertmanagerSrv) AlertmanagerFor(orgID int64) (Alertmanager, *response if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) { return nil, response.Error(http.StatusNotFound, err.Error(), err) } - if errors.Is(err, notifier.ErrAlertmanagerNotReady) { return am, response.Error(http.StatusConflict, err.Error(), err) } diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index 75ee2176024..eec2b513e00 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -357,7 +357,8 @@ func createSut(t *testing.T, accessControl accesscontrol.AccessControl) Alertman accessControl = acMock.New().WithDisabled() } log := log.NewNopLogger() - return AlertmanagerSrv{mam: mam, store: &configStore, secrets: secrets, ac: accessControl, log: log} + crypto := notifier.NewCrypto(secrets, &configStore, log) + return AlertmanagerSrv{mam: mam, crypto: crypto, ac: accessControl, log: log} } func createAmConfigRequest(t *testing.T) apimodels.PostableUserConfig { @@ -395,7 +396,7 @@ func createMultiOrgAlertmanager(t *testing.T) *notifier.MultiOrgAlertmanager { }, // do not poll in tests. } - mam, err := notifier.NewMultiOrgAlertmanager(cfg, &configStore, &orgStore, kvStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger")) + mam, err := notifier.NewMultiOrgAlertmanager(cfg, &configStore, &orgStore, kvStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService) require.NoError(t, err) err = mam.LoadAndSyncAlertmanagersForOrgs(context.Background()) require.NoError(t, err) diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 94eb137d7b7..ad75f5215b3 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -18,6 +18,7 @@ import ( "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/util" ) @@ -712,6 +713,8 @@ type Route struct { GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` RepeatInterval *model.Duration `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"` + + Provenance models.Provenance `yaml:"provenance,omitempty" json:"provenance,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for Route. This is a copy of alertmanager's upstream except it removes validation on the label key. diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 9d03ecfe9cd..49158bc52b5 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -99,7 +99,7 @@ func (ng *AlertNG) init() error { decryptFn := ng.SecretsService.GetDecryptedValue multiOrgMetrics := ng.Metrics.GetMultiOrgAlertmanagerMetrics() - ng.MultiOrgAlertmanager, err = notifier.NewMultiOrgAlertmanager(ng.Cfg, store, store, ng.KVStore, decryptFn, multiOrgMetrics, ng.NotificationService, log.New("ngalert.multiorg.alertmanager")) + ng.MultiOrgAlertmanager, err = notifier.NewMultiOrgAlertmanager(ng.Cfg, store, store, ng.KVStore, decryptFn, multiOrgMetrics, ng.NotificationService, log.New("ngalert.multiorg.alertmanager"), ng.SecretsService) if err != nil { return err } diff --git a/pkg/services/ngalert/notifier/alertmanager_config.go b/pkg/services/ngalert/notifier/alertmanager_config.go new file mode 100644 index 00000000000..7efd6561589 --- /dev/null +++ b/pkg/services/ngalert/notifier/alertmanager_config.go @@ -0,0 +1,119 @@ +package notifier + +import ( + "context" + "errors" + "fmt" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" +) + +type UnknownReceiverError struct { + UID string +} + +func (e UnknownReceiverError) Error() string { + return fmt.Sprintf("unknown receiver: %s", e.UID) +} + +type AlertmanagerConfigRejectedError struct { + Inner error +} + +func (e AlertmanagerConfigRejectedError) Error() string { + return fmt.Sprintf("failed to save and apply Alertmanager configuration: %s", e.Inner.Error()) +} + +type configurationStore interface { + GetLatestAlertmanagerConfiguration(ctx context.Context, query *models.GetLatestAlertmanagerConfigurationQuery) error +} + +func (moa *MultiOrgAlertmanager) GetAlertmanagerConfiguration(ctx context.Context, org int64) (definitions.GettableUserConfig, error) { + query := models.GetLatestAlertmanagerConfigurationQuery{OrgID: org} + err := moa.configStore.GetLatestAlertmanagerConfiguration(ctx, &query) + if err != nil { + return definitions.GettableUserConfig{}, fmt.Errorf("failed to get latest configuration: %w", err) + } + cfg, err := Load([]byte(query.Result.AlertmanagerConfiguration)) + if err != nil { + return definitions.GettableUserConfig{}, fmt.Errorf("failed to unmarshal alertmanager configuration: %w", err) + } + + result := definitions.GettableUserConfig{ + TemplateFiles: cfg.TemplateFiles, + AlertmanagerConfig: definitions.GettableApiAlertingConfig{ + Config: cfg.AlertmanagerConfig.Config, + }, + } + + for _, recv := range cfg.AlertmanagerConfig.Receivers { + receivers := make([]*definitions.GettableGrafanaReceiver, 0, len(recv.PostableGrafanaReceivers.GrafanaManagedReceivers)) + for _, pr := range recv.PostableGrafanaReceivers.GrafanaManagedReceivers { + secureFields := make(map[string]bool, len(pr.SecureSettings)) + for k := range pr.SecureSettings { + decryptedValue, err := moa.Crypto.getDecryptedSecret(pr, k) + if err != nil { + return definitions.GettableUserConfig{}, fmt.Errorf("failed to decrypt stored secure setting: %w", err) + } + if decryptedValue == "" { + continue + } + secureFields[k] = true + } + gr := definitions.GettableGrafanaReceiver{ + UID: pr.UID, + Name: pr.Name, + Type: pr.Type, + DisableResolveMessage: pr.DisableResolveMessage, + Settings: pr.Settings, + SecureFields: secureFields, + } + receivers = append(receivers, &gr) + } + gettableApiReceiver := definitions.GettableApiReceiver{ + GettableGrafanaReceivers: definitions.GettableGrafanaReceivers{ + GrafanaManagedReceivers: receivers, + }, + } + gettableApiReceiver.Name = recv.Name + result.AlertmanagerConfig.Receivers = append(result.AlertmanagerConfig.Receivers, &gettableApiReceiver) + } + + return result, nil +} + +func (moa *MultiOrgAlertmanager) ApplyAlertmanagerConfiguration(ctx context.Context, org int64, config definitions.PostableUserConfig) error { + // Get the last known working configuration + query := models.GetLatestAlertmanagerConfigurationQuery{OrgID: org} + if err := moa.configStore.GetLatestAlertmanagerConfiguration(ctx, &query); err != nil { + // If we don't have a configuration there's nothing for us to know and we should just continue saving the new one + if !errors.Is(err, store.ErrNoAlertmanagerConfiguration) { + return fmt.Errorf("failed to get latest configuration %w", err) + } + } + + if err := moa.Crypto.LoadSecureSettings(ctx, org, config.AlertmanagerConfig.Receivers); err != nil { + return err + } + + if err := config.ProcessConfig(moa.Crypto.Encrypt); err != nil { + return fmt.Errorf("failed to post process Alertmanager configuration: %w", err) + } + + am, err := moa.AlertmanagerFor(org) + if err != nil { + // It's okay if the alertmanager isn't ready yet, we're changing its config anyway. + if !errors.Is(err, ErrAlertmanagerNotReady) { + return err + } + } + + if err := am.SaveAndApplyConfig(ctx, &config); err != nil { + moa.logger.Error("unable to save and apply alertmanager configuration", "err", err) + return AlertmanagerConfigRejectedError{err} + } + + return nil +} diff --git a/pkg/services/ngalert/notifier/crypto.go b/pkg/services/ngalert/notifier/crypto.go new file mode 100644 index 00000000000..0d5e06a55f1 --- /dev/null +++ b/pkg/services/ngalert/notifier/crypto.go @@ -0,0 +1,118 @@ +package notifier + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/secrets" +) + +// Crypto allows decryption of Alertmanager Configuration and encryption of arbitrary payloads. +type Crypto interface { + LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error + Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error) + + getDecryptedSecret(r *definitions.PostableGrafanaReceiver, key string) (string, error) +} + +// alertmanagerCrypto implements decryption of Alertmanager configuration and encryption of arbitrary payloads based on Grafana's encryptions. +type alertmanagerCrypto struct { + secrets secrets.Service + configs configurationStore + log log.Logger +} + +func NewCrypto(secrets secrets.Service, configs configurationStore, log log.Logger) Crypto { + return &alertmanagerCrypto{ + secrets: secrets, + configs: configs, + log: log, + } +} + +// LoadSecureSettings adds the corresponding unencrypted secrets stored to the list of input receivers. +func (c *alertmanagerCrypto) LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error { + // Get the last known working configuration. + query := models.GetLatestAlertmanagerConfigurationQuery{OrgID: orgId} + if err := c.configs.GetLatestAlertmanagerConfiguration(ctx, &query); err != nil { + // If we don't have a configuration there's nothing for us to know and we should just continue saving the new one. + if !errors.Is(err, store.ErrNoAlertmanagerConfiguration) { + return fmt.Errorf("failed to get latest configuration: %w", err) + } + } + + currentReceiverMap := make(map[string]*definitions.PostableGrafanaReceiver) + if query.Result != nil { + currentConfig, err := Load([]byte(query.Result.AlertmanagerConfiguration)) + // If the current config is un-loadable, treat it as if it never existed. Providing a new, valid config should be able to "fix" this state. + if err != nil { + c.log.Warn("Last known alertmanager configuration was invalid. Overwriting...") + } else { + currentReceiverMap = currentConfig.GetGrafanaReceiverMap() + } + } + + // Copy the previously known secure settings. + for i, r := range receivers { + for j, gr := range r.PostableGrafanaReceivers.GrafanaManagedReceivers { + if gr.UID == "" { // new receiver + continue + } + + cgmr, ok := currentReceiverMap[gr.UID] + if !ok { + // It tries to update a receiver that didn't previously exist + return UnknownReceiverError{UID: gr.UID} + } + + // Frontend sends only the secure settings that have to be updated + // Therefore we have to copy from the last configuration only those secure settings not included in the request + for key := range cgmr.SecureSettings { + _, ok := gr.SecureSettings[key] + if !ok { + decryptedValue, err := c.getDecryptedSecret(cgmr, key) + if err != nil { + return fmt.Errorf("failed to decrypt stored secure setting: %s: %w", key, err) + } + + if receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings == nil { + receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings = make(map[string]string, len(cgmr.SecureSettings)) + } + + receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings[key] = decryptedValue + } + } + } + } + return nil +} + +func (c *alertmanagerCrypto) getDecryptedSecret(r *definitions.PostableGrafanaReceiver, key string) (string, error) { + storedValue, ok := r.SecureSettings[key] + if !ok { + return "", nil + } + + decodeValue, err := base64.StdEncoding.DecodeString(storedValue) + if err != nil { + return "", err + } + + decryptedValue, err := c.secrets.Decrypt(context.Background(), decodeValue) + if err != nil { + return "", err + } + + return string(decryptedValue), nil +} + +// Encrypt delegates encryption to secrets.Service. +func (c *alertmanagerCrypto) Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error) { + return c.secrets.Encrypt(ctx, payload, opt) +} diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager.go b/pkg/services/ngalert/notifier/multiorg_alertmanager.go index 3c8a2f1d987..d34677c3e44 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels" "github.com/grafana/grafana/pkg/services/notifications" + "github.com/grafana/grafana/pkg/services/secrets" "github.com/prometheus/alertmanager/cluster" "github.com/prometheus/client_golang/prometheus" @@ -29,6 +30,8 @@ var ( ) type MultiOrgAlertmanager struct { + Crypto Crypto + alertmanagersMtx sync.RWMutex alertmanagers map[int64]*Alertmanager @@ -51,9 +54,11 @@ type MultiOrgAlertmanager struct { func NewMultiOrgAlertmanager(cfg *setting.Cfg, configStore store.AlertingStore, orgStore store.OrgStore, kvStore kvstore.KVStore, decryptFn channels.GetDecryptedValueFn, m *metrics.MultiOrgAlertmanager, - ns notifications.Service, l log.Logger, + ns notifications.Service, l log.Logger, s secrets.Service, ) (*MultiOrgAlertmanager, error) { moa := &MultiOrgAlertmanager{ + Crypto: NewCrypto(s, configStore, l), + logger: l, settings: cfg, alertmanagers: map[int64]*Alertmanager{}, diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go index 6dd2f020be0..56899182532 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go @@ -44,7 +44,7 @@ func TestMultiOrgAlertmanager_SyncAlertmanagersForOrgs(t *testing.T) { DisabledOrgs: map[int64]struct{}{5: {}}, }, // do not poll in tests. } - mam, err := NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger")) + mam, err := NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService) require.NoError(t, err) ctx := context.Background() @@ -168,7 +168,7 @@ func TestMultiOrgAlertmanager_SyncAlertmanagersForOrgsWithFailures(t *testing.T) DefaultConfiguration: setting.GetAlertmanagerDefaultConfiguration(), }, // do not poll in tests. } - mam, err := NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger")) + mam, err := NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService) require.NoError(t, err) ctx := context.Background() @@ -218,7 +218,7 @@ func TestMultiOrgAlertmanager_AlertmanagerFor(t *testing.T) { decryptFn := secretsService.GetDecryptedValue reg := prometheus.NewPedanticRegistry() m := metrics.NewNGAlert(reg) - mam, err := NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger")) + mam, err := NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService) require.NoError(t, err) ctx := context.Background() diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 4824f05d95f..7f5f8da4cac 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -1015,7 +1015,7 @@ func setupScheduler(t *testing.T, rs store.RuleStore, is store.InstanceStore, ac m := metrics.NewNGAlert(registry) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - moa, err := notifier.NewMultiOrgAlertmanager(&setting.Cfg{}, ¬ifier.FakeConfigStore{}, ¬ifier.FakeOrgStore{}, ¬ifier.FakeKVStore{}, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger")) + moa, err := notifier.NewMultiOrgAlertmanager(&setting.Cfg{}, ¬ifier.FakeConfigStore{}, ¬ifier.FakeOrgStore{}, ¬ifier.FakeKVStore{}, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService) require.NoError(t, err) schedCfg := SchedulerCfg{ From 2a178bd73c67001e3a42989c4b35d44a16c4b3bb Mon Sep 17 00:00:00 2001 From: sam boyer Date: Thu, 14 Apr 2022 14:54:35 -0400 Subject: [PATCH 106/121] Introduce coremodels framework (extracted from intent-api) (#47653) * Copy over most of coremodel from intent-api branch * Fix import paths * Fix incorrect provider name * Add root compgen file, fixup componentroot.yaml * go mod tidy * Remove compgen for now * Add dashboard coremodel * Remove datasource coremodel, for now * Tweak comments on dashboard struct model * devenv: add dashboard testing directly * Fixup dashboard schema for openness, heatmap * Update Thema to tip * Fix wire/registry references * Fix hclog version --- devenv/dev-dashboards/dashboards.go | 6 + devenv/dev-dashboards/dashboards_test.go | 84 +++++ go.mod | 42 ++- go.sum | 84 +++-- pkg/coremodel/dashboard/coremodel.go | 40 ++ pkg/coremodel/dashboard/lineage.cue | 344 ++++++++++++++++++ pkg/coremodel/dashboard/schema.go | 89 +++++ pkg/cuectx/ctx.go | 116 ++++++ pkg/framework/coremodel/helpers.go | 1 + pkg/framework/coremodel/interface.go | 25 ++ pkg/framework/coremodel/registry.go | 78 ++++ .../coremodel/staticregistry/provide.go | 17 + pkg/server/wire.go | 8 + 13 files changed, 890 insertions(+), 44 deletions(-) create mode 100644 devenv/dev-dashboards/dashboards.go create mode 100644 devenv/dev-dashboards/dashboards_test.go create mode 100644 pkg/coremodel/dashboard/coremodel.go create mode 100644 pkg/coremodel/dashboard/lineage.cue create mode 100644 pkg/coremodel/dashboard/schema.go create mode 100644 pkg/cuectx/ctx.go create mode 100644 pkg/framework/coremodel/helpers.go create mode 100644 pkg/framework/coremodel/interface.go create mode 100644 pkg/framework/coremodel/registry.go create mode 100644 pkg/framework/coremodel/staticregistry/provide.go diff --git a/devenv/dev-dashboards/dashboards.go b/devenv/dev-dashboards/dashboards.go new file mode 100644 index 00000000000..4531431bc7a --- /dev/null +++ b/devenv/dev-dashboards/dashboards.go @@ -0,0 +1,6 @@ +package dev_dashboards + +import "embed" + +//go:embed *.json */*.json +var DevDashboardFS embed.FS diff --git a/devenv/dev-dashboards/dashboards_test.go b/devenv/dev-dashboards/dashboards_test.go new file mode 100644 index 00000000000..66f29040f84 --- /dev/null +++ b/devenv/dev-dashboards/dashboards_test.go @@ -0,0 +1,84 @@ +package dev_dashboards + +import ( + "encoding/json" + "io" + "io/fs" + "path/filepath" + "strings" + "testing" + + "cuelang.org/go/cue/errors" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/coremodel/dashboard" + "github.com/grafana/grafana/pkg/cuectx" +) + +func TestDevenvDashboardValidity(t *testing.T) { + m, err := themaTestableDashboards() + require.NoError(t, err) + cm, err := dashboard.ProvideCoremodel(cuectx.ProvideThemaLibrary()) + require.NoError(t, err) + + for path, b := range m { + t.Run(path, func(t *testing.T) { + // The path arg here only matters for error output + cv, err := cuectx.JSONtoCUE(path, b) + require.NoError(t, err, "error while decoding dashboard JSON into a CUE value") + + _, err = cm.CurrentSchema().Validate(cv) + if err != nil { + // Testify trims errors to short length. We want the full text + errstr := errors.Details(err, nil) + t.Log(errstr) + if strings.Contains(errstr, "null") { + t.Log("validation failure appears to involve nulls - see if scripts/stripnulls.sh has any effect?") + } + t.FailNow() + } + }) + } +} + +func themaTestableDashboards() (map[string][]byte, error) { + m := make(map[string][]byte) + in := DevDashboardFS + + err := fs.WalkDir(in, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() || filepath.Ext(d.Name()) != ".json" { + return nil + } + + // nolint:gosec + f, err := in.Open(path) + if err != nil { + return err + } + defer f.Close() // nolint: errcheck + + b, err := io.ReadAll(f) + if err != nil { + return err + } + + jtree := make(map[string]interface{}) + json.Unmarshal(b, &jtree) + if oldschemav, has := jtree["schemaVersion"]; !has || !(oldschemav.(float64) > 32) { + return nil + } + + m[path] = b + return nil + }) + + if err != nil { + return nil, err + } + + return m, nil +} diff --git a/go.mod b/go.mod index 28dc2616525..dee9c1e4cbd 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ replace github.com/russellhaering/goxmldsig@v1.1.0 => github.com/russellhaering/ require ( cloud.google.com/go/storage v1.18.2 - cuelang.org/go v0.4.0 + cuelang.org/go v0.4.1 github.com/Azure/azure-sdk-for-go v59.3.0+incompatible github.com/Azure/go-autorest/autorest v0.11.22 github.com/BurntSushi/toml v0.3.1 @@ -31,7 +31,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/denisenkom/go-mssqldb v0.11.0 github.com/dop251/goja v0.0.0-20210804101310-32956a348b49 - github.com/fatih/color v1.10.0 + github.com/fatih/color v1.13.0 github.com/gchaincl/sqlhooks v1.3.0 github.com/getsentry/sentry-go v0.10.0 github.com/go-kit/kit v0.11.0 // indirect @@ -56,7 +56,7 @@ require ( github.com/grafana/grafana-plugin-sdk-go v0.131.0 github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 - github.com/hashicorp/go-hclog v0.16.1 + github.com/hashicorp/go-hclog v1.0.0 github.com/hashicorp/go-plugin v1.4.3 github.com/hashicorp/go-version v1.3.0 github.com/influxdata/influxdb-client-go/v2 v2.6.0 @@ -69,7 +69,7 @@ require ( github.com/linkedin/goavro/v2 v2.10.0 github.com/m3db/prometheus_remote_client_golang v0.4.4 github.com/magefile/mage v1.12.1 - github.com/mattn/go-isatty v0.0.12 + github.com/mattn/go-isatty v0.0.14 github.com/mattn/go-sqlite3 v1.14.7 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f @@ -110,11 +110,11 @@ require ( golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 golang.org/x/tools v0.1.9 gonum.org/v1/gonum v0.11.0 - google.golang.org/api v0.60.0 + google.golang.org/api v0.62.0 google.golang.org/grpc v1.42.0 google.golang.org/protobuf v1.27.1 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/ini.v1 v1.62.0 + gopkg.in/ini.v1 v1.66.2 gopkg.in/ldap.v3 v3.1.0 gopkg.in/mail.v2 v2.3.1 gopkg.in/square/go-jose.v2 v2.5.1 @@ -126,7 +126,6 @@ require ( ) require ( - cloud.google.com/go v0.97.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect @@ -141,7 +140,6 @@ require ( github.com/alecthomas/units v0.0.0-20210912230133-d1bdfacee922 // indirect github.com/andybalholm/brotli v1.0.3 github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect - github.com/armon/go-metrics v0.3.8 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect @@ -151,7 +149,6 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cheekybits/genny v1.0.0 // indirect github.com/cockroachdb/apd/v2 v2.0.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/deepmap/oapi-codegen v1.8.2 // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -187,12 +184,10 @@ require ( github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-immutable-radix v1.3.0 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/go-multierror v1.1.0 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.4 - github.com/hashicorp/memberlist v0.2.4 // indirect github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493 // indirect github.com/igm/sockjs-go/v3 v3.0.1 // indirect github.com/jessevdk/go-flags v1.5.0 // indirect @@ -202,11 +197,9 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect - github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/miekg/dns v1.1.43 // indirect github.com/mitchellh/go-testing-interface v1.14.0 // indirect - github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/mna/redisc v1.3.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -224,13 +217,11 @@ require ( github.com/protocolbuffers/txtpbfmt v0.0.0-20201118171849-f6a6b3f636fc // indirect github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect github.com/rs/cors v1.8.0 // indirect - github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/segmentio/encoding v0.3.2 github.com/sercand/kuberesolver v2.4.0+incompatible // indirect github.com/sergi/go-diff v1.0.0 // indirect github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/stretchr/objx v0.2.0 // indirect @@ -246,7 +237,7 @@ require ( golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 + google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect ) @@ -257,9 +248,22 @@ require ( github.com/Azure/go-autorest/autorest/adal v0.9.17 github.com/golang-migrate/migrate/v4 v4.7.0 github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f + github.com/grafana/thema v0.0.0-20220413232647-fc54c169b508 gocloud.dev v0.24.0 ) +require ( + cloud.google.com/go v0.99.0 // indirect + github.com/armon/go-metrics v0.3.10 // indirect + github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/memberlist v0.3.0 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect +) + require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 // indirect @@ -267,7 +271,6 @@ require ( github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect - github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 // indirect github.com/containerd/containerd v1.5.9 // indirect github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac // indirect github.com/envoyproxy/go-control-plane v0.10.1 // indirect @@ -292,6 +295,11 @@ replace github.com/apache/thrift => github.com/apache/thrift v0.14.1 replace github.com/hashicorp/consul => github.com/hashicorp/consul v1.10.2 +// Thema's thema CLI requires cobra, which eventually works its way down to go-hclog@v1.0.0. +// Upgrading affects backend plugins: https://github.com/grafana/grafana/pull/47653#discussion_r850508593 +// No harm to Thema because it's only a dependency in its main package. +replace github.com/hashicorp/go-hclog => github.com/hashicorp/go-hclog v0.16.1 + // TODO: remove once gocloud.dev releases 0.25.x // `fileblob` implementation has buggy key ordering in 0.24.0 replace gocloud.dev v0.24.0 => github.com/google/go-cloud v0.24.1-0.20220209172924-99801bbb523a diff --git a/go.sum b/go.sum index 8ffd434c46a..8756dd4d304 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,10 @@ cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWc cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0 h1:3DXvAyifywvq64LfkKaMOmkWPS1CikIQdMe2lY9vxU8= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -71,8 +73,9 @@ contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeS contrib.go.opencensus.io/exporter/prometheus v0.3.0/go.mod h1:rpCPVQKhiyH8oomWgm34ZmgIdZa8OVYO5WAIygPbBBE= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= -cuelang.org/go v0.4.0 h1:GLJblw6m2WGGCA3k1v6Wbk9gTOt2qto48ahO2MmSd6I= cuelang.org/go v0.4.0/go.mod h1:tz/edkPi+T37AZcb5GlPY+WJkL6KiDlDVupKwL3vvjs= +cuelang.org/go v0.4.1 h1:rxG2cyZzdAymrNICI0L/wagK2EitoR8Xsiiq2D7qkaQ= +cuelang.org/go v0.4.1/go.mod h1:P09/R4UfAEzLkV9DXxwlxQnIZbkaT4uIhiEgs6Vsz2Q= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= @@ -327,8 +330,9 @@ github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQh github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-metrics v0.3.6/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-metrics v0.3.8 h1:oOxq3KPj0WhCuy50EhzwiyMyG2ovRQZpZLXQuOh2a/M= github.com/armon/go-metrics v0.3.8/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= @@ -519,8 +523,9 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 h1:KwaoQzs/WeUxxJqiJsZ4euOly1Az/IgZXXSxlD/UBNk= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v2 v2.0.1/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= @@ -662,8 +667,9 @@ github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFl github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -832,8 +838,8 @@ github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structtag v1.1.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= @@ -1290,6 +1296,7 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210827144239-02619b876842/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/tcpproxy v0.0.0-20180808230851-dfa16c61dad2/go.mod h1:DavVbd41y+b7ukKDmlnPR4nGYmkWXR6vHUkjQNiHPBs= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -1379,6 +1386,8 @@ github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa/go.mod h1:0O8o/juxN github.com/grafana/saml v0.0.0-20211007135653-aed1b2edd86b h1:YiSGp34F4V0G08HHx1cJBf2GVgwYAkXQjzuVs1t8jYk= github.com/grafana/saml v0.0.0-20211007135653-aed1b2edd86b/go.mod h1:q83kyQoMD0vhy+RzFLlbw0UgHJ6TAihQpuXvdFmm4s4= github.com/grafana/sqlds/v2 v2.3.2/go.mod h1:34uyqPBWsEvg4V/xxh6V4uIqwu1qLfOfsmScll/ukrk= +github.com/grafana/thema v0.0.0-20220413232647-fc54c169b508 h1:6k0scTj6kRDjn/qLigsLc9OTiMMaWkRUxjKBFowWEWg= +github.com/grafana/thema v0.0.0-20220413232647-fc54c169b508/go.mod h1:KuqTKX9lfM87uu9vt9DS/q+REqSrAm2xYMnBBvlmevA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -1417,8 +1426,9 @@ github.com/hashicorp/consul/api v1.7.0/go.mod h1:1NSuaUUkFaJzMasbfq/11wKYWSR67Xn github.com/hashicorp/consul/api v1.8.1/go.mod h1:sDjTOq0yUyv5G4h+BqSea7Fn6BU+XbolEz1952UB+mk= github.com/hashicorp/consul/api v1.9.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/api v1.10.0/go.mod h1:sDjTOq0yUyv5G4h+BqSea7Fn6BU+XbolEz1952UB+mk= -github.com/hashicorp/consul/api v1.10.1 h1:MwZJp86nlnL+6+W1Zly4JUuVn9YHhMggBirMpHGD7kw= github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/api v1.11.0 h1:Hw/G8TtRvOElqxVIhBzXciiSTbapq8hZ2XKZsXk5ZCE= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= @@ -1432,24 +1442,19 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-bexpr v0.1.2/go.mod h1:ANbpTX1oAql27TZkKVeW8p1w8NTdnyzPe/0qqPCKohU= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-connlimit v0.3.0/go.mod h1:OUj9FGL1tPIhl/2RCfzYHrIiWj+VVPGNyVPnUX8AqS0= github.com/hashicorp/go-discover v0.0.0-20200501174627-ad1e96bde088/go.mod h1:vZu6Opqf49xX5lsFAu7iFNewkcVF1sn/wyapZh5ytlg= -github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= -github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.12.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.1 h1:IVQwpTGNRRIHafnTs2dQLIk4ENtneRIEEJWOVDqz99o= github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.2.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE= github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-memdb v1.3.1/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -1505,8 +1510,9 @@ github.com/hashicorp/memberlist v0.1.5/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/memberlist v0.2.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.2.3/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/memberlist v0.2.4 h1:OOhYzSvFnkFQXm1ysE8RjXTHsqSRDyP4emusC9K7DYg= github.com/hashicorp/memberlist v0.2.4/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/net-rpc-msgpackrpc v0.0.0-20151116020338-a14192a58a69/go.mod h1:/z+jUGRBlwVpUZfjute9jWaF6/HuhjuFQuL1YXzVD1Q= github.com/hashicorp/raft v1.1.1/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= github.com/hashicorp/raft v1.2.0/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= @@ -1519,8 +1525,9 @@ github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc github.com/hashicorp/serf v0.8.5/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k= github.com/hashicorp/serf v0.9.0/go.mod h1:YL0HO+FifKOW2u1ke99DGVu1zhcpZzNwrLIqBC7vbYU= github.com/hashicorp/serf v0.9.3/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk= github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= @@ -1805,8 +1812,10 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.0-20191113090002-7c0f6868bffe/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= @@ -1818,8 +1827,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -1896,8 +1906,9 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1-0.20210112042008-8ebf2d61a8b4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/pointerstructure v1.0.0/go.mod h1:k4XwG94++jLVsSiTxo7qdIfXA9pj9EAeo0QsNNJOLZ8= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -2075,6 +2086,7 @@ github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUr github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= @@ -2277,8 +2289,9 @@ github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5se github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -2286,6 +2299,7 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20200218184317-f459e2d13664/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/samuel/go-zookeeper v0.0.0-20190810000440-0ceca61e4d75/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= @@ -2328,7 +2342,6 @@ github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20180825020608-02ddb050ef6b/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= @@ -2374,6 +2387,7 @@ github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -2381,6 +2395,7 @@ github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -2393,6 +2408,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/streadway/amqp v0.0.0-20180528204448-e5adc2ada8b8/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY= @@ -2594,9 +2610,12 @@ go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3C go.etcd.io/etcd v3.3.25+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= go.etcd.io/etcd/client/v3 v3.5.0-alpha.0.0.20210225194612-fa82d11a958a/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= @@ -2739,6 +2758,7 @@ golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= @@ -3086,6 +3106,7 @@ golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -3093,6 +3114,7 @@ golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -3293,8 +3315,10 @@ google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqiv google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= -google.golang.org/api v0.60.0 h1:eq/zs5WPH4J9undYM9IP1O7dSr7Yh8Y0GtSCpzGzIUk= google.golang.org/api v0.60.0/go.mod h1:d7rl65NZAkEQ90JFzqBjcRq1TVeG5ZoGV3sSpEnnVb4= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0 h1:PhGymJMXfGBzc4lBRmrx9+1w4w2wEzURHNGF/sD/xGc= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -3396,8 +3420,12 @@ google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211019152133-63b7e35f4404/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 h1:b9mVrqYfq3P4bCdaLg1qtBnPzUYgglsIdjZkL/fQVOE= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -3437,6 +3465,7 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= @@ -3491,8 +3520,9 @@ gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= diff --git a/pkg/coremodel/dashboard/coremodel.go b/pkg/coremodel/dashboard/coremodel.go new file mode 100644 index 00000000000..ed36c36dc7b --- /dev/null +++ b/pkg/coremodel/dashboard/coremodel.go @@ -0,0 +1,40 @@ +package dashboard + +import ( + "github.com/grafana/thema" +) + +// Coremodel contains the foundational schema declaration for dashboards. +type Coremodel struct { + lin thema.Lineage +} + +// Lineage returns the canonical dashboard Lineage. +func (c *Coremodel) Lineage() thema.Lineage { + return c.lin +} + +func (c *Coremodel) CurrentSchema() thema.Schema { + sch, err := c.lin.Schema(currentVersion) + if err != nil { + // Only reachable if our own schema currentVersion does not exist, which + // can really only happen transitionally during development + panic(err) + } + return sch +} + +func (c *Coremodel) GoType() interface{} { + return &model{} +} + +func ProvideCoremodel(lib thema.Library) (*Coremodel, error) { + lin, err := Lineage(lib) + if err != nil { + return nil, err + } + + return &Coremodel{ + lin: lin, + }, nil +} diff --git a/pkg/coremodel/dashboard/lineage.cue b/pkg/coremodel/dashboard/lineage.cue new file mode 100644 index 00000000000..1910539d5a6 --- /dev/null +++ b/pkg/coremodel/dashboard/lineage.cue @@ -0,0 +1,344 @@ +package dashboard + +import "github.com/grafana/thema" + +thema.#Lineage +name: "dashboard" +seqs: [ + { + schemas: [ + { // 0.0 + // Unique numeric identifier for the dashboard. + // TODO must isolate or remove identifiers local to a Grafana instance...? + id?: int64 + // Unique dashboard identifier that can be generated by anyone. string (8-40) + uid?: string + // Title of dashboard. + title?: string + // Description of dashboard. + description?: string + + gnetId?: string + // Tags associated with dashboard. + tags?: [...string] + // Theme of dashboard. + style: *"light" | "dark" + // Timezone of dashboard, + timezone?: *"browser" | "utc" | "" + // Whether a dashboard is editable or not. + editable: bool | *true + // 0 for no shared crosshair or tooltip (default). + // 1 for shared crosshair. + // 2 for shared crosshair AND shared tooltip. + graphTooltip: uint8 & >=0 & <=2 | *0 + // Time range for dashboard, e.g. last 6 hours, last 7 days, etc + time?: { + from: string | *"now-6h" + to: string | *"now" + } + // Timepicker metadata. + timepicker?: { + // Whether timepicker is collapsed or not. + collapse: bool | *false + // Whether timepicker is enabled or not. + enable: bool | *true + // Whether timepicker is visible or not. + hidden: bool | *false + // Selectable intervals for auto-refresh. + refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } + // Templating. + templating?: list: [...{...}] + // Annotations. + annotations?: list: [...{ + builtIn: uint8 | *0 + // Datasource to use for annotation. + datasource: string + // Whether annotation is enabled. + enable: bool | *true + // Whether to hide annotation. + hide?: bool | *false + // Annotation icon color. + iconColor?: string + // Name of annotation. + name?: string + type: string | *"dashboard" + // Query for annotation data. + rawQuery?: string + showIn: uint8 | *0 + }] + // Auto-refresh interval. + refresh?: string | false + // Version of the JSON schema, incremented each time a Grafana update brings + // changes to said schema. + // FIXME this is the old schema numbering system, and will be replaced by Thema's themaVersion + schemaVersion: uint16 | *33 + // Version of the dashboard, incremented each time the dashboard is updated. + version?: uint32 + panels?: [...(#Panel | #GraphPanel | #HeatmapPanel | #RowPanel)] + + // TODO docs + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum") + + // TODO docs + #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") + + // TODO docs + #FieldColor: { + // The main color scheme mode + mode: #FieldColorModeId | string + // Stores the fixed color value if mode is fixed + fixedColor?: string + // Some visualizations need to know how to assign a series color from by value color schemes + seriesBy?: #FieldColorSeriesByMode + } @cuetsy(kind="interface") + + // TODO docs + #Threshold: { + // TODO docs + // FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON + value?: number + // TODO docs + color: string + // TODO docs + // TODO are the values here enumerable into a disjunction? + // Some seem to be listed in typescript comment + state?: string + } @cuetsy(kind="interface") + + #ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum") + + #ThresholdsConfig: { + mode: #ThresholdsMode + + // Must be sorted by 'value', first value is always -Infinity + steps: [...#Threshold] + } @cuetsy(kind="interface") + + // TODO docs + // FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it + #Transformation: { + id: string + options: {...} + } + + // Schema for panel targets is specified by datasource + // plugins. We use a placeholder definition, which the Go + // schema loader either left open/as-is with the Base + // variant of the Dashboard and Panel families, or filled + // with types derived from plugins in the Instance variant. + // When working directly from CUE, importers can extend this + // type directly to achieve the same effect. + #Target: {...} + + // Dashboard panels. Panels are canonically defined inline + // because they share a version timeline with the dashboard + // schema; they do not evolve independently. + #Panel: { + // The panel plugin type id. + type: !="" + + // TODO docs + id?: uint32 + + // FIXME this almost certainly has to be changed in favor of scuemata versions + pluginVersion?: string + + // TODO docs + tags?: [...string] + + // Internal - the exact major and minor versions of the panel plugin + // schema. Hidden and therefore not a part of the data model, but + // expected to be filled with panel plugin schema versions so that it's + // possible to figure out which schema version matched on a successful + // unification. + // _pv: { maj: int, min: int } + // The major and minor versions of the panel plugin for this schema. + // TODO 2-tuple list instead of struct? + // panelSchema?: { maj: number, min: number } + panelSchema?: [number, number] + + // TODO docs + targets?: [...#Target] + + // Panel title. + title?: string + // Description. + description?: string + // Whether to display the panel without a background. + transparent: bool | *false + // The datasource used in all targets. + datasource?: { + type?: string + uid?: string + } + // Grid position. + gridPos?: { + // Panel + h: uint32 & >0 | *9 + // Panel + w: uint32 & >0 & <=24 | *12 + // Panel x + x: uint32 & >=0 & <24 | *0 + // Panel y + y: uint32 & >=0 | *0 + // true if fixed + static?: bool + } + // Panel links. + // FIXME this is temporarily specified as a closed list so + // that validation will pass when no links are present, but + // to force a failure as soon as it's checked against there + // being anything in the list so it can be fixed in + // accordance with that object + links?: [] + + // Name of template variable to repeat for. + repeat?: string + // Direction to repeat in if 'repeat' is set. + // "h" for horizontal, "v" for vertical. + repeatDirection: *"h" | "v" + + // TODO docs + maxDataPoints?: number + + // TODO docs + thresholds?: [...] + + // TODO docs + timeRegions?: [...] + + transformations: [...#Transformation] + + // TODO docs + // TODO tighter constraint + interval?: string + + // TODO docs + // TODO tighter constraint + timeFrom?: string + + // TODO docs + // TODO tighter constraint + timeShift?: string + + // options is specified by the PanelOptions field in panel + // plugin schemas. + options: {...} + + fieldConfig: { + defaults: { + // The display value for this field. This supports template variables blank is auto + displayName?: string + + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + displayNameFromDS?: string + + // Human readable field metadata + description?: string + + // An explict path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + path?: string + + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: bool + + // True if data source field supports ad-hoc filters + filterable?: bool + + // Numeric Options + unit?: string + + // Significant digits (for display) + decimals?: number + + min?: number + max?: number + + // Convert input values into a display string + // + // TODO this one corresponds to a complex type with + // generics on the typescript side. Ouch. Will + // either need special care, or we'll just need to + // accept a very loosely specified schema. It's very + // unlikely we'll be able to translate cue to + // typescript generics in the general case, though + // this particular one *may* be able to work. + mappings?: [...{...}] + + // Map numeric values to states + thresholds?: #ThresholdsConfig + + // // Map values to a display color + color?: #FieldColor + + // // Used when reducing field values + // nullValueMode?: NullValueMode + + // // The behavior when clicking on a result + links?: [...] + + // Alternative to empty string + noValue?: string + + // custom is specified by the PanelFieldConfig field + // in panel plugin schemas. + custom?: {...} + } + overrides: [...{ + matcher: { + id: string | *"" + options?: _ + } + properties: [...{ + id: string | *"" + value?: _ + }] + }] + } + } + + // Row panel + #RowPanel: { + type: "row" + collapsed: bool | *false + title?: string + + // Name of default datasource. + datasource?: string + + gridPos?: { + // Panel + h: uint32 & >0 | *9 + // Panel + w: uint32 & >0 & <=24 | *12 + // Panel x + x: uint32 & >=0 & <24 | *0 + // Panel y + y: uint32 & >=0 | *0 + // true if fixed + static?: bool + } + id: uint32 + panels: [...(#Panel | #GraphPanel | #HeatmapPanel)] + // Name of template variable to repeat for. + repeat?: string + } + // Support for legacy graph and heatmap panels. + #GraphPanel: { + ... + type: "graph" + } + #HeatmapPanel: { + ... + type: "heatmap" + } + } + ] + } +] diff --git a/pkg/coremodel/dashboard/schema.go b/pkg/coremodel/dashboard/schema.go new file mode 100644 index 00000000000..96c235a1712 --- /dev/null +++ b/pkg/coremodel/dashboard/schema.go @@ -0,0 +1,89 @@ +package dashboard + +import ( + "embed" + "path/filepath" + + "github.com/grafana/thema" + + "github.com/grafana/grafana/pkg/cuectx" +) + +var ( + //go:embed lineage.cue + cueFS embed.FS + + // TODO: this should be generated by Thema. + currentVersion = thema.SV(0, 0) +) + +// Lineage returns the Thema lineage representing Grafana dashboards. The +// lineage is the canonical specification of the current datasource schema, all +// prior schema versions, and the mappings that allow migration between schema +// versions. +// +// This is the base variant of the schema, which does not include any composed +// plugin schemas. +func Lineage(lib thema.Library, opts ...thema.BindOption) (thema.Lineage, error) { + return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "dashboard"), cueFS, lib, opts...) +} + +// Model is a dummy struct stand-in for dashboards. +// +// It exists solely to trick compgen into accepting the dashboard coremodel as valid. +type Model struct{} + +// model is a hacky Go struct representing a dashboard. +// +// This exists solely because the coremodel framework enforces that there is a Go struct to which +// all valid Thema schema instances can be assigned, per Thema's assignability checker. See +// https://github.com/grafana/thema/blob/main/docs/invariants.md#go-assignability for rules. +// +// DO NOT RELY ON THIS FOR ANYTHING REAL. It is unclear whether we will ever attempt to have a correct, complete +// Go struct representation of dashboards, let alone compress it into a single struct. +type model struct { + Title string `json:"title"` + Description string `json:"description"` + GnetId string `json:"gnetId"` + Tags []string `json:"tags"` + Style string `json:"style"` + Timezone string `json:"timezone"` + Editable bool `json:"editable"` + GraphTooltip uint8 `json:"graphTooltip"` + Time struct { + From string `json:"from"` + To string `json:"to"` + } `json:"time"` + Timepicker struct { + Collapse bool `json:"collapse"` + Enable bool `json:"enable"` + Hidden bool `json:"hidden"` + RefreshIntervals []string `json:"refresh_intervals"` + } `json:"timepicker"` + Templating struct { + List []interface{} `json:"list"` + } `json:"templating"` + Annotations struct { + List []struct { + Name string `json:"name"` + Type string `json:"type"` + BuiltIn uint8 `json:"builtIn"` + Datasource string `json:"datasource"` + Enable bool `json:"enable"` + Hide bool `json:"hide,omitempty"` + IconColor string `json:"iconColor"` + RawQuery string `json:"rawQuery,omitempty"` + ShowIn int `json:"showIn"` + } `json:"list"` + } `json:"annotations"` + Refresh interface{} `json:"refresh"` // (bool|string) + SchemaVersion int `json:"schemaVersion"` + Panels []interface{} `json:"panels"` + + //// + + Uid string `json:"uid"` + // OrgId int64 `json:"orgId"` + Id int64 `json:"id,omitempty"` + Version int `json:"version"` +} diff --git a/pkg/cuectx/ctx.go b/pkg/cuectx/ctx.go new file mode 100644 index 00000000000..bddbe1ad1a0 --- /dev/null +++ b/pkg/cuectx/ctx.go @@ -0,0 +1,116 @@ +// Package cuectx provides a single, central CUE context (runtime) and Thema +// library that can be used uniformly across Grafana, and related helper +// functions for loading Thema lineages. + +package cuectx + +import ( + "io" + "io/fs" + "path/filepath" + "testing/fstest" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "github.com/grafana/thema" + "github.com/grafana/thema/kernel" + "github.com/grafana/thema/load" +) + +var ctx *cue.Context = cuecontext.New() +var lib thema.Library = thema.NewLibrary(ctx) + +// ProvideCUEContext is a wire service provider of a central cue.Context. +func ProvideCUEContext() *cue.Context { + return ctx +} + +// ProvideThemaLibrary is a wire service provider of a central thema.Library. +func ProvideThemaLibrary() thema.Library { + return lib +} + +// JSONtoCUE attempts to decode the given []byte into a cue.Value, relying on +// the central Grafana cue.Context provided in this package. +// +// The provided path argument determines the name given to the input bytes if +// later CUE operations (e.g. Thema validation) produce errors related to the +// returned cue.Value. +// +// This is a convenience function for one-off JSON decoding. It's wasteful to +// call it repeatedly. Most use cases use cases should probably prefer making +// their own Thema/CUE decoders. +func JSONtoCUE(path string, b []byte) (cue.Value, error) { + return kernel.NewJSONDecoder(path)(ctx, b) +} + +// LoadGrafanaInstancesWithThema loads CUE files containing a lineage +// representing some Grafana core model schema. It is expected to be used when +// implementing a thema.LineageFactory. +// +// This function primarily juggles paths to make CUE's loader happy. Provide the +// path from the grafana root to the directory containing the lineage.cue. The +// lineage.cue file must be the sole contents of the provided fs.FS. +// +// More details on underlying behavior can be found in the docs for github.com/grafana/thema/load.InstancesWithThema. +func LoadGrafanaInstancesWithThema( + path string, + cueFS fs.FS, + lib thema.Library, + opts ...thema.BindOption, +) (thema.Lineage, error) { + prefix := filepath.FromSlash(path) + fs, err := prefixWithGrafanaCUE(prefix, cueFS) + if err != nil { + return nil, err + } + inst, err := load.InstancesWithThema(fs, prefix) + + // Need to trick loading by creating the embedded file and + // making it look like a module in the root dir. + if err != nil { + return nil, err + } + + val := lib.Context().BuildInstance(inst) + + lin, err := thema.BindLineage(val, lib, opts...) + if err != nil { + return nil, err + } + + return lin, nil +} + +func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) { + m := fstest.MapFS{ + filepath.Join("cue.mod", "module.cue"): &fstest.MapFile{Data: []byte(`module: "github.com/grafana/grafana"`)}, + } + + prefix = filepath.FromSlash(prefix) + err := fs.WalkDir(inputfs, ".", (func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + f, err := inputfs.Open(path) + if err != nil { + return err + } + defer f.Close() // nolint: errcheck + + b, err := io.ReadAll(f) + if err != nil { + return err + } + + m[filepath.Join(prefix, path)] = &fstest.MapFile{Data: b} + return nil + })) + + return m, err +} diff --git a/pkg/framework/coremodel/helpers.go b/pkg/framework/coremodel/helpers.go new file mode 100644 index 00000000000..d3f88a856d5 --- /dev/null +++ b/pkg/framework/coremodel/helpers.go @@ -0,0 +1 @@ +package coremodel diff --git a/pkg/framework/coremodel/interface.go b/pkg/framework/coremodel/interface.go new file mode 100644 index 00000000000..07957dd3b44 --- /dev/null +++ b/pkg/framework/coremodel/interface.go @@ -0,0 +1,25 @@ +package coremodel + +import ( + "github.com/grafana/thema" +) + +// Interface is the primary coremodel interface that must be implemented by all +// Grafana coremodels. A coremodel is the foundational, canonical schema for +// some known-at-compile-time Grafana object. +// +// Currently, all Coremodels are expressed as Thema lineages. +type Interface interface { + // Lineage should return the canonical Thema lineage for the coremodel. + Lineage() thema.Lineage + + // CurrentSchema should return the schema of the version that the Grafana backend + // is currently written against. (While Grafana can accept data from all + // older versions of the Thema schema, backend Go code is written against a + // single version for simplicity) + CurrentSchema() thema.Schema + + // GoType should return a pointer to the Go struct type that corresponds to + // the Current() schema. + GoType() interface{} +} diff --git a/pkg/framework/coremodel/registry.go b/pkg/framework/coremodel/registry.go new file mode 100644 index 00000000000..0fe44809d2f --- /dev/null +++ b/pkg/framework/coremodel/registry.go @@ -0,0 +1,78 @@ +package coremodel + +import ( + "errors" + "fmt" + "sync" + + "github.com/grafana/thema" +) + +var ( + // ErrModelAlreadyRegistered is returned when trying to register duplicate model to Registry. + ErrModelAlreadyRegistered = errors.New("error registering duplicate model") +) + +// Registry is a registry of coremodel instances. +type Registry struct { + lock sync.RWMutex + models []Interface + modelIdx map[string]Interface +} + +// NewRegistry returns a new Registry with the provided coremodel instances. +func NewRegistry(models ...Interface) (*Registry, error) { + r := &Registry{ + models: make([]Interface, 0, len(models)), + modelIdx: make(map[string]Interface, len(models)), + } + + if err := r.addModels(models); err != nil { + return nil, err + } + + return r, nil +} + +// Register adds coremodels to the Registry. +func (r *Registry) Register(models ...Interface) error { + return r.addModels(models) +} + +// List returns all coremodels registered in this Registry. +func (r *Registry) List() []Interface { + r.lock.RLock() + defer r.lock.RUnlock() + + return r.models +} + +func (r *Registry) addModels(models []Interface) error { + r.lock.Lock() + defer r.lock.Unlock() + + // Update model index and return an error if trying to register a duplicate. + for _, m := range models { + k := m.Lineage().Name() + + // Ensure assignability first. TODO will this blow up for dashboards? + if err := thema.AssignableTo(m.CurrentSchema(), m.GoType()); err != nil { + return fmt.Errorf("%s schema version %v not assignable to provided Go type: %w", k, m.CurrentSchema().Version(), err) + } + + if _, ok := r.modelIdx[k]; ok { + return ErrModelAlreadyRegistered + } + + r.modelIdx[k] = m + } + + // Remake model list. + // TODO: this can be more performant (proper resizing, maybe single loop with index building, etc.). + r.models = r.models[:0] + for _, m := range r.modelIdx { + r.models = append(r.models, m) + } + + return nil +} diff --git a/pkg/framework/coremodel/staticregistry/provide.go b/pkg/framework/coremodel/staticregistry/provide.go new file mode 100644 index 00000000000..09a12bd3c77 --- /dev/null +++ b/pkg/framework/coremodel/staticregistry/provide.go @@ -0,0 +1,17 @@ +package staticregistry + +import ( + "github.com/grafana/grafana/pkg/coremodel/dashboard" + "github.com/grafana/grafana/pkg/framework/coremodel" +) + +// ProvideRegistry provides a simple static Registry for coremodels. +// Coremodels have to be manually added. +// TODO dynamism +func ProvideRegistry( + dashboard *dashboard.Coremodel, +) (*coremodel.Registry, error) { + return coremodel.NewRegistry( + dashboard, + ) +} diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 74418497d83..7c22a7dc356 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -7,11 +7,15 @@ import ( "github.com/google/wire" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/coremodel/dashboard" + "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/grafana/pkg/expr" + cmreg "github.com/grafana/grafana/pkg/framework/coremodel/staticregistry" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider" "github.com/grafana/grafana/pkg/infra/kvstore" @@ -239,6 +243,10 @@ var wireBasicSet = wire.NewSet( avatar.ProvideAvatarCacheServer, authproxy.ProvideAuthProxy, statscollector.ProvideService, + dashboard.ProvideCoremodel, + cmreg.ProvideRegistry, + cuectx.ProvideCUEContext, + cuectx.ProvideThemaLibrary, ) var wireSet = wire.NewSet( From b43e9b50b497f315f31fb35a006d820871d88dda Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Thu, 14 Apr 2022 23:06:08 +0100 Subject: [PATCH 107/121] Service accounts: RBAC the service account UI (#47788) * WIP * fix: bug for saving name did not remove edit * refactor: better error msg * Display the column Roles even when user can't see the role picker * Remove spaces when building the search query request * Disable Edit button and fix token addition and deletion * Fix the error message text Co-authored-by: Vardan Torosyan --- pkg/services/serviceaccounts/api/api.go | 2 +- .../serviceaccounts/database/errors.go | 8 ++++---- .../serviceaccounts/database/token_store.go | 2 +- .../serviceaccounts/ServiceAccountPage.tsx | 18 ++++++++++++++--- .../serviceaccounts/ServiceAccountProfile.tsx | 20 ++++++++++++++----- .../ServiceAccountTokensTable.tsx | 12 +++++++---- .../ServiceAccountsListItem.tsx | 8 ++++---- 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index d4da2814925..5acc84f090c 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -93,7 +93,7 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) respon serviceAccount, err := api.store.CreateServiceAccount(c.Req.Context(), c.OrgId, cmd.Name) switch { case errors.Is(err, &database.ErrSAInvalidName{}): - return response.Error(http.StatusBadRequest, "Invalid service account name", err) + return response.Error(http.StatusBadRequest, "Failed due to %s", err) case err != nil: return response.Error(http.StatusInternalServerError, "Failed to create service account", err) } diff --git a/pkg/services/serviceaccounts/database/errors.go b/pkg/services/serviceaccounts/database/errors.go index 3cd555480a6..8001b2bd889 100644 --- a/pkg/services/serviceaccounts/database/errors.go +++ b/pkg/services/serviceaccounts/database/errors.go @@ -17,14 +17,14 @@ func (e *ErrSAInvalidName) Unwrap() error { return models.ErrUserAlreadyExists } -type ErrMisingSAToken struct { +type ErrMissingSAToken struct { } -func (e *ErrMisingSAToken) Error() string { +func (e *ErrMissingSAToken) Error() string { return "service account token not found" } -func (e *ErrMisingSAToken) Unwrap() error { +func (e *ErrMissingSAToken) Unwrap() error { return models.ErrApiKeyNotFound } @@ -44,7 +44,7 @@ type ErrDuplicateSAToken struct { } func (e *ErrDuplicateSAToken) Error() string { - return fmt.Sprintf("service account token %s already exists", e.name) + return fmt.Sprintf("service account token %s already exists in the organization", e.name) } func (e *ErrDuplicateSAToken) Unwrap() error { diff --git a/pkg/services/serviceaccounts/database/token_store.go b/pkg/services/serviceaccounts/database/token_store.go index 320bc2f1ed5..4a4b04f9585 100644 --- a/pkg/services/serviceaccounts/database/token_store.go +++ b/pkg/services/serviceaccounts/database/token_store.go @@ -57,7 +57,7 @@ func (s *ServiceAccountsStoreImpl) DeleteServiceAccountToken(ctx context.Context if err != nil { return err } else if n == 0 { - return &ErrMisingSAToken{} + return &ErrMissingSAToken{} } return nil }) diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 66e59e0cc83..061e7a0d309 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { getNavModel } from 'app/core/selectors/navModel'; import Page from 'app/core/components/Page/Page'; import { ServiceAccountProfile } from './ServiceAccountProfile'; -import { StoreState, ServiceAccountDTO, ApiKey, Role } from 'app/types'; +import { StoreState, ServiceAccountDTO, ApiKey, Role, AccessControlAction } from 'app/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { deleteServiceAccountToken, @@ -114,12 +114,24 @@ const ServiceAccountPageUnconnected = ({

    Tokens

    - +
    {tokens && ( )} - + {contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) && ( + + )} ); diff --git a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx index 408d4346d25..368134f9077 100644 --- a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx @@ -1,9 +1,10 @@ import React, { PureComponent, useRef, useState } from 'react'; -import { Role, ServiceAccountDTO } from 'app/types'; +import { Role, ServiceAccountDTO, AccessControlAction } from 'app/types'; import { css, cx } from '@emotion/css'; import { dateTimeFormat, GrafanaTheme2, OrgRole, TimeZone } from '@grafana/data'; import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, useStyles2 } from '@grafana/ui'; import { ServiceAccountRoleRow } from './ServiceAccountRoleRow'; +import { contextSrv } from 'app/core/core'; interface Props { serviceAccount: ServiceAccountDTO; @@ -26,6 +27,8 @@ export function ServiceAccountProfile({ const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDisableModal, setShowDisableModal] = useState(false); + const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite); + const deleteServiceAccountRef = useRef(null); const showDeleteServiceAccountModal = (show: boolean) => () => { setShowDeleteModal(show); @@ -83,9 +86,10 @@ export function ServiceAccountProfile({ Delete service account @@ -123,7 +128,7 @@ export function ServiceAccountProfile({ /> {serviceAccount.isDisabled ? ( - ) : ( @@ -133,6 +138,7 @@ export function ServiceAccountProfile({ variant="secondary" onClick={showDisableServiceAccountModal(true)} ref={disableServiceAccountRef} + disabled={!ableToWrite} > Disable service account @@ -168,6 +174,7 @@ interface ServiceAccountProfileRowProps { value?: string; inputType?: string; onChange?: (value: string) => void; + disabled?: boolean; } interface ServiceAccountProfileRowState { @@ -226,6 +233,7 @@ export class ServiceAccountProfileRow extends PureComponent< }; onSave = () => { + this.setState({ editing: false }); if (this.props.onChange) { this.props.onChange(this.state.value); } @@ -248,7 +256,7 @@ export class ServiceAccountProfileRow extends PureComponent< - + {contextSrv.hasPermission(AccessControlAction.ServiceAccountsDelete) && ( + + )} ); })} @@ -82,7 +86,7 @@ const TokenExpiration = ({ timeZone, token }: TokenExpirationProps) => { Expired - + diff --git a/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx b/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx index d0de3b2e9e9..b2d2d3aabb8 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx @@ -65,8 +65,8 @@ const ServiceAccountListItem = memo( {contextSrv.licensedAccessControlEnabled() ? ( - displayRolePicker && ( - - ) + )} + ) : (
    - {this.state.editing ? ( + {!this.props.disabled && this.state.editing ? ( {this.props.onChange && ( Edit diff --git a/public/app/features/serviceaccounts/ServiceAccountTokensTable.tsx b/public/app/features/serviceaccounts/ServiceAccountTokensTable.tsx index be500c1f26e..e3c517bdef3 100644 --- a/public/app/features/serviceaccounts/ServiceAccountTokensTable.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountTokensTable.tsx @@ -2,8 +2,10 @@ import React, { FC } from 'react'; import { DeleteButton, Icon, Tooltip, useStyles2, useTheme2 } from '@grafana/ui'; import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; +import { AccessControlAction } from 'app/types'; import { ApiKey } from '../../types'; import { css } from '@emotion/css'; +import { contextSrv } from 'app/core/core'; interface Props { tokens: ApiKey[]; @@ -35,9 +37,11 @@ export const ServiceAccountTokensTable: FC = ({ tokens, timeZone, onDelet {formatDate(timeZone, key.created)} - onDelete(key)} /> - + onDelete(key)} /> +
    + + {displayRolePicker && ( - Date: Fri, 15 Apr 2022 09:31:54 +0200 Subject: [PATCH 108/121] Swagger: update swagger for datasource proxy api (#47690) * update the swagger for datasource proxy api * add enterprise spec changes --- pkg/api/docs/definitions/datasources.go | 48 ++- public/api-merged.json | 518 ++++++++++++++++++++---- public/api-spec.json | 329 +++++++++++---- 3 files changed, 740 insertions(+), 155 deletions(-) diff --git a/pkg/api/docs/definitions/datasources.go b/pkg/api/docs/definitions/datasources.go index c5eb1d54752..e911644d0bb 100644 --- a/pkg/api/docs/definitions/datasources.go +++ b/pkg/api/docs/definitions/datasources.go @@ -167,6 +167,20 @@ import ( // 404: notFoundError // 500: internalServerError +// swagger:route GET /datasources/proxy/uid/{datasource_uid}/{datasource_proxy_route} datasources datasourceProxyGETByUIDcalls +// +// Data source proxy GET calls. +// +// Proxies all calls to the actual data source. +// +// Responses: +// 200: +// 400: badRequestError +// 401: unauthorisedError +// 403: forbiddenError +// 404: notFoundError +// 500: internalServerError + // swagger:route POST /datasources/proxy/{datasource_id}/{datasource_proxy_route} datasources datasourceProxyPOSTcalls // // Data source proxy POST calls. @@ -182,6 +196,21 @@ import ( // 404: notFoundError // 500: internalServerError +// swagger:route POST /datasources/proxy/uid/{datasource_uid}/{datasource_proxy_route} datasources datasourceProxyPOSTByUIDcalls +// +// Data source proxy POST calls. +// +// Proxies all calls to the actual data source. The data source should support POST methods for the specific path and role as defined +// +// Responses: +// 201: +// 202: +// 400: badRequestError +// 401: unauthorisedError +// 403: forbiddenError +// 404: notFoundError +// 500: internalServerError + // swagger:route DELETE /datasources/proxy/{datasource_id}/{datasource_proxy_route} datasources datasourceProxyDELETEcalls // // Data source proxy DELETE calls. @@ -196,6 +225,20 @@ import ( // 404: notFoundError // 500: internalServerError +// swagger:route DELETE /datasources/proxy/uid/{datasource_uid}/{datasource_proxy_route} datasources datasourceProxyDELETEByUIDcalls +// +// Data source proxy DELETE calls. +// +// Proxies all calls to the actual data source. +// +// Responses: +// 202: +// 400: badRequestError +// 401: unauthorisedError +// 403: forbiddenError +// 404: notFoundError +// 500: internalServerError + // swagger:route POST /tsdb/query datasources queryDatasource // // Query metrics. @@ -227,7 +270,7 @@ type DatasourceID struct { DatasourceID string `json:"datasource_id"` } -// swagger:parameters deleteDatasourceByUID getDatasourceByUID +// swagger:parameters deleteDatasourceByUID getDatasourceByUID datasourceProxyGETByUIDcalls datasourceProxyPOSTByUIDcalls datasourceProxyDELETEByUIDcalls type DatasourceUID struct { // in:path // required:true @@ -241,7 +284,8 @@ type DatasourceName struct { DatasourceName string `json:"datasource_name"` } -// swagger:parameters datasourceProxyGETcalls datasourceProxyPOSTcalls datasourceProxyDELETEcalls +// swagger:parameters datasourceProxyGETcalls datasourceProxyPOSTcalls datasourceProxyDELETEcalls datasourceProxyGETByUIDcalls +// swagger:parameters datasourceProxyPOSTByUIDcalls datasourceProxyDELETEByUIDcalls type DatasourceProxyRouteParam struct { // in:path // required:true diff --git a/public/api-merged.json b/public/api-merged.json index ff5e20db5e9..2a329b78226 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -506,6 +506,14 @@ "summary": "Add a user role assignment.", "operationId": "addUserRole", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true + }, { "x-go-name": "Body", "name": "body", @@ -514,14 +522,6 @@ "schema": { "$ref": "#/definitions/AddUserRoleCommand" } - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true } ], "responses": { @@ -547,6 +547,14 @@ "summary": "Remove a user role assignment.", "operationId": "removeUserRole", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true + }, { "type": "string", "x-go-name": "RoleUID", @@ -560,14 +568,6 @@ "description": "A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to remove assignment.", "name": "global", "in": "query" - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true } ], "responses": { @@ -1025,7 +1025,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:delete` and scope `global:users:*`.", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:delete` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Delete global User.", "operationId": "deleteUser", @@ -1065,7 +1065,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:list` and scope `global:users:*`.", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:list` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Return a list of all auth tokens (devices) that the user currently have logged in from.", "operationId": "getAuthTokens", @@ -1102,7 +1102,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:disable` and scope `global:users:1` (userIDScope).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:disable` and scope `global.users:1` (userIDScope).", "tags": ["admin_users"], "summary": "Disable user.", "operationId": "disableUser", @@ -1142,7 +1142,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:enable` and scope `global:users:1` (userIDScope).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:enable` and scope `global.users:1` (userIDScope).", "tags": ["admin_users"], "summary": "Enable user.", "operationId": "enableUser", @@ -1182,7 +1182,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.logout` and scope `global:users:*`.", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.logout` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "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.", "operationId": "logoutUser", @@ -1225,7 +1225,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.password:update` and scope `global:users:*`.", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.password:update` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Set password for user.", "operationId": "setPassword", @@ -1269,7 +1269,7 @@ }, "/admin/users/{user_id}/permissions": { "put": { - "description": "Only works with Basic Authentication (username and password). See introduction for an explanation.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.permissions:update` and scope `global:users:*`.", + "description": "Only works with Basic Authentication (username and password). See introduction for an explanation.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.permissions:update` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Set permissions for user.", "operationId": "setPermissions", @@ -1318,7 +1318,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:list` and scope `global:users:1` (userIDScope).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:list` and scope `global.users:1` (userIDScope).", "tags": ["admin_users"], "summary": "Fetch user quota.", "operationId": "getUserQuota", @@ -1358,7 +1358,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:update` and scope `global:users:1` (userIDScope).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:update` and scope `global.users:1` (userIDScope).", "tags": ["admin_users"], "summary": "Update user quota.", "operationId": "updateUserQuota", @@ -1414,7 +1414,7 @@ "basic": [] } ], - "description": "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.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:update` and scope `global:users:*`.", + "description": "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.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:update` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Revoke auth token for user.", "operationId": "revokeAuthToken", @@ -3150,7 +3150,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/DeleteAnnotationsCmd" + "$ref": "#/definitions/MassDeleteAnnotationsCmd" } } ], @@ -4063,6 +4063,137 @@ } } }, + "/datasources/proxy/uid/{datasource_uid}/{datasource_proxy_route}": { + "get": { + "description": "Proxies all calls to the actual data source.", + "tags": ["datasources"], + "summary": "Data source proxy GET calls.", + "operationId": "datasourceProxyGETByUIDcalls", + "parameters": [ + { + "type": "string", + "x-go-name": "DatasourceUID", + "name": "datasource_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "DatasourceProxyRoute", + "name": "datasource_proxy_route", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, + "post": { + "description": "Proxies all calls to the actual data source. The data source should support POST methods for the specific path and role as defined", + "tags": ["datasources"], + "summary": "Data source proxy POST calls.", + "operationId": "datasourceProxyPOSTByUIDcalls", + "parameters": [ + { + "type": "string", + "x-go-name": "DatasourceUID", + "name": "datasource_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "DatasourceProxyRoute", + "name": "datasource_proxy_route", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "" + }, + "202": { + "description": "" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, + "delete": { + "description": "Proxies all calls to the actual data source.", + "tags": ["datasources"], + "summary": "Data source proxy DELETE calls.", + "operationId": "datasourceProxyDELETEByUIDcalls", + "parameters": [ + { + "type": "string", + "x-go-name": "DatasourceUID", + "name": "datasource_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "DatasourceProxyRoute", + "name": "datasource_proxy_route", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/datasources/proxy/{datasource_id}/{datasource_proxy_route}": { "get": { "description": "Proxies all calls to the actual data source.", @@ -4273,7 +4404,7 @@ }, "/datasources/{datasource_id}": { "get": { - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).", "tags": ["datasources"], "summary": "Get a single data source by Id.", "operationId": "getDatasourceByID", @@ -4308,7 +4439,7 @@ } }, "put": { - "description": "Similar to creating a data source, `password` and `basicAuthPassword` should be defined under\nsecureJsonData in order to be stored securely as an encrypted blob in the database. Then, the\nencrypted fields are listed under secureJsonFields section in the response.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:write` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).", + "description": "Similar to creating a data source, `password` and `basicAuthPassword` should be defined under\nsecureJsonData in order to be stored securely as an encrypted blob in the database. Then, the\nencrypted fields are listed under secureJsonFields section in the response.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:write` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).", "tags": ["datasources"], "summary": "Update an existing data source.", "operationId": "updateDatasource", @@ -4345,7 +4476,7 @@ } }, "delete": { - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).", "tags": ["datasources"], "summary": "Delete an existing data source by id.", "operationId": "deleteDatasourceByID", @@ -4494,15 +4625,15 @@ "parameters": [ { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "datasource_id", "in": "path", "required": true }, { "type": "string", - "x-go-name": "DatasourceID", - "name": "datasource_id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true } @@ -6469,6 +6600,145 @@ } } }, + "/provisioning/contact-points": { + "get": { + "tags": ["provisioning"], + "summary": "Get all the contact points.", + "operationId": "RouteGetContactpoints", + "responses": { + "200": { + "description": "Route", + "schema": { + "$ref": "#/definitions/Route" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, + "put": { + "consumes": ["application/json"], + "tags": ["provisioning"], + "summary": "Update an existing contact point.", + "operationId": "RoutePutContactpoints", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/EmbeddedContactPoint" + } + } + ], + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, + "post": { + "consumes": ["application/json"], + "tags": ["provisioning"], + "summary": "Create a contact point.", + "operationId": "RoutePostContactpoints", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/EmbeddedContactPoint" + } + } + ], + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, + "/provisioning/contact-points/{ID}": { + "delete": { + "consumes": ["application/json"], + "tags": ["provisioning"], + "summary": "Delete a contact point.", + "operationId": "RouteDeleteContactpoints", + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, + "/provisioning/policies": { + "get": { + "tags": ["provisioning"], + "summary": "Get the notification policy tree.", + "operationId": "RouteGetPolicyTree", + "responses": { + "200": { + "description": "Route", + "schema": { + "$ref": "#/definitions/Route" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, + "post": { + "consumes": ["application/json"], + "tags": ["provisioning"], + "summary": "Sets the notification policy tree.", + "operationId": "RoutePostPolicyTree", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/Route" + } + } + ], + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, "/recording-rules": { "get": { "tags": ["recording_rules", "enterprise"], @@ -10122,6 +10392,36 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "AnnotationActions": { + "type": "object", + "properties": { + "canAdd": { + "type": "boolean", + "x-go-name": "CanAdd" + }, + "canDelete": { + "type": "boolean", + "x-go-name": "CanDelete" + }, + "canEdit": { + "type": "boolean", + "x-go-name": "CanEdit" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" + }, + "AnnotationPermission": { + "type": "object", + "properties": { + "dashboard": { + "$ref": "#/definitions/AnnotationActions" + }, + "organization": { + "$ref": "#/definitions/AnnotationActions" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" + }, "ApiKeyDTO": { "type": "object", "properties": { @@ -10240,7 +10540,7 @@ "x-go-name": "ReportLogo" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "CalculateDiffTarget": { "type": "object", @@ -10390,7 +10690,7 @@ "x-go-name": "UserID" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "CreateAlertNotificationCommand": { "type": "object", @@ -10576,7 +10876,7 @@ "x-go-name": "TemplateVars" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "CreateOrgCommand": { "type": "object", @@ -10836,6 +11136,9 @@ "DashboardMeta": { "type": "object", "properties": { + "annotationsPermissions": { + "$ref": "#/definitions/AnnotationPermission" + }, "canAdmin": { "type": "boolean", "x-go-name": "CanAdmin" @@ -11499,32 +11802,6 @@ }, "x-go-package": "github.com/prometheus/alertmanager/timeinterval" }, - "DeleteAnnotationsCmd": { - "type": "object", - "properties": { - "alertId": { - "type": "integer", - "format": "int64", - "x-go-name": "AlertId" - }, - "annotationId": { - "type": "integer", - "format": "int64", - "x-go-name": "AnnotationId" - }, - "dashboardId": { - "type": "integer", - "format": "int64", - "x-go-name": "DashboardId" - }, - "panelId": { - "type": "integer", - "format": "int64", - "x-go-name": "PanelId" - } - }, - "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" - }, "DeleteTokenCommand": { "type": "object", "properties": { @@ -11631,6 +11908,36 @@ }, "x-go-package": "github.com/prometheus/alertmanager/config" }, + "EmbeddedContactPoint": { + "description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.", + "type": "object", + "properties": { + "disableResolveMessage": { + "type": "boolean", + "x-go-name": "DisableResolveMessage" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "provanance": { + "type": "string", + "x-go-name": "Provenance" + }, + "settings": { + "$ref": "#/definitions/Json" + }, + "type": { + "type": "string", + "x-go-name": "Type" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, "ErrorResponseBody": { "type": "object", "required": ["message"], @@ -12479,18 +12786,18 @@ "x-go-name": "URL" } }, - "x-go-package": "github.com/grafana/grafana/pkg/services/search" + "x-go-package": "github.com/grafana/grafana/pkg/models" }, "HitList": { "type": "array", "items": { "$ref": "#/definitions/Hit" }, - "x-go-package": "github.com/grafana/grafana/pkg/services/search" + "x-go-package": "github.com/grafana/grafana/pkg/models" }, "HitType": { "type": "string", - "x-go-package": "github.com/grafana/grafana/pkg/services/search" + "x-go-package": "github.com/grafana/grafana/pkg/models" }, "HostPort": { "type": "object", @@ -13033,6 +13340,27 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/libraryelements" }, + "MassDeleteAnnotationsCmd": { + "type": "object", + "properties": { + "annotationId": { + "type": "integer", + "format": "int64", + "x-go-name": "AnnotationId" + }, + "dashboardId": { + "type": "integer", + "format": "int64", + "x-go-name": "DashboardId" + }, + "panelId": { + "type": "integer", + "format": "int64", + "x-go-name": "PanelId" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" + }, "MatchRegexps": { "type": "object", "title": "MatchRegexps represents a map of Regexp.", @@ -13691,6 +14019,9 @@ "navbar": { "$ref": "#/definitions/NavbarPreference" }, + "queryHistory": { + "$ref": "#/definitions/QueryHistoryPreference" + }, "theme": { "type": "string", "enum": ["light", "dark"], @@ -14153,6 +14484,9 @@ "navbar": { "$ref": "#/definitions/NavbarPreference" }, + "queryHistory": { + "$ref": "#/definitions/QueryHistoryPreference" + }, "theme": { "type": "string", "x-go-name": "Theme" @@ -14250,6 +14584,16 @@ }, "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/backend" }, + "QueryHistoryPreference": { + "type": "object", + "properties": { + "homeTab": { + "type": "string", + "x-go-name": "HomeTab" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/models" + }, "Receiver": { "type": "object", "title": "Receiver configuration provides configuration on how to contact a receiver.", @@ -14427,7 +14771,7 @@ "x-go-name": "UseEmailsFromReport" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "ReportOptionsDTO": { "type": "object", @@ -14444,7 +14788,7 @@ "$ref": "#/definitions/TimeRangeDTO" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "ResponseDetails": { "type": "object", @@ -14900,7 +15244,7 @@ "x-go-name": "WorkdaysOnly" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "SearchTeamQueryResult": { "type": "object", @@ -15020,7 +15364,7 @@ "x-go-name": "UserID" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "SigV4Config": { "description": "SigV4Config is the configuration for signing remote write requests with\nAWS's SigV4 verification process. Empty values will be retrieved using the\nAWS default credentials chain.", @@ -15713,7 +16057,7 @@ "x-go-name": "To" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "Token": { "type": "object", @@ -15814,6 +16158,15 @@ "format": "int64", "x-go-name": "TokenExpiresWarnDays" }, + "trial": { + "type": "boolean", + "x-go-name": "Trial" + }, + "trial_exp": { + "type": "integer", + "format": "int64", + "x-go-name": "TrialExpires" + }, "update_days": { "type": "integer", "format": "int64", @@ -15856,9 +16209,8 @@ "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "properties": { "ForceQuery": { "type": "boolean" @@ -15891,7 +16243,7 @@ "$ref": "#/definitions/Userinfo" } }, - "x-go-package": "net/url" + "x-go-package": "github.com/prometheus/common/config" }, "UpdateAlertNotificationCommand": { "type": "object", @@ -16204,6 +16556,9 @@ "navbar": { "$ref": "#/definitions/NavbarPreference" }, + "queryHistory": { + "$ref": "#/definitions/QueryHistoryPreference" + }, "theme": { "type": "string", "enum": ["light", "dark"], @@ -16792,12 +17147,11 @@ } }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" - }, - "x-go-name": "AlertGroups", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + } }, "alertStatus": { "description": "AlertStatus alert status", @@ -16900,7 +17254,6 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": ["labels", "annotations", "endsAt", "fingerprint", "receivers", "startsAt", "status", "updatedAt"], "properties": { @@ -16950,7 +17303,9 @@ "format": "date-time", "x-go-name": "UpdatedAt" } - } + }, + "x-go-name": "GettableAlert", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableAlerts": { "description": "GettableAlerts gettable alerts", @@ -17006,12 +17361,11 @@ } }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" - }, - "x-go-name": "GettableSilences", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + } }, "labelSet": { "description": "LabelSet label set", diff --git a/public/api-spec.json b/public/api-spec.json index 2d5f6cbbf2b..117b03382c3 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -506,6 +506,14 @@ "summary": "Add a user role assignment.", "operationId": "addUserRole", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true + }, { "x-go-name": "Body", "name": "body", @@ -514,14 +522,6 @@ "schema": { "$ref": "#/definitions/AddUserRoleCommand" } - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true } ], "responses": { @@ -547,6 +547,14 @@ "summary": "Remove a user role assignment.", "operationId": "removeUserRole", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true + }, { "type": "string", "x-go-name": "RoleUID", @@ -560,14 +568,6 @@ "description": "A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to remove assignment.", "name": "global", "in": "query" - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true } ], "responses": { @@ -1025,7 +1025,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:delete` and scope `global:users:*`.", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:delete` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Delete global User.", "operationId": "deleteUser", @@ -1065,7 +1065,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:list` and scope `global:users:*`.", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:list` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Return a list of all auth tokens (devices) that the user currently have logged in from.", "operationId": "getAuthTokens", @@ -1102,7 +1102,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:disable` and scope `global:users:1` (userIDScope).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:disable` and scope `global.users:1` (userIDScope).", "tags": ["admin_users"], "summary": "Disable user.", "operationId": "disableUser", @@ -1142,7 +1142,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:enable` and scope `global:users:1` (userIDScope).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:enable` and scope `global.users:1` (userIDScope).", "tags": ["admin_users"], "summary": "Enable user.", "operationId": "enableUser", @@ -1182,7 +1182,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.logout` and scope `global:users:*`.", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.logout` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "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.", "operationId": "logoutUser", @@ -1225,7 +1225,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.password:update` and scope `global:users:*`.", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.password:update` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Set password for user.", "operationId": "setPassword", @@ -1269,7 +1269,7 @@ }, "/admin/users/{user_id}/permissions": { "put": { - "description": "Only works with Basic Authentication (username and password). See introduction for an explanation.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.permissions:update` and scope `global:users:*`.", + "description": "Only works with Basic Authentication (username and password). See introduction for an explanation.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.permissions:update` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Set permissions for user.", "operationId": "setPermissions", @@ -1318,7 +1318,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:list` and scope `global:users:1` (userIDScope).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:list` and scope `global.users:1` (userIDScope).", "tags": ["admin_users"], "summary": "Fetch user quota.", "operationId": "getUserQuota", @@ -1358,7 +1358,7 @@ "basic": [] } ], - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:update` and scope `global:users:1` (userIDScope).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:update` and scope `global.users:1` (userIDScope).", "tags": ["admin_users"], "summary": "Update user quota.", "operationId": "updateUserQuota", @@ -1414,7 +1414,7 @@ "basic": [] } ], - "description": "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.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:update` and scope `global:users:*`.", + "description": "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.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:update` and scope `global.users:*`.", "tags": ["admin_users"], "summary": "Revoke auth token for user.", "operationId": "revokeAuthToken", @@ -2200,7 +2200,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/DeleteAnnotationsCmd" + "$ref": "#/definitions/MassDeleteAnnotationsCmd" } } ], @@ -3113,6 +3113,137 @@ } } }, + "/datasources/proxy/uid/{datasource_uid}/{datasource_proxy_route}": { + "get": { + "description": "Proxies all calls to the actual data source.", + "tags": ["datasources"], + "summary": "Data source proxy GET calls.", + "operationId": "datasourceProxyGETByUIDcalls", + "parameters": [ + { + "type": "string", + "x-go-name": "DatasourceUID", + "name": "datasource_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "DatasourceProxyRoute", + "name": "datasource_proxy_route", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, + "post": { + "description": "Proxies all calls to the actual data source. The data source should support POST methods for the specific path and role as defined", + "tags": ["datasources"], + "summary": "Data source proxy POST calls.", + "operationId": "datasourceProxyPOSTByUIDcalls", + "parameters": [ + { + "type": "string", + "x-go-name": "DatasourceUID", + "name": "datasource_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "DatasourceProxyRoute", + "name": "datasource_proxy_route", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "" + }, + "202": { + "description": "" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + }, + "delete": { + "description": "Proxies all calls to the actual data source.", + "tags": ["datasources"], + "summary": "Data source proxy DELETE calls.", + "operationId": "datasourceProxyDELETEByUIDcalls", + "parameters": [ + { + "type": "string", + "x-go-name": "DatasourceUID", + "name": "datasource_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "DatasourceProxyRoute", + "name": "datasource_proxy_route", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "403": { + "$ref": "#/responses/forbiddenError" + }, + "404": { + "$ref": "#/responses/notFoundError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/datasources/proxy/{datasource_id}/{datasource_proxy_route}": { "get": { "description": "Proxies all calls to the actual data source.", @@ -3323,7 +3454,7 @@ }, "/datasources/{datasource_id}": { "get": { - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).", "tags": ["datasources"], "summary": "Get a single data source by Id.", "operationId": "getDatasourceByID", @@ -3358,7 +3489,7 @@ } }, "put": { - "description": "Similar to creating a data source, `password` and `basicAuthPassword` should be defined under\nsecureJsonData in order to be stored securely as an encrypted blob in the database. Then, the\nencrypted fields are listed under secureJsonFields section in the response.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:write` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).", + "description": "Similar to creating a data source, `password` and `basicAuthPassword` should be defined under\nsecureJsonData in order to be stored securely as an encrypted blob in the database. Then, the\nencrypted fields are listed under secureJsonFields section in the response.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:write` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).", "tags": ["datasources"], "summary": "Update an existing data source.", "operationId": "updateDatasource", @@ -3395,7 +3526,7 @@ } }, "delete": { - "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).", + "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).", "tags": ["datasources"], "summary": "Delete an existing data source by id.", "operationId": "deleteDatasourceByID", @@ -3544,15 +3675,15 @@ "parameters": [ { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "datasource_id", "in": "path", "required": true }, { "type": "string", - "x-go-name": "DatasourceID", - "name": "datasource_id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true } @@ -8374,6 +8505,36 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, + "AnnotationActions": { + "type": "object", + "properties": { + "canAdd": { + "type": "boolean", + "x-go-name": "CanAdd" + }, + "canDelete": { + "type": "boolean", + "x-go-name": "CanDelete" + }, + "canEdit": { + "type": "boolean", + "x-go-name": "CanEdit" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" + }, + "AnnotationPermission": { + "type": "object", + "properties": { + "dashboard": { + "$ref": "#/definitions/AnnotationActions" + }, + "organization": { + "$ref": "#/definitions/AnnotationActions" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" + }, "ApiKeyDTO": { "type": "object", "properties": { @@ -8421,7 +8582,7 @@ "x-go-name": "ReportLogo" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "CalculateDiffTarget": { "type": "object", @@ -8537,7 +8698,7 @@ "x-go-name": "UserID" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "CreateAlertNotificationCommand": { "type": "object", @@ -8723,7 +8884,7 @@ "x-go-name": "TemplateVars" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "CreateOrgCommand": { "type": "object", @@ -8983,6 +9144,9 @@ "DashboardMeta": { "type": "object", "properties": { + "annotationsPermissions": { + "$ref": "#/definitions/AnnotationPermission" + }, "canAdmin": { "type": "boolean", "x-go-name": "CanAdmin" @@ -9625,32 +9789,6 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" }, - "DeleteAnnotationsCmd": { - "type": "object", - "properties": { - "alertId": { - "type": "integer", - "format": "int64", - "x-go-name": "AlertId" - }, - "annotationId": { - "type": "integer", - "format": "int64", - "x-go-name": "AnnotationId" - }, - "dashboardId": { - "type": "integer", - "format": "int64", - "x-go-name": "DashboardId" - }, - "panelId": { - "type": "integer", - "format": "int64", - "x-go-name": "PanelId" - } - }, - "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" - }, "DeleteTokenCommand": { "type": "object", "properties": { @@ -9952,18 +10090,18 @@ "x-go-name": "URL" } }, - "x-go-package": "github.com/grafana/grafana/pkg/services/search" + "x-go-package": "github.com/grafana/grafana/pkg/models" }, "HitList": { "type": "array", "items": { "$ref": "#/definitions/Hit" }, - "x-go-package": "github.com/grafana/grafana/pkg/services/search" + "x-go-package": "github.com/grafana/grafana/pkg/models" }, "HitType": { "type": "string", - "x-go-package": "github.com/grafana/grafana/pkg/services/search" + "x-go-package": "github.com/grafana/grafana/pkg/models" }, "ImportDashboardInput": { "type": "object", @@ -10395,6 +10533,27 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/libraryelements" }, + "MassDeleteAnnotationsCmd": { + "type": "object", + "properties": { + "annotationId": { + "type": "integer", + "format": "int64", + "x-go-name": "AnnotationId" + }, + "dashboardId": { + "type": "integer", + "format": "int64", + "x-go-name": "DashboardId" + }, + "panelId": { + "type": "integer", + "format": "int64", + "x-go-name": "PanelId" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" + }, "Metadata": { "description": "Metadata contains user accesses for a given resource\nEx: map[string]bool{\"create\":true, \"delete\": true}", "type": "object", @@ -10711,6 +10870,9 @@ "navbar": { "$ref": "#/definitions/NavbarPreference" }, + "queryHistory": { + "$ref": "#/definitions/QueryHistoryPreference" + }, "theme": { "type": "string", "enum": ["light", "dark"], @@ -10857,6 +11019,9 @@ "navbar": { "$ref": "#/definitions/NavbarPreference" }, + "queryHistory": { + "$ref": "#/definitions/QueryHistoryPreference" + }, "theme": { "type": "string", "x-go-name": "Theme" @@ -10901,6 +11066,16 @@ }, "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/backend" }, + "QueryHistoryPreference": { + "type": "object", + "properties": { + "homeTab": { + "type": "string", + "x-go-name": "HomeTab" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/models" + }, "RecordingRuleJSON": { "description": "RecordingRuleJSON is the external representation of a recording rule", "type": "object", @@ -10984,7 +11159,7 @@ "x-go-name": "UseEmailsFromReport" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "ReportOptionsDTO": { "type": "object", @@ -11001,7 +11176,7 @@ "$ref": "#/definitions/TimeRangeDTO" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "Responses": { "description": "The QueryData method the QueryDataHandler method will set the RefId\nproperty on the DataRespones' frames based on these RefIDs.", @@ -11175,7 +11350,7 @@ "x-go-name": "WorkdaysOnly" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "SearchTeamQueryResult": { "type": "object", @@ -11286,7 +11461,7 @@ "x-go-name": "UserID" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "Status": { "type": "object", @@ -11562,7 +11737,7 @@ "x-go-name": "To" } }, - "x-go-package": "github.com/grafana/grafana/pkg/extensions/report" + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/api" }, "Token": { "type": "object", @@ -11663,6 +11838,15 @@ "format": "int64", "x-go-name": "TokenExpiresWarnDays" }, + "trial": { + "type": "boolean", + "x-go-name": "Trial" + }, + "trial_exp": { + "type": "integer", + "format": "int64", + "x-go-name": "TrialExpires" + }, "update_days": { "type": "integer", "format": "int64", @@ -12015,6 +12199,9 @@ "navbar": { "$ref": "#/definitions/NavbarPreference" }, + "queryHistory": { + "$ref": "#/definitions/QueryHistoryPreference" + }, "theme": { "type": "string", "enum": ["light", "dark"], From 508cc58a637342a9206286d4acd84ebac6f5f7e8 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Fri, 15 Apr 2022 10:42:58 +0300 Subject: [PATCH 109/121] CI: Remove unused `--github-token` flag (#47773) * Remove github token flag * Minor cleanup * Update grabpl --- .drone.yml | 128 ++++++++++----------------- scripts/drone/pipelines/release.star | 14 +-- scripts/drone/steps/lib.star | 20 ++--- 3 files changed, 54 insertions(+), 108 deletions(-) diff --git a/.drone.yml b/.drone.yml index 5ef3316da98..a634bf1ec4d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -15,7 +15,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -126,7 +126,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -152,7 +152,6 @@ steps: depends_on: - gen-version - wire-install - environment: {} image: grafana/build-container:1.5.3 name: build-backend - commands: @@ -378,7 +377,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -446,7 +445,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -534,7 +533,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -623,7 +622,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -732,7 +731,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -795,7 +794,6 @@ steps: depends_on: - gen-version - wire-install - environment: {} image: grafana/build-container:1.5.3 name: build-backend - commands: @@ -853,8 +851,6 @@ steps: - build-frontend - build-frontend-packages environment: - GITHUB_TOKEN: - from_secret: github_token GPG_KEY_PASSWORD: from_secret: gpg_key_password GPG_PRIV_KEY: @@ -1167,7 +1163,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1239,7 +1235,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -1326,7 +1322,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1410,7 +1406,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1431,19 +1427,14 @@ steps: image: grafana/build-container:1.5.3 name: yarn-install - commands: - - ./bin/grabpl build-backend --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} - ${DRONE_TAG} + - ./bin/grabpl build-backend --jobs 8 --edition oss ${DRONE_TAG} depends_on: - gen-version - wire-install - environment: - GITHUB_TOKEN: - from_secret: github_token image: grafana/build-container:1.5.3 name: build-backend - commands: - - ./bin/grabpl build-frontend --jobs 8 --github-token $${GITHUB_TOKEN} --edition - oss ${DRONE_TAG} + - ./bin/grabpl build-frontend --jobs 8 --edition oss ${DRONE_TAG} depends_on: - gen-version - yarn-install @@ -1452,8 +1443,7 @@ steps: image: grafana/build-container:1.5.3 name: build-frontend - commands: - - ./bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN} - --edition oss ${DRONE_TAG} + - ./bin/grabpl build-frontend-packages --jobs 8 --edition oss ${DRONE_TAG} depends_on: - gen-version - yarn-install @@ -1491,16 +1481,13 @@ steps: image: grafana/build-container:1.5.3 name: ensure-cuetsified - commands: - - ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --sign - ${DRONE_TAG} + - ./bin/grabpl package --jobs 8 --edition oss --sign ${DRONE_TAG} depends_on: - build-plugins - build-backend - build-frontend - build-frontend-packages environment: - GITHUB_TOKEN: - from_secret: github_token GPG_KEY_PASSWORD: from_secret: gpg_key_password GPG_PRIV_KEY: @@ -1725,7 +1712,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1849,7 +1836,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1926,7 +1913,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -1985,7 +1972,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2037,19 +2024,14 @@ steps: image: grafana/build-container:1.5.3 name: gen-version - commands: - - ./bin/grabpl build-backend --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN} - ${DRONE_TAG} + - ./bin/grabpl build-backend --jobs 8 --edition enterprise ${DRONE_TAG} depends_on: - gen-version - wire-install - environment: - GITHUB_TOKEN: - from_secret: github_token image: grafana/build-container:1.5.3 name: build-backend - commands: - - ./bin/grabpl build-frontend --jobs 8 --github-token $${GITHUB_TOKEN} --edition - enterprise ${DRONE_TAG} + - ./bin/grabpl build-frontend --jobs 8 --edition enterprise ${DRONE_TAG} depends_on: - gen-version - yarn-install @@ -2058,8 +2040,7 @@ steps: image: grafana/build-container:1.5.3 name: build-frontend - commands: - - ./bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN} - --edition enterprise ${DRONE_TAG} + - ./bin/grabpl build-frontend-packages --jobs 8 --edition enterprise ${DRONE_TAG} depends_on: - gen-version - yarn-install @@ -2097,19 +2078,14 @@ steps: image: grafana/build-container:1.5.3 name: ensure-cuetsified - commands: - - ./bin/grabpl build-backend --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN} - ${DRONE_TAG} + - ./bin/grabpl build-backend --jobs 8 --edition enterprise2 ${DRONE_TAG} depends_on: - gen-version - wire-install - environment: - GITHUB_TOKEN: - from_secret: github_token image: grafana/build-container:1.5.3 name: build-backend-enterprise2 - commands: - - ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN} - --sign ${DRONE_TAG} + - ./bin/grabpl package --jobs 8 --edition enterprise --sign ${DRONE_TAG} depends_on: - build-plugins - build-backend @@ -2117,8 +2093,6 @@ steps: - build-frontend-packages - build-backend-enterprise2 environment: - GITHUB_TOKEN: - from_secret: github_token GPG_KEY_PASSWORD: from_secret: gpg_key_password GPG_PRIV_KEY: @@ -2277,8 +2251,7 @@ steps: image: grafana/grafana-ci-deploy:1.3.1 name: store-npm-packages - commands: - - ./bin/grabpl package --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN} - --sign ${DRONE_TAG} + - ./bin/grabpl package --jobs 8 --edition enterprise2 --sign ${DRONE_TAG} depends_on: - build-plugins - build-backend @@ -2286,8 +2259,6 @@ steps: - build-frontend-packages - build-backend-enterprise2 environment: - GITHUB_TOKEN: - from_secret: github_token GPG_KEY_PASSWORD: from_secret: gpg_key_password GPG_PRIV_KEY: @@ -2358,7 +2329,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2537,7 +2508,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2661,7 +2632,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -2736,7 +2707,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2814,7 +2785,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2875,7 +2846,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2954,7 +2925,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3016,7 +2987,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3054,7 +3025,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3092,7 +3063,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3142,7 +3113,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3209,7 +3180,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3234,7 +3205,6 @@ steps: depends_on: - gen-version - wire-install - environment: {} image: grafana/build-container:1.5.3 name: build-backend - commands: @@ -3292,8 +3262,6 @@ steps: - build-frontend - build-frontend-packages environment: - GITHUB_TOKEN: - from_secret: github_token GPG_KEY_PASSWORD: from_secret: gpg_key_password GPG_PRIV_KEY: @@ -3488,7 +3456,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3606,7 +3574,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3677,7 +3645,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -3725,7 +3693,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3778,7 +3746,6 @@ steps: depends_on: - gen-version - wire-install - environment: {} image: grafana/build-container:1.5.3 name: build-backend - commands: @@ -3835,7 +3802,6 @@ steps: depends_on: - gen-version - wire-install - environment: {} image: grafana/build-container:1.5.3 name: build-backend-enterprise2 - commands: @@ -3848,8 +3814,6 @@ steps: - build-frontend-packages - build-backend-enterprise2 environment: - GITHUB_TOKEN: - from_secret: github_token GPG_KEY_PASSWORD: from_secret: gpg_key_password GPG_PRIV_KEY: @@ -4021,8 +3985,6 @@ steps: - build-frontend-packages - build-backend-enterprise2 environment: - GITHUB_TOKEN: - from_secret: github_token GPG_KEY_PASSWORD: from_secret: gpg_key_password GPG_PRIV_KEY: @@ -4086,7 +4048,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4256,7 +4218,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4371,7 +4333,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.35/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -4568,6 +4530,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 6b2cdb67f637bdd527ba527741fd7d0af7e61b0700487be912821b93d8d16ca1 +hmac: 203f82c4bb5060b26ecabb9a5d979ad3ae1eea3c586afddb083d685bbd4328f8 ... diff --git a/scripts/drone/pipelines/release.star b/scripts/drone/pipelines/release.star index cb7a8e9cb6b..24f21985882 100644 --- a/scripts/drone/pipelines/release.star +++ b/scripts/drone/pipelines/release.star @@ -224,7 +224,7 @@ def get_steps(edition, ver_mode): build_steps.append(build_storybook) if include_enterprise2: - integration_test_steps.extend([redis_integration_tests_step(edition=edition2, ver_mode=ver_mode), memcached_integration_tests_step(edition=edition2, ver_mode=ver_mode)]) + integration_test_steps.extend([redis_integration_tests_step(), memcached_integration_tests_step()]) if should_upload: publish_steps.append(upload_cdn_step(edition=edition, ver_mode=ver_mode, trigger=trigger_oss)) @@ -431,9 +431,8 @@ def publish_npm_pipelines(mode): name='publish-npm-packages-{}'.format(mode), trigger=trigger, steps = steps, edition="all" )] -def release_pipelines(ver_mode='release', trigger=None, environment=None): +def release_pipelines(ver_mode='release', trigger=None): # 'enterprise' edition services contain both OSS and enterprise services - services = integration_test_services(edition='enterprise') if not trigger: trigger = { 'event': { @@ -447,8 +446,6 @@ def release_pipelines(ver_mode='release', trigger=None, environment=None): }, } - should_publish = ver_mode == 'release' - # The release pipelines include also enterprise ones, so both editions are built for a release. # We could also solve this by triggering a downstream build for the enterprise repo, but by including enterprise # in OSS release builds, we simplify the UX for the release engineer. @@ -457,13 +454,6 @@ def release_pipelines(ver_mode='release', trigger=None, environment=None): pipelines = oss_pipelines + enterprise_pipelines - # if ver_mode == 'release': - # pipelines.append(publish_artifacts_pipelines()) - #pipelines.append(notify_pipeline( - # name='notify-{}'.format(ver_mode), slack_channel='grafana-ci-notifications', trigger=dict(trigger, status = ['failure']), - # depends_on=[p['name'] for p in pipelines], template=failure_template, secret='slack_webhook', - #)) - return pipelines def get_e2e_suffix(): diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 44e4c6ae5b7..77e7391394e 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1,6 +1,6 @@ load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket') -grabpl_version = 'v2.9.35' +grabpl_version = 'v2.9.36' build_image = 'grafana/build-container:1.5.3' publish_image = 'grafana/grafana-ci-deploy:1.3.1' deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image' @@ -368,17 +368,13 @@ def build_backend_step(edition, ver_mode, variants=None): # TODO: Convert number of jobs to percentage if ver_mode == 'release': - env = { - 'GITHUB_TOKEN': from_secret(github_token), - } cmds = [ - './bin/grabpl build-backend --jobs 8 --edition {} --github-token $${{GITHUB_TOKEN}} ${{DRONE_TAG}}'.format( + './bin/grabpl build-backend --jobs 8 --edition {} ${{DRONE_TAG}}'.format( edition, ), ] else: build_no = '${DRONE_BUILD_NUMBER}' - env = {} cmds = [ './bin/grabpl build-backend --jobs 8 --edition {} --build-id {}{}'.format( edition, build_no, variants_str, @@ -388,7 +384,6 @@ def build_backend_step(edition, ver_mode, variants=None): return { 'name': 'build-backend' + enterprise2_suffix(edition), 'image': build_image, - 'environment': env, 'depends_on': [ 'gen-version', 'wire-install', @@ -403,7 +398,7 @@ def build_frontend_step(edition, ver_mode): # TODO: Use percentage for num jobs if ver_mode == 'release': cmds = [ - './bin/grabpl build-frontend --jobs 8 --github-token $${GITHUB_TOKEN} ' + \ + './bin/grabpl build-frontend --jobs 8 ' + \ '--edition {} ${{DRONE_TAG}}'.format(edition), ] else: @@ -432,7 +427,7 @@ def build_frontend_package_step(edition, ver_mode): # TODO: Use percentage for num jobs if ver_mode == 'release': cmds = [ - './bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN} ' + \ + './bin/grabpl build-frontend-packages --jobs 8 ' + \ '--edition {} ${{DRONE_TAG}}'.format(edition), ] else: @@ -654,7 +649,6 @@ def package_step(edition, ver_mode, include_enterprise2=False, variants=None): sign_args = ' --sign' env = { 'GRAFANA_API_KEY': from_secret('grafana_api_key'), - 'GITHUB_TOKEN': from_secret(github_token), 'GPG_PRIV_KEY': from_secret('gpg_priv_key'), 'GPG_PUB_KEY': from_secret('gpg_pub_key'), 'GPG_KEY_PASSWORD': from_secret('gpg_key_password'), @@ -669,7 +663,7 @@ def package_step(edition, ver_mode, include_enterprise2=False, variants=None): if ver_mode == 'release': cmds = [ '{}./bin/grabpl package --jobs 8 --edition {} '.format(test_args, edition) + \ - '--github-token $${{GITHUB_TOKEN}}{} ${{DRONE_TAG}}'.format( + '{} ${{DRONE_TAG}}'.format( sign_args ), ] @@ -884,7 +878,7 @@ def mysql_integration_tests_step(edition, ver_mode): } -def redis_integration_tests_step(edition, ver_mode): +def redis_integration_tests_step(): deps = [] deps.extend(['grabpl']) return { @@ -901,7 +895,7 @@ def redis_integration_tests_step(edition, ver_mode): } -def memcached_integration_tests_step(edition, ver_mode): +def memcached_integration_tests_step(): deps = [] deps.extend(['grabpl']) return { From adbe37475495a7faad774a29d93b777a41076040 Mon Sep 17 00:00:00 2001 From: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Date: Fri, 15 Apr 2022 09:57:45 +0200 Subject: [PATCH 110/121] Alerting: fix SCC-S1017 linting issue in codebase (#47683) --- pkg/services/alerting/notifiers/hipchat.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 7a21903fb6c..78a141cea7e 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -55,9 +55,7 @@ const ( // for the HipChatNotifier func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) { url := model.Settings.Get("url").MustString() - if strings.HasSuffix(url, "/") { - url = url[:len(url)-1] - } + url = strings.TrimSuffix(url, "/") if url == "" { return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } From 5579dcaa6fa97b719dcbf3d8a67dd8069ef0ee1a Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Fri, 15 Apr 2022 10:33:28 +0200 Subject: [PATCH 111/121] Chore: Fix TeamStore interface (#47810) --- pkg/services/sqlstore/team.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 6e266fed697..46c88f8f0bc 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -21,7 +21,6 @@ type TeamStore interface { RemoveTeamMember(ctx context.Context, cmd *models.RemoveTeamMemberCommand) error GetTeamMembers(ctx context.Context, cmd *models.GetTeamMembersQuery) error GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*models.TeamMemberDTO, error) - AddOrUpdateTeamMember(userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error } func getFilteredUsers(signedInUser *models.SignedInUser, hiddenUsers map[string]struct{}) []string { From 7905957ee836e4aed96256189583a2037f311557 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 15 Apr 2022 10:32:53 +0100 Subject: [PATCH 112/121] Navigation: Add shortcut to add new alert rule to alerting section behind feature toggle (#47713) * Add new alert rule to alerting section * Check access control for ability to create --- pkg/api/index.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index 6f1bf97ee54..ab2c2270e7e 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -233,7 +233,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto uaVisibleForOrg := hs.Cfg.UnifiedAlerting.IsEnabled() && !uaIsDisabledForOrg if setting.AlertingEnabled != nil && *setting.AlertingEnabled || uaVisibleForOrg { - navTree = append(navTree, hs.buildAlertNavLinks(c, uaVisibleForOrg)...) + navTree = append(navTree, hs.buildAlertNavLinks(c, uaVisibleForOrg, hasEditPerm)...) } appLinks, err := hs.getAppLinks(c) @@ -479,7 +479,7 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b return dashboardChildNavs } -func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext, uaVisibleForOrg bool) []*dtos.NavLink { +func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext, uaVisibleForOrg bool, hasEditPerm bool) []*dtos.NavLink { hasAccess := ac.HasAccess(hs.AccessControl, c) var alertChildNavs []*dtos.NavLink @@ -516,6 +516,19 @@ func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext, uaVisibleForOrg b }) } + if hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) { + if uaVisibleForOrg && hasEditPerm && hasAccess(ac.ReqSignedIn, ac.EvalPermission(ac.ActionAlertingRuleCreate)) { + alertChildNavs = append(alertChildNavs, &dtos.NavLink{ + Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true, + }) + + alertChildNavs = append(alertChildNavs, &dtos.NavLink{ + Text: "Alert rule", SubTitle: "Create an alert rule", Id: "alert", + Icon: "plus", Url: hs.Cfg.AppSubURL + "/alerting/new", HideFromTabs: true, ShowIconInNavbar: true, + }) + } + } + if len(alertChildNavs) > 0 { return []*dtos.NavLink{ { From 720c6aac62f12937d068f5265e0208499a1fff35 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 15 Apr 2022 12:05:53 +0100 Subject: [PATCH 113/121] Dashboard: Add vertical spacing between variables when they wrap (#47762) --- public/sass/components/_submenu.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_submenu.scss b/public/sass/components/_submenu.scss index b9c8d3afc73..b253b8b86be 100644 --- a/public/sass/components/_submenu.scss +++ b/public/sass/components/_submenu.scss @@ -4,6 +4,7 @@ flex-wrap: wrap; align-content: flex-start; align-items: flex-start; + gap: $space-sm $space-md; padding: 0 0 $space-sm 0; } @@ -18,7 +19,6 @@ .submenu-item { display: inline-block; - margin-right: 15px; .fa-caret-down { font-size: 75%; From f263cad8ab027216b8736ec7c6e7113b32a12825 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Fri, 15 Apr 2022 14:32:20 +0300 Subject: [PATCH 114/121] Devenv: fix alert_webhook_listener (#44820) --- devenv/docker/blocks/alert_webhook_listener/Dockerfile | 2 +- devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/devenv/docker/blocks/alert_webhook_listener/Dockerfile b/devenv/docker/blocks/alert_webhook_listener/Dockerfile index a6bb87f15f5..84c2b31588e 100644 --- a/devenv/docker/blocks/alert_webhook_listener/Dockerfile +++ b/devenv/docker/blocks/alert_webhook_listener/Dockerfile @@ -2,6 +2,6 @@ FROM golang:latest ADD main.go / WORKDIR / -RUN go build -o main . +RUN go mod init alert_webhook_listener && go build -o main . EXPOSE 3010 ENTRYPOINT ["/main"] diff --git a/devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml b/devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml index 3e6c5389f6c..087ce5bac00 100644 --- a/devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml +++ b/devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml @@ -1,5 +1,4 @@ alert_webhook_listener: build: docker/blocks/alert_webhook_listener - network_mode: host ports: - "3010:3010" From 7ddae870e7f81980b921f06da1af63464aaeea27 Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Fri, 15 Apr 2022 14:01:58 +0200 Subject: [PATCH 115/121] fix status code 200 (#47818) --- pkg/api/admin.go | 2 +- pkg/api/admin_users.go | 2 +- pkg/api/alerting.go | 38 ++++++++++++------------- pkg/api/annotations.go | 8 +++--- pkg/api/apikey.go | 4 +-- pkg/api/comments.go | 4 +-- pkg/api/dashboard.go | 24 ++++++++-------- pkg/api/dashboard_permission.go | 2 +- pkg/api/dashboard_snapshot.go | 12 ++++---- pkg/api/datasources.go | 18 ++++++------ pkg/api/folder.go | 12 ++++---- pkg/api/folder_permission.go | 4 +-- pkg/api/frontendsettings.go | 3 +- pkg/api/index.go | 3 +- pkg/api/ldap_debug.go | 2 +- pkg/api/login.go | 8 +++--- pkg/api/org.go | 8 +++--- pkg/api/org_invite.go | 8 +++--- pkg/api/org_users.go | 10 +++---- pkg/api/password.go | 2 +- pkg/api/playlist.go | 14 ++++----- pkg/api/plugins.go | 10 +++---- pkg/api/preferences.go | 2 +- pkg/api/quota.go | 4 +-- pkg/api/response/response.go | 2 +- pkg/api/search.go | 2 +- pkg/api/short_url.go | 2 +- pkg/api/signup.go | 6 ++-- pkg/api/swagger.go | 8 ++++-- pkg/api/team.go | 6 ++-- pkg/api/team_members.go | 4 +-- pkg/api/user.go | 12 ++++---- pkg/api/user_token.go | 6 ++-- pkg/middleware/middleware_test.go | 4 +-- pkg/middleware/rate_limit_test.go | 2 +- pkg/services/featuremgmt/manager.go | 3 +- pkg/services/libraryelements/api.go | 14 ++++----- pkg/services/live/live.go | 4 +-- pkg/services/searchusers/searchusers.go | 6 ++-- pkg/services/store/http.go | 10 ++++--- pkg/services/thumbs/dummy.go | 7 +++-- pkg/services/thumbs/service.go | 11 +++---- pkg/tsdb/loki/framing_test.go | 2 +- pkg/tsdb/loki/loki_bench_test.go | 3 +- 44 files changed, 166 insertions(+), 152 deletions(-) diff --git a/pkg/api/admin.go b/pkg/api/admin.go index 453db021900..2ebe625d398 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -25,7 +25,7 @@ func (hs *HTTPServer) AdminGetStats(c *models.ReqContext) response.Response { return response.Error(500, "Failed to get admin stats from database", err) } - return response.JSON(200, statsQuery.Result) + return response.JSON(http.StatusOK, statsQuery.Result) } func (hs *HTTPServer) getAuthorizedSettings(ctx context.Context, user *models.SignedInUser, bag setting.SettingsBag) (setting.SettingsBag, error) { diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index c9fa2407533..78393beb864 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -58,7 +58,7 @@ func (hs *HTTPServer) AdminCreateUser(c *models.ReqContext) response.Response { Id: user.Id, } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (hs *HTTPServer) AdminUpdateUserPassword(c *models.ReqContext) response.Response { diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 86a57e8e9a6..4fa1589a9c7 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -54,7 +54,7 @@ func (hs *HTTPServer) GetAlertStatesForDashboard(c *models.ReqContext) response. return response.Error(500, "Failed to fetch alert states", err) } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } // GET /api/alerts @@ -106,7 +106,7 @@ func (hs *HTTPServer) GetAlerts(c *models.ReqContext) response.Response { // if we didn't find any dashboards, return empty result if len(dashboardIDs) == 0 { - return response.JSON(200, []*models.AlertListItemDTO{}) + return response.JSON(http.StatusOK, []*models.AlertListItemDTO{}) } } @@ -132,7 +132,7 @@ func (hs *HTTPServer) GetAlerts(c *models.ReqContext) response.Response { alert.Url = models.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug) } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } // POST /api/alerts/test @@ -176,7 +176,7 @@ func (hs *HTTPServer) AlertTest(c *models.ReqContext) response.Response { dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs()) - return response.JSON(200, dtoRes) + return response.JSON(http.StatusOK, dtoRes) } // GET /api/alerts/:id @@ -191,17 +191,17 @@ func (hs *HTTPServer) GetAlert(c *models.ReqContext) response.Response { return response.Error(500, "List alerts failed", err) } - return response.JSON(200, &query.Result) + return response.JSON(http.StatusOK, &query.Result) } func (hs *HTTPServer) GetAlertNotifiers(ngalertEnabled bool) func(*models.ReqContext) response.Response { return func(_ *models.ReqContext) response.Response { if ngalertEnabled { - return response.JSON(200, notifier.GetAvailableNotifiers()) + return response.JSON(http.StatusOK, notifier.GetAvailableNotifiers()) } // TODO(codesome): This wont be required in 8.0 since ngalert // will be enabled by default with no disabling. This is to be removed later. - return response.JSON(200, alerting.GetNotifiers()) + return response.JSON(http.StatusOK, alerting.GetNotifiers()) } } @@ -217,7 +217,7 @@ func (hs *HTTPServer) GetAlertNotificationLookup(c *models.ReqContext) response. result = append(result, dtos.NewAlertNotificationLookup(notification)) } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (hs *HTTPServer) GetAlertNotifications(c *models.ReqContext) response.Response { @@ -232,7 +232,7 @@ func (hs *HTTPServer) GetAlertNotifications(c *models.ReqContext) response.Respo result = append(result, dtos.NewAlertNotification(notification)) } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (hs *HTTPServer) getAlertNotificationsInternal(c *models.ReqContext) ([]*models.AlertNotification, error) { @@ -267,7 +267,7 @@ func (hs *HTTPServer) GetAlertNotificationByID(c *models.ReqContext) response.Re return response.Error(404, "Alert notification not found", nil) } - return response.JSON(200, dtos.NewAlertNotification(query.Result)) + return response.JSON(http.StatusOK, dtos.NewAlertNotification(query.Result)) } func (hs *HTTPServer) GetAlertNotificationByUID(c *models.ReqContext) response.Response { @@ -288,7 +288,7 @@ func (hs *HTTPServer) GetAlertNotificationByUID(c *models.ReqContext) response.R return response.Error(404, "Alert notification not found", nil) } - return response.JSON(200, dtos.NewAlertNotification(query.Result)) + return response.JSON(http.StatusOK, dtos.NewAlertNotification(query.Result)) } func (hs *HTTPServer) CreateAlertNotification(c *models.ReqContext) response.Response { @@ -309,7 +309,7 @@ func (hs *HTTPServer) CreateAlertNotification(c *models.ReqContext) response.Res return response.Error(500, "Failed to create alert notification", err) } - return response.JSON(200, dtos.NewAlertNotification(cmd.Result)) + return response.JSON(http.StatusOK, dtos.NewAlertNotification(cmd.Result)) } func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Response { @@ -344,7 +344,7 @@ func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Res return response.Error(500, "Failed to get alert notification", err) } - return response.JSON(200, dtos.NewAlertNotification(query.Result)) + return response.JSON(http.StatusOK, dtos.NewAlertNotification(query.Result)) } func (hs *HTTPServer) UpdateAlertNotificationByUID(c *models.ReqContext) response.Response { @@ -376,7 +376,7 @@ func (hs *HTTPServer) UpdateAlertNotificationByUID(c *models.ReqContext) respons return response.Error(500, "Failed to get alert notification", err) } - return response.JSON(200, dtos.NewAlertNotification(query.Result)) + return response.JSON(http.StatusOK, dtos.NewAlertNotification(query.Result)) } func (hs *HTTPServer) fillWithSecureSettingsData(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error { @@ -469,7 +469,7 @@ func (hs *HTTPServer) DeleteAlertNotificationByUID(c *models.ReqContext) respons return response.Error(500, "Failed to delete alert notification", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Notification deleted", "id": cmd.DeletedAlertNotificationId, }) @@ -536,11 +536,11 @@ func (hs *HTTPServer) PauseAlert(c *models.ReqContext) response.Response { if query.Result.State != models.AlertStatePaused && !dto.Paused { result["state"] = "un-paused" result["message"] = "Alert is already un-paused" - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } else if query.Result.State == models.AlertStatePaused && dto.Paused { result["state"] = models.AlertStatePaused result["message"] = "Alert is already paused" - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } cmd := models.PauseAlertCommand{ @@ -562,7 +562,7 @@ func (hs *HTTPServer) PauseAlert(c *models.ReqContext) response.Response { result["state"] = resp result["message"] = "Alert " + pausedState - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } // POST /api/admin/pause-all-alerts @@ -592,5 +592,5 @@ func (hs *HTTPServer) PauseAllAlerts(c *models.ReqContext) response.Response { "alertsAffected": updateCmd.ResultCount, } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 9fd7cc61195..195c931fe76 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -47,7 +47,7 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { } } - return response.JSON(200, items) + return response.JSON(http.StatusOK, items) } type AnnotationError struct { @@ -111,7 +111,7 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response { startID := item.Id - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Annotation added", "id": startID, }) @@ -174,7 +174,7 @@ func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Resp return response.Error(500, "Failed to save Graphite annotation", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Graphite annotation added", "id": item.Id, }) @@ -433,7 +433,7 @@ func (hs *HTTPServer) GetAnnotationTags(c *models.ReqContext) response.Response return response.Error(500, "Failed to find annotation tags", err) } - return response.JSON(200, annotations.GetAnnotationTagsResponse{Result: result}) + return response.JSON(http.StatusOK, annotations.GetAnnotationTagsResponse{Result: result}) } // AnnotationTypeScopeResolver provides an AttributeScopeResolver able to diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 15599f037f6..9d1aab24bf3 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -36,7 +36,7 @@ func (hs *HTTPServer) GetAPIKeys(c *models.ReqContext) response.Response { } } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } // DeleteAPIKey deletes an API key @@ -107,5 +107,5 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response { Key: newKeyInfo.ClientSecret, } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } diff --git a/pkg/api/comments.go b/pkg/api/comments.go index d84d4a8cd8f..7f32d64ac08 100644 --- a/pkg/api/comments.go +++ b/pkg/api/comments.go @@ -23,7 +23,7 @@ func (hs *HTTPServer) commentsGet(c *models.ReqContext) response.Response { } return response.Error(http.StatusInternalServerError, "internal error", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "comments": items, }) } @@ -43,7 +43,7 @@ func (hs *HTTPServer) commentsCreate(c *models.ReqContext) response.Response { } return response.Error(http.StatusInternalServerError, "internal error", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "comment": comment, }) } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 343841cabaf..791a30172ce 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -72,7 +72,7 @@ func (hs *HTTPServer) TrimDashboard(c *models.ReqContext) response.Response { } c.TimeRequest(metrics.MApiDashboardGet) - return response.JSON(200, dto) + return response.JSON(http.StatusOK, dto) } func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { @@ -196,7 +196,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { } c.TimeRequest(metrics.MApiDashboardGet) - return response.JSON(200, dto) + return response.JSON(http.StatusOK, dto) } func (hs *HTTPServer) getAnnotationPermissionsByScope(c *models.ReqContext, actions *dtos.AnnotationActions, scope string) { @@ -282,7 +282,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { hs.log.Error("Failed to broadcast delete info", "dashboard", dash.Uid, "error", err) } } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "title": dash.Title, "message": fmt.Sprintf("Dashboard %s deleted", dash.Title), "id": dash.Id, @@ -396,7 +396,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa } c.TimeRequest(metrics.MApiDashboardSave) - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "status": "success", "slug": dashboard.Slug, "version": dashboard.Version, @@ -417,7 +417,7 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { if prefsQuery.Result.HomeDashboardId == 0 && len(homePage) > 0 { homePageRedirect := dtos.DashboardRedirect{RedirectUri: homePage} - return response.JSON(200, &homePageRedirect) + return response.JSON(http.StatusOK, &homePageRedirect) } if prefsQuery.Result.HomeDashboardId != 0 { @@ -426,7 +426,7 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { if err == nil { url := models.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) dashRedirect := dtos.DashboardRedirect{RedirectUri: url} - return response.JSON(200, &dashRedirect) + return response.JSON(http.StatusOK, &dashRedirect) } hs.log.Warn("Failed to get slug from database", "err", err) } @@ -462,7 +462,7 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { hs.addGettingStartedPanelToHomeDashboard(c, dash.Dashboard) - return response.JSON(200, &dash) + return response.JSON(http.StatusOK, &dash) } func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *models.ReqContext, dash *simplejson.Json) { @@ -530,7 +530,7 @@ func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Respon } } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } // GetDashboardVersion returns the dashboard version with the given ID. @@ -573,7 +573,7 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons CreatedBy: creator, } - return response.JSON(200, dashVersionMeta) + return response.JSON(http.StatusOK, dashVersionMeta) } // POST /api/dashboards/calculate-diff performs diffs on two dashboards @@ -648,10 +648,10 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *models.ReqContext) response.Resp } if options.DiffType == dashdiffs.DiffDelta { - return response.Respond(200, result.Delta).SetHeader("Content-Type", "application/json") + return response.Respond(http.StatusOK, result.Delta).SetHeader("Content-Type", "application/json") } - return response.Respond(200, result.Delta).SetHeader("Content-Type", "text/html") + return response.Respond(http.StatusOK, result.Delta).SetHeader("Content-Type", "text/html") } // RestoreDashboardVersion restores a dashboard to the given version. @@ -703,5 +703,5 @@ func (hs *HTTPServer) GetDashboardTags(c *models.ReqContext) { return } - c.JSON(200, query.Result) + c.JSON(http.StatusOK, query.Result) } diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 34ea0b797f8..0415fe0262e 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -56,7 +56,7 @@ func (hs *HTTPServer) GetDashboardPermissionList(c *models.ReqContext) response. filteredAcls = append(filteredAcls, perm) } - return response.JSON(200, filteredAcls) + return response.JSON(http.StatusOK, filteredAcls) } func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.Response { diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 1d5c45cc5f7..93fa34cd385 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -24,7 +24,7 @@ var client = &http.Client{ } func GetSharingOptions(c *models.ReqContext) { - c.JSON(200, util.DynMap{ + c.JSON(http.StatusOK, util.DynMap{ "externalSnapshotURL": setting.ExternalSnapshotUrl, "externalSnapshotName": setting.ExternalSnapshotName, "externalEnabled": setting.ExternalEnabled, @@ -138,7 +138,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res return nil } - c.JSON(200, util.DynMap{ + c.JSON(http.StatusOK, util.DynMap{ "key": cmd.Key, "deleteKey": cmd.DeleteKey, "url": url, @@ -181,7 +181,7 @@ func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Respon metrics.MApiDashboardSnapshotGet.Inc() - return response.JSON(200, dto).SetHeader("Cache-Control", "public, max-age=3600") + return response.JSON(http.StatusOK, dto).SetHeader("Cache-Control", "public, max-age=3600") } func deleteExternalDashboardSnapshot(externalUrl string) error { @@ -242,7 +242,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r return response.Error(500, "Failed to delete dashboard snapshot", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches.", "id": query.Result.Id, }) @@ -290,7 +290,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res return response.Error(500, "Failed to delete dashboard snapshot", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches.", "id": query.Result.Id, }) @@ -333,5 +333,5 @@ func (hs *HTTPServer) SearchDashboardSnapshots(c *models.ReqContext) response.Re } } - return response.JSON(200, dtos) + return response.JSON(http.StatusOK, dtos) } diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 71e18314d72..61a25517107 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -68,7 +68,7 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response { sort.Sort(result) - return response.JSON(200, &result) + return response.JSON(http.StatusOK, &result) } // GET /api/datasources/:id @@ -102,7 +102,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response // Add accesscontrol metadata dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, datasources.ScopePrefix, dto.UID) - return response.JSON(200, &dto) + return response.JSON(http.StatusOK, &dto) } // DELETE /api/datasources/:id @@ -161,7 +161,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response // Add accesscontrol metadata dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, datasources.ScopePrefix, dto.UID) - return response.JSON(200, &dto) + return response.JSON(http.StatusOK, &dto) } // DELETE /api/datasources/uid/:uid @@ -193,7 +193,7 @@ func (hs *HTTPServer) DeleteDataSourceByUID(c *models.ReqContext) response.Respo hs.Live.HandleDatasourceDelete(c.OrgId, ds.Uid) - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Data source deleted", "id": ds.Id, }) @@ -227,7 +227,7 @@ func (hs *HTTPServer) DeleteDataSourceByName(c *models.ReqContext) response.Resp hs.Live.HandleDatasourceDelete(c.OrgId, getCmd.Result.Uid) - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Data source deleted", "id": getCmd.Result.Id, }) @@ -266,7 +266,7 @@ func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response { } ds := convertModelToDtos(cmd.Result) - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Datasource added", "id": cmd.Result.Id, "name": cmd.Result.Name, @@ -331,7 +331,7 @@ func (hs *HTTPServer) UpdateDataSource(c *models.ReqContext) response.Response { hs.Live.HandleDatasourceUpdate(c.OrgId, datasourceDTO.UID) - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Datasource updated", "id": cmd.Id, "name": cmd.Name, @@ -409,7 +409,7 @@ func (hs *HTTPServer) GetDataSourceByName(c *models.ReqContext) response.Respons } dto := convertModelToDtos(filtered[0]) - return response.JSON(200, &dto) + return response.JSON(http.StatusOK, &dto) } // Get /api/datasources/id/:name @@ -428,7 +428,7 @@ func (hs *HTTPServer) GetDataSourceIdByName(c *models.ReqContext) response.Respo Id: ds.Id, } - return response.JSON(200, &dtos) + return response.JSON(http.StatusOK, &dtos) } // /api/datasources/:id/resources/* diff --git a/pkg/api/folder.go b/pkg/api/folder.go index faee784f8a8..3a14be06d33 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -34,7 +34,7 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { }) } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { @@ -44,7 +44,7 @@ func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { } g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser) - return response.JSON(200, hs.toFolderDto(c.Req.Context(), g, folder)) + return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, folder)) } func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response { @@ -58,7 +58,7 @@ func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response { } g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser) - return response.JSON(200, hs.toFolderDto(c.Req.Context(), g, folder)) + return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, folder)) } func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response { @@ -72,7 +72,7 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response { } g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser) - return response.JSON(200, hs.toFolderDto(c.Req.Context(), g, folder)) + return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, folder)) } func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response { @@ -86,7 +86,7 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response { } g := guardian.New(c.Req.Context(), cmd.Result.Id, c.OrgId, c.SignedInUser) - return response.JSON(200, hs.toFolderDto(c.Req.Context(), g, cmd.Result)) + return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, cmd.Result)) } func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed @@ -103,7 +103,7 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // return apierrors.ToFolderErrorResponse(err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "title": f.Title, "message": fmt.Sprintf("Folder %s deleted", f.Title), "id": f.Id, diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 548201c7ba2..830c71b7afd 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -55,7 +55,7 @@ func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Res filteredAcls = append(filteredAcls, perm) } - return response.JSON(200, filteredAcls) + return response.JSON(http.StatusOK, filteredAcls) } func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Response { @@ -141,7 +141,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res return response.Error(500, "Failed to create permission", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "Folder permissions updated", "id": folder.Id, "title": folder.Title, diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index cc23170a9ed..2c341d53979 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -2,6 +2,7 @@ package api import ( "context" + "net/http" "strconv" "github.com/grafana/grafana/pkg/models" @@ -21,7 +22,7 @@ func (hs *HTTPServer) GetFrontendSettings(c *models.ReqContext) { return } - c.JSON(200, settings) + c.JSON(http.StatusOK, settings) } // getFrontendSettingsMap returns a json object with all the settings needed for front end initialisation. diff --git a/pkg/api/index.go b/pkg/api/index.go index ab2c2270e7e..d7d8f5e133a 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "net/http" "path" "sort" "strings" @@ -750,7 +751,7 @@ func (hs *HTTPServer) Index(c *models.ReqContext) { c.Handle(hs.Cfg, 500, "Failed to get settings", err) return } - c.HTML(200, "index", data) + c.HTML(http.StatusOK, "index", data) } func (hs *HTTPServer) NotFoundHandler(c *models.ReqContext) { diff --git a/pkg/api/ldap_debug.go b/pkg/api/ldap_debug.go index a477bceedd6..97b1e5142fe 100644 --- a/pkg/api/ldap_debug.go +++ b/pkg/api/ldap_debug.go @@ -314,7 +314,7 @@ func (hs *HTTPServer) GetUserFromLDAP(c *models.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "Unable to find the teams for this user", err) } - return response.JSON(200, u) + return response.JSON(http.StatusOK, u) } // splitName receives the full name of a user and splits it into two parts: A name and a surname. diff --git a/pkg/api/login.go b/pkg/api/login.go index 49a33fa5744..b158afcbef8 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -85,7 +85,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { urlParams := c.Req.URL.Query() if _, disableAutoLogin := urlParams["disableAutoLogin"]; disableAutoLogin { hs.log.Debug("Auto login manually disabled") - c.HTML(200, getViewIndex(), viewData) + c.HTML(http.StatusOK, getViewIndex(), viewData) return } @@ -109,7 +109,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { // to login again via OAuth and enter to a redirect loop cookies.DeleteCookie(c.Resp, loginErrorCookieName, hs.CookieOptionsFromCfg) viewData.Settings["loginError"] = loginError - c.HTML(200, getViewIndex(), viewData) + c.HTML(http.StatusOK, getViewIndex(), viewData) return } @@ -144,7 +144,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { return } - c.HTML(200, getViewIndex(), viewData) + c.HTML(http.StatusOK, getViewIndex(), viewData) } func (hs *HTTPServer) tryOAuthAutoLogin(c *models.ReqContext) bool { @@ -167,7 +167,7 @@ func (hs *HTTPServer) tryOAuthAutoLogin(c *models.ReqContext) bool { func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) response.Response { if c.IsSignedIn || c.IsAnonymous { - return response.JSON(200, "Logged in") + return response.JSON(http.StatusOK, "Logged in") } return response.Error(401, "Unauthorized", nil) diff --git a/pkg/api/org.go b/pkg/api/org.go index 21abb8fe5f0..d6cd5b3799b 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -54,7 +54,7 @@ func (hs *HTTPServer) GetOrgByName(c *models.ReqContext) response.Response { }, } - return response.JSON(200, &result) + return response.JSON(http.StatusOK, &result) } func (hs *HTTPServer) getOrgHelper(ctx context.Context, orgID int64) response.Response { @@ -81,7 +81,7 @@ func (hs *HTTPServer) getOrgHelper(ctx context.Context, orgID int64) response.Re }, } - return response.JSON(200, &result) + return response.JSON(http.StatusOK, &result) } // POST /api/orgs @@ -105,7 +105,7 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response { metrics.MApiOrgCreate.Inc() - return response.JSON(200, &util.DynMap{ + return response.JSON(http.StatusOK, &util.DynMap{ "orgId": cmd.Result.Id, "message": "Organization created", }) @@ -226,5 +226,5 @@ func (hs *HTTPServer) SearchOrgs(c *models.ReqContext) response.Response { return response.Error(500, "Failed to search orgs", err) } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 957053455dd..312971cdedc 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -28,7 +28,7 @@ func (hs *HTTPServer) GetPendingOrgInvites(c *models.ReqContext) response.Respon invite.Url = setting.ToAbsUrl("invite/" + invite.Code) } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { @@ -131,7 +131,7 @@ func (hs *HTTPServer) inviteExistingUserToOrg(c *models.ReqContext, user *models } } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName), "userId": user.Id, }) @@ -162,7 +162,7 @@ func (hs *HTTPServer) GetInviteInfoByCode(c *models.ReqContext) response.Respons return response.Error(404, "Invite not found", nil) } - return response.JSON(200, dtos.InviteInfo{ + return response.JSON(http.StatusOK, dtos.InviteInfo{ Email: invite.Email, Name: invite.Name, Username: invite.Email, @@ -225,7 +225,7 @@ func (hs *HTTPServer) CompleteInvite(c *models.ReqContext) response.Response { metrics.MApiUserSignUpCompleted.Inc() metrics.MApiUserSignUpInvite.Inc() - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "User created and logged in", "id": user.Id, }) diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index dcd45bbe523..b10aae80c45 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -64,7 +64,7 @@ func (hs *HTTPServer) addOrgUserHelper(ctx context.Context, cmd models.AddOrgUse return response.Error(500, "Could not add user to organization", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "User added to organization", "userId": cmd.UserId, }) @@ -83,7 +83,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Re return response.Error(500, "Failed to get users for current organization", err) } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } // GET /api/org/users/lookup @@ -109,7 +109,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) respo }) } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } // GET /api/orgs/:orgId/users @@ -130,7 +130,7 @@ func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response { return response.Error(500, "Failed to get users for organization", err) } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *models.GetOrgUsersQuery, signedInUser *models.SignedInUser) ([]*models.OrgUserDTO, error) { @@ -201,7 +201,7 @@ func (hs *HTTPServer) SearchOrgUsersWithPaging(c *models.ReqContext) response.Re query.Result.Page = page query.Result.PerPage = perPage - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } // PATCH /api/org/users/:userId diff --git a/pkg/api/password.go b/pkg/api/password.go index 487c1e1c33d..4454a486001 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -29,7 +29,7 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Resp if err := hs.SQLStore.GetUserByLogin(c.Req.Context(), &userQuery); err != nil { c.Logger.Info("Requested password reset for user that was not found", "user", userQuery.LoginOrEmail) - return response.Error(200, "Email sent", err) + return response.Error(http.StatusOK, "Email sent", err) } emailCmd := models.SendResetPasswordEmailCommand{User: userQuery.Result} diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 5e496c0fd0e..d30f8c58b23 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -54,7 +54,7 @@ func (hs *HTTPServer) SearchPlaylists(c *models.ReqContext) response.Response { return response.Error(500, "Search failed", err) } - return response.JSON(200, searchQuery.Result) + return response.JSON(http.StatusOK, searchQuery.Result) } func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response { @@ -78,7 +78,7 @@ func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response { Items: playlistDTOs, } - return response.JSON(200, dto) + return response.JSON(http.StatusOK, dto) } func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, id int64) ([]models.PlaylistItemDTO, error) { @@ -125,7 +125,7 @@ func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response { return response.Error(500, "Could not load playlist items", err) } - return response.JSON(200, playlistDTOs) + return response.JSON(http.StatusOK, playlistDTOs) } func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Response { @@ -139,7 +139,7 @@ func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Respo return response.Error(500, "Could not load dashboards", err) } - return response.JSON(200, playlists) + return response.JSON(http.StatusOK, playlists) } func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { @@ -153,7 +153,7 @@ func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { return response.Error(500, "Failed to delete playlist", err) } - return response.JSON(200, "") + return response.JSON(http.StatusOK, "") } func (hs *HTTPServer) CreatePlaylist(c *models.ReqContext) response.Response { @@ -167,7 +167,7 @@ func (hs *HTTPServer) CreatePlaylist(c *models.ReqContext) response.Response { return response.Error(500, "Failed to create playlist", err) } - return response.JSON(200, cmd.Result) + return response.JSON(http.StatusOK, cmd.Result) } func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response { @@ -192,5 +192,5 @@ func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response { } cmd.Result.Items = playlistDTOs - return response.JSON(200, cmd.Result) + return response.JSON(http.StatusOK, cmd.Result) } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 25a1dcbfe60..227a6d6fba1 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -111,7 +111,7 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { } sort.Sort(result) - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Response { @@ -163,7 +163,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon dto.HasUpdate = true } - return response.JSON(200, dto) + return response.JSON(http.StatusOK, dto) } func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext) response.Response { @@ -217,7 +217,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response } } - resp := response.Respond(200, content) + resp := response.Respond(http.StatusOK, content) resp.SetHeader("Content-Type", "text/plain; charset=utf-8") return resp } @@ -343,7 +343,7 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response { return response.JSON(503, payload) } - return response.JSON(200, payload) + return response.JSON(http.StatusOK, payload) } // CallResource passes a resource call from a plugin to the backend plugin. @@ -354,7 +354,7 @@ func (hs *HTTPServer) CallResource(c *models.ReqContext) { } func (hs *HTTPServer) GetPluginErrorsList(_ *models.ReqContext) response.Response { - return response.JSON(200, hs.pluginErrorResolver.PluginErrors()) + return response.JSON(http.StatusOK, hs.pluginErrorResolver.PluginErrors()) } func (hs *HTTPServer) InstallPlugin(c *models.ReqContext) response.Response { diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index c15530d8475..3792bee5a90 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -56,7 +56,7 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team dto.QueryHistory = prefsQuery.Result.JsonData.QueryHistory } - return response.JSON(200, &dto) + return response.JSON(http.StatusOK, &dto) } // PUT /api/user/preferences diff --git a/pkg/api/quota.go b/pkg/api/quota.go index cdd8c9762d1..79d304dbf9e 100644 --- a/pkg/api/quota.go +++ b/pkg/api/quota.go @@ -32,7 +32,7 @@ func (hs *HTTPServer) getOrgQuotasHelper(c *models.ReqContext, orgID int64) resp return response.Error(500, "Failed to get org quotas", err) } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } func (hs *HTTPServer) UpdateOrgQuota(c *models.ReqContext) response.Response { @@ -76,7 +76,7 @@ func (hs *HTTPServer) GetUserQuotas(c *models.ReqContext) response.Response { return response.Error(500, "Failed to get org quotas", err) } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } func (hs *HTTPServer) UpdateUserQuota(c *models.ReqContext) response.Response { diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index 58de6282652..e08ed4eb974 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -180,7 +180,7 @@ func JSONStreaming(status int, body interface{}) StreamingResponse { func Success(message string) *NormalResponse { resp := make(map[string]interface{}) resp["message"] = message - return JSON(200, resp) + return JSON(http.StatusOK, resp) } // Error creates an error response. diff --git a/pkg/api/search.go b/pkg/api/search.go index 2f1a2cbf407..a981250d4d4 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -67,7 +67,7 @@ func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { } c.TimeRequest(metrics.MApiDashboardSearch) - return response.JSON(200, searchQuery.Result) + return response.JSON(http.StatusOK, searchQuery.Result) } func (hs *HTTPServer) ListSortOptions(c *models.ReqContext) response.Response { diff --git a/pkg/api/short_url.go b/pkg/api/short_url.go index 180aeedfd73..6e9d1614d1f 100644 --- a/pkg/api/short_url.go +++ b/pkg/api/short_url.go @@ -47,7 +47,7 @@ func (hs *HTTPServer) createShortURL(c *models.ReqContext) response.Response { URL: url, } - return response.JSON(200, dto) + return response.JSON(http.StatusOK, dto) } func (hs *HTTPServer) redirectFromShortURL(c *models.ReqContext) { diff --git a/pkg/api/signup.go b/pkg/api/signup.go index f3568d03daf..69591220c93 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -18,7 +18,7 @@ import ( // GET /api/user/signup/options func GetSignUpOptions(c *models.ReqContext) response.Response { - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "verifyEmailEnabled": setting.VerifyEmailEnabled, "autoAssignOrg": setting.AutoAssignOrg, }) @@ -64,7 +64,7 @@ func (hs *HTTPServer) SignUp(c *models.ReqContext) response.Response { metrics.MApiUserSignUpStarted.Inc() - return response.JSON(200, util.DynMap{"status": "SignUpCreated"}) + return response.JSON(http.StatusOK, util.DynMap{"status": "SignUpCreated"}) } func (hs *HTTPServer) SignUpStep2(c *models.ReqContext) response.Response { @@ -135,7 +135,7 @@ func (hs *HTTPServer) SignUpStep2(c *models.ReqContext) response.Response { metrics.MApiUserSignUpCompleted.Inc() - return response.JSON(200, apiResponse) + return response.JSON(http.StatusOK, apiResponse) } func (hs *HTTPServer) verifyUserSignUpEmail(ctx context.Context, email string, code string) (bool, response.Response) { diff --git a/pkg/api/swagger.go b/pkg/api/swagger.go index 37d88541e72..7c96b576b9f 100644 --- a/pkg/api/swagger.go +++ b/pkg/api/swagger.go @@ -1,7 +1,11 @@ package api -import "github.com/grafana/grafana/pkg/models" +import ( + "net/http" + + "github.com/grafana/grafana/pkg/models" +) func swaggerUI(c *models.ReqContext) { - c.HTML(200, "swagger", nil) + c.HTML(http.StatusOK, "swagger", nil) } diff --git a/pkg/api/team.go b/pkg/api/team.go index 77416f5bce8..79c0eac9982 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -44,7 +44,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response { c.Logger.Warn("Could not add creator to team because is not a real user") } } - return response.JSON(200, &util.DynMap{ + return response.JSON(http.StatusOK, &util.DynMap{ "teamId": team.Id, "message": "Team created", }) @@ -151,7 +151,7 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { query.Result.Page = page query.Result.PerPage = perPage - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } // UserFilter returns the user ID used in a filter when querying a team @@ -198,7 +198,7 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { query.Result.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, "teams:id:", strconv.FormatInt(query.Result.Id, 10)) query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name) - return response.JSON(200, &query.Result) + return response.JSON(http.StatusOK, &query.Result) } // GET /api/teams/:teamId/preferences diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index e39028872e8..a8c44cde66a 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -54,7 +54,7 @@ func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response { filteredMembers = append(filteredMembers, member) } - return response.JSON(200, filteredMembers) + return response.JSON(http.StatusOK, filteredMembers) } // POST /api/teams/:teamId/members @@ -89,7 +89,7 @@ func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response { return response.Error(500, "Failed to add Member to Team", err) } - return response.JSON(200, &util.DynMap{ + return response.JSON(http.StatusOK, &util.DynMap{ "message": "Member added to Team", }) } diff --git a/pkg/api/user.go b/pkg/api/user.go index 95b69d0b462..8b0c59debf7 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -49,7 +49,7 @@ func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) res query.Result.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, "global.users:id:", strconv.FormatInt(userID, 10)) query.Result.AvatarUrl = dtos.GetGravatarUrl(query.Result.Email) - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } // GET /api/users/lookup @@ -73,7 +73,7 @@ func (hs *HTTPServer) GetUserByLoginOrEmail(c *models.ReqContext) response.Respo UpdatedAt: user.Updated, CreatedAt: user.Created, } - return response.JSON(200, &result) + return response.JSON(http.StatusOK, &result) } // POST /api/user @@ -176,7 +176,7 @@ func (hs *HTTPServer) getUserTeamList(ctx context.Context, orgID int64, userID i for _, team := range query.Result { team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name) } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } // GET /api/users/:id/orgs @@ -195,7 +195,7 @@ func (hs *HTTPServer) getUserOrgList(ctx context.Context, userID int64) response return response.Error(500, "Failed to get user organizations", err) } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } func (hs *HTTPServer) validateUsingOrg(ctx context.Context, userID int64, orgID int64) bool { @@ -321,7 +321,7 @@ func (hs *HTTPServer) SetHelpFlag(c *models.ReqContext) response.Response { return response.Error(500, "Failed to update help flag", err) } - return response.JSON(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) + return response.JSON(http.StatusOK, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) } func (hs *HTTPServer) ClearHelpFlags(c *models.ReqContext) response.Response { @@ -334,7 +334,7 @@ func (hs *HTTPServer) ClearHelpFlags(c *models.ReqContext) response.Response { return response.Error(500, "Failed to update help flag", err) } - return response.JSON(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) + return response.JSON(http.StatusOK, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) } func GetAuthProviderLabel(authModule string) string { diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 53b040bd531..edb5c34da41 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -43,7 +43,7 @@ func (hs *HTTPServer) logoutUserFromAllDevicesInternal(ctx context.Context, user return response.Error(500, "Failed to logout user", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "User logged out", }) } @@ -112,7 +112,7 @@ func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int }) } - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd models.RevokeAuthTokenCmd) response.Response { @@ -144,7 +144,7 @@ func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID i return response.Error(500, "Failed to revoke user auth token", err) } - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "message": "User auth token revoked", }) } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 76dbd87b1be..a4fbd7c7629 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -111,7 +111,7 @@ func TestMiddlewareContext(t *testing.T) { NavTree: []*dtos.NavLink{}, } t.Log("Calling HTML", "data", data) - c.HTML(200, "index-template", data) + c.HTML(http.StatusOK, "index-template", data) t.Log("Returned HTML with code 200") } sc.fakeReq("GET", "/").exec() @@ -616,7 +616,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( t.Log("Returning JSON OK") resp := make(map[string]interface{}) resp["message"] = "OK" - c.JSON(200, resp) + c.JSON(http.StatusOK, resp) } } diff --git a/pkg/middleware/rate_limit_test.go b/pkg/middleware/rate_limit_test.go index 641e385018a..88fb7e021c5 100644 --- a/pkg/middleware/rate_limit_test.go +++ b/pkg/middleware/rate_limit_test.go @@ -25,7 +25,7 @@ func rateLimiterScenario(t *testing.T, desc string, rps int, burst int, fn rateL defaultHandler := func(c *models.ReqContext) { resp := make(map[string]interface{}) resp["message"] = "OK" - c.JSON(200, resp) + c.JSON(http.StatusOK, resp) } currentTime := time.Now() diff --git a/pkg/services/featuremgmt/manager.go b/pkg/services/featuremgmt/manager.go index 1b795d0348d..8593e6635cf 100644 --- a/pkg/services/featuremgmt/manager.go +++ b/pkg/services/featuremgmt/manager.go @@ -3,6 +3,7 @@ package featuremgmt import ( "context" "fmt" + "net/http" "reflect" "github.com/grafana/grafana/pkg/infra/log" @@ -157,7 +158,7 @@ func (fm *FeatureManager) HandleGetSettings(c *models.ReqContext) { res["info"] = vv - response.JSON(200, res).WriteTo(c) + response.JSON(http.StatusOK, res).WriteTo(c) } // WithFeatures is used to define feature toggles for testing. diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 8babbe60046..f14c876b8e4 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -35,7 +35,7 @@ func (l *LibraryElementService) createHandler(c *models.ReqContext) response.Res return toLibraryElementError(err, "Failed to create library element") } - return response.JSON(200, LibraryElementResponse{Result: element}) + return response.JSON(http.StatusOK, LibraryElementResponse{Result: element}) } // deleteHandler handles DELETE /api/library-elements/:uid. @@ -45,7 +45,7 @@ func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Res return toLibraryElementError(err, "Failed to delete library element") } - return response.JSON(200, DeleteLibraryElementResponse{ + return response.JSON(http.StatusOK, DeleteLibraryElementResponse{ Message: "Library element deleted", ID: id, }) @@ -58,7 +58,7 @@ func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Respon return toLibraryElementError(err, "Failed to get library element") } - return response.JSON(200, LibraryElementResponse{Result: element}) + return response.JSON(http.StatusOK, LibraryElementResponse{Result: element}) } // getAllHandler handles GET /api/library-elements/. @@ -78,7 +78,7 @@ func (l *LibraryElementService) getAllHandler(c *models.ReqContext) response.Res return toLibraryElementError(err, "Failed to get library elements") } - return response.JSON(200, LibraryElementSearchResponse{Result: elementsResult}) + return response.JSON(http.StatusOK, LibraryElementSearchResponse{Result: elementsResult}) } // patchHandler handles PATCH /api/library-elements/:uid @@ -93,7 +93,7 @@ func (l *LibraryElementService) patchHandler(c *models.ReqContext) response.Resp return toLibraryElementError(err, "Failed to update library element") } - return response.JSON(200, LibraryElementResponse{Result: element}) + return response.JSON(http.StatusOK, LibraryElementResponse{Result: element}) } // getConnectionsHandler handles GET /api/library-panels/:uid/connections/. @@ -103,7 +103,7 @@ func (l *LibraryElementService) getConnectionsHandler(c *models.ReqContext) resp return toLibraryElementError(err, "Failed to get connections") } - return response.JSON(200, LibraryElementConnectionsResponse{Result: connections}) + return response.JSON(http.StatusOK, LibraryElementConnectionsResponse{Result: connections}) } // getByNameHandler handles GET /api/library-elements/name/:name/. @@ -113,7 +113,7 @@ func (l *LibraryElementService) getByNameHandler(c *models.ReqContext) response. return toLibraryElementError(err, "Failed to get library element") } - return response.JSON(200, LibraryElementArrayResponse{Result: elements}) + return response.JSON(http.StatusOK, LibraryElementArrayResponse{Result: elements}) } func toLibraryElementError(err error, message string) response.Response { diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 3924d6f810e..1dba33d32b3 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -1042,14 +1042,14 @@ func (g *GrafanaLive) HandleListHTTP(c *models.ReqContext) response.Response { info := streamChannelListResponse{ Channels: channels, } - return response.JSONStreaming(200, info) + return response.JSONStreaming(http.StatusOK, info) } // HandleInfoHTTP special http response for func (g *GrafanaLive) HandleInfoHTTP(ctx *models.ReqContext) response.Response { path := web.Params(ctx.Req)["*"] if path == "grafana/dashboards/gitops" { - return response.JSON(200, util.DynMap{ + return response.JSON(http.StatusOK, util.DynMap{ "active": g.GrafanaScope.Dashboards.HasGitOpsObserver(ctx.SignedInUser.OrgId), }) } diff --git a/pkg/services/searchusers/searchusers.go b/pkg/services/searchusers/searchusers.go index 79dc1b59196..fbed2051dae 100644 --- a/pkg/services/searchusers/searchusers.go +++ b/pkg/services/searchusers/searchusers.go @@ -1,6 +1,8 @@ package searchusers import ( + "net/http" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" @@ -27,7 +29,7 @@ func (s *OSSService) SearchUsers(c *models.ReqContext) response.Response { return response.Error(500, "Failed to fetch users", err) } - return response.JSON(200, query.Result.Users) + return response.JSON(http.StatusOK, query.Result.Users) } func (s *OSSService) SearchUsersWithPaging(c *models.ReqContext) response.Response { @@ -36,7 +38,7 @@ func (s *OSSService) SearchUsersWithPaging(c *models.ReqContext) response.Respon return response.Error(500, "Failed to fetch users", err) } - return response.JSON(200, query.Result) + return response.JSON(http.StatusOK, query.Result) } func (s *OSSService) SearchUser(c *models.ReqContext) (*models.SearchUsersQuery, error) { diff --git a/pkg/services/store/http.go b/pkg/services/store/http.go index 5d23128a8cf..7334049d395 100644 --- a/pkg/services/store/http.go +++ b/pkg/services/store/http.go @@ -1,6 +1,8 @@ package store import ( + "net/http" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/web" @@ -28,7 +30,7 @@ func (s *httpStorage) Upload(c *models.ReqContext) response.Response { action := "Upload" scope, path := getPathAndScope(c) - return response.JSON(200, map[string]string{ + return response.JSON(http.StatusOK, map[string]string{ "action": action, "scope": scope, "path": path, @@ -39,7 +41,7 @@ func (s *httpStorage) Read(c *models.ReqContext) response.Response { action := "Read" scope, path := getPathAndScope(c) - return response.JSON(200, map[string]string{ + return response.JSON(http.StatusOK, map[string]string{ "action": action, "scope": scope, "path": path, @@ -50,7 +52,7 @@ func (s *httpStorage) Delete(c *models.ReqContext) response.Response { action := "Delete" scope, path := getPathAndScope(c) - return response.JSON(200, map[string]string{ + return response.JSON(http.StatusOK, map[string]string{ "action": action, "scope": scope, "path": path, @@ -67,5 +69,5 @@ func (s *httpStorage) List(c *models.ReqContext) response.Response { if frame == nil { return response.Error(404, "not found", nil) } - return response.JSONStreaming(200, frame) + return response.JSONStreaming(http.StatusOK, frame) } diff --git a/pkg/services/thumbs/dummy.go b/pkg/services/thumbs/dummy.go index 6c984d412ad..769b70443a8 100644 --- a/pkg/services/thumbs/dummy.go +++ b/pkg/services/thumbs/dummy.go @@ -2,6 +2,7 @@ package thumbs import ( "context" + "net/http" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" @@ -39,19 +40,19 @@ func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) func (ds *dummyService) StartCrawler(c *models.ReqContext) response.Response { result := make(map[string]string) result["error"] = "Not enabled" - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (ds *dummyService) StopCrawler(c *models.ReqContext) response.Response { result := make(map[string]string) result["error"] = "Not enabled" - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (ds *dummyService) CrawlerStatus(c *models.ReqContext) response.Response { result := make(map[string]string) result["error"] = "Not enabled" - return response.JSON(200, result) + return response.JSON(http.StatusOK, result) } func (ds *dummyService) Run(ctx context.Context) error { diff --git a/pkg/services/thumbs/service.go b/pkg/services/thumbs/service.go index 4b837baaaa3..45cba2828aa 100644 --- a/pkg/services/thumbs/service.go +++ b/pkg/services/thumbs/service.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "io/ioutil" + "net/http" "time" "github.com/grafana/grafana/pkg/api/response" @@ -192,7 +193,7 @@ func (hs *thumbService) UpdateThumbnailState(c *models.ReqContext) { } hs.log.Info("Updated dashboard thumbnail state", "dashboardUid", req.UID, "theme", req.Theme, "newState", body.State) - c.JSON(200, map[string]string{"success": "true"}) + c.JSON(http.StatusOK, map[string]string{"success": "true"}) } func (hs *thumbService) GetImage(c *models.ReqContext) { @@ -320,7 +321,7 @@ func (hs *thumbService) SetImage(c *models.ReqContext) { return } - c.JSON(200, map[string]int{"OK": len(fileBytes)}) + c.JSON(http.StatusOK, map[string]int{"OK": len(fileBytes)}) } func (hs *thumbService) StartCrawler(c *models.ReqContext) response.Response { @@ -348,7 +349,7 @@ func (hs *thumbService) StartCrawler(c *models.ReqContext) response.Response { return response.Error(500, "error starting", err) } - return response.JSON(200, status) + return response.JSON(http.StatusOK, status) } func (hs *thumbService) StopCrawler(c *models.ReqContext) response.Response { @@ -356,7 +357,7 @@ func (hs *thumbService) StopCrawler(c *models.ReqContext) response.Response { if err != nil { return response.Error(500, "error starting", err) } - return response.JSON(200, msg) + return response.JSON(http.StatusOK, msg) } func (hs *thumbService) CrawlerStatus(c *models.ReqContext) response.Response { @@ -364,7 +365,7 @@ func (hs *thumbService) CrawlerStatus(c *models.ReqContext) response.Response { if err != nil { return response.Error(500, "error starting", err) } - return response.JSON(200, msg) + return response.JSON(http.StatusOK, msg) } // Ideally this service would not require first looking up the full dashboard just to bet the id! diff --git a/pkg/tsdb/loki/framing_test.go b/pkg/tsdb/loki/framing_test.go index 78d8df2cfec..696cc5d3fb6 100644 --- a/pkg/tsdb/loki/framing_test.go +++ b/pkg/tsdb/loki/framing_test.go @@ -59,7 +59,7 @@ func TestSuccessResponse(t *testing.T) { bytes, err := os.ReadFile(responseFileName) require.NoError(t, err) - frames, err := runQuery(context.Background(), makeMockedAPI(200, "application/json", bytes), &test.query) + frames, err := runQuery(context.Background(), makeMockedAPI(http.StatusOK, "application/json", bytes), &test.query) require.NoError(t, err) dr := &backend.DataResponse{ diff --git a/pkg/tsdb/loki/loki_bench_test.go b/pkg/tsdb/loki/loki_bench_test.go index 331b8b533f1..de0b6d50a17 100644 --- a/pkg/tsdb/loki/loki_bench_test.go +++ b/pkg/tsdb/loki/loki_bench_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/rand" + "net/http" "strings" "testing" ) @@ -16,7 +17,7 @@ func BenchmarkMatrixJson(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _, _ = runQuery(context.Background(), makeMockedAPI(200, "application/json", bytes), &lokiQuery{}) + _, _ = runQuery(context.Background(), makeMockedAPI(http.StatusOK, "application/json", bytes), &lokiQuery{}) } } From d4f7ea9f014250bbe4ced0caff37ab5e320c62b4 Mon Sep 17 00:00:00 2001 From: Florian Cassayre Date: Fri, 15 Apr 2022 14:10:34 +0200 Subject: [PATCH 116/121] Chore: Fix typos (#47816) --- public/app/features/live/data/StreamingDataFrame.ts | 2 +- .../dashboards/vMInsightsRG.json | 4 ++-- .../dashboards/vMInsightsWorkspace.json | 4 ++-- .../app/plugins/panel/status-history/StatusHistoryPanel.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/features/live/data/StreamingDataFrame.ts b/public/app/features/live/data/StreamingDataFrame.ts index 0493728dee4..1c076c1c934 100644 --- a/public/app/features/live/data/StreamingDataFrame.ts +++ b/public/app/features/live/data/StreamingDataFrame.ts @@ -301,7 +301,7 @@ export class StreamingDataFrame implements DataFrame { if (values.length !== this.fields.length) { if (this.fields.length) { throw new Error( - `push message mismatch. Expected: ${this.fields.length}, recieved: ${values.length} (labels=${ + `push message mismatch. Expected: ${this.fields.length}, received: ${values.length} (labels=${ this.pushMode === PushMode.labels })` ); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/dashboards/vMInsightsRG.json b/public/app/plugins/datasource/grafana-azure-monitor-datasource/dashboards/vMInsightsRG.json index 5dddc761e15..780b889e14f 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/dashboards/vMInsightsRG.json +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/dashboards/vMInsightsRG.json @@ -1497,7 +1497,7 @@ "subscription": "" } ], - "title": "Max Available Bytes Sent and Trend Line", + "title": "Max Available Bytes Sent and Trend Line", "transformations": [ { "id": "organize", @@ -1979,7 +1979,7 @@ "subscription": "" } ], - "title": "Max Available Bytes Recieved and Trend Line", + "title": "Max Available Bytes Received and Trend Line", "transformations": [ { "id": "organize", diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/dashboards/vMInsightsWorkspace.json b/public/app/plugins/datasource/grafana-azure-monitor-datasource/dashboards/vMInsightsWorkspace.json index ee18e0db44a..551b645f398 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/dashboards/vMInsightsWorkspace.json +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/dashboards/vMInsightsWorkspace.json @@ -1494,7 +1494,7 @@ "subscription": "" } ], - "title": "Max Available Bytes Sent and Trend Line", + "title": "Max Available Bytes Sent and Trend Line", "transformations": [ { "id": "organize", @@ -1970,7 +1970,7 @@ "subscription": "" } ], - "title": "Max Available Bytes Recieved and Trend Line", + "title": "Max Available Bytes Received and Trend Line", "transformations": [ { "id": "organize", diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index d6f2b407a63..4552ac8477e 100755 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -43,7 +43,7 @@ export const StatusHistoryPanel: React.FC = ({

    Too many points to visualize properly.
    - Update the query to return fewer points.
    ({frames[0].length} points recieved) + Update the query to return fewer points.
    ({frames[0].length} points received)

    ); From 67ff5f58151a315c29b4d74847059670c6121941 Mon Sep 17 00:00:00 2001 From: Petros Kolyvas Date: Fri, 15 Apr 2022 10:14:09 -0300 Subject: [PATCH 117/121] Docs: Addition of more prominent link to docker conf (#46504) * Docs: Addition of more prominent link to docker conf Saw a comment on Reddit indicating a lack of information - docs were available, just a little hard to find. Added a note pointing our community to where they might find docker image configuration help. * Update docs/sources/installation/docker.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- docs/sources/installation/docker.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index fbb80fa3eed..c5206ec0ee7 100755 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -16,6 +16,8 @@ You can install and run Grafana using the official Docker images. Our docker ima Each edition is available in two variants: Alpine and Ubuntu. See below. +For documentation regarding the configuration of a docker image, refer to [configure a Grafana Docker image](https://grafana.com/docs/grafana/latest/administration/configure-docker/). + This topic also contains important information about [migrating from earlier Docker image versions](#migrate-from-previous-docker-containers-versions). > **Note:** You can use [Grafana Cloud](https://grafana.com/products/cloud/features/#cloud-logs) to avoid the overhead of installing, maintaining, and scaling your observability stack. The free forever plan includes Grafana, 10K Prometheus series, 50 GB logs, and more.[Create a free account to get started](https://grafana.com/auth/sign-up/create-user?pg=docs-grafana-install&plcmt=in-text). From 45105199411616dc2e93c2a07410a378d86b6b42 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Fri, 15 Apr 2022 17:00:26 +0300 Subject: [PATCH 118/121] Update grabpl (#47824) --- .drone.yml | 70 ++++++++++++++++++------------------ scripts/drone/steps/lib.star | 2 +- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.drone.yml b/.drone.yml index a634bf1ec4d..b28736013e1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -15,7 +15,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -126,7 +126,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -377,7 +377,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -445,7 +445,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -533,7 +533,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -622,7 +622,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -731,7 +731,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1163,7 +1163,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1235,7 +1235,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -1322,7 +1322,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1406,7 +1406,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1712,7 +1712,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1836,7 +1836,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1913,7 +1913,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -1972,7 +1972,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2329,7 +2329,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2508,7 +2508,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2632,7 +2632,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -2707,7 +2707,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2785,7 +2785,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2846,7 +2846,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2925,7 +2925,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2987,7 +2987,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3025,7 +3025,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3063,7 +3063,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3113,7 +3113,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3180,7 +3180,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3456,7 +3456,7 @@ steps: name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3574,7 +3574,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3645,7 +3645,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -3693,7 +3693,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4048,7 +4048,7 @@ services: [] steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4218,7 +4218,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -4333,7 +4333,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.36/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.37/windows/grabpl.exe -OutFile grabpl.exe - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - cd grafana-enterprise @@ -4530,6 +4530,6 @@ kind: secret name: gcp_upload_artifacts_key --- kind: signature -hmac: 203f82c4bb5060b26ecabb9a5d979ad3ae1eea3c586afddb083d685bbd4328f8 +hmac: 803ec2a67b1758ce82ddda8ba1fb276fd23e4cfbfc3c5736bf2fa9e98edc2e6b ... diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 77e7391394e..55c861d9968 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1,6 +1,6 @@ load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket') -grabpl_version = 'v2.9.36' +grabpl_version = 'v2.9.37' build_image = 'grafana/build-container:1.5.3' publish_image = 'grafana/grafana-ci-deploy:1.3.1' deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image' From 031a8e140a0358cdf415f1b17b28e2ea170637ae Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 15 Apr 2022 07:48:44 -0700 Subject: [PATCH 119/121] Chore: upgrade gocloud.dev and go mod tidy (#47797) --- go.mod | 51 ++--- go.sum | 271 ++++++++++++++++------- pkg/components/imguploader/s3uploader.go | 2 + 3 files changed, 213 insertions(+), 111 deletions(-) diff --git a/go.mod b/go.mod index dee9c1e4cbd..c5e28247313 100644 --- a/go.mod +++ b/go.mod @@ -14,14 +14,14 @@ replace k8s.io/client-go => k8s.io/client-go v0.22.1 replace github.com/russellhaering/goxmldsig@v1.1.0 => github.com/russellhaering/goxmldsig v1.1.1 require ( - cloud.google.com/go/storage v1.18.2 + cloud.google.com/go/storage v1.21.0 cuelang.org/go v0.4.1 github.com/Azure/azure-sdk-for-go v59.3.0+incompatible github.com/Azure/go-autorest/autorest v0.11.22 github.com/BurntSushi/toml v0.3.1 github.com/Masterminds/semver v1.5.0 github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f - github.com/aws/aws-sdk-go v1.42.8 + github.com/aws/aws-sdk-go v1.43.31 github.com/beevik/etree v1.1.0 github.com/benbjohnson/clock v1.1.0 github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b @@ -29,7 +29,7 @@ require ( github.com/cortexproject/cortex v1.10.1-0.20211014125347-85c378182d0d github.com/crewjam/saml v0.4.6-0.20210521115923-29c6295245bd github.com/davecgh/go-spew v1.1.1 - github.com/denisenkom/go-mssqldb v0.11.0 + github.com/denisenkom/go-mssqldb v0.12.0 github.com/dop251/goja v0.0.0-20210804101310-32956a348b49 github.com/fatih/color v1.13.0 github.com/gchaincl/sqlhooks v1.3.0 @@ -93,7 +93,7 @@ require ( github.com/unknwon/com v1.0.1 github.com/urfave/cli/v2 v2.3.0 github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae - github.com/weaveworks/common v0.0.0-20210913144402-035033b78a78 + github.com/weaveworks/common v0.0.0-20210913144402-035033b78a78 // indirect github.com/xorcare/pointer v1.1.0 github.com/yudai/gojsondiff v1.0.0 go.opentelemetry.io/collector v0.31.0 @@ -102,17 +102,17 @@ require ( go.opentelemetry.io/otel/exporters/jaeger v1.0.0 go.opentelemetry.io/otel/sdk v1.0.0 go.opentelemetry.io/otel/trace v1.6.3 - golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 golang.org/x/exp v0.0.0-20210220032938-85be41e4509f - golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect - golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 + golang.org/x/net v0.0.0-20220401154927-543a649e0bdd // indirect + golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 + golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 golang.org/x/tools v0.1.9 gonum.org/v1/gonum v0.11.0 - google.golang.org/api v0.62.0 - google.golang.org/grpc v1.42.0 - google.golang.org/protobuf v1.27.1 + google.golang.org/api v0.74.0 + google.golang.org/grpc v1.45.0 + google.golang.org/protobuf v1.28.0 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.66.2 gopkg.in/ldap.v3 v3.1.0 @@ -171,7 +171,7 @@ require ( github.com/go-openapi/validate v0.20.2 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/status v1.1.0 // indirect - github.com/golang-jwt/jwt/v4 v4.2.0 // indirect + github.com/golang-jwt/jwt/v4 v4.4.1 // indirect github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -179,7 +179,7 @@ require ( github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/google/btree v1.0.1 // indirect github.com/google/flatbuffers v2.0.0+incompatible // indirect - github.com/googleapis/gax-go/v2 v2.1.1 + github.com/googleapis/gax-go/v2 v2.2.0 github.com/gorilla/mux v1.8.0 // indirect github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect @@ -232,30 +232,29 @@ require ( go.mongodb.org/mongo-driver v1.7.0 // indirect go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.9.0 - go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 // indirect - golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect + go.uber.org/goleak v1.1.11 // indirect + golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa + google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect ) require ( - cloud.google.com/go/kms v1.1.0 + cloud.google.com/go/kms v1.4.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2 github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.4.0 github.com/Azure/go-autorest/autorest/adal v0.9.17 github.com/golang-migrate/migrate/v4 v4.7.0 github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f github.com/grafana/thema v0.0.0-20220413232647-fc54c169b508 - gocloud.dev v0.24.0 + gocloud.dev v0.25.0 ) require ( - cloud.google.com/go v0.99.0 // indirect + cloud.google.com/go v0.100.2 // indirect github.com/armon/go-metrics v0.3.10 // indirect - github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/memberlist v0.3.0 // indirect @@ -265,23 +264,21 @@ require ( ) require ( + cloud.google.com/go/compute v1.5.0 // indirect + cloud.google.com/go/iam v0.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect - github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect - github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect github.com/containerd/containerd v1.5.9 // indirect github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac // indirect - github.com/envoyproxy/go-control-plane v0.10.1 // indirect - github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect github.com/getkin/kin-openapi v0.94.0 // indirect github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt v3.2.1+incompatible // indirect github.com/imdario/mergo v0.3.12 // indirect - github.com/klauspost/compress v1.13.6 // indirect + github.com/klauspost/compress v1.15.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pierrec/lz4/v4 v4.1.8 // indirect @@ -299,7 +296,3 @@ replace github.com/hashicorp/consul => github.com/hashicorp/consul v1.10.2 // Upgrading affects backend plugins: https://github.com/grafana/grafana/pull/47653#discussion_r850508593 // No harm to Thema because it's only a dependency in its main package. replace github.com/hashicorp/go-hclog => github.com/hashicorp/go-hclog v0.16.1 - -// TODO: remove once gocloud.dev releases 0.25.x -// `fileblob` implementation has buggy key ordering in 0.24.0 -replace gocloud.dev v0.24.0 => github.com/google/go-cloud v0.24.1-0.20220209172924-99801bbb523a diff --git a/go.sum b/go.sum index 8756dd4d304..291c9de9a53 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,10 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= -cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -43,29 +45,41 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7 cloud.google.com/go/bigtable v1.1.0/go.mod h1:B6ByKcIdYmhoyDzmOnQxyOhN6r05qnewYIxxG6L0/b4= cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= cloud.google.com/go/bigtable v1.3.0/go.mod h1:z5EyKrPE8OQmeg4h5MNdKvuSnI9CCT49Ki3f23aBzio= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiLM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= +cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/kms v1.0.0/go.mod h1:nhUehi+w7zht2XrUfvTRNpxrfayBHqP4lu2NSywui/0= -cloud.google.com/go/kms v1.1.0 h1:1yc4rLqCkVDS9Zvc7m+3mJ47kw0Uo5Q5+sMjcmUVUeM= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= +cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= +cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.17.1/go.mod h1:4qDxMr1WsM9+aQAz36ltDwCIM+R0QdlseyFjBuNvnss= -cloud.google.com/go/secretmanager v1.0.0/go.mod h1:+Qkm5qxIJ5mk74xxIXA+87fseaY1JLYBcFPQoc/GQxg= +cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= +cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.3.0/go.mod h1:9IAwXhoyBJ7z9LcAwkj0/7NnPzYaPeZxxVp3zm+5IqA= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.18.2 h1:5NQw6tOn3eMm0oE8vTkfjau18kjL79FlMjy/CHTpmoY= -cloud.google.com/go/storage v1.18.2/go.mod h1:AiIj7BWXyhO5gGVmYJ+S8tbkCx3yb0IMjua8Aw4naVM= +cloud.google.com/go/storage v1.21.0 h1:HwnT2u2D309SFDHQII6m18HlrCi3jAXhUMTLOWXYH14= +cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= +cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= code.cloudfoundry.org/clock v1.0.0/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= @@ -175,7 +189,6 @@ github.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVF github.com/Azure/go-autorest/autorest/azure/auth v0.5.9/go.mod h1:hg3/1yw0Bq87O3KvvnJoAh34/0zbP7SFizX/qN5JvjU= github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw= github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.4/go.mod h1:yAQ2b6eP/CmLPnmLvxtT1ALIY3OR1oFcCqVBi8vHiTc= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= @@ -218,7 +231,7 @@ github.com/FZambia/eagle v0.0.1 h1:FN1yTkPihMb5nE8SrlRjoCf7T9H9bTKJFQOm6ach2YU= github.com/FZambia/eagle v0.0.1/go.mod h1:xq6u/JeNZ5/8mrAQ76MMhzNTodASh9FavQlCgg4j48w= github.com/FZambia/sentinel v1.1.0 h1:qrCBfxc8SvJihYNjBWgwUI93ZCvFe/PJIPTHKmlp8a8= github.com/FZambia/sentinel v1.1.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= -github.com/GoogleCloudPlatform/cloudsql-proxy v1.27.0/go.mod h1:bn9iHmAjogMoIPkqBGyJ9R1m9cXGCjBE/cuhBs3oEsQ= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= github.com/HdrHistogram/hdrhistogram-go v0.9.0/go.mod h1:nxrse8/Tzg2tg3DZcZjm6qEclQKK70g0KxO61gFFZD4= github.com/HdrHistogram/hdrhistogram-go v1.0.1/go.mod h1:BWJ+nMSHY3L41Zj7CA3uXnloDp7xxV0YvstAE7nKTaM= github.com/HdrHistogram/hdrhistogram-go v1.1.0 h1:6dpdDPTRoo78HxAJ6T1HfMiKSnqhgRRqzCuPshRkQ7I= @@ -373,49 +386,51 @@ github.com/aws/aws-sdk-go v1.38.60/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.40.11/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.40.37/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= -github.com/aws/aws-sdk-go v1.42.8 h1:Tj2RP4Fas1mYchwbmw0qWLJIEATAseyp5iTa1D+LWYQ= -github.com/aws/aws-sdk-go v1.42.8/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go v1.43.31 h1:yJZIr8nMV1hXjAvvOLUFqZRJcHV7udPQBfhJqawDzI0= +github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4= -github.com/aws/aws-sdk-go-v2 v1.11.0 h1:HxyD62DyNhCfiFGUHqJ/xITD6rAjJ7Dm/2nLxLmO4Ag= -github.com/aws/aws-sdk-go-v2 v1.11.0/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 h1:yVUAwvJC/0WNPbyl0nA3j1L6CW1CN8wBubCRqtG7JLI= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0/go.mod h1:Xn6sxgRuIDflLRJFj5Ev7UxABIkNbccFPV/p8itDReM= -github.com/aws/aws-sdk-go-v2/config v1.10.1 h1:z/ViqIjW6ZeuLWgTWMTSyZzaVWo/1cWeVf1Uu+RF01E= -github.com/aws/aws-sdk-go-v2/config v1.10.1/go.mod h1:auIv5pIIn3jIBHNRcVQcsczn6Pfa6Dyv80Fai0ueoJU= -github.com/aws/aws-sdk-go-v2/credentials v1.6.1 h1:A39JYth2fFCx+omN/gib/jIppx3rRnt2r7UKPq7Mh5Y= -github.com/aws/aws-sdk-go-v2/credentials v1.6.1/go.mod h1:QyvQk1IYTqBWSi1T6UgT/W8DMxBVa5pVuLFSRLLhGf8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.0 h1:OpZjuUy8Jt3CA1WgJgBC5Bz+uOjE5Ppx4NFTRaooUuA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.0/go.mod h1:5E1J3/TTYy6z909QNR0QnXGBpfESYGDqd3O0zqONghU= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.1 h1:p9Dys1g2YdaqMalnp6AwCA+tpMMdJNGw5YYKP/u3sUk= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.1/go.mod h1:wN/mvkow08GauDwJ70jnzJ1e+hE+Q3Q7TwpYLXOe9oI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.0 h1:zY8cNmbBXt3pzjgWgdIbzpQ6qxoCwt+Nx9JbrAf2mbY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.0/go.mod h1:NO3Q5ZTTQtO2xIg2+xTXYDiT7knSejfeDm7WGDaOo0U= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.0 h1:Z3aR/OXBnkYK9zXkNkfitHX6SmUBzSsx8VMHbH4Lvhw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.0/go.mod h1:anlUzBoEWglcUxUQwZA7HQOEVEnQALVZsizAapB2hq8= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.0 h1:c10Z7fWxtJCoyc8rv06jdh9xrKnu7bAJiRaKWvTb2mU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.0/go.mod h1:6oXGy4GLpypD3uCh8wcqztigGgmhLToMfjavgh+VySg= +github.com/aws/aws-sdk-go-v2 v1.16.2 h1:fqlCk6Iy3bnCumtrLz9r3mJ/2gUT0pJ0wLFVIdWh+JA= +github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 h1:SdK4Ppk5IzLs64ZMvr6MrSficMtjY2oS0WOORXTlxwU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= +github.com/aws/aws-sdk-go-v2/config v1.15.3 h1:5AlQD0jhVXlGzwo+VORKiUuogkG7pQcLJNzIzK7eodw= +github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= +github.com/aws/aws-sdk-go-v2/credentials v1.11.2 h1:RQQ5fzclAKJyY5TvF+fkjJEwzK4hnxQCLOu5JXzDmQo= +github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3 h1:LWPg5zjHV9oz/myQr4wMs0gi4CjnDN/ILmyZUFYXZsU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3 h1:ir7iEq78s4txFGgwcLqD6q9IIPzTQNRJXulJd9h/zQo= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9 h1:onz/VaaxZ7Z4V+WIN9Txly9XLTmoOh1oJ8XcAC3pako= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3 h1:9stUQR/u2KXU6HkFJYlqnZEjBnbgrVbG6I5HN09xZh0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10 h1:by9P+oy3P/CwggN4ClnW2D4oL91QV7pBzBICi1chZvQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.5.0/go.mod h1:acH3+MQoiMzozT/ivU+DbRg7Ooo2298RdRaWcOv+4vM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 h1:lPLbw4Gn59uoKqvOfSnkJr54XWk5Ak1NK20ZEiSWb3U= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0/go.mod h1:80NaCIH9YU3rzTTs/J/ECATjXuRqzo/wB6ukO6MZ0XY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.0 h1:qGZWS/WgiFY+Zgad2u0gwBHpJxz6Ne401JE7iQI1nKs= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.0/go.mod h1:Mq6AEc+oEjCUlBuLiK5YwW4shSOAKCQ3tXN0sQeYoBA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.0 h1:0BOlTqnNnrEO04oYKzDxMMe68t107pmIotn18HtVonY= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.0/go.mod h1:xKCZ4YFSF2s4Hnb/J0TLeOsKuGzICzcElaOKNGrVnx4= -github.com/aws/aws-sdk-go-v2/service/kms v1.10.0/go.mod h1:ZkHWL8m5Nw1g9yMXqpCjnIJtSDToAmNbXXZ9gj0bO7s= -github.com/aws/aws-sdk-go-v2/service/s3 v1.19.0 h1:5mRAms4TjSTOGYsqKYte5kHr1PzpMJSyLThjF3J+hw0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.19.0/go.mod h1:Gwz3aVctJe6mUY9T//bcALArPUaFmNAy2rTB9qN4No8= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.10.0/go.mod h1:qAgsrzF3Z2vvV01j79fs7D75ofCMQe81/OKBJx0rjFY= -github.com/aws/aws-sdk-go-v2/service/sns v1.11.0/go.mod h1:LIPf3BTbSY5UeVli+x/1y2Qw1w8T9DYyp7p18Qt8Zc8= -github.com/aws/aws-sdk-go-v2/service/sqs v1.12.0/go.mod h1:TDqDmQnsbgL2ZMIGUf3z9xTzCMqFX7FP1geAgIlYqvA= -github.com/aws/aws-sdk-go-v2/service/ssm v1.15.0/go.mod h1:kJa2uHklY03rKsNSbEsToeUgWJ1PambXBtRNacorRhg= -github.com/aws/aws-sdk-go-v2/service/sso v1.6.0 h1:JDgKIUZOmLFu/Rv6zXLrVTWCmzA0jcTdvsT8iFIKrAI= -github.com/aws/aws-sdk-go-v2/service/sso v1.6.0/go.mod h1:Q/l0ON1annSU+mc0JybDy1Gy6dnJxIcWjphO6qJPzvM= -github.com/aws/aws-sdk-go-v2/service/sts v1.10.0 h1:1jh8J+JjYRp+QWKOsaZt7rGUgoyrqiiVwIm+w0ymeUw= -github.com/aws/aws-sdk-go-v2/service/sts v1.10.0/go.mod h1:jLKCFqS+1T4i7HDqCP9GM4Uk75YW1cS0o82LdxpMyOE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 h1:T4pFel53bkHjL2mMo+4DKE6r6AuoZnM0fg7k1/ratr4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3 h1:I0dcwWitE752hVSMrsLCxqNQ+UdEp3nACx2bYNMQq+k= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3 h1:Gh1Gpyh01Yvn7ilO/b/hr01WgNpaszfbKMUgqM186xQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3 h1:BKjwCJPnANbkwQ8vzSbaZDKawwagDubrH/z/c0X+kbQ= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= +github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= +github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3 h1:rMPtwA7zzkSQZhhz9U3/SoIDz/NZ7Q+iRn4EIO8rSyU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= +github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= +github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= +github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.3 h1:frW4ikGcxfAEDfmQqWgMLp+F1n4nRo9sF39OcIb5BkQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.3 h1:cJGRyzCSVwZC7zZZ1xbx9m32UnrKydRYhOvcD1NYP9Q= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/smithy-go v1.5.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58= -github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE= +github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= @@ -476,7 +491,6 @@ github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/centrifugal/centrifuge v0.19.0 h1:YHws0dRpgsBiI73tRl1wwaB13gzuaI1AM4IFcQQQqcw= github.com/centrifugal/centrifuge v0.19.0/go.mod h1:O2elf8q3Qkie3z97wkqVqxB52pnOpPsfFUa7L88Lpy0= @@ -517,7 +531,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -643,6 +656,7 @@ github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= @@ -1163,9 +1177,9 @@ github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= +github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= @@ -1235,8 +1249,6 @@ github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cloud v0.24.1-0.20220209172924-99801bbb523a h1:Kw18HR30Firrm4cB1BzcVVaFrrdDlAtnrCKgHxemgMI= -github.com/google/go-cloud v0.24.1-0.20220209172924-99801bbb523a/go.mod h1:TqAL9Q5Q8jFUfIDNbDiFbU3w0z3MQ7q4r1wm57tBhmI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -1257,7 +1269,7 @@ github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:od github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= -github.com/google/go-replayers/httpreplay v1.0.0/go.mod h1:LJhKoTwS5Wy5Ld/peq8dFFG5OfJyHEz7ft+DsTUv25M= +github.com/google/go-replayers/httpreplay v1.1.1/go.mod h1:gN9GeLIs7l6NUoVaSSnv2RiqK1NiwAmD0MrKeC9IIks= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -1268,8 +1280,9 @@ github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible h1:x github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -1313,8 +1326,9 @@ github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0 h1:s7jOdKSaksJVOxE0Y/S32otcfiP+UQ0cL8/GTKaONwE= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= @@ -1612,10 +1626,48 @@ github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7Ua github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jaegertracing/jaeger v1.24.0/go.mod h1:mqdtFDA447va5j0UewDaAWyNlGreGQyhGxXVhbF58gQ= github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= @@ -1706,8 +1758,8 @@ github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= +github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= @@ -1756,10 +1808,11 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/leodido/ragel-machinery v0.0.0-20181214104525-299bdde78165/go.mod h1:WZxr2/6a/Ar9bMDc2rN/LJrE/hF6bXE4LPyDSIxwAfg= github.com/leoluk/perflib_exporter v0.1.0/go.mod h1:rpV0lYj7lemdTm31t7zpCqYqPnw7xs86f+BaaNBVYFM= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= @@ -1808,6 +1861,7 @@ github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxq github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -1820,8 +1874,10 @@ github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HN github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.0-20191113090002-7c0f6868bffe/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-ieproxy v0.0.3/go.mod h1:6ZpRmhBaYuBX1U2za+9rC9iCGLsSp2tftelZne7CPko= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= @@ -1906,7 +1962,6 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1-0.20210112042008-8ebf2d61a8b4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= @@ -2284,6 +2339,8 @@ github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.4.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russellhaering/goxmldsig v1.1.0/go.mod h1:QK8GhXPB3+AfuCrfo0oRISa9NfzeCpWmxeGnqEpDF9o= github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5sesSBsebIxM= github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= @@ -2586,6 +2643,7 @@ github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJB github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -2689,14 +2747,14 @@ go.uber.org/automaxprocs v1.2.0/go.mod h1:YfO3fm683kQpzETxlTGZhGIVmXAhaw3gxeBADb go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q= go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -2706,10 +2764,12 @@ go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc= go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk= +gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180505025534-4ec37c66abab/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -2721,6 +2781,7 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -2751,6 +2812,7 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -2758,11 +2820,12 @@ golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2912,8 +2975,12 @@ golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= -golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220401154927-543a649e0bdd h1:zYlwaUHTmxuf6H7hwO2dgwqozQmH7zf4x+/qql4oVWc= +golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2936,8 +3003,10 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -3109,18 +3178,26 @@ golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -3144,8 +3221,9 @@ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -3169,6 +3247,7 @@ golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -3181,6 +3260,7 @@ golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190813034749-528a2984e271/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190918214516-5a1a30219888/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -3255,6 +3335,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -3315,10 +3397,18 @@ google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqiv google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= -google.golang.org/api v0.60.0/go.mod h1:d7rl65NZAkEQ90JFzqBjcRq1TVeG5ZoGV3sSpEnnVb4= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.62.0 h1:PhGymJMXfGBzc4lBRmrx9+1w4w2wEzURHNGF/sD/xGc= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM= +google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.68.0/go.mod h1:sOM8pTpwgflXRhz+oC8H2Dr+UcbMqkPPWNJo88Q7TH8= +google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7C80= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0 h1:ExR2D+5TYIrMphWgs5JCgwRhEDlPDXXrLwHHMgPHTXE= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -3415,17 +3505,30 @@ google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211019152133-63b7e35f4404/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de h1:9Ti5SG2U4cAcluryUo/sFay3TQKoxiFMfaT0pbizU7k= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -3467,8 +3570,10 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -3483,8 +3588,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= @@ -3513,6 +3619,7 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8 gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= diff --git a/pkg/components/imguploader/s3uploader.go b/pkg/components/imguploader/s3uploader.go index 9cb0a7f7c57..7228ac85a75 100644 --- a/pkg/components/imguploader/s3uploader.go +++ b/pkg/components/imguploader/s3uploader.go @@ -114,6 +114,8 @@ func webIdentityProvider(sess client.ConfigProvider) credentials.Provider { roleARN := os.Getenv("AWS_ROLE_ARN") tokenFilepath := os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE") roleSessionName := os.Getenv("AWS_ROLE_SESSION_NAME") + + // nolint:staticcheck return stscreds.NewWebIdentityRoleProvider(svc, roleARN, roleSessionName, tokenFilepath) } From 5e11af01211481407cbbe11d6e0777b53e39b16c Mon Sep 17 00:00:00 2001 From: sam boyer Date: Fri, 15 Apr 2022 13:41:07 -0400 Subject: [PATCH 120/121] schema: Finish converting dashboard schema datasource references to objects (#47806) * coremodel: finish string -> object datasource ref Seems we missed updating a couple of the datasource references from strings to objects. * cue fmt * Also fix dashboard in scuemata dashboard schema * Update devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json Co-authored-by: Ryan McKinley Co-authored-by: Ryan McKinley --- devenv/dev-dashboards/dashboards_test.go | 2 +- .../panel-graph/graph-ng-stacking2.json | 85 +- .../src/scuemata/dashboard/dashboard.cue | 759 +++++++++--------- pkg/coremodel/dashboard/lineage.cue | 585 +++++++------- pkg/coremodel/dashboard/schema.go | 11 +- pkg/schema/load/load_test.go | 6 +- scripts/stripnulls.sh | 2 +- 7 files changed, 723 insertions(+), 727 deletions(-) diff --git a/devenv/dev-dashboards/dashboards_test.go b/devenv/dev-dashboards/dashboards_test.go index 66f29040f84..e330b53bb0f 100644 --- a/devenv/dev-dashboards/dashboards_test.go +++ b/devenv/dev-dashboards/dashboards_test.go @@ -68,7 +68,7 @@ func themaTestableDashboards() (map[string][]byte, error) { jtree := make(map[string]interface{}) json.Unmarshal(b, &jtree) - if oldschemav, has := jtree["schemaVersion"]; !has || !(oldschemav.(float64) > 32) { + if oldschemav, has := jtree["schemaVersion"]; !has || !(oldschemav.(float64) > dashboard.HandoffSchemaVersion-1) { return nil } diff --git a/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json b/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json index 29bda45b50f..410540966f0 100644 --- a/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json +++ b/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json @@ -71,8 +71,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -178,8 +177,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -266,8 +264,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -354,8 +351,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -485,8 +481,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -573,8 +568,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -661,8 +655,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -792,8 +785,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -893,8 +885,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -982,8 +973,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1069,8 +1059,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1155,8 +1144,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1253,8 +1241,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1354,8 +1341,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1455,8 +1441,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1555,8 +1540,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1692,8 +1676,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1800,8 +1783,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1909,8 +1891,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2040,8 +2021,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2128,8 +2108,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2240,8 +2219,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2356,8 +2334,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2460,8 +2437,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2564,8 +2540,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2701,8 +2676,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2838,8 +2812,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2934,7 +2907,7 @@ } ], "refresh": false, - "schemaVersion": 32, + "schemaVersion": 36, "style": "dark", "tags": [ "gdev", @@ -2954,4 +2927,4 @@ "uid": "1KxMUdE7k", "version": 1, "weekStart": "" -} \ No newline at end of file +} diff --git a/packages/grafana-schema/src/scuemata/dashboard/dashboard.cue b/packages/grafana-schema/src/scuemata/dashboard/dashboard.cue index cb7e767ca01..9040f0a28c3 100644 --- a/packages/grafana-schema/src/scuemata/dashboard/dashboard.cue +++ b/packages/grafana-schema/src/scuemata/dashboard/dashboard.cue @@ -1,434 +1,441 @@ package dashboard import ( - "list" + "list" - "github.com/grafana/grafana/cue/scuemata" + "github.com/grafana/grafana/cue/scuemata" ) Family: scuemata.#Family & { lineages: [ [ - { // 0.0 - // Unique numeric identifier for the dashboard. - // TODO must isolate or remove identifiers local to a Grafana instance...? - id?: number - // Unique dashboard identifier that can be generated by anyone. string (8-40) - uid?: string - // Title of dashboard. - title?: string - // Description of dashboard. - description?: string + {// 0.0 + // Unique numeric identifier for the dashboard. + // TODO must isolate or remove identifiers local to a Grafana instance...? + id?: number + // Unique dashboard identifier that can be generated by anyone. string (8-40) + uid?: string + // Title of dashboard. + title?: string + // Description of dashboard. + description?: string - gnetId?: string - // Tags associated with dashboard. - tags?: [...string] - // Theme of dashboard. - style: *"light" | "dark" - // Timezone of dashboard, - timezone?: *"browser" | "utc" | "" - // Whether a dashboard is editable or not. - editable: bool | *true - // 0 for no shared crosshair or tooltip (default). - // 1 for shared crosshair. - // 2 for shared crosshair AND shared tooltip. - graphTooltip: >=0 & <=2 | *0 - // Time range for dashboard, e.g. last 6 hours, last 7 days, etc - time?: { - from: string | *"now-6h" - to: string | *"now" - } - // Timepicker metadata. - timepicker?: { - // Whether timepicker is collapsed or not. - collapse: bool | *false - // Whether timepicker is enabled or not. - enable: bool | *true - // Whether timepicker is visible or not. - hidden: bool | *false - // Selectable intervals for auto-refresh. - refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] - } - // Templating. - templating?: list: [...{...}] - // Annotations. - annotations?: list: [...{ - builtIn: number | *0 - // Datasource to use for annotation. - datasource: string - // Whether annotation is enabled. - enable: bool | *true - // Whether to hide annotation. - hide?: bool | *false - // Annotation icon color. - iconColor?: string - // Name of annotation. - name?: string - type: string | *"dashboard" - // Query for annotation data. - rawQuery?: string - showIn: number | *0 - }] - // Auto-refresh interval. - refresh?: string | false - // Version of the JSON schema, incremented each time a Grafana update brings - // changes to said schema. - // FIXME this is the old schema numbering system, and will be replaced by scuemata - schemaVersion: number | *30 - // Version of the dashboard, incremented each time the dashboard is updated. - version?: number - panels?: [...(#Panel | #GraphPanel | #RowPanel)] + gnetId?: string + // Tags associated with dashboard. + tags?: [...string] + // Theme of dashboard. + style: *"light" | "dark" + // Timezone of dashboard, + timezone?: *"browser" | "utc" | "" + // Whether a dashboard is editable or not. + editable: bool | *true + // 0 for no shared crosshair or tooltip (default). + // 1 for shared crosshair. + // 2 for shared crosshair AND shared tooltip. + graphTooltip: >=0 & <=2 | *0 + // Time range for dashboard, e.g. last 6 hours, last 7 days, etc + time?: { + from: string | *"now-6h" + to: string | *"now" + } + // Timepicker metadata. + timepicker?: { + // Whether timepicker is collapsed or not. + collapse: bool | *false + // Whether timepicker is enabled or not. + enable: bool | *true + // Whether timepicker is visible or not. + hidden: bool | *false + // Selectable intervals for auto-refresh. + refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } + // Templating. + templating?: list: [...{...}] + // Annotations. + annotations?: list: [...{ + builtIn: number | *0 + // Datasource to use for annotation. + datasource: { + type?: string + uid?: string + } + // Whether annotation is enabled. + enable: bool | *true + // Whether to hide annotation. + hide?: bool | *false + // Annotation icon color. + iconColor?: string + // Name of annotation. + name?: string + type: string | *"dashboard" + // Query for annotation data. + rawQuery?: string + showIn: number | *0 + }] + // Auto-refresh interval. + refresh?: string | false + // Version of the JSON schema, incremented each time a Grafana update brings + // changes to said schema. + // FIXME this is the old schema numbering system, and will be replaced by scuemata + schemaVersion: number | *30 + // Version of the dashboard, incremented each time the dashboard is updated. + version?: number + panels?: [...(#Panel | #GraphPanel | #RowPanel)] - // TODO docs - #FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum") + // TODO docs + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum") - // TODO docs - #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") + // TODO docs + #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") - // TODO docs - #FieldColor: { - // The main color scheme mode - mode: #FieldColorModeId | string - // Stores the fixed color value if mode is fixed - fixedColor?: string - // Some visualizations need to know how to assign a series color from by value color schemes - seriesBy?: #FieldColorSeriesByMode - } @cuetsy(kind="interface") + // TODO docs + #FieldColor: { + // The main color scheme mode + mode: #FieldColorModeId | string + // Stores the fixed color value if mode is fixed + fixedColor?: string + // Some visualizations need to know how to assign a series color from by value color schemes + seriesBy?: #FieldColorSeriesByMode + } @cuetsy(kind="interface") - // TODO docs - #Threshold: { - // TODO docs - // FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON - value?: number - // TODO docs - color: string - // TODO docs - // TODO are the values here enumerable into a disjunction? - // Some seem to be listed in typescript comment - state?: string - } @cuetsy(kind="interface") + // TODO docs + #Threshold: { + // TODO docs + // FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON + value?: number + // TODO docs + color: string + // TODO docs + // TODO are the values here enumerable into a disjunction? + // Some seem to be listed in typescript comment + state?: string + } @cuetsy(kind="interface") - #ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum") + #ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum") - #ThresholdsConfig: { - mode: #ThresholdsMode + #ThresholdsConfig: { + mode: #ThresholdsMode - // Must be sorted by 'value', first value is always -Infinity - steps: [...#Threshold] - } @cuetsy(kind="interface") + // Must be sorted by 'value', first value is always -Infinity + steps: [...#Threshold] + } @cuetsy(kind="interface") - // TODO docs - // FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it - #Transformation: { - id: string - options: {...} - } + // TODO docs + // FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it + #Transformation: { + id: string + options: {...} + } - // Schema for panel targets is specified by datasource - // plugins. We use a placeholder definition, which the Go - // schema loader either left open/as-is with the Base - // variant of the Dashboard and Panel families, or filled - // with types derived from plugins in the Instance variant. - // When working directly from CUE, importers can extend this - // type directly to achieve the same effect. - #Target: {...} + // Schema for panel targets is specified by datasource + // plugins. We use a placeholder definition, which the Go + // schema loader either left open/as-is with the Base + // variant of the Dashboard and Panel families, or filled + // with types derived from plugins in the Instance variant. + // When working directly from CUE, importers can extend this + // type directly to achieve the same effect. + #Target: {...} - // Dashboard panels. Panels are canonically defined inline - // because they share a version timeline with the dashboard - // schema; they do not evolve independently. - #Panel: { - // The panel plugin type id. - type: !="" + // Dashboard panels. Panels are canonically defined inline + // because they share a version timeline with the dashboard + // schema; they do not evolve independently. + #Panel: { + // The panel plugin type id. + type: !="" - // TODO docs - id?: number + // TODO docs + id?: number - // FIXME this almost certainly has to be changed in favor of scuemata versions - pluginVersion?: string + // FIXME this almost certainly has to be changed in favor of scuemata versions + pluginVersion?: string - // TODO docs - tags?: [...string] + // TODO docs + tags?: [...string] - // Internal - the exact major and minor versions of the panel plugin - // schema. Hidden and therefore not a part of the data model, but - // expected to be filled with panel plugin schema versions so that it's - // possible to figure out which schema version matched on a successful - // unification. - // _pv: { maj: int, min: int } - // The major and minor versions of the panel plugin for this schema. - // TODO 2-tuple list instead of struct? - // panelSchema?: { maj: number, min: number } - panelSchema?: [number, number] + // Internal - the exact major and minor versions of the panel plugin + // schema. Hidden and therefore not a part of the data model, but + // expected to be filled with panel plugin schema versions so that it's + // possible to figure out which schema version matched on a successful + // unification. + // _pv: { maj: int, min: int } + // The major and minor versions of the panel plugin for this schema. + // TODO 2-tuple list instead of struct? + // panelSchema?: { maj: number, min: number } + panelSchema?: [number, number] - // TODO docs - targets?: [...#Target] + // TODO docs + targets?: [...#Target] - // Panel title. - title?: string - // Description. - description?: string - // Whether to display the panel without a background. - transparent: bool | *false - // The datasource used in all targets. - datasource?: { - type?: string - uid?: string - } - // Grid position. - gridPos?: { - // Panel - h: number & >0 | *9 - // Panel - w: number & >0 & <=24 | *12 - // Panel x - x: number & >=0 & <24 | *0 - // Panel y - y: number & >=0 | *0 - // true if fixed - static?: bool - } - // Panel links. - // FIXME this is temporarily specified as a closed list so - // that validation will pass when no links are present, but - // to force a failure as soon as it's checked against there - // being anything in the list so it can be fixed in - // accordance with that object - links?: [] + // Panel title. + title?: string + // Description. + description?: string + // Whether to display the panel without a background. + transparent: bool | *false + // The datasource used in all targets. + datasource?: { + type?: string + uid?: string + } + // Grid position. + gridPos?: { + // Panel + h: number & >0 | *9 + // Panel + w: number & >0 & <=24 | *12 + // Panel x + x: number & >=0 & <24 | *0 + // Panel y + y: number & >=0 | *0 + // true if fixed + static?: bool + } + // Panel links. + // FIXME this is temporarily specified as a closed list so + // that validation will pass when no links are present, but + // to force a failure as soon as it's checked against there + // being anything in the list so it can be fixed in + // accordance with that object + links?: [] - // Name of template variable to repeat for. - repeat?: string - // Direction to repeat in if 'repeat' is set. - // "h" for horizontal, "v" for vertical. - repeatDirection: *"h" | "v" + // Name of template variable to repeat for. + repeat?: string + // Direction to repeat in if 'repeat' is set. + // "h" for horizontal, "v" for vertical. + repeatDirection: *"h" | "v" - // TODO docs - maxDataPoints?: number + // TODO docs + maxDataPoints?: number - // TODO docs - thresholds?: [...] + // TODO docs + thresholds?: [...] - // TODO docs - timeRegions?: [...] + // TODO docs + timeRegions?: [...] - transformations: [...#Transformation] + transformations: [...#Transformation] - // TODO docs - // TODO tighter constraint - interval?: string + // TODO docs + // TODO tighter constraint + interval?: string - // TODO docs - // TODO tighter constraint - timeFrom?: string + // TODO docs + // TODO tighter constraint + timeFrom?: string - // TODO docs - // TODO tighter constraint - timeShift?: string + // TODO docs + // TODO tighter constraint + timeShift?: string - // options is specified by the PanelOptions field in panel - // plugin schemas. - options: {} + // options is specified by the PanelOptions field in panel + // plugin schemas. + options: {} - fieldConfig: { - defaults: { - // The display value for this field. This supports template variables blank is auto - displayName?: string + fieldConfig: { + defaults: { + // The display value for this field. This supports template variables blank is auto + displayName?: string - // This can be used by data sources that return and explicit naming structure for values and labels - // When this property is configured, this value is used rather than the default naming strategy. - displayNameFromDS?: string + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + displayNameFromDS?: string - // Human readable field metadata - description?: string + // Human readable field metadata + description?: string - // An explict path to the field in the datasource. When the frame meta includes a path, - // This will default to `${frame.meta.path}/${field.name} - // - // When defined, this value can be used as an identifier within the datasource scope, and - // may be used to update the results - path?: string + // An explict path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + path?: string - // True if data source can write a value to the path. Auth/authz are supported separately - writeable?: bool + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: bool - // True if data source field supports ad-hoc filters - filterable?: bool + // True if data source field supports ad-hoc filters + filterable?: bool - // Numeric Options - unit?: string + // Numeric Options + unit?: string - // Significant digits (for display) - decimals?: number + // Significant digits (for display) + decimals?: number - min?: number - max?: number + min?: number + max?: number - // Convert input values into a display string - // - // TODO this one corresponds to a complex type with - // generics on the typescript side. Ouch. Will - // either need special care, or we'll just need to - // accept a very loosely specified schema. It's very - // unlikely we'll be able to translate cue to - // typescript generics in the general case, though - // this particular one *may* be able to work. - mappings?: [...{...}] + // Convert input values into a display string + // + // TODO this one corresponds to a complex type with + // generics on the typescript side. Ouch. Will + // either need special care, or we'll just need to + // accept a very loosely specified schema. It's very + // unlikely we'll be able to translate cue to + // typescript generics in the general case, though + // this particular one *may* be able to work. + mappings?: [...{...}] - // Map numeric values to states - thresholds?: #ThresholdsConfig + // Map numeric values to states + thresholds?: #ThresholdsConfig - // // Map values to a display color - color?: #FieldColor + // // Map values to a display color + color?: #FieldColor - // // Used when reducing field values - // nullValueMode?: NullValueMode + // // Used when reducing field values + // nullValueMode?: NullValueMode - // // The behavior when clicking on a result - links?: [...] + // // The behavior when clicking on a result + links?: [...] - // Alternative to empty string - noValue?: string + // Alternative to empty string + noValue?: string - // custom is specified by the PanelFieldConfig field - // in panel plugin schemas. - custom?: {} - } - overrides: [...{ - matcher: { - id: string | *"" - options?: _ - } - properties: [...{ - id: string | *"" - value?: _ - }] - }] - } - // Embed the disjunction of all injected panel schema, if any were injected. - if len(compose._panelSchemas) > 0 { - or(compose._panelSchemas) // TODO try to stick graph in here - } + // custom is specified by the PanelFieldConfig field + // in panel plugin schemas. + custom?: {} + } + overrides: [...{ + matcher: { + id: string | *"" + options?: _ + } + properties: [...{ + id: string | *"" + value?: _ + }] + }] + } + // Embed the disjunction of all injected panel schema, if any were injected. + if len(compose._panelSchemas) > 0 { + or(compose._panelSchemas)// TODO try to stick graph in here + } - // Make the plugin-composed subtrees open if the panel is - // of unknown types. This is important in every possible case: - // - Base (this file only): no real dashboard json - // containing any panels would ever validate - // - Dist (this file + core plugin schema): dashboard json containing - // panels with any third-party panel plugins would fail to validate, - // as well as any core plugins lacking a models.cue. The latter case - // is not normally expected, but this is not the appropriate place - // to enforce the invariant, anyway. - // - Instance (this file + core + third-party plugin schema): dashboard - // json containing panels with a third-party plugin that exists but - // is not currently installed would fail to validate. - if !list.Contains(compose._panelTypes, type) { - options: {...} - fieldConfig: defaults: custom: {...} - ... - } - } + // Make the plugin-composed subtrees open if the panel is + // of unknown types. This is important in every possible case: + // - Base (this file only): no real dashboard json + // containing any panels would ever validate + // - Dist (this file + core plugin schema): dashboard json containing + // panels with any third-party panel plugins would fail to validate, + // as well as any core plugins lacking a models.cue. The latter case + // is not normally expected, but this is not the appropriate place + // to enforce the invariant, anyway. + // - Instance (this file + core + third-party plugin schema): dashboard + // json containing panels with a third-party plugin that exists but + // is not currently installed would fail to validate. + if !list.Contains(compose._panelTypes, type) { + options: {...} + fieldConfig: defaults: custom: {...} + ... + } + } - // Row panel - #RowPanel: { - type: "row" - collapsed: bool | *false - title?: string + // Row panel + #RowPanel: { + type: "row" + collapsed: bool | *false + title?: string - // Name of default datasource. - datasource?: string + // Name of default datasource. + datasource: { + type?: string + uid?: string + } - gridPos?: { - // Panel - h: number & >0 | *9 - // Panel - w: number & >0 & <=24 | *12 - // Panel x - x: number & >=0 & <24 | *0 - // Panel y - y: number & >=0 | *0 - // true if fixed - static?: bool - } - id: number - panels: [...(#Panel | #GraphPanel)] - // Name of template variable to repeat for. - repeat?: string - } - // Support for legacy graph panels. - #GraphPanel: { - ... - type: "graph" - thresholds: [...{...}] - timeRegions?: [...{...}] - seriesOverrides: [...{...}] - aliasColors?: [string]: string - bars: bool | *false - dashes: bool | *false - dashLength: number | *10 - fill?: number - fillGradient?: number - hiddenSeries: bool | *false - legend: {...} - lines: bool | *false - linewidth?: number - nullPointMode: *"null" | "connected" | "null as zero" - percentage: bool | *false - points: bool | *false - pointradius?: number - renderer: string - spaceLength: number | *10 - stack: bool | *false - steppedLine: bool | *false - tooltip?: { - shared?: bool - sort: number | *0 - value_type: *"individual" | "cumulative" - } - } - } - ] + gridPos?: { + // Panel + h: number & >0 | *9 + // Panel + w: number & >0 & <=24 | *12 + // Panel x + x: number & >=0 & <24 | *0 + // Panel y + y: number & >=0 | *0 + // true if fixed + static?: bool + } + id: number + panels: [...(#Panel | #GraphPanel)] + // Name of template variable to repeat for. + repeat?: string + } + + // Support for legacy graph panels. + #GraphPanel: { + ... + type: "graph" + thresholds: [...{...}] + timeRegions?: [...{...}] + seriesOverrides: [...{...}] + aliasColors?: [string]: string + bars: bool | *false + dashes: bool | *false + dashLength: number | *10 + fill?: number + fillGradient?: number + hiddenSeries: bool | *false + legend: {...} + lines: bool | *false + linewidth?: number + nullPointMode: *"null" | "connected" | "null as zero" + percentage: bool | *false + points: bool | *false + pointradius?: number + renderer: string + spaceLength: number | *10 + stack: bool | *false + steppedLine: bool | *false + tooltip?: { + shared?: bool + sort: number | *0 + value_type: *"individual" | "cumulative" + } + } + }, + ], ] - compose: { - // Scuemata families for all panel types that should be composed into the - // dashboard schema. - Panel: [string]: scuemata.#PanelFamily + compose: { + // Scuemata families for all panel types that should be composed into the + // dashboard schema. + Panel: [string]: scuemata.#PanelFamily - // _panelTypes: [for typ, _ in Panel {typ}] - _panelTypes: [for typ, _ in Panel {typ}, "graph", "row"] - _panelSchemas: [for typ, scue in Panel { - for lv, lin in scue.lineages { - for sv, sch in lin { - (_mapPanel & {arg: { - type: typ - v: [lv, sv] // TODO add optionality for exact, at least, at most, any - model: sch // TODO Does this need to be close()d? - }}).out - } - } - }, { type: string }] - _mapPanel: { - arg: { - type: string & !="" - v: [number, number] - model: {...} - } - // Until CUE introduces the must() constraint, we have to enforce - // that the input model is as expected by checking for unification - // in this hidden property (see https://github.com/cue-lang/cue/issues/575). - // If we unified arg.model with the scuemata.#PanelSchema - // meta-schema directly, the struct openness (PanelOptions: {...}) - // would be applied to the actual schema instance in the arg. Here, - // where we're actually putting those in the dashboard schema, want - // those to be closed, or at least preserve closed-ness. - _checkSchema: scuemata.#PanelSchema & arg.model - out: { - type: arg.type - panelSchema: arg.v // TODO add optionality for exact, at least, at most, any - options: arg.model.PanelOptions - fieldConfig: defaults: custom: {} - if arg.model.PanelFieldConfig != _|_ { - fieldConfig: defaults: custom: arg.model.PanelFieldConfig - } - } - } - } + // _panelTypes: [for typ, _ in Panel {typ}] + _panelTypes: [ for typ, _ in Panel {typ}, "graph", "row"] + _panelSchemas: [ for typ, scue in Panel { + for lv, lin in scue.lineages { + for sv, sch in lin { + (_mapPanel & {arg: { + type: typ + v: [lv, sv] // TODO add optionality for exact, at least, at most, any + model: sch // TODO Does this need to be close()d? + }}).out + } + } + }, {type: string}] + _mapPanel: { + arg: { + type: string & !="" + v: [number, number] + model: {...} + } + // Until CUE introduces the must() constraint, we have to enforce + // that the input model is as expected by checking for unification + // in this hidden property (see https://github.com/cue-lang/cue/issues/575). + // If we unified arg.model with the scuemata.#PanelSchema + // meta-schema directly, the struct openness (PanelOptions: {...}) + // would be applied to the actual schema instance in the arg. Here, + // where we're actually putting those in the dashboard schema, want + // those to be closed, or at least preserve closed-ness. + _checkSchema: scuemata.#PanelSchema & arg.model + out: { + type: arg.type + panelSchema: arg.v // TODO add optionality for exact, at least, at most, any + options: arg.model.PanelOptions + fieldConfig: defaults: custom: {} + if arg.model.PanelFieldConfig != _|_ { + fieldConfig: defaults: custom: arg.model.PanelFieldConfig + } + } + } + } } diff --git a/pkg/coremodel/dashboard/lineage.cue b/pkg/coremodel/dashboard/lineage.cue index 1910539d5a6..bd1c1db4f2e 100644 --- a/pkg/coremodel/dashboard/lineage.cue +++ b/pkg/coremodel/dashboard/lineage.cue @@ -7,338 +7,345 @@ name: "dashboard" seqs: [ { schemas: [ - { // 0.0 - // Unique numeric identifier for the dashboard. - // TODO must isolate or remove identifiers local to a Grafana instance...? - id?: int64 - // Unique dashboard identifier that can be generated by anyone. string (8-40) - uid?: string - // Title of dashboard. - title?: string - // Description of dashboard. - description?: string + {// 0.0 + // Unique numeric identifier for the dashboard. + // TODO must isolate or remove identifiers local to a Grafana instance...? + id?: int64 + // Unique dashboard identifier that can be generated by anyone. string (8-40) + uid?: string + // Title of dashboard. + title?: string + // Description of dashboard. + description?: string - gnetId?: string - // Tags associated with dashboard. - tags?: [...string] - // Theme of dashboard. - style: *"light" | "dark" - // Timezone of dashboard, - timezone?: *"browser" | "utc" | "" - // Whether a dashboard is editable or not. - editable: bool | *true - // 0 for no shared crosshair or tooltip (default). - // 1 for shared crosshair. - // 2 for shared crosshair AND shared tooltip. - graphTooltip: uint8 & >=0 & <=2 | *0 - // Time range for dashboard, e.g. last 6 hours, last 7 days, etc - time?: { - from: string | *"now-6h" - to: string | *"now" - } - // Timepicker metadata. - timepicker?: { - // Whether timepicker is collapsed or not. - collapse: bool | *false - // Whether timepicker is enabled or not. - enable: bool | *true - // Whether timepicker is visible or not. - hidden: bool | *false - // Selectable intervals for auto-refresh. - refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] - } - // Templating. - templating?: list: [...{...}] - // Annotations. - annotations?: list: [...{ - builtIn: uint8 | *0 - // Datasource to use for annotation. - datasource: string - // Whether annotation is enabled. - enable: bool | *true - // Whether to hide annotation. - hide?: bool | *false - // Annotation icon color. - iconColor?: string - // Name of annotation. - name?: string - type: string | *"dashboard" - // Query for annotation data. - rawQuery?: string - showIn: uint8 | *0 - }] - // Auto-refresh interval. - refresh?: string | false - // Version of the JSON schema, incremented each time a Grafana update brings - // changes to said schema. - // FIXME this is the old schema numbering system, and will be replaced by Thema's themaVersion - schemaVersion: uint16 | *33 - // Version of the dashboard, incremented each time the dashboard is updated. - version?: uint32 - panels?: [...(#Panel | #GraphPanel | #HeatmapPanel | #RowPanel)] + gnetId?: string + // Tags associated with dashboard. + tags?: [...string] + // Theme of dashboard. + style: *"light" | "dark" + // Timezone of dashboard, + timezone?: *"browser" | "utc" | "" + // Whether a dashboard is editable or not. + editable: bool | *true + // 0 for no shared crosshair or tooltip (default). + // 1 for shared crosshair. + // 2 for shared crosshair AND shared tooltip. + graphTooltip: uint8 & >=0 & <=2 | *0 + // Time range for dashboard, e.g. last 6 hours, last 7 days, etc + time?: { + from: string | *"now-6h" + to: string | *"now" + } + // Timepicker metadata. + timepicker?: { + // Whether timepicker is collapsed or not. + collapse: bool | *false + // Whether timepicker is enabled or not. + enable: bool | *true + // Whether timepicker is visible or not. + hidden: bool | *false + // Selectable intervals for auto-refresh. + refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } + // Templating. + templating?: list: [...{...}] + // Annotations. + annotations?: list: [...{ + builtIn: uint8 | *0 + // Datasource to use for annotation. + datasource: { + type?: string + uid?: string + } + // Whether annotation is enabled. + enable: bool | *true + // Whether to hide annotation. + hide?: bool | *false + // Annotation icon color. + iconColor?: string + // Name of annotation. + name?: string + type: string | *"dashboard" + // Query for annotation data. + rawQuery?: string + showIn: uint8 | *0 + }] + // Auto-refresh interval. + refresh?: string | false + // Version of the JSON schema, incremented each time a Grafana update brings + // changes to said schema. + // FIXME this is the old schema numbering system, and will be replaced by Thema's themaVersion + schemaVersion: uint16 | *33 + // Version of the dashboard, incremented each time the dashboard is updated. + version?: uint32 + panels?: [...(#Panel | #GraphPanel | #HeatmapPanel | #RowPanel)] - // TODO docs - #FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum") + // TODO docs + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum") - // TODO docs - #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") + // TODO docs + #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") - // TODO docs - #FieldColor: { - // The main color scheme mode - mode: #FieldColorModeId | string - // Stores the fixed color value if mode is fixed - fixedColor?: string - // Some visualizations need to know how to assign a series color from by value color schemes - seriesBy?: #FieldColorSeriesByMode - } @cuetsy(kind="interface") + // TODO docs + #FieldColor: { + // The main color scheme mode + mode: #FieldColorModeId | string + // Stores the fixed color value if mode is fixed + fixedColor?: string + // Some visualizations need to know how to assign a series color from by value color schemes + seriesBy?: #FieldColorSeriesByMode + } @cuetsy(kind="interface") - // TODO docs - #Threshold: { - // TODO docs - // FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON - value?: number - // TODO docs - color: string - // TODO docs - // TODO are the values here enumerable into a disjunction? - // Some seem to be listed in typescript comment - state?: string - } @cuetsy(kind="interface") + // TODO docs + #Threshold: { + // TODO docs + // FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON + value?: number + // TODO docs + color: string + // TODO docs + // TODO are the values here enumerable into a disjunction? + // Some seem to be listed in typescript comment + state?: string + } @cuetsy(kind="interface") - #ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum") + #ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum") - #ThresholdsConfig: { - mode: #ThresholdsMode + #ThresholdsConfig: { + mode: #ThresholdsMode - // Must be sorted by 'value', first value is always -Infinity - steps: [...#Threshold] - } @cuetsy(kind="interface") + // Must be sorted by 'value', first value is always -Infinity + steps: [...#Threshold] + } @cuetsy(kind="interface") - // TODO docs - // FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it - #Transformation: { - id: string - options: {...} - } + // TODO docs + // FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it + #Transformation: { + id: string + options: {...} + } - // Schema for panel targets is specified by datasource - // plugins. We use a placeholder definition, which the Go - // schema loader either left open/as-is with the Base - // variant of the Dashboard and Panel families, or filled - // with types derived from plugins in the Instance variant. - // When working directly from CUE, importers can extend this - // type directly to achieve the same effect. - #Target: {...} + // Schema for panel targets is specified by datasource + // plugins. We use a placeholder definition, which the Go + // schema loader either left open/as-is with the Base + // variant of the Dashboard and Panel families, or filled + // with types derived from plugins in the Instance variant. + // When working directly from CUE, importers can extend this + // type directly to achieve the same effect. + #Target: {...} - // Dashboard panels. Panels are canonically defined inline - // because they share a version timeline with the dashboard - // schema; they do not evolve independently. - #Panel: { - // The panel plugin type id. - type: !="" + // Dashboard panels. Panels are canonically defined inline + // because they share a version timeline with the dashboard + // schema; they do not evolve independently. + #Panel: { + // The panel plugin type id. + type: !="" - // TODO docs - id?: uint32 + // TODO docs + id?: uint32 - // FIXME this almost certainly has to be changed in favor of scuemata versions - pluginVersion?: string + // FIXME this almost certainly has to be changed in favor of scuemata versions + pluginVersion?: string - // TODO docs - tags?: [...string] + // TODO docs + tags?: [...string] - // Internal - the exact major and minor versions of the panel plugin - // schema. Hidden and therefore not a part of the data model, but - // expected to be filled with panel plugin schema versions so that it's - // possible to figure out which schema version matched on a successful - // unification. - // _pv: { maj: int, min: int } - // The major and minor versions of the panel plugin for this schema. - // TODO 2-tuple list instead of struct? - // panelSchema?: { maj: number, min: number } - panelSchema?: [number, number] + // Internal - the exact major and minor versions of the panel plugin + // schema. Hidden and therefore not a part of the data model, but + // expected to be filled with panel plugin schema versions so that it's + // possible to figure out which schema version matched on a successful + // unification. + // _pv: { maj: int, min: int } + // The major and minor versions of the panel plugin for this schema. + // TODO 2-tuple list instead of struct? + // panelSchema?: { maj: number, min: number } + panelSchema?: [number, number] - // TODO docs - targets?: [...#Target] + // TODO docs + targets?: [...#Target] - // Panel title. - title?: string - // Description. - description?: string - // Whether to display the panel without a background. - transparent: bool | *false - // The datasource used in all targets. - datasource?: { - type?: string - uid?: string - } - // Grid position. - gridPos?: { - // Panel - h: uint32 & >0 | *9 - // Panel - w: uint32 & >0 & <=24 | *12 - // Panel x - x: uint32 & >=0 & <24 | *0 - // Panel y - y: uint32 & >=0 | *0 - // true if fixed - static?: bool - } - // Panel links. - // FIXME this is temporarily specified as a closed list so - // that validation will pass when no links are present, but - // to force a failure as soon as it's checked against there - // being anything in the list so it can be fixed in - // accordance with that object - links?: [] + // Panel title. + title?: string + // Description. + description?: string + // Whether to display the panel without a background. + transparent: bool | *false + // The datasource used in all targets. + datasource?: { + type?: string + uid?: string + } + // Grid position. + gridPos?: { + // Panel + h: uint32 & >0 | *9 + // Panel + w: uint32 & >0 & <=24 | *12 + // Panel x + x: uint32 & >=0 & <24 | *0 + // Panel y + y: uint32 & >=0 | *0 + // true if fixed + static?: bool + } + // Panel links. + // FIXME this is temporarily specified as a closed list so + // that validation will pass when no links are present, but + // to force a failure as soon as it's checked against there + // being anything in the list so it can be fixed in + // accordance with that object + links?: [] - // Name of template variable to repeat for. - repeat?: string - // Direction to repeat in if 'repeat' is set. - // "h" for horizontal, "v" for vertical. - repeatDirection: *"h" | "v" + // Name of template variable to repeat for. + repeat?: string + // Direction to repeat in if 'repeat' is set. + // "h" for horizontal, "v" for vertical. + repeatDirection: *"h" | "v" - // TODO docs - maxDataPoints?: number + // TODO docs + maxDataPoints?: number - // TODO docs - thresholds?: [...] + // TODO docs + thresholds?: [...] - // TODO docs - timeRegions?: [...] + // TODO docs + timeRegions?: [...] - transformations: [...#Transformation] + transformations: [...#Transformation] - // TODO docs - // TODO tighter constraint - interval?: string + // TODO docs + // TODO tighter constraint + interval?: string - // TODO docs - // TODO tighter constraint - timeFrom?: string + // TODO docs + // TODO tighter constraint + timeFrom?: string - // TODO docs - // TODO tighter constraint - timeShift?: string + // TODO docs + // TODO tighter constraint + timeShift?: string - // options is specified by the PanelOptions field in panel - // plugin schemas. - options: {...} + // options is specified by the PanelOptions field in panel + // plugin schemas. + options: {...} - fieldConfig: { - defaults: { - // The display value for this field. This supports template variables blank is auto - displayName?: string + fieldConfig: { + defaults: { + // The display value for this field. This supports template variables blank is auto + displayName?: string - // This can be used by data sources that return and explicit naming structure for values and labels - // When this property is configured, this value is used rather than the default naming strategy. - displayNameFromDS?: string + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + displayNameFromDS?: string - // Human readable field metadata - description?: string + // Human readable field metadata + description?: string - // An explict path to the field in the datasource. When the frame meta includes a path, - // This will default to `${frame.meta.path}/${field.name} - // - // When defined, this value can be used as an identifier within the datasource scope, and - // may be used to update the results - path?: string + // An explict path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + path?: string - // True if data source can write a value to the path. Auth/authz are supported separately - writeable?: bool + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: bool - // True if data source field supports ad-hoc filters - filterable?: bool + // True if data source field supports ad-hoc filters + filterable?: bool - // Numeric Options - unit?: string + // Numeric Options + unit?: string - // Significant digits (for display) - decimals?: number + // Significant digits (for display) + decimals?: number - min?: number - max?: number + min?: number + max?: number - // Convert input values into a display string - // - // TODO this one corresponds to a complex type with - // generics on the typescript side. Ouch. Will - // either need special care, or we'll just need to - // accept a very loosely specified schema. It's very - // unlikely we'll be able to translate cue to - // typescript generics in the general case, though - // this particular one *may* be able to work. - mappings?: [...{...}] + // Convert input values into a display string + // + // TODO this one corresponds to a complex type with + // generics on the typescript side. Ouch. Will + // either need special care, or we'll just need to + // accept a very loosely specified schema. It's very + // unlikely we'll be able to translate cue to + // typescript generics in the general case, though + // this particular one *may* be able to work. + mappings?: [...{...}] - // Map numeric values to states - thresholds?: #ThresholdsConfig + // Map numeric values to states + thresholds?: #ThresholdsConfig - // // Map values to a display color - color?: #FieldColor + // // Map values to a display color + color?: #FieldColor - // // Used when reducing field values - // nullValueMode?: NullValueMode + // // Used when reducing field values + // nullValueMode?: NullValueMode - // // The behavior when clicking on a result - links?: [...] + // // The behavior when clicking on a result + links?: [...] - // Alternative to empty string - noValue?: string + // Alternative to empty string + noValue?: string - // custom is specified by the PanelFieldConfig field - // in panel plugin schemas. - custom?: {...} - } - overrides: [...{ - matcher: { - id: string | *"" - options?: _ - } - properties: [...{ - id: string | *"" - value?: _ - }] - }] - } - } + // custom is specified by the PanelFieldConfig field + // in panel plugin schemas. + custom?: {...} + } + overrides: [...{ + matcher: { + id: string | *"" + options?: _ + } + properties: [...{ + id: string | *"" + value?: _ + }] + }] + } + } - // Row panel - #RowPanel: { - type: "row" - collapsed: bool | *false - title?: string + // Row panel + #RowPanel: { + type: "row" + collapsed: bool | *false + title?: string - // Name of default datasource. - datasource?: string + // Name of default datasource. + datasource?: { + type?: string + uid?: string + } - gridPos?: { - // Panel - h: uint32 & >0 | *9 - // Panel - w: uint32 & >0 & <=24 | *12 - // Panel x - x: uint32 & >=0 & <24 | *0 - // Panel y - y: uint32 & >=0 | *0 - // true if fixed - static?: bool - } - id: uint32 - panels: [...(#Panel | #GraphPanel | #HeatmapPanel)] - // Name of template variable to repeat for. - repeat?: string - } - // Support for legacy graph and heatmap panels. - #GraphPanel: { - ... - type: "graph" - } - #HeatmapPanel: { - ... - type: "heatmap" - } - } - ] - } + gridPos?: { + // Panel + h: uint32 & >0 | *9 + // Panel + w: uint32 & >0 & <=24 | *12 + // Panel x + x: uint32 & >=0 & <24 | *0 + // Panel y + y: uint32 & >=0 | *0 + // true if fixed + static?: bool + } + id: uint32 + panels: [...(#Panel | #GraphPanel | #HeatmapPanel)] + // Name of template variable to repeat for. + repeat?: string + } + + // Support for legacy graph and heatmap panels. + #GraphPanel: { + type: "graph" + ... + } + #HeatmapPanel: { + type: "heatmap" + ... + } + }, + ] + }, ] diff --git a/pkg/coremodel/dashboard/schema.go b/pkg/coremodel/dashboard/schema.go index 96c235a1712..c7b7abc084a 100644 --- a/pkg/coremodel/dashboard/schema.go +++ b/pkg/coremodel/dashboard/schema.go @@ -17,6 +17,15 @@ var ( currentVersion = thema.SV(0, 0) ) +// HandoffSchemaVersion is the minimum schemaVersion for dashboards at which the +// Thema-based dashboard schema is known to be valid. +// +// schemaVersion is the original version numbering system for dashboards. If a +// dashboard is below this schemaVersion, it is necessary for the frontend +// typescript dashboard migration logic to first run and get it past this +// number, after which Thema can take over. +const HandoffSchemaVersion = 36 + // Lineage returns the Thema lineage representing Grafana dashboards. The // lineage is the canonical specification of the current datasource schema, all // prior schema versions, and the mappings that allow migration between schema @@ -80,7 +89,7 @@ type model struct { SchemaVersion int `json:"schemaVersion"` Panels []interface{} `json:"panels"` - //// + // // Uid string `json:"uid"` // OrgId int64 `json:"orgId"` diff --git a/pkg/schema/load/load_test.go b/pkg/schema/load/load_test.go index 65d7655bac5..d9f96849cc0 100644 --- a/pkg/schema/load/load_test.go +++ b/pkg/schema/load/load_test.go @@ -19,6 +19,7 @@ import ( "cuelang.org/go/cue/errors" "cuelang.org/go/cue/load" cuejson "cuelang.org/go/pkg/encoding/json" + "github.com/grafana/grafana/pkg/coremodel/dashboard" "github.com/grafana/grafana/pkg/schema" "github.com/laher/mergefs" "github.com/stretchr/testify/require" @@ -48,7 +49,6 @@ var doTestAgainstDevenv = func(sch schema.VersionedCueSchema, validdir string, f b, err := os.Open(path) require.NoError(t, err, "failed to open dashboard file") - // Only try to validate dashboards with schemaVersion >= 30 jtree := make(map[string]interface{}) byt, err := io.ReadAll(b) if err != nil { @@ -59,9 +59,9 @@ var doTestAgainstDevenv = func(sch schema.VersionedCueSchema, validdir string, f t.Logf("no schemaVersion in %s", path) return nil } else { - if !(oldschemav.(float64) > 32) { + if !(oldschemav.(float64) > dashboard.HandoffSchemaVersion-1) { if testing.Verbose() { - t.Logf("schemaVersion is %v, older than 33, skipping %s", oldschemav, path) + t.Logf("schemaVersion is %v, older than %v, skipping %s", oldschemav, dashboard.HandoffSchemaVersion-1, path) } return nil } diff --git a/scripts/stripnulls.sh b/scripts/stripnulls.sh index 90a873ed50b..d64ff2fb8c9 100755 --- a/scripts/stripnulls.sh +++ b/scripts/stripnulls.sh @@ -7,7 +7,7 @@ SED=$(command -v gsed) SED=${SED:-"sed"} -FILES=$(grep -rl '"schemaVersion": 3[34]' devenv) +FILES=$(grep -rl '"schemaVersion": 3[3456]' devenv) set -e set -x for DASH in ${FILES}; do echo "${DASH}"; grep -v 'null,$' "${DASH}" > "${DASH}-nulless"; mv "${DASH}-nulless" "${DASH}"; done From 516c8b60ee0b706b14c5c503b7c7c80cab472e76 Mon Sep 17 00:00:00 2001 From: Joe Blubaugh Date: Mon, 18 Apr 2022 10:42:45 +0800 Subject: [PATCH 121/121] Unified Alerting: Stable order for state history annotations (#47674) This change sorts the State History list returned by the backend by the id in addition to the timeEnd and time fields by which it is already sorted. This results in a stable view of the State History table. Fixes #45873 Signed-off-by: Joe Blubaugh --- .../components/rules/StateHistory.test.tsx | 40 +++++++++++++++++++ .../unified/components/rules/StateHistory.tsx | 32 ++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 public/app/features/alerting/unified/components/rules/StateHistory.test.tsx diff --git a/public/app/features/alerting/unified/components/rules/StateHistory.test.tsx b/public/app/features/alerting/unified/components/rules/StateHistory.test.tsx new file mode 100644 index 00000000000..31d0bc9e188 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/StateHistory.test.tsx @@ -0,0 +1,40 @@ +import { StateHistoryItem } from 'app/types/unified-alerting'; +import { sortStateHistory } from './StateHistory'; + +describe(sortStateHistory, () => { + describe('should stably sort', () => { + describe('when timeEnd is different', () => { + it('should not sort by rule id', () => { + let data: StateHistoryItem[] = [ + { timeEnd: 23, time: 22, id: 1 } as StateHistoryItem, + { timeEnd: 22, time: 21, id: 3 } as StateHistoryItem, + { timeEnd: 22, time: 22, id: 2 } as StateHistoryItem, + { timeEnd: 24, id: 3 } as StateHistoryItem, + ]; + + data.sort(sortStateHistory); + expect(data[0].timeEnd).toBe(24); + expect(data[1].timeEnd).toBe(23); + expect(data[2].time).toBe(22); + expect(data[3].id).toBe(3); + }); + }); + + describe('when only the rule id is different', () => { + it('should sort by rule id', () => { + let data: StateHistoryItem[] = [ + { timeEnd: 23, time: 22, id: 1 } as StateHistoryItem, + { timeEnd: 23, time: 22, id: 3 } as StateHistoryItem, + { timeEnd: 23, time: 22, id: 2 } as StateHistoryItem, + { timeEnd: 23, time: 22, id: 6 } as StateHistoryItem, + ]; + + data.sort(sortStateHistory); + expect(data[0].id).toBe(6); + expect(data[1].id).toBe(3); + expect(data[2].id).toBe(2); + expect(data[3].id).toBe(1); + }); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/rules/StateHistory.tsx b/public/app/features/alerting/unified/components/rules/StateHistory.tsx index f7050eb198c..75d17e883fe 100644 --- a/public/app/features/alerting/unified/components/rules/StateHistory.tsx +++ b/public/app/features/alerting/unified/components/rules/StateHistory.tsx @@ -24,6 +24,32 @@ interface RuleStateHistoryProps { alertId: string; } +function sortStateHistory(a: StateHistoryItem, b: StateHistoryItem): number { + const compareDesc = (a: number, b: number): number => { + // Larger numbers first. + if (a > b) { + return -1; + } + + if (b > a) { + return 1; + } + return 0; + }; + + const endNeq = compareDesc(a.timeEnd, b.timeEnd); + if (endNeq) { + return endNeq; + } + + const timeNeq = compareDesc(a.time, b.time); + if (timeNeq) { + return timeNeq; + } + + return compareDesc(a.id, b.id); +} + const StateHistory: FC = ({ alertId }) => { const { loading, error, result = [] } = useManagedAlertStateHistory(alertId); @@ -42,6 +68,7 @@ const StateHistory: FC = ({ alertId }) => { ]; const items: StateHistoryRow[] = result + .sort(sortStateHistory) .reduce((acc: StateHistoryRowItem[], item, index) => { acc.push({ id: String(item.id), @@ -123,4 +150,7 @@ function hasMatchingPrecedingState(index: number, items: StateHistoryItem[]): bo return previousHistoryItem.newState === currentHistoryItem.prevState; } -export { StateHistory }; +export { + StateHistory, + sortStateHistory, // exported for testing. +};