diff --git a/.circleci/config.yml b/.circleci/config.yml index b4d3eef4924..ae79cef520d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -54,7 +54,7 @@ commands: - run: name: "Install Grafana build pipeline tool" command: | - VERSION=0.4.3 + VERSION=0.4.4 curl -fLO https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v${VERSION}/grabpl chmod +x grabpl mv grabpl /tmp @@ -192,7 +192,13 @@ jobs: - run: name: Build internal Grafana plug-ins command: | - /tmp/grabpl build-plugins --jobs 2 --edition << parameters.edition >> + if [[ -n "$CIRCLE_PR_NUMBER" ]]; then + # This is a forked PR, so don't sign as it requires an API secret + /tmp/grabpl build-plugins --jobs 2 --edition << parameters.edition >> + else + export GRAFANA_API_KEY=$GRAFANA_COM_API_KEY + /tmp/grabpl build-plugins --jobs 2 --edition << parameters.edition >> --sign --signing-admin + fi - run: name: Move artifacts command: | @@ -622,8 +628,12 @@ jobs: if [[ $CIRCLE_BRANCH == "chore/test-release-pipeline" ]]; then # We're testing the release pipeline /tmp/grabpl publish-docker --jobs 4 --edition << parameters.edition >> --ubuntu=<< parameters.ubuntu >> --dry-run - else + elif [[ -n $CIRCLE_TAG ]]; then + # This is a release /tmp/grabpl publish-docker --jobs 4 --edition << parameters.edition >> --ubuntu=<< parameters.ubuntu >> + else + # TODO: Don't ignore errors, temporary workaround until we fix #22955 + /tmp/grabpl publish-docker --jobs 4 --edition << parameters.edition >> --ubuntu=<< parameters.ubuntu >> || echo Publishing failed! fi - run: name: CI job failed diff --git a/Dockerfile b/Dockerfile index a7b60f6e222..f90f08aae7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,3 @@ -# Golang build container -FROM golang:1.14.2-alpine3.11 as go-builder - -RUN apk add --no-cache gcc g++ - -WORKDIR $GOPATH/src/github.com/grafana/grafana - -COPY go.mod go.sum ./ - -RUN go mod verify - -COPY pkg pkg -COPY build.go package.json ./ - -RUN go run build.go build - -# Node build container FROM node:12.16.3-alpine3.11 as js-builder WORKDIR /usr/src/app/ @@ -33,7 +16,22 @@ COPY emails emails ENV NODE_ENV production RUN ./node_modules/.bin/grunt build -# Final container +FROM golang:1.14.2-alpine3.11 as go-builder + +RUN apk add --no-cache gcc g++ + +WORKDIR $GOPATH/src/github.com/grafana/grafana + +COPY go.mod go.sum ./ + +RUN go mod verify + +COPY pkg pkg +COPY build.go package.json ./ + +RUN go run build.go build + +# Final stage FROM alpine:3.11 LABEL maintainer="Grafana team " diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 39e5e5ba7e9..90855b09adc 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,16 +1,3 @@ -FROM golang:1.14.2 AS go-builder - -WORKDIR /src/grafana - -COPY go.mod go.sum ./ - -RUN go mod verify - -COPY build.go package.json ./ -COPY pkg pkg/ - -RUN go run build.go build - FROM node:12.16.3-slim AS js-builder WORKDIR /usr/src/app/ @@ -29,6 +16,19 @@ COPY emails emails ENV NODE_ENV production RUN ./node_modules/.bin/grunt build +FROM golang:1.14.2 AS go-builder + +WORKDIR /src/grafana + +COPY go.mod go.sum ./ + +RUN go mod verify + +COPY build.go package.json ./ +COPY pkg pkg/ + +RUN go run build.go build + FROM ubuntu:20.04 LABEL maintainer="Grafana team " diff --git a/conf/defaults.ini b/conf/defaults.ini index 8574ab35498..7bdbb97a47d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -695,6 +695,8 @@ disable_sanitize_html = false [plugins] enable_alpha = false app_tls_skip_verify_insecure = false +# Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature. +allow_loading_unsigned_plugins = #################################### Grafana Image Renderer Plugin ########################## [plugin.grafana-image-renderer] diff --git a/conf/sample.ini b/conf/sample.ini index 6d180436415..d0a1fffdff9 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -684,6 +684,8 @@ [plugins] ;enable_alpha = false ;app_tls_skip_verify_insecure = false +# Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature. +;allow_loading_unsigned_plugins = #################################### Grafana Image Renderer Plugin ########################## [plugin.grafana-image-renderer] diff --git a/contribute/style-guides/documentation-style-guide.md b/contribute/style-guides/documentation-style-guide.md index 2f0465a647f..82541a2fee3 100644 --- a/contribute/style-guides/documentation-style-guide.md +++ b/contribute/style-guides/documentation-style-guide.md @@ -33,6 +33,7 @@ The following sections provide general guidelines on topics specific to Grafana - Use: The panel opens. Grafana opens the panel. * Do not use an ampersand (&) as an abbreviation for "and." - **Exceptions:** If an ampersand is used in the Grafana UI, then match the UI. +* Avoid using internal slang and jargon in technical documentation. ### File naming conventions @@ -112,7 +113,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ### Word usage -Grafana products has some words, abbreviations, and slang particular to this discourse community. +Grafana products has some words, abbreviations, and terms particular to the Grafana discourse community. #### checkout, check out @@ -141,6 +142,10 @@ Two words, not one * Correct, but passive voice: Your list of active alarms is displayed. * Incorrect: The list of active alarms displays. +#### drawer + +Do not use. This is developer jargon that refers to a UI panel. Refer to the panel or feature by its proper name. + #### intro, introduction "Introduction" is the preferred word. Use "intro" if there are space constraints (like on the side menu) or you are specifically trying for a less formal, more conversational tone. diff --git a/contribute/style-guides/frontend.md b/contribute/style-guides/frontend.md index d4628eda1c7..0d40993898d 100644 --- a/contribute/style-guides/frontend.md +++ b/contribute/style-guides/frontend.md @@ -8,7 +8,7 @@ Generally we follow the Airbnb [React Style Guide](https://github.com/airbnb/jav - [Table of Contents](#table-of-contents) - [Basic rules](#basic-rules) - [Naming conventions](#naming-conventions) - - [Files and directories naming conventions](#files-and-directories-naming-conventions) + - [File and directory naming conventions](#file-and-directory-naming-conventions) - [Code organization](#code-organization) - [Exports](#exports) - [Comments](#comments) @@ -172,6 +172,44 @@ const CONSTANT_VALUE = "This string won't change"; _SASS styles are deprecated. Please migrate to Emotion whenever you need to modify SASS styles._ +### Typing + +In general, you should let Typescript infer the types so that there's no need to explicitly define type for each variable. + +There are some exceptions to this: + +```typescript +// Typescript needs to know type of arrays or objects otherwise it would infer it as array of any + +// bad +const stringArray = []; + +// good +const stringArray: string[] = []; +``` + +Specify function return types explicitly in new code. This improves readability by being able to tell what a function returns just by looking at the signature. It also prevents errors when a function's return type is broader than expected by the author. + +> Note: We don't have linting for this enabled because of lots of old code that needs to be fixed first. + +```typescript +// bad +function transform(value?: string) { + if (!value) { + return undefined + } + return applyTransform(value) +}; + +// good +function transform(value?: string): TransformedValue | undefined { + if (!value) { + return undefined + } + return applyTransform(value) +}; +``` + ### File and directory naming conventions Name files according to the primary export: diff --git a/devenv/README.md b/devenv/README.md index 2dca8818cf6..39164d7f43e 100644 --- a/devenv/README.md +++ b/devenv/README.md @@ -39,4 +39,11 @@ Some of the blocks support dynamic change of the image version used in the Docke make devenv sources=postgres,openldap,grafana postgres_version=9.2 grafana_version=6.7.0-beta1 ``` -Note: The grafana block is pre-configured with the dev-datasources and dashboards. + +### Notes per block + +#### Grafana +The grafana block is pre-configured with the dev-datasources and dashboards. + +#### Jaeger +Jaeger block runs both Jaeger and Loki container. Loki container sends traces to Jaeger and also logs its own logs into itself so it is possible to setup derived field for traceID from Loki to Jaeger. You need to install a docker plugin for the self logging to work, without it the container won't start. See https://github.com/grafana/loki/tree/master/cmd/docker-driver#plugin-installation for installation instructions. diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml index 8634382bac7..73fa9812255 100644 --- a/devenv/datasources.yaml +++ b/devenv/datasources.yaml @@ -246,5 +246,29 @@ datasources: access: proxy url: http://localhost:3100 editable: false + jsonData: + derivedFields: + - name: "traceID" + matcherRegex: "traceID=(\\w+)" + url: "$${__value.raw}" + datasourceUid: gdev-jaeger + - name: "traceID" + matcherRegex: "traceID=(\\w+)" + url: "$${__value.raw}" + datasourceUid: gdev-zipkin + + - name: gdev-jaeger + type: jaeger + uid: gdev-jaeger + access: proxy + url: http://localhost:16686 + editable: false + + - name: gdev-zipkin + type: zipkin + uid: gdev-zipkin + access: proxy + url: http://localhost:9411 + editable: false diff --git a/devenv/docker/blocks/jaeger/docker-compose.yaml b/devenv/docker/blocks/jaeger/docker-compose.yaml index f3f9f9252f2..119f1c87150 100644 --- a/devenv/docker/blocks/jaeger/docker-compose.yaml +++ b/devenv/docker/blocks/jaeger/docker-compose.yaml @@ -10,6 +10,11 @@ ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yaml + # For this to work you need to install the logging driver see https://github.com/grafana/loki/tree/master/cmd/docker-driver#plugin-installation + logging: + driver: loki + options: + loki-url: "http://localhost:3100/loki/api/v1/push" # Optional jaeger tracing environment: - JAEGER_AGENT_HOST=jaeger diff --git a/docs/Makefile b/docs/Makefile index 272e69fd437..f7766a2c26d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,9 +1,11 @@ .PHONY: docs docs-test -IMAGE = grafana/docs-base:latest +IMAGE = grafana/docs-base:latest docs: + docker pull ${IMAGE} docker run -v $(shell pwd)/sources:/hugo/content/docs/grafana/latest -p 3002:3002 --rm -it $(IMAGE) /bin/bash -c 'make server' docs-test: + docker pull ${IMAGE} docker run -v $(shell pwd)/sources:/hugo/content/docs/grafana/latest --rm -it $(IMAGE) /bin/bash -c 'make prod' diff --git a/docs/sources/_index.md b/docs/sources/_index.md index 99bf35c18c7..71b9959136f 100755 --- a/docs/sources/_index.md +++ b/docs/sources/_index.md @@ -45,11 +45,11 @@ aliases = ["/docs/grafana/v1.1", "/docs/grafana/latest/guides/reference/admin", ## Guides - - -
-
-
-
- Type -
- -
-
- -
- With tags - -
- - -
- Title - -
-
-
-
  • Url
  • - -
    - -
    - Title - -
    - -
    - Tooltip - -
    - -
    - Icon -
    - -
    -
    -
    -
    - -
    -
    Include
    -
    - - - -
    -
    -
    - - -
    diff --git a/public/app/features/dashboard/components/DashLinks/index.ts b/public/app/features/dashboard/components/DashLinks/index.ts index ef118d4a84c..61585668048 100644 --- a/public/app/features/dashboard/components/DashLinks/index.ts +++ b/public/app/features/dashboard/components/DashLinks/index.ts @@ -1,2 +1 @@ -export { DashLinksContainerCtrl } from './DashLinksContainerCtrl'; export { DashLinksEditorCtrl } from './DashLinksEditorCtrl'; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 38a29c8eb58..c100415aaa4 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -67,7 +67,7 @@ class DashNav extends PureComponent { onClose = () => { this.props.updateLocation({ - query: { edit: null, viewPanel: null }, + query: { viewPanel: null }, partial: true, }); }; diff --git a/public/app/features/dashboard/components/Inspector/InspectJSONTab.tsx b/public/app/features/dashboard/components/Inspector/InspectJSONTab.tsx index 24673353c37..4136db0462c 100644 --- a/public/app/features/dashboard/components/Inspector/InspectJSONTab.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectJSONTab.tsx @@ -17,7 +17,7 @@ enum ShowContent { const options: Array> = [ { label: 'Panel JSON', - description: 'The model saved in the dashboard JSON that configures how everythign works.', + description: 'The model saved in the dashboard JSON that configures how everything works.', value: ShowContent.PanelJSON, }, { diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index 93404b26146..3df631e4ee7 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -27,6 +27,7 @@ import { config } from 'app/core/config'; import { getPanelInspectorStyles } from './styles'; import { StoreState } from 'app/types'; import { InspectDataTab } from './InspectDataTab'; +import { supportsDataQuery } from '../PanelEditor/utils'; interface OwnProps { dashboard: DashboardModel; @@ -269,7 +270,7 @@ export class PanelInspectorUnconnected extends PureComponent { const error = last?.error; const tabs = []; - if (plugin && !plugin.meta.skipDataQuery) { + if (supportsDataQuery(plugin)) { tabs.push({ label: 'Data', value: InspectTab.Data }); tabs.push({ label: 'Stats', value: InspectTab.Stats }); } @@ -284,7 +285,7 @@ export class PanelInspectorUnconnected extends PureComponent { tabs.push({ label: 'Error', value: InspectTab.Error }); } - if (dashboard.meta.canEdit) { + if (dashboard.meta.canEdit && supportsDataQuery(plugin)) { tabs.push({ label: 'Query', value: InspectTab.Query }); } return tabs; diff --git a/public/app/features/dashboard/components/Inspector/QueryInspector.tsx b/public/app/features/dashboard/components/Inspector/QueryInspector.tsx index b4050d66024..0f71b6767c6 100644 --- a/public/app/features/dashboard/components/Inspector/QueryInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/QueryInspector.tsx @@ -8,6 +8,7 @@ import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipb import { CoreEvents } from 'app/types'; import { PanelModel } from 'app/features/dashboard/state'; import { getPanelInspectorStyles } from './styles'; +import { supportsDataQuery } from '../PanelEditor/utils'; interface DsQuery { isLoading: boolean; @@ -188,6 +189,10 @@ export class QueryInspector extends PureComponent { const styles = getPanelInspectorStyles(); const haveData = Object.keys(response).length > 0; + if (!supportsDataQuery(this.props.panel.plugin)) { + return null; + } + return ( <>
    diff --git a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx index a1aa7cdee5c..c7db5ab7cb4 100644 --- a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx @@ -2,18 +2,20 @@ import React, { useCallback } from 'react'; import cloneDeep from 'lodash/cloneDeep'; import { DataFrame, + FeatureState, FieldConfigPropertyItem, FieldConfigSource, PanelPlugin, SelectableValue, VariableSuggestionsScope, } from '@grafana/data'; -import { Container, Counter, Field, fieldMatchersUI, Label, ValuePicker } from '@grafana/ui'; +import { Container, Counter, FeatureInfoBox, Field, fieldMatchersUI, Label, useTheme, ValuePicker } from '@grafana/ui'; import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv'; import { OverrideEditor } from './OverrideEditor'; import groupBy from 'lodash/groupBy'; import { OptionsGroup } from './OptionsGroup'; import { selectors } from '@grafana/e2e-selectors'; +import { css } from 'emotion'; interface Props { plugin: PanelPlugin; @@ -27,6 +29,8 @@ interface Props { * Expects the container div to have size set and will fill it 100% */ export const OverrideFieldConfigEditor: React.FC = props => { + const theme = useTheme(); + const { config } = props; const onOverrideChange = (index: number, override: any) => { const { config } = props; let overrides = cloneDeep(config.overrides); @@ -104,6 +108,19 @@ export const OverrideFieldConfigEditor: React.FC = props => { return (
    + {config.overrides.length === 0 && ( + + Field options overrides give you a fine grained control over how your data is displayed. + + )} + {renderOverrides()} {renderAddOverride()}
    diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx index b5cd598fc04..a05c67dad54 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx @@ -98,7 +98,7 @@ export const OptionsPaneContent: React.FC = ({ /> - + {showMainTab ? ( = ({ const onDynamicConfigValueAdd = useCallback( (id: string) => { + const registryItem = registry.get(id); const propertyConfig: DynamicConfigValue = { id, + value: registryItem.defaultValue, }; + if (override.properties) { override.properties.push(propertyConfig); } else { override.properties = [propertyConfig]; } + onChange(override); }, [override, onChange] 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 0a17952c866..d124fbb9baf 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts @@ -33,7 +33,7 @@ describe('panelEditor actions', () => { panels: [{ id: 12, type: 'graph' }], }); - const panel = sourcePanel.getEditClone(); + const panel = dashboard.initEditPanel(sourcePanel); panel.updateOptions({ prop: true }); const state: PanelEditorState = { @@ -65,7 +65,7 @@ describe('panelEditor actions', () => { panels: [{ id: 12, type: 'graph' }], }); - const panel = sourcePanel.getEditClone(); + const panel = dashboard.initEditPanel(sourcePanel); panel.type = 'table'; panel.plugin = getPanelPlugin({ id: 'table' }); panel.updateOptions({ prop: true }); @@ -77,6 +77,8 @@ describe('panelEditor actions', () => { querySubscription: { unsubscribe: jest.fn() }, }; + const panelDestroy = (panel.destroy = jest.fn()); + const dispatchedActions = await thunkTester({ panelEditor: state, dashboard: { @@ -89,6 +91,7 @@ describe('panelEditor actions', () => { expect(dispatchedActions.length).toBe(3); expect(dispatchedActions[0].type).toBe(panelModelAndPluginReady.type); expect(sourcePanel.plugin).toEqual(panel.plugin); + expect(panelDestroy.mock.calls.length).toEqual(1); }); it('should discard changes when shouldDiscardChanges is true', async () => { @@ -101,7 +104,7 @@ describe('panelEditor actions', () => { panels: [{ id: 12, type: 'graph' }], }); - const panel = sourcePanel.getEditClone(); + const panel = dashboard.initEditPanel(sourcePanel); panel.updateOptions({ prop: true }); const state: PanelEditorState = { diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.ts index 78541e2260d..3aff52d282c 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.ts @@ -58,10 +58,7 @@ export function panelEditorCleanUp(): ThunkResult { // Resend last query result on source panel query runner // But do this after the panel edit editor exit process has completed setTimeout(() => { - const lastResult = panel.getQueryRunner().getLastResult(); - if (lastResult) { - sourcePanel.getQueryRunner().pipeDataToSubject(lastResult); - } + sourcePanel.getQueryRunner().useLastResultFrom(panel.getQueryRunner()); }, 20); } diff --git a/public/app/features/dashboard/components/PanelEditor/utils.test.ts b/public/app/features/dashboard/components/PanelEditor/utils.test.ts index a76f77ac16b..66d321a976c 100644 --- a/public/app/features/dashboard/components/PanelEditor/utils.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/utils.test.ts @@ -1,4 +1,5 @@ -import { FieldConfig, standardFieldConfigEditorRegistry } from '@grafana/data'; +import { FieldConfig, PanelPlugin, standardFieldConfigEditorRegistry } from '@grafana/data'; +import { supportsDataQuery } from './utils'; describe('standardFieldConfigEditorRegistry', () => { const dummyConfig: FieldConfig = { @@ -20,3 +21,32 @@ describe('standardFieldConfigEditorRegistry', () => { }); }); }); + +describe('supportsDataQuery', () => { + describe('when called with plugin that supports queries', () => { + it('then it should return true', () => { + const plugin = ({ meta: { skipDataQuery: false } } as unknown) as PanelPlugin; + expect(supportsDataQuery(plugin)).toBe(true); + }); + }); + + describe('when called with plugin that does not support queries', () => { + it('then it should return false', () => { + const plugin = ({ meta: { skipDataQuery: true } } as unknown) as PanelPlugin; + expect(supportsDataQuery(plugin)).toBe(false); + }); + }); + + describe('when called without skipDataQuery', () => { + it('then it should return false', () => { + const plugin = ({ meta: {} } as unknown) as PanelPlugin; + expect(supportsDataQuery(plugin)).toBe(false); + }); + }); + + describe('when called without plugin', () => { + it('then it should return false', () => { + expect(supportsDataQuery(undefined)).toBe(false); + }); + }); +}); diff --git a/public/app/features/dashboard/components/PanelEditor/utils.ts b/public/app/features/dashboard/components/PanelEditor/utils.ts index 636a53558c3..dae7960332e 100644 --- a/public/app/features/dashboard/components/PanelEditor/utils.ts +++ b/public/app/features/dashboard/components/PanelEditor/utils.ts @@ -2,6 +2,7 @@ import { CSSProperties } from 'react'; import { PanelModel } from '../../state/PanelModel'; import { DisplayMode } from './types'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; +import { PanelPlugin } from '@grafana/data'; export function calculatePanelSize(mode: DisplayMode, width: number, height: number, panel: PanelModel): CSSProperties { if (mode === DisplayMode.Fill) { @@ -24,3 +25,7 @@ export function calculatePanelSize(mode: DisplayMode, width: number, height: num height: pHeight * scale, }; } + +export function supportsDataQuery(plugin: PanelPlugin | undefined): boolean { + return plugin?.meta.skipDataQuery === false; +} diff --git a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx index 10caf5aeb5e..03583c717ae 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx @@ -95,7 +95,7 @@ describe('ShareModal', () => { it('should generate share url absolute time', () => { const state = ctx.wrapper?.state(); - expect(state?.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&panelId=22&fullscreen'); + expect(state?.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&viewPanel=22'); }); it('should generate render url', () => { @@ -139,25 +139,14 @@ describe('ShareModal', () => { expect(state?.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light'); }); - it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', () => { - mockLocationHref('http://server/#!/test?fullscreen&edit'); + it('should remove editPanel from image url when is first param in querystring', () => { + mockLocationHref('http://server/#!/test?editPanel=1'); ctx.mount({ panel: { id: 1, options: {}, fieldConfig: { defaults: {}, overrides: [] } }, }); const state = ctx.wrapper?.state(); - expect(state?.shareUrl).toContain('?fullscreen&edit&from=1000&to=2000&orgId=1&panelId=1'); - expect(state?.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC'); - }); - - it('should remove edit from image url when is first param in querystring and modeSharePanel is true', () => { - mockLocationHref('http://server/#!/test?edit&fullscreen'); - ctx.mount({ - panel: { id: 1, options: {}, fieldConfig: { defaults: {}, overrides: [] } }, - }); - - const state = ctx.wrapper?.state(); - expect(state?.shareUrl).toContain('?edit&fullscreen&from=1000&to=2000&orgId=1&panelId=1'); + expect(state?.shareUrl).toContain('?editPanel=1&from=1000&to=2000&orgId=1'); expect(state?.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC'); }); diff --git a/public/app/features/dashboard/components/ShareModal/utils.ts b/public/app/features/dashboard/components/ShareModal/utils.ts index dacc19d530b..00aec7d7fb2 100644 --- a/public/app/features/dashboard/components/ShareModal/utils.ts +++ b/public/app/features/dashboard/components/ShareModal/utils.ts @@ -29,12 +29,10 @@ export function buildParams( params.theme = selectedTheme; } - if (panel) { - params.panelId = panel.id; - params.fullscreen = true; + if (panel && !params.editPanel) { + params.viewPanel = panel.id; } else { - delete params.panelId; - delete params.fullscreen; + delete params.viewPanel; } return params; @@ -74,8 +72,11 @@ export function buildSoloUrl( let soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); - delete params.fullscreen; - delete params.edit; + + params.panelId = params.editPanel ?? params.viewPanel; + delete params.editPanel; + delete params.viewPanel; + return urlUtil.appendQueryToUrl(soloUrl, urlUtil.toUrlParams(params)); } diff --git a/public/app/features/dashboard/components/SubMenu/AngularDashboardLinks.tsx b/public/app/features/dashboard/components/SubMenu/AngularDashboardLinks.tsx deleted file mode 100644 index 82e4159d500..00000000000 --- a/public/app/features/dashboard/components/SubMenu/AngularDashboardLinks.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { PureComponent } from 'react'; -import { AngularComponent, getAngularLoader } from '@grafana/runtime'; - -import { DashboardModel } from '../../state/DashboardModel'; - -export interface Props { - dashboard: DashboardModel | null; -} - -export class AngularDashboardLinks extends PureComponent { - element: HTMLElement; - angularCmp: AngularComponent; - - componentDidMount() { - if (!this.hasLinks()) { - return; - } - - const loader = getAngularLoader(); - const template = ''; - const scopeProps = { - dashboard: this.props.dashboard, - links: this.props.dashboard.links, - }; - - this.angularCmp = loader.load(this.element, scopeProps, template); - } - - componentWillUnmount() { - if (this.angularCmp) { - this.angularCmp.destroy(); - } - } - - hasLinks = () => this.props.dashboard.links.length > 0; - - render() { - if (!this.hasLinks()) { - return null; - } - return
    (this.element = element)} />; - } -} diff --git a/public/app/features/dashboard/components/SubMenu/AngularSubMenu.tsx b/public/app/features/dashboard/components/SubMenu/AngularSubMenu.tsx deleted file mode 100644 index 4faca44be2c..00000000000 --- a/public/app/features/dashboard/components/SubMenu/AngularSubMenu.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// Libaries -import React, { PureComponent } from 'react'; - -// Utils & Services -import { AngularComponent, getAngularLoader } from '@grafana/runtime'; - -// Types -import { DashboardModel } from '../../state/DashboardModel'; - -export interface Props { - dashboard: DashboardModel | null; -} - -export class AngularSubMenu extends PureComponent { - element: HTMLElement; - angularCmp: AngularComponent; - - componentDidMount() { - const loader = getAngularLoader(); - - const template = ''; - const scopeProps = { dashboard: this.props.dashboard }; - - this.angularCmp = loader.load(this.element, scopeProps, template); - } - - componentWillUnmount() { - if (this.angularCmp) { - this.angularCmp.destroy(); - } - } - - render() { - return
    (this.element = element)} />; - } -} diff --git a/public/app/features/dashboard/components/SubMenu/DashboardLinks.tsx b/public/app/features/dashboard/components/SubMenu/DashboardLinks.tsx new file mode 100644 index 00000000000..39092e48806 --- /dev/null +++ b/public/app/features/dashboard/components/SubMenu/DashboardLinks.tsx @@ -0,0 +1,43 @@ +import React, { FC } from 'react'; +import { Icon, IconName, Tooltip } from '@grafana/ui'; +import { sanitize, sanitizeUrl } from '@grafana/data/src/text/sanitize'; +import { DashboardsDropdown } from './DashboardsDropdown'; +import { getLinkSrv } from '../../../panel/panellinks/link_srv'; + +import { DashboardModel } from '../../state'; +import { DashboardLink } from '../../state/DashboardModel'; +import { iconMap } from '../DashLinks/DashLinksEditorCtrl'; + +export interface Props { + dashboard: DashboardModel; +} + +export const DashboardLinks: FC = ({ dashboard }) => { + return ( + dashboard.links.length > 0 && ( + <> + {dashboard.links.map((link: DashboardLink, index: number) => { + const linkInfo = getLinkSrv().getAnchorInfo(link); + const key = `${link.title}-$${index}`; + + if (link.asDropdown) { + return ; + } + + const linkElement = ( + + + {sanitize(linkInfo.title)} + + ); + + return ( +
    + {link.tooltip ? {linkElement} : linkElement} +
    + ); + })} + + ) + ); +}; diff --git a/public/app/features/dashboard/components/SubMenu/DashboardsDropdown.tsx b/public/app/features/dashboard/components/SubMenu/DashboardsDropdown.tsx new file mode 100644 index 00000000000..08fc46d0324 --- /dev/null +++ b/public/app/features/dashboard/components/SubMenu/DashboardsDropdown.tsx @@ -0,0 +1,71 @@ +import React, { PureComponent } from 'react'; +import { Icon } from '@grafana/ui'; +import { sanitize, sanitizeUrl } from '@grafana/data/src/text/sanitize'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { DashboardSearchHit } from 'app/features/search/types'; +import { getLinkSrv } from '../../../panel/panellinks/link_srv'; +import { DashboardLink } from '../../state/DashboardModel'; + +interface Props { + link: DashboardLink; + linkInfo: { title: string; href: string }; + dashboardId: any; +} + +interface State { + searchHits: DashboardSearchHit[]; +} + +export class DashboardsDropdown extends PureComponent { + state = { searchHits: [] as DashboardSearchHit[] }; + onDropDownClick = async () => { + const { dashboardId, link } = this.props; + + const limit = 7; + const dashboards = await getBackendSrv().search({ tag: link.tags, limit }); + const processed = dashboards + .filter(dash => dash.id !== dashboardId) + .map(dash => { + return { + ...dash, + url: getLinkSrv().getLinkUrl(dash), + }; + }); + + this.setState({ + searchHits: processed, + }); + }; + + render() { + const { link, linkInfo } = this.props; + const { searchHits } = this.state; + + return ( +
    + + + {linkInfo.title} + + +
    + ); + } +} diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx index a3a3f92ab5f..45ce7ea9c31 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx @@ -4,7 +4,7 @@ import { StoreState } from '../../../../types'; import { getVariables } from '../../../variables/state/selectors'; import { VariableHide, VariableModel } from '../../../templating/types'; import { DashboardModel } from '../../state'; -import { AngularDashboardLinks } from './AngularDashboardLinks'; +import { DashboardLinks } from './DashboardLinks'; import { Annotations } from './Annotations'; import { SubMenuItems } from './SubMenuItems'; @@ -49,19 +49,17 @@ class SubMenuUnConnected extends PureComponent { }; render() { + const { dashboard, variables } = this.props; if (!this.isSubMenuVisible()) { return null; } return (
    - - + +
    - + {dashboard && }
    ); diff --git a/public/app/features/dashboard/components/SubMenu/SubMenuCtrl.ts b/public/app/features/dashboard/components/SubMenu/SubMenuCtrl.ts deleted file mode 100644 index 8a93d67a502..00000000000 --- a/public/app/features/dashboard/components/SubMenu/SubMenuCtrl.ts +++ /dev/null @@ -1,52 +0,0 @@ -import angular, { ILocationService } from 'angular'; -import _ from 'lodash'; -import { selectors } from '@grafana/e2e-selectors'; -import { VariableSrv } from 'app/features/templating/all'; -import { CoreEvents } from '../../../../types'; - -export class SubMenuCtrl { - annotations: any; - variables: any; - dashboard: any; - submenuEnabled: boolean; - selectors: typeof selectors.pages.Dashboard.SubMenu; - - /** @ngInject */ - constructor(private variableSrv: VariableSrv, private $location: ILocationService) { - this.annotations = this.dashboard.templating.list; - this.variables = this.variableSrv.variables; - this.submenuEnabled = this.dashboard.meta.submenuEnabled; - this.dashboard.events.on(CoreEvents.submenuVisibilityChanged, (enabled: boolean) => { - this.submenuEnabled = enabled; - }); - this.selectors = selectors.pages.Dashboard.SubMenu; - } - - annotationStateChanged() { - this.dashboard.startRefresh(); - } - - variableUpdated(variable: any) { - this.variableSrv.variableUpdated(variable, true); - } - - openEditView(editview: any) { - const search = _.extend(this.$location.search(), { editview: editview }); - this.$location.search(search); - } -} - -export function submenuDirective() { - return { - restrict: 'E', - templateUrl: 'public/app/features/dashboard/components/SubMenu/template.html', - controller: SubMenuCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - dashboard: '=', - }, - }; -} - -angular.module('grafana.directives').directive('dashboardSubmenu', submenuDirective); diff --git a/public/app/features/dashboard/components/SubMenu/index.ts b/public/app/features/dashboard/components/SubMenu/index.ts deleted file mode 100644 index 4148083a2e2..00000000000 --- a/public/app/features/dashboard/components/SubMenu/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { SubMenuCtrl } from './SubMenuCtrl'; -export { AngularSubMenu } from './AngularSubMenu'; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index f4ed8135f7e..f994426f5b1 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -1,8 +1,18 @@ import React from 'react'; -import { Button, Container, CustomScrollbar, stylesFactory, useTheme, ValuePicker, VerticalGroup } from '@grafana/ui'; +import { + Button, + Container, + CustomScrollbar, + FeatureInfoBox, + stylesFactory, + useTheme, + ValuePicker, + VerticalGroup, +} from '@grafana/ui'; import { DataFrame, DataTransformerConfig, + FeatureState, GrafanaTheme, SelectableValue, standardTransformersRegistry, @@ -114,13 +124,23 @@ export class TransformationsEditor extends React.PureComponent { renderNoAddedTransformsState() { return ( - <> -

    - Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being - visualized.
    - Choose one of the transformations below to start with: -

    - + + + +

    + Transformations allow you to join, calculate, re-order, hide and rename your query results before being + visualized.
    + Many transforms are not suitable if your using the Graph visualisation as it currently only supports time + series.
    + It can help to switch to Table visualisation to understand what a transformation is doing.
    +

    +

    Select one of the transformations below to start.

    +
    +
    {standardTransformersRegistry.list().map(t => { return ( @@ -136,7 +156,7 @@ export class TransformationsEditor extends React.PureComponent { ); })} - +
    ); } @@ -170,6 +190,11 @@ const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => { border: none; padding: ${theme.spacing.sm}; + // hack because these cards use classes from a very different card for some reason + .add-data-source-item-text { + font-size: ${theme.typography.size.md}; + } + &:hover { background: ${theme.colors.bg3}; box-shadow: none; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 2c3ec361d66..36158cc4d67 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -12,7 +12,6 @@ import { Branding } from 'app/core/components/Branding/Branding'; // Components import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { DashNav } from '../components/DashNav'; -import { AngularSubMenu } from '../components/SubMenu'; import { DashboardSettings } from '../components/DashboardSettings'; import { PanelEditor } from '../components/PanelEditor/PanelEditor'; import { Alert, CustomScrollbar, Icon } from '@grafana/ui'; @@ -163,9 +162,8 @@ export class DashboardPage extends PureComponent { // Clear url state this.props.updateLocation({ query: { - edit: null, - fullscreen: null, - panelId: null, + editPanel: null, + viewPanel: null, }, partial: true, }); @@ -297,7 +295,7 @@ export class DashboardPage extends PureComponent { {initError && this.renderInitFailedState()}
    - {!editPanel && !featureToggles.newVariables && } + {!featureToggles.newVariables && } {!editPanel && featureToggles.newVariables && } { return panel.snapshotData && panel.snapshotData.length; } - panelHasLastResult = () => { - return !!this.props.panel.getQueryRunner().getLastResult(); - }; - get wantsQueryExecution() { return !(this.props.plugin.meta.skipDataQuery || this.hasPanelSnapshot); } diff --git a/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap b/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap index d9494e674a8..c36e1fd60af 100644 --- a/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap +++ b/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap @@ -235,7 +235,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 24, + "schemaVersion": 25, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -477,7 +477,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 24, + "schemaVersion": 25, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -719,7 +719,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 24, + "schemaVersion": 25, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -961,7 +961,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 24, + "schemaVersion": 25, "snapshot": undefined, "style": "dark", "tags": Array [], diff --git a/public/app/features/dashboard/index.ts b/public/app/features/dashboard/index.ts index 1f77e11b9bc..e9361c1ca5b 100644 --- a/public/app/features/dashboard/index.ts +++ b/public/app/features/dashboard/index.ts @@ -9,7 +9,6 @@ import './components/DashExportModal'; import './components/DashNav'; import './components/VersionHistory'; import './components/DashboardSettings'; -import './components/SubMenu'; import './components/AdHocFilters'; import './components/RowOptions'; diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 9aacaf6cfb6..cbb374c3682 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -4,7 +4,7 @@ import React, { PureComponent } from 'react'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { QueryOptions } from './QueryOptions'; import { Button, CustomScrollbar, HorizontalGroup, Modal, stylesFactory } from '@grafana/ui'; -import { getLocationSrv } from '@grafana/runtime'; +import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime'; import { QueryEditorRows } from './QueryEditorRows'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -13,7 +13,14 @@ import config from 'app/core/config'; // Types import { PanelModel } from '../state/PanelModel'; import { DashboardModel } from '../state/DashboardModel'; -import { DataQuery, DataSourceSelectItem, DefaultTimeRange, LoadingState, PanelData } from '@grafana/data'; +import { + DataQuery, + DataSourceSelectItem, + DefaultTimeRange, + LoadingState, + PanelData, + DataSourceApi, +} from '@grafana/data'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { addQuery } from 'app/core/utils/query'; import { Unsubscribable } from 'rxjs'; @@ -28,7 +35,8 @@ interface Props { } interface State { - currentDS: DataSourceSelectItem; + dataSource?: DataSourceApi; + dataSourceItem: DataSourceSelectItem; helpContent: JSX.Element; isLoadingHelp: boolean; isPickerOpen: boolean; @@ -45,7 +53,7 @@ export class QueriesTab extends PureComponent { state: State = { isLoadingHelp: false, - currentDS: this.findCurrentDataSource(), + dataSourceItem: this.findCurrentDataSource(), helpContent: null, isPickerOpen: false, isAddingMixed: false, @@ -58,13 +66,16 @@ export class QueriesTab extends PureComponent { }, }; - componentDidMount() { + async componentDidMount() { const { panel } = this.props; const queryRunner = panel.getQueryRunner(); this.querySubscription = queryRunner.getData(false).subscribe({ next: (data: PanelData) => this.onPanelDataUpdate(data), }); + + const ds = await getDataSourceSrv().get(panel.datasource); + this.setState({ dataSource: ds }); } componentWillUnmount() { @@ -83,12 +94,12 @@ export class QueriesTab extends PureComponent { return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0]; } - onChangeDataSource = (datasource: DataSourceSelectItem) => { + onChangeDataSource = async (newDsItem: DataSourceSelectItem) => { const { panel } = this.props; - const { currentDS } = this.state; + const { dataSourceItem } = this.state; // switching to mixed - if (datasource.meta.mixed) { + if (newDsItem.meta.mixed) { // Set the datasource on all targets panel.targets.forEach(target => { if (target.datasource !== ExpressionDatasourceID) { @@ -98,27 +109,32 @@ export class QueriesTab extends PureComponent { } } }); - } else if (currentDS) { + } else if (dataSourceItem) { // if switching from mixed - if (currentDS.meta.mixed) { + if (dataSourceItem.meta.mixed) { // Remove the explicit datasource for (const target of panel.targets) { if (target.datasource !== ExpressionDatasourceID) { delete target.datasource; } } - } else if (currentDS.meta.id !== datasource.meta.id) { + } else if (dataSourceItem.meta.id !== newDsItem.meta.id) { // we are changing data source type, clear queries panel.targets = [{ refId: 'A' }]; } } - panel.datasource = datasource.value; - panel.refresh(); + const dataSource = await getDataSourceSrv().get(newDsItem.value); - this.setState({ - currentDS: datasource, - }); + panel.datasource = newDsItem.value; + + this.setState( + { + dataSourceItem: newDsItem, + dataSource: dataSource, + }, + () => panel.refresh() + ); }; openQueryInspector = () => { @@ -143,7 +159,7 @@ export class QueriesTab extends PureComponent { }; onAddQueryClick = () => { - if (this.state.currentDS.meta.mixed) { + if (this.state.dataSourceItem.meta.mixed) { this.setState({ isAddingMixed: true }); return; } @@ -163,13 +179,21 @@ export class QueriesTab extends PureComponent { renderTopSection(styles: QueriesTabStyls) { const { panel } = this.props; - const { currentDS, data } = this.state; + const { dataSourceItem, data, dataSource } = this.state; + + if (!dataSource) { + return null; + } return (
    - +
    - +
    ); diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index aafb94f8ffa..00a0040020b 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -51,7 +51,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { sort: css` width: 170px; `, - feedback: css` + footer: css` height: 60px; margin-top: ${theme.spacing.lg}; display: flex; @@ -129,10 +129,7 @@ export function RichHistoryStarredTab(props: Props) { /> ); })} -
    - Query history is a beta feature. The history is local to your browser and is not shared with others. - Feedback? -
    +
    The history is local to your browser and is not shared with others.
    ); diff --git a/public/app/features/explore/RunButton.tsx b/public/app/features/explore/RunButton.tsx index a5d975d8748..7f33ca81f69 100644 --- a/public/app/features/explore/RunButton.tsx +++ b/public/app/features/explore/RunButton.tsx @@ -21,6 +21,7 @@ const getStyles = memoizeOne(() => { type Props = { splitted: boolean; loading: boolean; + isLive: boolean; onRun: (loading: boolean) => void; refreshInterval?: string; onChangeRefreshInterval: (interval: string) => void; @@ -28,17 +29,17 @@ type Props = { }; export function RunButton(props: Props) { - const { splitted, loading, onRun, onChangeRefreshInterval, refreshInterval, showDropdown } = props; + const { splitted, loading, onRun, onChangeRefreshInterval, refreshInterval, showDropdown, isLive } = props; const styles = getStyles(); const runButton = ( onRun(loading)} buttonClassName={classNames({ - 'navbar-button--primary': !loading, - 'navbar-button--danger': loading, + 'navbar-button--primary': isLive || !loading, + 'navbar-button--danger': loading && !isLive, 'btn--radius-right-0': showDropdown, })} icon={loading ? 'fa fa-spinner' : 'sync'} diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index fd21fa8eb0f..3c2dba39178 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -56,7 +56,16 @@ export function TraceView(props: Props) { const theme = useTheme(); const traceTheme = useMemo( - () => ({ type: theme.isDark ? ThemeType.Dark : ThemeType.Light, servicesColorPalette: colors } as ThemeOptions), + () => + ({ + type: theme.isDark ? ThemeType.Dark : ThemeType.Light, + servicesColorPalette: colors, + components: { + TraceName: { + fontSize: theme.typography.size.lg, + }, + }, + } as ThemeOptions), [theme] ); const traceTimeline: TTraceTimeline = useMemo( @@ -75,7 +84,7 @@ export function TraceView(props: Props) { {}, [])} focusUiFindMatches={useCallback(() => {}, [])} hideMap={false} diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index e3bf248952c..aa3d194eba8 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -302,7 +302,6 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac latency: 0, queryResponse: createEmptyQueryResponse(), loading: false, - queryKeys: [], supportedModes, mode: mode ?? newMode, originPanelId: state.urlState && state.urlState.originPanelId, diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts index 580c3e5d0a6..7158f21ca43 100644 --- a/public/app/features/explore/utils/links.test.ts +++ b/public/app/features/explore/utils/links.test.ts @@ -66,6 +66,12 @@ function setup(link: DataLink) { origin: origin, }; }, + getAnchorInfo(link: DataLink) { + return { ...link }; + }, + getLinkUrl(link: DataLink) { + return link.url; + }, }); setDataSourceSrv({ getDataSourceSettingsByUid(uid: string) { diff --git a/public/app/features/manage-dashboards/SnapshotListCtrl.ts b/public/app/features/manage-dashboards/SnapshotListCtrl.ts deleted file mode 100644 index f13a7b3e86a..00000000000 --- a/public/app/features/manage-dashboards/SnapshotListCtrl.ts +++ /dev/null @@ -1,60 +0,0 @@ -import _ from 'lodash'; -import { ILocationService, IScope } from 'angular'; -import { getBackendSrv } from '@grafana/runtime'; - -import { NavModelSrv } from 'app/core/core'; -import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; -import { CoreEvents } from 'app/types'; -import { promiseToDigest } from '../../core/utils/promiseToDigest'; - -export class SnapshotListCtrl { - navModel: any; - snapshots: any; - - /** @ngInject */ - constructor( - private $rootScope: GrafanaRootScope, - navModelSrv: NavModelSrv, - private $location: ILocationService, - private $scope: IScope - ) { - this.navModel = navModelSrv.getNav('dashboards', 'snapshots', 0); - promiseToDigest(this.$scope)( - getBackendSrv() - .get('/api/dashboard/snapshots') - .then((result: any) => { - const baseUrl = this.$location.absUrl().replace($location.url(), ''); - this.snapshots = result.map((snapshot: any) => ({ - ...snapshot, - url: snapshot.externalUrl || `${baseUrl}/dashboard/snapshot/${snapshot.key}`, - })); - }) - ); - } - - removeSnapshotConfirmed(snapshot: any) { - _.remove(this.snapshots, { key: snapshot.key }); - promiseToDigest(this.$scope)( - getBackendSrv() - .delete('/api/snapshots/' + snapshot.key) - .then( - () => {}, - () => { - this.snapshots.push(snapshot); - } - ) - ); - } - - removeSnapshot(snapshot: any) { - this.$rootScope.appEvent(CoreEvents.showConfirmModal, { - title: 'Delete', - text: 'Are you sure you want to delete snapshot ' + snapshot.name + '?', - yesText: 'Delete', - icon: 'trash-alt', - onConfirm: () => { - this.removeSnapshotConfirmed(snapshot); - }, - }); - } -} diff --git a/public/app/features/manage-dashboards/SnapshotListPage.tsx b/public/app/features/manage-dashboards/SnapshotListPage.tsx new file mode 100644 index 00000000000..571354e0dbe --- /dev/null +++ b/public/app/features/manage-dashboards/SnapshotListPage.tsx @@ -0,0 +1,30 @@ +import React, { FC } from 'react'; +import { MapStateToProps, connect } from 'react-redux'; +import { NavModel } from '@grafana/data'; +import Page from 'app/core/components/Page/Page'; +import { getUrl } from 'app/core/selectors/location'; +import { StoreState } from 'app/types'; +import { SnapshotListTable } from './components/SnapshotListTable'; +import { getDashboardNavModel } from './state/selectors'; + +interface Props { + navModel: NavModel; + url: string; +} + +export const SnapshotListPage: FC = ({ navModel, url }) => { + return ( + + + + + + ); +}; + +const mapStateToProps: MapStateToProps = (state: StoreState) => ({ + navModel: getDashboardNavModel(state), + url: getUrl(state.location), +}); + +export default connect(mapStateToProps)(SnapshotListPage); diff --git a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx index 63b0b7a1235..197d3b74b63 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx @@ -63,7 +63,14 @@ export const ImportDashboardForm: FC = ({ /> - + = ({ url }) => { + const [snapshots, setSnapshots] = useState([]); + const [removeSnapshot, setRemoveSnapshot] = useState(); + + const getSnapshots = useCallback(async () => { + await getBackendSrv() + .get('/api/dashboard/snapshots') + .then((result: Snapshot[]) => { + const absUrl = window.location.href; + const baseUrl = absUrl.replace(url, ''); + const snapshots = result.map(snapshot => ({ + ...snapshot, + url: snapshot.externalUrl || `${baseUrl}/dashboard/snapshot/${snapshot.key}`, + })); + setSnapshots(snapshots); + }); + }, []); + + const doRemoveSnapshot = useCallback( + async (snapshot: Snapshot) => { + setSnapshots(snapshots.filter(ss => ss.key !== snapshot.key)); + await getBackendSrv() + .delete(`/api/snapshots/${snapshot.key}`) + .then(noop, () => { + setSnapshots(snapshots.concat(snapshot)); + }); + }, + [snapshots] + ); + + useEffect(() => { + getSnapshots(); + }, []); + + return ( +
    + + + + + + + + + + + + {snapshots.map((snapshot, key) => { + return ( + + + + + + + + ); + })} + +
    + Name + + Snapshot url +
    + {snapshot.name} + + {snapshot.url} + {snapshot.external && External} + + View + + +
    + + setRemoveSnapshot(undefined)} + onConfirm={() => { + doRemoveSnapshot(removeSnapshot); + setRemoveSnapshot(undefined); + }} + /> +
    + ); +}; diff --git a/public/app/features/manage-dashboards/index.ts b/public/app/features/manage-dashboards/index.ts index 79b9b7148e3..a1790ce0340 100644 --- a/public/app/features/manage-dashboards/index.ts +++ b/public/app/features/manage-dashboards/index.ts @@ -3,10 +3,3 @@ export { ValidationSrv } from './services/ValidationSrv'; // Components export * from './components/UploadDashboard'; - -// Controllers -import { SnapshotListCtrl } from './SnapshotListCtrl'; - -import coreModule from 'app/core/core_module'; - -coreModule.controller('SnapshotListCtrl', SnapshotListCtrl); diff --git a/public/app/features/manage-dashboards/partials/snapshot_list.html b/public/app/features/manage-dashboards/partials/snapshot_list.html deleted file mode 100644 index acdb08d71f7..00000000000 --- a/public/app/features/manage-dashboards/partials/snapshot_list.html +++ /dev/null @@ -1,37 +0,0 @@ - - -
    - - - - - - - - - - - - - - - -
    NameSnapshot url
    - {{snapshot.name}} - - {{snapshot.url}} - - External - - - - View - - - - - -
    -
    - -