-
- Clear query history
-
-
- Delete all of your query history, permanently.
-
+
Clear query history
+
Delete all of your query history, permanently.
diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx
index 362d8ca38db..8b3a2d20576 100644
--- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx
+++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx
@@ -1,9 +1,9 @@
import { css } from '@emotion/css';
import React, { useEffect } from 'react';
-import { GrafanaTheme, SelectableValue } from '@grafana/data';
+import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
-import { stylesFactory, useTheme, Select, MultiSelect, FilterInput, Button } from '@grafana/ui';
+import { useStyles2, Select, MultiSelect, FilterInput, Button } from '@grafana/ui';
import {
createDatasourcesList,
SortOrder,
@@ -28,8 +28,8 @@ export interface Props {
exploreId: ExploreId;
}
-const getStyles = stylesFactory((theme: GrafanaTheme) => {
- const bgColor = theme.isLight ? theme.palette.gray5 : theme.palette.dark4;
+const getStyles = (theme: GrafanaTheme2) => {
+ const bgColor = theme.isLight ? theme.v1.palette.gray5 : theme.v1.palette.dark4;
return {
container: css`
display: flex;
@@ -44,33 +44,33 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
`,
multiselect: css`
width: 100%;
- margin-bottom: ${theme.spacing.sm};
+ margin-bottom: ${theme.spacing(1)};
.gf-form-select-box__multi-value {
background-color: ${bgColor};
- padding: ${theme.spacing.xxs} ${theme.spacing.xs} ${theme.spacing.xxs} ${theme.spacing.sm};
- border-radius: ${theme.border.radius.sm};
+ padding: ${theme.spacing(0.25, 0.5, 0.25, 1)};
+ border-radius: ${theme.shape.borderRadius(1)};
}
`,
filterInput: css`
- margin-bottom: ${theme.spacing.sm};
+ margin-bottom: ${theme.spacing(1)};
`,
sort: css`
width: 170px;
`,
footer: css`
height: 60px;
- margin-top: ${theme.spacing.lg};
+ margin-top: ${theme.spacing(3)};
display: flex;
justify-content: center;
- font-weight: ${theme.typography.weight.light};
- font-size: ${theme.typography.size.sm};
+ font-weight: ${theme.typography.fontWeightLight};
+ font-size: ${theme.typography.bodySmall.fontSize};
a {
- font-weight: ${theme.typography.weight.semibold};
- margin-left: ${theme.spacing.xxs};
+ font-weight: ${theme.typography.fontWeightMedium};
+ margin-left: ${theme.spacing(0.25)};
}
`,
};
-});
+};
export function RichHistoryStarredTab(props: Props) {
const {
@@ -86,8 +86,7 @@ export function RichHistoryStarredTab(props: Props) {
exploreId,
} = props;
- const theme = useTheme();
- const styles = getStyles(theme);
+ const styles = useStyles2(getStyles);
const listOfDatasources = createDatasourcesList();
diff --git a/public/app/features/expressions/components/Condition.tsx b/public/app/features/expressions/components/Condition.tsx
index abcb8c90d68..7ca15d80584 100644
--- a/public/app/features/expressions/components/Condition.tsx
+++ b/public/app/features/expressions/components/Condition.tsx
@@ -1,9 +1,9 @@
import { css, cx } from '@emotion/css';
-import React, { FC, FormEvent } from 'react';
+import React, { FormEvent } from 'react';
-import { GrafanaTheme, SelectableValue } from '@grafana/data';
+import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Stack } from '@grafana/experimental';
-import { Button, ButtonSelect, Icon, InlineFieldRow, Input, Select, useStyles } from '@grafana/ui';
+import { Button, ButtonSelect, Icon, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui';
import alertDef, { EvalFunction } from '../../alerting/state/alertDef';
import { ClassicCondition, ReducerType } from '../types';
@@ -20,8 +20,8 @@ const reducerFunctions = alertDef.reducerTypes.map((rt) => ({ label: rt.text, va
const evalOperators = alertDef.evalOperators.map((eo) => ({ label: eo.text, value: eo.value }));
const evalFunctions = alertDef.evalFunctions.map((ef) => ({ label: ef.text, value: ef.value }));
-export const Condition: FC
= ({ condition, index, onChange, onRemoveCondition, refIds }) => {
- const styles = useStyles(getStyles);
+export const Condition = ({ condition, index, onChange, onRemoveCondition, refIds }: Props) => {
+ const styles = useStyles2(getStyles);
const onEvalOperatorChange = (evalOperator: SelectableValue) => {
onChange({
@@ -137,10 +137,10 @@ export const Condition: FC = ({ condition, index, onChange, onRemoveCondi
);
};
-const getStyles = (theme: GrafanaTheme) => {
+const getStyles = (theme: GrafanaTheme2) => {
const buttonStyle = css`
- color: ${theme.colors.textBlue};
- font-size: ${theme.typography.size.sm};
+ color: ${theme.colors.primary.text};
+ font-size: ${theme.typography.bodySmall.fontSize};
`;
return {
buttonSelectText: buttonStyle,
@@ -148,12 +148,12 @@ const getStyles = (theme: GrafanaTheme) => {
css`
display: flex;
align-items: center;
- border-radius: ${theme.border.radius.sm};
- font-weight: ${theme.typography.weight.semibold};
- border: 1px solid ${theme.colors.border1};
+ border-radius: ${theme.shape.borderRadius(1)};
+ font-weight: ${theme.typography.fontWeightMedium};
+ border: 1px solid ${theme.colors.border.weak};
white-space: nowrap;
- padding: 0 ${theme.spacing.sm};
- background-color: ${theme.colors.bodyBg};
+ padding: 0 ${theme.spacing(1)};
+ background-color: ${theme.colors.background.canvas};
`,
buttonStyle
),
diff --git a/public/app/features/inspector/DetailText.tsx b/public/app/features/inspector/DetailText.tsx
index 6e2868daabf..11972f8417f 100644
--- a/public/app/features/inspector/DetailText.tsx
+++ b/public/app/features/inspector/DetailText.tsx
@@ -1,17 +1,17 @@
import { css } from '@emotion/css';
import React from 'react';
-import { GrafanaTheme } from '@grafana/data';
-import { useStyles } from '@grafana/ui';
+import { GrafanaTheme2 } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
-const getStyles = (theme: GrafanaTheme) => css`
+const getStyles = (theme: GrafanaTheme2) => css`
margin: 0;
- margin-left: ${theme.spacing.md};
- font-size: ${theme.typography.size.sm};
- color: ${theme.colors.textWeak};
+ margin-left: ${theme.spacing(2)};
+ font-size: ${theme.typography.bodySmall.fontSize};
+ color: ${theme.colors.text.secondary};
`;
export const DetailText = ({ children }: React.PropsWithChildren<{}>) => {
- const collapsedTextStyles = useStyles(getStyles);
+ const collapsedTextStyles = useStyles2(getStyles);
return {children}
;
};
diff --git a/public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx b/public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx
index 92dbf8fb666..c46cae3fdf2 100644
--- a/public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx
+++ b/public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx
@@ -1,7 +1,7 @@
import React, { FC, useEffect, useMemo, useReducer } from 'react';
import { LoadingState } from '@grafana/data';
-import { Button, Modal, useStyles } from '@grafana/ui';
+import { Button, Modal, useStyles2 } from '@grafana/ui';
import { getModalStyles } from '../../styles';
import { LibraryElementDTO } from '../../types';
@@ -17,7 +17,7 @@ interface Props {
}
export const DeleteLibraryPanelModal: FC = ({ libraryPanel, onDismiss, onConfirm }) => {
- const styles = useStyles(getModalStyles);
+ const styles = useStyles2(getModalStyles);
const [{ dashboardTitles, loadingState }, dispatch] = useReducer(
deleteLibraryPanelModalReducer,
initialDeleteLibraryPanelModalState
@@ -54,13 +54,13 @@ export const DeleteLibraryPanelModal: FC = ({ libraryPanel, onDismiss, on
const LoadingIndicator = () => Loading library panel...;
const Confirm = () => {
- const styles = useStyles(getModalStyles);
+ const styles = useStyles2(getModalStyles);
return Do you want to delete this panel?
;
};
const HasConnectedDashboards: FC<{ dashboardTitles: string[] }> = ({ dashboardTitles }) => {
- const styles = useStyles(getModalStyles);
+ const styles = useStyles2(getModalStyles);
const suffix = dashboardTitles.length === 1 ? 'dashboard.' : 'dashboards.';
const message = `${dashboardTitles.length} ${suffix}`;
if (dashboardTitles.length === 0) {
diff --git a/public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx b/public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx
index cc06e6bbf43..3560ce189c3 100644
--- a/public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx
+++ b/public/app/features/library-panels/components/LibraryPanelInfo/LibraryPanelInfo.tsx
@@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import React from 'react';
-import { DateTimeInput, GrafanaTheme } from '@grafana/data';
-import { useStyles } from '@grafana/ui';
+import { DateTimeInput, GrafanaTheme2 } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
import { PanelModelWithLibraryPanel } from '../../types';
@@ -12,7 +12,7 @@ interface Props {
}
export const LibraryPanelInformation = ({ panel, formatDate }: Props) => {
- const styles = useStyles(getStyles);
+ const styles = useStyles2(getStyles);
const meta = panel.libraryPanel?.meta;
if (!meta) {
@@ -42,22 +42,22 @@ export const LibraryPanelInformation = ({ panel, formatDate }: Props) => {
);
};
-const getStyles = (theme: GrafanaTheme) => {
+const getStyles = (theme: GrafanaTheme2) => {
return {
info: css`
line-height: 1;
`,
libraryPanelInfo: css`
- color: ${theme.colors.textSemiWeak};
- font-size: ${theme.typography.size.sm};
+ color: ${theme.colors.text.secondary};
+ font-size: ${theme.typography.bodySmall.fontSize};
`,
userAvatar: css`
border-radius: 50%;
box-sizing: content-box;
width: 22px;
height: 22px;
- padding-left: ${theme.spacing.sm};
- padding-right: ${theme.spacing.sm};
+ padding-left: ${theme.spacing(1)};
+ padding-right: ${theme.spacing(1)};
`,
};
};
diff --git a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx
index 39431a8c3d3..56cde2c3787 100644
--- a/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx
+++ b/public/app/features/library-panels/components/LibraryPanelsView/LibraryPanelsView.tsx
@@ -2,8 +2,8 @@ import { css, cx } from '@emotion/css';
import React, { useMemo, useReducer } from 'react';
import { useDebounce } from 'react-use';
-import { GrafanaTheme, LoadingState } from '@grafana/data';
-import { Pagination, useStyles } from '@grafana/ui';
+import { GrafanaTheme2, LoadingState } from '@grafana/data';
+import { Pagination, useStyles2 } from '@grafana/ui';
import { LibraryElementDTO } from '../../types';
import { LibraryPanelCard } from '../LibraryPanelCard/LibraryPanelCard';
@@ -34,7 +34,7 @@ export const LibraryPanelsView: React.FC = ({
currentPanelId: currentPanel,
perPage: propsPerPage = 40,
}) => {
- const styles = useStyles(getPanelViewStyles);
+ const styles = useStyles2(getPanelViewStyles);
const [{ libraryPanels, page, perPage, numberOfPages, loadingState, currentPanelId }, dispatch] = useReducer(
libraryPanelsViewReducer,
{
@@ -97,7 +97,7 @@ export const LibraryPanelsView: React.FC = ({
);
};
-const getPanelViewStyles = (theme: GrafanaTheme) => {
+const getPanelViewStyles = (theme: GrafanaTheme2) => {
return {
container: css`
display: flex;
@@ -107,7 +107,7 @@ const getPanelViewStyles = (theme: GrafanaTheme) => {
libraryPanelList: css`
max-width: 100%;
display: grid;
- grid-gap: ${theme.spacing.sm};
+ grid-gap: ${theme.spacing(1)};
`,
searchHeader: css`
display: flex;
@@ -118,7 +118,7 @@ const getPanelViewStyles = (theme: GrafanaTheme) => {
`,
pagination: css`
align-self: center;
- margin-top: ${theme.spacing.sm};
+ margin-top: ${theme.spacing(1)};
`,
noPanelsFound: css`
label: noPanelsFound;
diff --git a/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx b/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx
index 34ef35fd7c1..442cce84952 100644
--- a/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx
+++ b/public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react';
import { useAsync, useDebounce } from 'react-use';
-import { Button, Icon, Input, Modal, useStyles } from '@grafana/ui';
+import { Button, Icon, Input, Modal, useStyles2 } from '@grafana/ui';
import { getConnectedDashboards } from '../../state/api';
import { getModalStyles } from '../../styles';
@@ -44,7 +44,7 @@ export const SaveLibraryPanelModal = ({ panel, folderId, isUnsavedPrompt, onDism
);
const { saveLibraryPanel } = usePanelSave();
- const styles = useStyles(getModalStyles);
+ const styles = useStyles2(getModalStyles);
const discardAndClose = useCallback(() => {
onDiscard();
}, [onDiscard]);
diff --git a/public/app/features/library-panels/styles.ts b/public/app/features/library-panels/styles.ts
index b76b2e7b43e..bfe0c543695 100644
--- a/public/app/features/library-panels/styles.ts
+++ b/public/app/features/library-panels/styles.ts
@@ -1,54 +1,54 @@
import { css } from '@emotion/css';
-import { GrafanaTheme } from '@grafana/data';
+import { GrafanaTheme2 } from '@grafana/data';
-export function getModalStyles(theme: GrafanaTheme) {
+export function getModalStyles(theme: GrafanaTheme2) {
return {
myTable: css`
max-height: 204px;
overflow-y: auto;
margin-top: 11px;
margin-bottom: 28px;
- border-radius: ${theme.border.radius.sm};
- border: 1px solid ${theme.colors.bg3};
- background: ${theme.colors.bg1};
- color: ${theme.colors.textSemiWeak};
- font-size: ${theme.typography.size.md};
+ border-radius: ${theme.shape.borderRadius(1)};
+ border: 1px solid ${theme.colors.action.hover};
+ background: ${theme.colors.background.primary};
+ color: ${theme.colors.text.secondary};
+ font-size: ${theme.typography.h6.fontSize};
width: 100%;
thead {
color: #538ade;
- font-size: ${theme.typography.size.sm};
+ font-size: ${theme.typography.bodySmall.fontSize};
}
th,
td {
padding: 6px 13px;
- height: ${theme.spacing.xl};
+ height: ${theme.spacing(4)};
}
tbody > tr:nth-child(odd) {
- background: ${theme.colors.bg2};
+ background: ${theme.colors.background.secondary};
}
`,
noteTextbox: css`
- margin-bottom: ${theme.spacing.xl};
+ margin-bottom: ${theme.spacing(4)};
`,
textInfo: css`
- color: ${theme.colors.textSemiWeak};
+ color: ${theme.colors.text.secondary};
font-size: ${theme.typography.size.sm};
`,
dashboardSearch: css`
- margin-top: ${theme.spacing.md};
+ margin-top: ${theme.spacing(2)};
`,
modal: css`
width: 500px;
`,
modalText: css`
- font-size: ${theme.typography.heading.h4};
- color: ${theme.colors.link};
- margin-bottom: calc(${theme.spacing.d} * 2);
- padding-top: ${theme.spacing.d};
+ font-size: ${theme.typography.h4.fontSize};
+ color: ${theme.colors.text.primary};
+ margin-bottom: ${theme.spacing(4)};
+ padding-top: ${theme.spacing(2)};
`,
};
}
diff --git a/public/app/features/live/dashboard/DashboardChangedModal.tsx b/public/app/features/live/dashboard/DashboardChangedModal.tsx
index 8ac9c6deb7b..24b534208c4 100644
--- a/public/app/features/live/dashboard/DashboardChangedModal.tsx
+++ b/public/app/features/live/dashboard/DashboardChangedModal.tsx
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import React, { PureComponent } from 'react';
-import { GrafanaTheme } from '@grafana/data';
+import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Modal, stylesFactory } from '@grafana/ui';
@@ -59,7 +59,7 @@ export class DashboardChangedModal extends PureComponent {
render() {
const { event } = this.props;
const { dismiss } = this.state;
- const styles = getStyles(config.theme);
+ const styles = getStyles(config.theme2);
const isDelete = event?.action === DashboardEventAction.Deleted;
@@ -98,7 +98,7 @@ export class DashboardChangedModal extends PureComponent {
}
}
-const getStyles = stylesFactory((theme: GrafanaTheme) => {
+const getStyles = stylesFactory((theme: GrafanaTheme2) => {
return {
modal: css`
width: 500px;
@@ -106,13 +106,13 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
radioItem: css`
margin: 0;
font-size: ${theme.typography.size.sm};
- color: ${theme.colors.textWeak};
+ color: ${theme.colors.text.secondary};
padding: 10px;
cursor: pointer;
width: 100%;
&:hover {
- background: ${theme.colors.bgBlue1};
+ background: ${theme.colors.primary.main};
color: ${theme.colors.text};
}
`,
diff --git a/public/app/features/live/pages/CloudAdminPage.tsx b/public/app/features/live/pages/CloudAdminPage.tsx
index 449c4c06c33..9a6fd74ca26 100644
--- a/public/app/features/live/pages/CloudAdminPage.tsx
+++ b/public/app/features/live/pages/CloudAdminPage.tsx
@@ -1,9 +1,7 @@
import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react';
-import { GrafanaTheme } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
-import { useStyles } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
@@ -13,7 +11,6 @@ export default function CloudAdminPage() {
const navModel = useNavModel('live-cloud');
const [cloud, setCloud] = useState([]);
const [error, setError] = useState();
- const styles = useStyles(getStyles);
useEffect(() => {
getBackendSrv()
@@ -47,10 +44,8 @@ export default function CloudAdminPage() {
);
}
-const getStyles = (theme: GrafanaTheme) => {
- return {
- row: css`
- cursor: pointer;
- `,
- };
+const styles = {
+ row: css`
+ cursor: pointer;
+ `,
};
diff --git a/public/app/features/live/pages/PipelineTable.tsx b/public/app/features/live/pages/PipelineTable.tsx
index 308b486ac72..791aa62f9d2 100644
--- a/public/app/features/live/pages/PipelineTable.tsx
+++ b/public/app/features/live/pages/PipelineTable.tsx
@@ -1,9 +1,8 @@
import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react';
-import { GrafanaTheme } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
-import { Tag, useStyles, IconButton } from '@grafana/ui';
+import { Tag, IconButton } from '@grafana/ui';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { RuleModal } from './RuleModal';
@@ -27,7 +26,6 @@ export const PipelineTable = (props: Props) => {
const [isOpen, setOpen] = useState(false);
const [selectedRule, setSelectedRule] = useState();
const [clickColumn, setClickColumn] = useState('converter');
- const styles = useStyles(getStyles);
const onRowClick = (rule: Rule, event?: any) => {
if (!rule) {
@@ -137,10 +135,8 @@ export const PipelineTable = (props: Props) => {
);
};
-const getStyles = (theme: GrafanaTheme) => {
- return {
- row: css`
- cursor: pointer;
- `,
- };
+const styles = {
+ row: css`
+ cursor: pointer;
+ `,
};
diff --git a/public/app/features/live/pages/RuleModal.tsx b/public/app/features/live/pages/RuleModal.tsx
index 2f37514b388..8d1465b07ec 100644
--- a/public/app/features/live/pages/RuleModal.tsx
+++ b/public/app/features/live/pages/RuleModal.tsx
@@ -1,9 +1,8 @@
import { css } from '@emotion/css';
import React, { useState, useMemo } from 'react';
-import { GrafanaTheme } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
-import { Modal, TabContent, TabsBar, Tab, Button, useStyles } from '@grafana/ui';
+import { Modal, TabContent, TabsBar, Tab, Button } from '@grafana/ui';
import { RuleSettingsArray } from './RuleSettingsArray';
import { RuleSettingsEditor } from './RuleSettingsEditor';
@@ -39,7 +38,6 @@ export const RuleModal = (props: Props) => {
const [hasChange, setChange] = useState(false);
const [ruleSetting, setRuleSetting] = useState(activeTab?.type ? rule?.settings?.[activeTab.type] : undefined);
const [entitiesInfo, setEntitiesInfo] = useState();
- const styles = useStyles(getStyles);
const onRuleSettingChange = (value: RuleSetting | RuleSetting[]) => {
setChange(true);
@@ -123,10 +121,8 @@ export const RuleModal = (props: Props) => {
);
};
-const getStyles = (theme: GrafanaTheme) => {
- return {
- save: css`
- margin-top: 5px;
- `,
- };
+const styles = {
+ save: css`
+ margin-top: 5px;
+ `,
};
diff --git a/public/app/features/live/pages/RuleTest.tsx b/public/app/features/live/pages/RuleTest.tsx
index d4e7be155b9..d8fc6516e9e 100644
--- a/public/app/features/live/pages/RuleTest.tsx
+++ b/public/app/features/live/pages/RuleTest.tsx
@@ -1,9 +1,9 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
-import { dataFrameFromJSON, getDisplayProcessor, GrafanaTheme } from '@grafana/data';
+import { dataFrameFromJSON, getDisplayProcessor } from '@grafana/data';
import { getBackendSrv, config } from '@grafana/runtime';
-import { Button, CodeEditor, Table, useStyles, Field } from '@grafana/ui';
+import { Button, CodeEditor, Table, Field } from '@grafana/ui';
import { ChannelFrame, Rule } from './types';
@@ -14,7 +14,6 @@ interface Props {
export const RuleTest = (props: Props) => {
const [response, setResponse] = useState();
const [data, setData] = useState();
- const styles = useStyles(getStyles);
const onBlur = (text: string) => {
setData(text);
@@ -72,10 +71,8 @@ export const RuleTest = (props: Props) => {
);
};
-const getStyles = (theme: GrafanaTheme) => {
- return {
- margin: css`
- margin-bottom: 15px;
- `,
- };
+const styles = {
+ margin: css`
+ margin-bottom: 15px;
+ `,
};
diff --git a/public/app/features/plugins/components/PluginsErrorsInfo.tsx b/public/app/features/plugins/components/PluginsErrorsInfo.tsx
index 9d085ee0fcf..b550cfcea67 100644
--- a/public/app/features/plugins/components/PluginsErrorsInfo.tsx
+++ b/public/app/features/plugins/components/PluginsErrorsInfo.tsx
@@ -1,16 +1,16 @@
import { css } from '@emotion/css';
import React from 'react';
-import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
+import { GrafanaTheme2, PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
-import { HorizontalGroup, InfoBox, List, PluginSignatureBadge, useTheme } from '@grafana/ui';
+import { HorizontalGroup, InfoBox, List, PluginSignatureBadge, useStyles2 } from '@grafana/ui';
import { useGetErrors, useFetchStatus } from '../admin/state/hooks';
-export function PluginsErrorsInfo(): React.ReactElement | null {
+export function PluginsErrorsInfo() {
const errors = useGetErrors();
const { isLoading } = useFetchStatus();
- const theme = useTheme();
+ const styles = useStyles2(getStyles);
if (isLoading || errors.length === 0) {
return null;
@@ -31,22 +31,14 @@ export function PluginsErrorsInfo(): React.ReactElement | null {
The following plugins are disabled and not shown in the list below:
(
-
+
@@ -69,3 +61,17 @@ function mapPluginErrorCodeToSignatureStatus(code: PluginErrorCode) {
return PluginSignatureStatus.missing;
}
}
+
+function getStyles(theme: GrafanaTheme2) {
+ return {
+ list: css({
+ listStyleType: 'circle',
+ }),
+ wrapper: css({
+ marginTop: theme.spacing(1),
+ }),
+ badge: css({
+ marginTop: 0,
+ }),
+ };
+}
diff --git a/public/app/features/query/components/QueryEditorRowHeader.tsx b/public/app/features/query/components/QueryEditorRowHeader.tsx
index 620a7781efd..4122e04eebc 100644
--- a/public/app/features/query/components/QueryEditorRowHeader.tsx
+++ b/public/app/features/query/components/QueryEditorRowHeader.tsx
@@ -1,10 +1,10 @@
import { css, cx } from '@emotion/css';
import React, { ReactNode, useState } from 'react';
-import { DataQuery, DataSourceInstanceSettings, GrafanaTheme } from '@grafana/data';
+import { DataQuery, DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { DataSourcePicker } from '@grafana/runtime';
-import { Icon, Input, FieldValidationMessage, useStyles } from '@grafana/ui';
+import { Icon, Input, FieldValidationMessage, useStyles2 } from '@grafana/ui';
export interface Props
{
query: TQuery;
@@ -22,7 +22,7 @@ export interface Props {
export const QueryEditorRowHeader = (props: Props) => {
const { query, queries, onClick, onChange, collapsedText, renderExtras, disabled } = props;
- const styles = useStyles(getStyles);
+ const styles = useStyles2(getStyles);
const [isEditing, setIsEditing] = useState(false);
const [validationError, setValidationError] = useState(null);
@@ -146,31 +146,31 @@ const renderDataSource = (
);
};
-const getStyles = (theme: GrafanaTheme) => {
+const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css`
label: Wrapper;
display: flex;
align-items: center;
- margin-left: ${theme.spacing.xs};
+ margin-left: ${theme.spacing(0.5)};
`,
queryNameWrapper: css`
display: flex;
cursor: pointer;
border: 1px solid transparent;
- border-radius: ${theme.border.radius.md};
+ border-radius: ${theme.shape.borderRadius(2)};
align-items: center;
- padding: 0 0 0 ${theme.spacing.xs};
+ padding: 0 0 0 ${theme.spacing(0.5)};
margin: 0;
background: transparent;
&:hover {
- background: ${theme.colors.bg3};
- border: 1px dashed ${theme.colors.border3};
+ background: ${theme.colors.action.hover};
+ border: 1px dashed ${theme.colors.border.strong};
}
&:focus {
- border: 2px solid ${theme.colors.formInputBorderActive};
+ border: 2px solid ${theme.colors.primary.border};
}
&:hover,
@@ -181,15 +181,15 @@ const getStyles = (theme: GrafanaTheme) => {
}
`,
queryName: css`
- font-weight: ${theme.typography.weight.semibold};
- color: ${theme.colors.textBlue};
+ font-weight: ${theme.typography.fontWeightMedium};
+ color: ${theme.colors.primary.text};
cursor: pointer;
overflow: hidden;
- margin-left: ${theme.spacing.xs};
+ margin-left: ${theme.spacing(0.5)};
`,
queryEditIcon: cx(
css`
- margin-left: ${theme.spacing.md};
+ margin-left: ${theme.spacing(2)};
visibility: hidden;
`,
'query-name-edit-icon'
@@ -199,10 +199,10 @@ const getStyles = (theme: GrafanaTheme) => {
margin: -4px 0;
`,
collapsedText: css`
- font-weight: ${theme.typography.weight.regular};
- font-size: ${theme.typography.size.sm};
- color: ${theme.colors.textWeak};
- padding-left: ${theme.spacing.sm};
+ font-weight: ${theme.typography.fontWeightRegular};
+ font-size: ${theme.typography.bodySmall.fontSize};
+ color: ${theme.colors.text.secondary};
+ padding-left: ${theme.spacing(1)};
align-items: center;
overflow: hidden;
font-style: italic;
@@ -210,9 +210,9 @@ const getStyles = (theme: GrafanaTheme) => {
text-overflow: ellipsis;
`,
contextInfo: css`
- font-size: ${theme.typography.size.sm};
+ font-size: ${theme.typography.bodySmall.fontSize};
font-style: italic;
- color: ${theme.colors.textWeak};
+ color: ${theme.colors.text.secondary};
padding-left: 10px;
`,
itemWrapper: css`
diff --git a/public/app/features/search/page/components/ConfirmDeleteModal.tsx b/public/app/features/search/page/components/ConfirmDeleteModal.tsx
index 7850be6819b..b6a548dadf4 100644
--- a/public/app/features/search/page/components/ConfirmDeleteModal.tsx
+++ b/public/app/features/search/page/components/ConfirmDeleteModal.tsx
@@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import React, { FC } from 'react';
-import { GrafanaTheme } from '@grafana/data';
-import { ConfirmModal, stylesFactory, useTheme } from '@grafana/ui';
+import { GrafanaTheme2 } from '@grafana/data';
+import { ConfirmModal, useStyles2 } from '@grafana/ui';
import { deleteFoldersAndDashboards } from 'app/features/manage-dashboards/state/actions';
import { OnMoveOrDeleleSelectedItems } from '../../types';
@@ -15,8 +15,7 @@ interface Props {
}
export const ConfirmDeleteModal: FC = ({ results, onDeleteItems, isOpen, onDismiss }) => {
- const theme = useTheme();
- const styles = getStyles(theme);
+ const styles = useStyles2(getStyles);
const dashboards = Array.from(results.get('dashboard') ?? []);
const folders = Array.from(results.get('folder') ?? []);
@@ -61,11 +60,9 @@ export const ConfirmDeleteModal: FC = ({ results, onDeleteItems, isOpen,
) : null;
};
-const getStyles = stylesFactory((theme: GrafanaTheme) => {
- return {
- subtitle: css`
- font-size: ${theme.typography.size.base};
- padding-top: ${theme.spacing.md};
- `,
- };
+const getStyles = (theme: GrafanaTheme2) => ({
+ subtitle: css`
+ font-size: ${theme.typography.fontSize}px;
+ padding-top: ${theme.spacing(2)};
+ `,
});
diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx
index 7ec926c4709..1ca6f74a29a 100644
--- a/public/app/features/search/page/components/FolderSection.tsx
+++ b/public/app/features/search/page/components/FolderSection.tsx
@@ -1,10 +1,10 @@
import { css, cx } from '@emotion/css';
-import React, { FC } from 'react';
+import React, { useCallback } from 'react';
import { useAsync, useLocalStorage } from 'react-use';
-import { GrafanaTheme } from '@grafana/data';
+import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
-import { Card, Checkbox, CollapsableSection, Icon, IconName, Spinner, stylesFactory, useTheme } from '@grafana/ui';
+import { Card, Checkbox, CollapsableSection, Icon, IconName, Spinner, useStyles2 } from '@grafana/ui';
import { getSectionStorageKey } from 'app/features/search/utils';
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
@@ -33,7 +33,7 @@ interface SectionHeaderProps {
tags?: string[];
}
-export const FolderSection: FC = ({
+export const FolderSection = ({
section,
selectionToggle,
onClickItem,
@@ -41,10 +41,14 @@ export const FolderSection: FC = ({
selection,
renderStandaloneBody,
tags,
-}) => {
+}: SectionHeaderProps) => {
const editable = selectionToggle != null;
- const theme = useTheme();
- const styles = getSectionHeaderStyles(theme, section.selected, editable);
+ const styles = useStyles2(
+ useCallback(
+ (theme: GrafanaTheme2) => getSectionHeaderStyles(theme, section.selected, editable),
+ [section.selected, editable]
+ )
+ );
const [sectionExpanded, setSectionExpanded] = useLocalStorage(getSectionStorageKey(section.title), false);
const results = useAsync(async () => {
@@ -194,8 +198,8 @@ export const FolderSection: FC = ({
);
};
-const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false, editable: boolean) => {
- const { sm } = theme.spacing;
+const getSectionHeaderStyles = (theme: GrafanaTheme2, selected = false, editable: boolean) => {
+ const sm = theme.spacing(1);
return {
wrapper: cx(
css`
@@ -203,7 +207,7 @@ const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = fa
font-size: ${theme.typography.size.base};
padding: 12px;
border-bottom: none;
- color: ${theme.colors.textWeak};
+ color: ${theme.colors.text.secondary};
z-index: 1;
&:hover,
@@ -240,7 +244,7 @@ const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = fa
`,
link: css`
padding: 2px 10px 0;
- color: ${theme.colors.textWeak};
+ color: ${theme.colors.text.secondary};
opacity: 0;
transition: opacity 150ms ease-in-out;
`,
@@ -257,4 +261,4 @@ const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = fa
padding-bottom: 1rem;
`,
};
-});
+};
diff --git a/public/app/features/search/page/components/MoveToFolderModal.tsx b/public/app/features/search/page/components/MoveToFolderModal.tsx
index 3675097ec7e..ef53bb2977c 100644
--- a/public/app/features/search/page/components/MoveToFolderModal.tsx
+++ b/public/app/features/search/page/components/MoveToFolderModal.tsx
@@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import React, { FC, useState } from 'react';
-import { GrafanaTheme } from '@grafana/data';
-import { Button, HorizontalGroup, Modal, stylesFactory, useTheme } from '@grafana/ui';
+import { GrafanaTheme2 } from '@grafana/data';
+import { Button, HorizontalGroup, Modal, useStyles2 } from '@grafana/ui';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { useAppNotification } from 'app/core/copy/appNotification';
import { moveDashboards } from 'app/features/manage-dashboards/state/actions';
@@ -19,8 +19,7 @@ interface Props {
export const MoveToFolderModal: FC = ({ results, onMoveItems, isOpen, onDismiss }) => {
const [folder, setFolder] = useState(null);
- const theme = useTheme();
- const styles = getStyles(theme);
+ const styles = useStyles2(getStyles);
const notifyApp = useAppNotification();
const selectedDashboards = Array.from(results.get('dashboard') ?? []);
const [moving, setMoving] = useState(false);
@@ -80,13 +79,13 @@ export const MoveToFolderModal: FC = ({ results, onMoveItems, isOpen, onD
) : null;
};
-const getStyles = stylesFactory((theme: GrafanaTheme) => {
+const getStyles = (theme: GrafanaTheme2) => {
return {
modal: css`
width: 500px;
`,
content: css`
- margin-bottom: ${theme.spacing.lg};
+ margin-bottom: ${theme.spacing(3)};
`,
};
-});
+};
diff --git a/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx b/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx
index 3fc97f52fa1..72b6a5312b5 100644
--- a/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx
+++ b/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx
@@ -4,14 +4,14 @@ import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautif
import {
DataTransformerID,
- GrafanaTheme,
+ GrafanaTheme2,
standardTransformers,
TransformerRegistryItem,
TransformerUIProps,
} from '@grafana/data';
import { createOrderFieldsComparer } from '@grafana/data/src/transformations/transformers/order';
import { OrganizeFieldsTransformerOptions } from '@grafana/data/src/transformations/transformers/organize';
-import { stylesFactory, useTheme, Input, IconButton, Icon, FieldValidationMessage } from '@grafana/ui';
+import { Input, IconButton, Icon, FieldValidationMessage, useStyles2 } from '@grafana/ui';
import { useAllFieldNamesFromDataFrames } from '../utils';
@@ -117,16 +117,15 @@ interface DraggableFieldProps {
onRenameField: (from: string, to: string) => void;
}
-const DraggableFieldName: React.FC = ({
+const DraggableFieldName = ({
fieldName,
renamedFieldName,
index,
visible,
onToggleVisibility,
onRenameField,
-}) => {
- const theme = useTheme();
- const styles = getFieldNameStyles(theme);
+}: DraggableFieldProps) => {
+ const styles = useStyles2(getFieldNameStyles);
return (
@@ -166,25 +165,25 @@ const DraggableFieldName: React.FC = ({
DraggableFieldName.displayName = 'DraggableFieldName';
-const getFieldNameStyles = stylesFactory((theme: GrafanaTheme) => ({
+const getFieldNameStyles = (theme: GrafanaTheme2) => ({
toggle: css`
margin: 0 8px;
- color: ${theme.colors.textWeak};
+ color: ${theme.colors.text.secondary};
`,
draggable: css`
opacity: 0.4;
&:hover {
- color: ${theme.colors.textStrong};
+ color: ${theme.colors.text.maxContrast};
}
`,
name: css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- font-size: ${theme.typography.size.sm};
- font-weight: ${theme.typography.weight.semibold};
+ font-size: ${theme.typography.bodySmall.fontSize};
+ font-weight: ${theme.typography.fontWeightMedium};
`,
-}));
+});
const reorderToIndex = (fieldNames: string[], startIndex: number, endIndex: number) => {
const result = Array.from(fieldNames);
diff --git a/public/app/features/variables/editor/VariableSelectField.tsx b/public/app/features/variables/editor/VariableSelectField.tsx
index 801ca3f74d8..ef7bb998369 100644
--- a/public/app/features/variables/editor/VariableSelectField.tsx
+++ b/public/app/features/variables/editor/VariableSelectField.tsx
@@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import React, { PropsWithChildren, ReactElement } from 'react';
-import { GrafanaTheme, SelectableValue } from '@grafana/data';
-import { Field, Select, useStyles } from '@grafana/ui';
+import { GrafanaTheme2, SelectableValue } from '@grafana/data';
+import { Field, Select, useStyles2 } from '@grafana/ui';
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
interface VariableSelectFieldProps {
@@ -24,7 +24,7 @@ export function VariableSelectField({
testId,
width,
}: PropsWithChildren>): ReactElement {
- const styles = useStyles(getStyles);
+ const styles = useStyles2(getStyles);
const uniqueId = useUniqueId();
const inputId = `variable-select-input-${name}-${uniqueId}`;
@@ -44,10 +44,10 @@ export function VariableSelectField({
);
}
-function getStyles(theme: GrafanaTheme) {
+function getStyles(theme: GrafanaTheme2) {
return {
selectContainer: css`
- margin-right: ${theme.spacing.xs};
+ margin-right: ${theme.spacing(0.5)};
`,
};
}
diff --git a/public/app/features/variables/inspect/VariablesUnknownTable.tsx b/public/app/features/variables/inspect/VariablesUnknownTable.tsx
index d9ef1157f24..b1d0a95d562 100644
--- a/public/app/features/variables/inspect/VariablesUnknownTable.tsx
+++ b/public/app/features/variables/inspect/VariablesUnknownTable.tsx
@@ -2,9 +2,9 @@ import { css } from '@emotion/css';
import React, { ReactElement, useEffect, useState } from 'react';
import { useAsync } from 'react-use';
-import { GrafanaTheme } from '@grafana/data';
+import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
-import { CollapsableSection, HorizontalGroup, Icon, Spinner, Tooltip, useStyles, VerticalGroup } from '@grafana/ui';
+import { CollapsableSection, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2, VerticalGroup } from '@grafana/ui';
import { DashboardModel } from '../../dashboard/state';
import { VariableModel } from '../types';
@@ -23,7 +23,7 @@ export function VariablesUnknownTable({ variables, dashboard }: VariablesUnknown
const [open, setOpen] = useState(false);
const [changed, setChanged] = useState(0);
const [usages, setUsages] = useState([]);
- const style = useStyles(getStyles);
+ const style = useStyles2(getStyles);
useEffect(() => setChanged((prevState) => prevState + 1), [variables, dashboard]);
const { loading } = useAsync(async () => {
if (open && changed > 0) {
@@ -74,7 +74,7 @@ export function VariablesUnknownTable({ variables, dashboard }: VariablesUnknown
}
function CollapseLabel(): ReactElement {
- const style = useStyles(getStyles);
+ const style = useStyles2(getStyles);
return (
Renamed or missing variables
@@ -90,7 +90,7 @@ function NoUnknowns(): ReactElement {
}
function UnknownTable({ usages }: { usages: UsagesToNetwork[] }): ReactElement {
- const style = useStyles(getStyles);
+ const style = useStyles2(getStyles);
return (
@@ -122,13 +122,13 @@ function UnknownTable({ usages }: { usages: UsagesToNetwork[] }): ReactElement {
);
}
-const getStyles = (theme: GrafanaTheme) => ({
+const getStyles = (theme: GrafanaTheme2) => ({
container: css`
- margin-top: ${theme.spacing.xl};
- padding-top: ${theme.spacing.xl};
+ margin-top: ${theme.spacing(4)};
+ padding-top: ${theme.spacing(4)};
`,
infoIcon: css`
- margin-left: ${theme.spacing.sm};
+ margin-left: ${theme.spacing(1)};
`,
defaultColumn: css`
width: 1%;
@@ -136,7 +136,7 @@ const getStyles = (theme: GrafanaTheme) => ({
firstColumn: css`
width: 1%;
vertical-align: top;
- color: ${theme.colors.textStrong};
+ color: ${theme.colors.text.maxContrast};
`,
lastColumn: css`
overflow: hidden;
diff --git a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.test.tsx b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.test.tsx
index 42b51c096f0..97051f5b8e0 100644
--- a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.test.tsx
+++ b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.test.tsx
@@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
-import { getTheme } from '@grafana/ui';
+import { createTheme } from '@grafana/data';
import PromQlLanguageProvider from '../language_provider';
@@ -131,7 +131,7 @@ describe('PrometheusMetricsBrowser', () => {
};
const defaults: BrowserProps = {
- theme: getTheme(),
+ theme: createTheme({ colors: { mode: 'dark' } }),
onChange: () => {},
autoSelect: 0,
languageProvider: mockLanguageProvider as unknown as PromQlLanguageProvider,
diff --git a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx
index 1e3dc95ed7b..0b2f27a9bf7 100644
--- a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx
+++ b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx
@@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css';
import React, { ChangeEvent } from 'react';
import { FixedSizeList } from 'react-window';
-import { GrafanaTheme } from '@grafana/data';
+import { GrafanaTheme2 } from '@grafana/data';
import {
Button,
HorizontalGroup,
@@ -10,8 +10,8 @@ import {
Label,
LoadingPlaceholder,
stylesFactory,
- withTheme,
BrowserLabel as PromLabel,
+ withTheme2,
} from '@grafana/ui';
import PromQlLanguageProvider from '../language_provider';
@@ -25,7 +25,7 @@ const LIST_ITEM_SIZE = 25;
export interface BrowserProps {
languageProvider: PromQlLanguageProvider;
onChange: (selector: string) => void;
- theme: GrafanaTheme;
+ theme: GrafanaTheme2;
autoSelect?: number;
hide?: () => void;
lastUsedLabels: string[];
@@ -112,14 +112,14 @@ export function facetLabels(
});
}
-const getStyles = stylesFactory((theme: GrafanaTheme) => ({
+const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
wrapper: css`
- background-color: ${theme.colors.bg2};
- padding: ${theme.spacing.sm};
+ background-color: ${theme.colors.background.secondary};
+ padding: ${theme.spacing(1)};
width: 100%;
`,
list: css`
- margin-top: ${theme.spacing.sm};
+ margin-top: ${theme.spacing(1)};
display: flex;
flex-wrap: wrap;
max-height: 200px;
@@ -128,17 +128,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
`,
section: css`
& + & {
- margin: ${theme.spacing.md} 0;
+ margin: ${theme.spacing(2)} 0;
}
position: relative;
`,
selector: css`
- font-family: ${theme.typography.fontFamily.monospace};
- margin-bottom: ${theme.spacing.sm};
+ font-family: ${theme.typography.fontFamilyMonospace};
+ margin-bottom: ${theme.spacing(1)};
`,
status: css`
- padding: ${theme.spacing.xs};
- color: ${theme.colors.textSemiWeak};
+ padding: ${theme.spacing(0.5)};
+ color: ${theme.colors.text.secondary};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -154,30 +154,30 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
opacity: 1;
`,
error: css`
- color: ${theme.palette.brandDanger};
+ color: ${theme.colors.error.main};
`,
valueList: css`
- margin-right: ${theme.spacing.sm};
+ margin-right: ${theme.spacing(1)};
resize: horizontal;
`,
valueListWrapper: css`
- border-left: 1px solid ${theme.colors.border2};
- margin: ${theme.spacing.sm} 0;
- padding: ${theme.spacing.sm} 0 ${theme.spacing.sm} ${theme.spacing.sm};
+ border-left: 1px solid ${theme.colors.border.medium};
+ margin: ${theme.spacing(1)} 0;
+ padding: ${theme.spacing(1)} 0 ${theme.spacing(1)} ${theme.spacing(1)};
`,
valueListArea: css`
display: flex;
flex-wrap: wrap;
- margin-top: ${theme.spacing.sm};
+ margin-top: ${theme.spacing(1)};
`,
valueTitle: css`
- margin-left: -${theme.spacing.xs};
- margin-bottom: ${theme.spacing.sm};
+ margin-left: -${theme.spacing(0.5)};
+ margin-bottom: ${theme.spacing(1)};
`,
validationStatus: css`
- padding: ${theme.spacing.xs};
- margin-bottom: ${theme.spacing.sm};
- color: ${theme.colors.textStrong};
+ padding: ${theme.spacing(0.5)};
+ margin-bottom: ${theme.spacing(1)};
+ color: ${theme.colors.text.maxContrast};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -656,4 +656,4 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component {}
export function QuerySettings({ options, onOptionsChange }: Props) {
- const styles = useStyles(getStyles);
+ const styles = useStyles2(getStyles);
return (
diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.tsx
index b5ec6b5335e..af02ec7a7e9 100644
--- a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.tsx
+++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.tsx
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React, { LegacyRef } from 'react';
import { createTheme, Field, getDisplayProcessor } from '@grafana/data';
-import { useStyles, Tooltip } from '@grafana/ui';
+import { useStyles2, Tooltip } from '@grafana/ui';
import { TooltipData, SampleUnit } from '../types';
@@ -13,7 +13,7 @@ type Props = {
};
const FlameGraphTooltip = ({ tooltipRef, tooltipData, showTooltip }: Props) => {
- const styles = useStyles(getStyles);
+ const styles = useStyles2(getStyles);
return (
diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx
index daa8f89b577..99e63d498b2 100644
--- a/public/app/plugins/panel/geomap/GeomapPanel.tsx
+++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx
@@ -11,9 +11,9 @@ import { fromLonLat } from 'ol/proj';
import React, { Component, ReactNode } from 'react';
import { Subscription } from 'rxjs';
-import { DataHoverEvent, GrafanaTheme, PanelData, PanelProps } from '@grafana/data';
+import { DataHoverEvent, PanelData, PanelProps } from '@grafana/data';
import { config } from '@grafana/runtime';
-import { PanelContext, PanelContextRoot, stylesFactory } from '@grafana/ui';
+import { PanelContext, PanelContextRoot } from '@grafana/ui';
import { PanelEditExitedEvent } from 'app/types/events';
import { GeomapOverlay, OverlayProps } from './GeomapOverlay';
@@ -53,7 +53,6 @@ export class GeomapPanel extends Component
{
globalCSS = getGlobalStyles(config.theme2);
mouseWheelZoom?: MouseWheelZoom;
- style = getStyles(config.theme);
hoverPayload: GeomapHoverPayload = { point: {}, pageX: -1, pageY: -1 };
readonly hoverEvent = new DataHoverEvent(this.hoverPayload);
@@ -383,8 +382,8 @@ export class GeomapPanel extends Component {
return (
<>
-
-
+
+
{
}
}
-const getStyles = stylesFactory((theme: GrafanaTheme) => ({
+const styles = {
wrap: css`
position: relative;
width: 100%;
@@ -410,4 +409,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
width: 100%;
height: 100%;
`,
-}));
+};
diff --git a/public/app/plugins/panel/xychart/ManualEditor.tsx b/public/app/plugins/panel/xychart/ManualEditor.tsx
index 22b00cfe7ff..e80a8f0849f 100644
--- a/public/app/plugins/panel/xychart/ManualEditor.tsx
+++ b/public/app/plugins/panel/xychart/ManualEditor.tsx
@@ -1,8 +1,8 @@
import { css, cx } from '@emotion/css';
import React, { useState, useEffect } from 'react';
-import { GrafanaTheme, StandardEditorProps } from '@grafana/data';
-import { Button, Field, IconButton, useStyles } from '@grafana/ui';
+import { GrafanaTheme2, StandardEditorProps } from '@grafana/data';
+import { Button, Field, IconButton, useStyles2 } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
import { LayerName } from 'app/core/components/Layers/LayerName';
import { ColorDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors';
@@ -14,8 +14,8 @@ export const ManualEditor = ({
onChange,
context,
}: StandardEditorProps) => {
- const [selected, setSelected] = useState(0);
- const style = useStyles(getStyles);
+ const [selected, setSelected] = useState(0);
+ const style = useStyles2(getStyles);
const onFieldChange = (val: any | undefined, index: number, field: string) => {
onChange(
@@ -125,34 +125,34 @@ export const ManualEditor = ({
);
};
-const getStyles = (theme: GrafanaTheme) => ({
+const getStyles = (theme: GrafanaTheme2) => ({
marginBot: css`
margin-bottom: 20px;
`,
row: css`
- padding: ${theme.spacing.xs} ${theme.spacing.sm};
- border-radius: ${theme.border.radius.sm};
- background: ${theme.colors.bg2};
- min-height: ${theme.spacing.formInputHeight}px;
+ padding: ${theme.spacing(0.5, 1)};
+ border-radius: ${theme.shape.borderRadius(1)};
+ background: ${theme.colors.background.secondary};
+ min-height: ${theme.spacing(4)};
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 3px;
cursor: pointer;
- border: 1px solid ${theme.colors.formInputBorder};
+ border: 1px solid ${theme.components.input.borderColor};
&:hover {
- border: 1px solid ${theme.colors.formInputBorderHover};
+ border: 1px solid ${theme.components.input.borderHover};
}
`,
sel: css`
- border: 1px solid ${theme.colors.formInputBorderActive};
+ border: 1px solid ${theme.colors.primary.border};
&:hover {
- border: 1px solid ${theme.colors.formInputBorderActive};
+ border: 1px solid ${theme.colors.primary.border};
}
`,
actionIcon: css`
- color: ${theme.colors.textWeak};
+ color: ${theme.colors.text.secondary};
&:hover {
color: ${theme.colors.text};
}
From 1722000309c6e6dc36c9e4f5ef9c2e40cc6e6c65 Mon Sep 17 00:00:00 2001
From: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
Date: Thu, 3 Nov 2022 13:42:23 -0500
Subject: [PATCH 023/926] fixes typo (#58159)
---
docs/sources/whatsnew/whats-new-in-v9-0.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/sources/whatsnew/whats-new-in-v9-0.md b/docs/sources/whatsnew/whats-new-in-v9-0.md
index 04698af2f91..81fde0d2a6a 100644
--- a/docs/sources/whatsnew/whats-new-in-v9-0.md
+++ b/docs/sources/whatsnew/whats-new-in-v9-0.md
@@ -41,7 +41,7 @@ All functions, aggregations and binary operations are added via the + Operation
### Range vector
-The query builder will automatically mange and add the range selector. It will be shown as a parameter to the operations that require a range vector (rate, delta, increase, etc).
+The query builder will automatically manage and add the range selector. It will be shown as a parameter to the operations that require a range vector (rate, delta, increase, etc).
### Binary operations
From 0367f61bb3701dcfaceb2800a16e71753a31a29b Mon Sep 17 00:00:00 2001
From: ismail simsek
Date: Thu, 3 Nov 2022 21:44:37 +0300
Subject: [PATCH 024/926] Share azureauth between prometheus clients (#58122)
* Move azureauth to upper package
* Refactor http transport options
---
pkg/tsdb/prometheus/{buffered => }/azureauth/azure.go | 0
.../prometheus/{buffered => }/azureauth/azure_test.go | 0
pkg/tsdb/prometheus/buffered/time_series_query.go | 9 +++++----
.../{buffered/client.go => client/transport.go} | 6 +++---
.../client_test.go => client/transport_test.go} | 2 +-
pkg/tsdb/prometheus/prometheus.go | 3 ++-
pkg/tsdb/prometheus/querydata/request_test.go | 4 ++--
7 files changed, 13 insertions(+), 11 deletions(-)
rename pkg/tsdb/prometheus/{buffered => }/azureauth/azure.go (100%)
rename pkg/tsdb/prometheus/{buffered => }/azureauth/azure_test.go (100%)
rename pkg/tsdb/prometheus/{buffered/client.go => client/transport.go} (92%)
rename pkg/tsdb/prometheus/{buffered/client_test.go => client/transport_test.go} (98%)
diff --git a/pkg/tsdb/prometheus/buffered/azureauth/azure.go b/pkg/tsdb/prometheus/azureauth/azure.go
similarity index 100%
rename from pkg/tsdb/prometheus/buffered/azureauth/azure.go
rename to pkg/tsdb/prometheus/azureauth/azure.go
diff --git a/pkg/tsdb/prometheus/buffered/azureauth/azure_test.go b/pkg/tsdb/prometheus/azureauth/azure_test.go
similarity index 100%
rename from pkg/tsdb/prometheus/buffered/azureauth/azure_test.go
rename to pkg/tsdb/prometheus/azureauth/azure_test.go
diff --git a/pkg/tsdb/prometheus/buffered/time_series_query.go b/pkg/tsdb/prometheus/buffered/time_series_query.go
index 272f776deb4..9e11c85cd92 100644
--- a/pkg/tsdb/prometheus/buffered/time_series_query.go
+++ b/pkg/tsdb/prometheus/buffered/time_series_query.go
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
sdkHTTPClient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/data"
+ "github.com/grafana/grafana/pkg/tsdb/prometheus/client"
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/prometheus/common/model"
"go.opentelemetry.io/otel/attribute"
@@ -68,7 +69,7 @@ type Buffered struct {
// New creates and object capable of executing and parsing a Prometheus queries. It's "buffered" because there is
// another implementation capable of streaming parse the response.
func New(roundTripper http.RoundTripper, tracer tracing.Tracer, settings backend.DataSourceInstanceSettings, plog log.Logger) (*Buffered, error) {
- promClient, err := CreateClient(roundTripper, settings.URL)
+ promClient, err := client.CreateAPIClient(roundTripper, settings.URL)
if err != nil {
return nil, fmt.Errorf("error creating prom client: %v", err)
}
@@ -232,7 +233,7 @@ func (b *Buffered) parseTimeSeriesQuery(req *backend.QueryDataRequest) ([]*Prome
if err != nil {
return nil, fmt.Errorf("error unmarshaling query model: %v", err)
}
- //Final interval value
+ // Final interval value
interval, err := calculatePrometheusInterval(model, b.TimeInterval, query, b.intervalCalculator)
if err != nil {
return nil, fmt.Errorf("error calculating interval: %v", err)
@@ -301,7 +302,7 @@ func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *P
func calculatePrometheusInterval(model *QueryModel, timeInterval string, query backend.DataQuery, intervalCalculator intervalv2.Calculator) (time.Duration, error) {
queryInterval := model.Interval
- //If we are using variable for interval/step, we will replace it with calculated interval
+ // If we are using variable for interval/step, we will replace it with calculated interval
if isVariableInterval(queryInterval) {
queryInterval = ""
}
@@ -656,7 +657,7 @@ func isVariableInterval(interval string) bool {
if interval == varInterval || interval == varIntervalMs || interval == varRateInterval {
return true
}
- //Repetitive code, we should have functionality to unify these
+ // Repetitive code, we should have functionality to unify these
if interval == varIntervalAlt || interval == varIntervalMsAlt || interval == varRateIntervalAlt {
return true
}
diff --git a/pkg/tsdb/prometheus/buffered/client.go b/pkg/tsdb/prometheus/client/transport.go
similarity index 92%
rename from pkg/tsdb/prometheus/buffered/client.go
rename to pkg/tsdb/prometheus/client/transport.go
index 79a0e619ba3..1d79dd98aca 100644
--- a/pkg/tsdb/prometheus/buffered/client.go
+++ b/pkg/tsdb/prometheus/client/transport.go
@@ -1,4 +1,4 @@
-package buffered
+package client
import (
"fmt"
@@ -9,7 +9,7 @@ import (
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
- "github.com/grafana/grafana/pkg/tsdb/prometheus/buffered/azureauth"
+ "github.com/grafana/grafana/pkg/tsdb/prometheus/azureauth"
"github.com/grafana/grafana/pkg/tsdb/prometheus/middleware"
"github.com/grafana/grafana/pkg/tsdb/prometheus/utils"
"github.com/grafana/grafana/pkg/util/maputil"
@@ -49,7 +49,7 @@ func CreateTransportOptions(settings backend.DataSourceInstanceSettings, cfg *se
return &opts, nil
}
-func CreateClient(roundTripper http.RoundTripper, url string) (apiv1.API, error) {
+func CreateAPIClient(roundTripper http.RoundTripper, url string) (apiv1.API, error) {
cfg := api.Config{
Address: url,
RoundTripper: roundTripper,
diff --git a/pkg/tsdb/prometheus/buffered/client_test.go b/pkg/tsdb/prometheus/client/transport_test.go
similarity index 98%
rename from pkg/tsdb/prometheus/buffered/client_test.go
rename to pkg/tsdb/prometheus/client/transport_test.go
index 105803503be..945e0ada0dc 100644
--- a/pkg/tsdb/prometheus/buffered/client_test.go
+++ b/pkg/tsdb/prometheus/client/transport_test.go
@@ -1,4 +1,4 @@
-package buffered
+package client
import (
"testing"
diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go
index 1851c856f4e..43b12d31ddb 100644
--- a/pkg/tsdb/prometheus/prometheus.go
+++ b/pkg/tsdb/prometheus/prometheus.go
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
+ "github.com/grafana/grafana/pkg/tsdb/prometheus/client"
"github.com/patrickmn/go-cache"
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/yudai/gojsondiff"
@@ -53,7 +54,7 @@ func ProvideService(httpClientProvider httpclient.Provider, cfg *setting.Cfg, fe
func newInstanceSettings(httpClientProvider httpclient.Provider, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer) datasource.InstanceFactoryFunc {
return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
// Creates a http roundTripper. Probably should be used for both buffered and streaming/querydata instances.
- opts, err := buffered.CreateTransportOptions(settings, cfg, plog)
+ opts, err := client.CreateTransportOptions(settings, cfg, plog)
if err != nil {
return nil, fmt.Errorf("error creating transport options: %v", err)
}
diff --git a/pkg/tsdb/prometheus/querydata/request_test.go b/pkg/tsdb/prometheus/querydata/request_test.go
index 426ccfa73e5..abc34dc608d 100644
--- a/pkg/tsdb/prometheus/querydata/request_test.go
+++ b/pkg/tsdb/prometheus/querydata/request_test.go
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/data"
+ "github.com/grafana/grafana/pkg/tsdb/prometheus/client"
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
p "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
@@ -21,7 +22,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/setting"
- "github.com/grafana/grafana/pkg/tsdb/prometheus/buffered"
"github.com/grafana/grafana/pkg/tsdb/prometheus/models"
"github.com/grafana/grafana/pkg/tsdb/prometheus/querydata"
)
@@ -417,7 +417,7 @@ func setup(wideFrames bool) (*testContext, error) {
features := &fakeFeatureToggles{flags: map[string]bool{"prometheusStreamingJSONParser": true, "prometheusWideSeries": wideFrames}}
- opts, err := buffered.CreateTransportOptions(settings, &setting.Cfg{}, &logtest.Fake{})
+ opts, err := client.CreateTransportOptions(settings, &setting.Cfg{}, &logtest.Fake{})
if err != nil {
return nil, err
}
From 6fcc5b42c0a5bdd83d8362c0321a36fa7b8ea3fb Mon Sep 17 00:00:00 2001
From: Jeff Levin
Date: Thu, 3 Nov 2022 11:30:12 -0800
Subject: [PATCH 025/926] publicdashboards: split create/update api paths
(#57940)
This PR splits the create and update paths for public dashboards and includes assorted refactors toward a proper REST API. Additionally, we removed the concept of a "public dashboard config" in favor of "public dashboard"
Co-authored-by: juanicabanas
Co-authored-by: Ezequiel Victorero
---
.../dashboard-public-create.spec.ts | 10 +-
.../dashboard-public-templating.spec.ts | 2 -
pkg/api/dashboard.go | 21 +-
pkg/api/dtos/dashboard.go | 1 +
.../dashboards/database/database_test.go | 4 +-
pkg/services/publicdashboards/api/api.go | 96 ++-
pkg/services/publicdashboards/api/api_test.go | 576 +++++++++++-------
.../publicdashboards/api/query_test.go | 2 +-
.../publicdashboards/database/database.go | 140 ++---
.../database/database_test.go | 67 +-
.../internal/tokens/tokens.go | 7 +
.../internal/tokens/tokens_test.go | 16 +
.../public_dashboard_service_mock.go | 27 +-
.../public_dashboard_store_mock.go | 52 +-
.../publicdashboards/publicdashboard.go | 7 +-
.../publicdashboards/service/query_test.go | 6 +-
.../publicdashboards/service/service.go | 167 ++---
.../publicdashboards/service/service_test.go | 291 +++++----
.../publicdashboards/validation/validation.go | 2 +-
.../validation/validation_test.go | 6 +-
.../dashboard/api/publicDashboardApi.ts | 42 +-
.../SharePublicDashboard.test.tsx | 31 +-
.../SharePublicDashboard.tsx | 34 +-
public/app/types/dashboard.ts | 1 +
24 files changed, 996 insertions(+), 612 deletions(-)
diff --git a/e2e/dashboards-suite/dashboard-public-create.spec.ts b/e2e/dashboards-suite/dashboard-public-create.spec.ts
index c5f82a5ffb9..745fcc4655a 100644
--- a/e2e/dashboards-suite/dashboard-public-create.spec.ts
+++ b/e2e/dashboards-suite/dashboard-public-create.spec.ts
@@ -8,7 +8,7 @@ e2e.scenario({
skipScenario: false,
scenario: () => {
// Opening a dashboard without template variables
- e2e().intercept('/api/ds/query').as('query');
+ e2e().intercept('POST', '/api/ds/query').as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
e2e().wait('@query');
@@ -16,9 +16,7 @@ e2e.scenario({
e2e.pages.ShareDashboardModal.shareButton().click();
// Select public dashboards tab
- e2e().intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
- e2e().wait('@query-public-dashboard');
// Saving button should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.disabled');
@@ -57,7 +55,7 @@ e2e.scenario({
skipScenario: false,
scenario: () => {
// Opening a dashboard without template variables
- e2e().intercept('/api/ds/query').as('query');
+ e2e().intercept('POST', '/api/ds/query').as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
e2e().wait('@query');
@@ -125,9 +123,9 @@ e2e.scenario({
e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('be.enabled').click({ force: true });
// Save public dashboard
- e2e().intercept('POST', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('save');
+ e2e().intercept('PUT', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards/*').as('update');
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().click();
- e2e().wait('@save');
+ e2e().wait('@update');
// Url should be hidden
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('not.exist');
diff --git a/e2e/dashboards-suite/dashboard-public-templating.spec.ts b/e2e/dashboards-suite/dashboard-public-templating.spec.ts
index 1262a564800..808cff56902 100644
--- a/e2e/dashboards-suite/dashboard-public-templating.spec.ts
+++ b/e2e/dashboards-suite/dashboard-public-templating.spec.ts
@@ -14,9 +14,7 @@ e2e.scenario({
e2e.pages.ShareDashboardModal.shareButton().click();
// Select public dashboards tab
- e2e().intercept('GET', '/api/dashboards/uid/HYaGDGIMk/public-dashboards').as('query-public-config');
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
- e2e().wait('@query-public-config');
// Warning Alert dashboard cannot be made public because it has template variables
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');
diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go
index 479160aae85..2f3f335d389 100644
--- a/pkg/api/dashboard.go
+++ b/pkg/api/dashboard.go
@@ -28,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
pref "github.com/grafana/grafana/pkg/services/preference"
+ publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
@@ -100,14 +101,23 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
}
var (
- hasPublicDashboard bool
- err error
+ hasPublicDashboard = false
+ publicDashboardEnabled = false
+ err error
)
+
+ // If public dashboards is enabled and we have a public dashboard, update meta
+ // values
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
- hasPublicDashboard, err = hs.PublicDashboardsApi.PublicDashboardService.ExistsEnabledByDashboardUid(c.Req.Context(), dash.Uid)
- if err != nil {
+ publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, dash.Uid)
+ if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) {
return response.Error(500, "Error while retrieving public dashboards", err)
}
+
+ if publicDashboard != nil {
+ hasPublicDashboard = true
+ publicDashboardEnabled = publicDashboard.IsEnabled
+ }
}
// When dash contains only keys id, uid that means dashboard data is not valid and json decode failed.
@@ -172,7 +182,8 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
Url: dash.GetUrl(),
FolderTitle: "General",
AnnotationsPermissions: annotationPermissions,
- PublicDashboardEnabled: hasPublicDashboard,
+ PublicDashboardEnabled: publicDashboardEnabled,
+ HasPublicDashboard: hasPublicDashboard,
}
// lookup folder title
diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go
index 3918374cf7f..0d13051f5be 100644
--- a/pkg/api/dtos/dashboard.go
+++ b/pkg/api/dtos/dashboard.go
@@ -32,6 +32,7 @@ type DashboardMeta struct {
Provisioned bool `json:"provisioned"`
ProvisionedExternalId string `json:"provisionedExternalId"`
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
+ HasPublicDashboard bool `json:"hasPublicDashboard"`
PublicDashboardAccessToken string `json:"publicDashboardAccessToken"`
PublicDashboardUID string `json:"publicDashboardUid"`
PublicDashboardEnabled bool `json:"publicDashboardEnabled"`
diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go
index 39222b6414e..5163e0d8d90 100644
--- a/pkg/services/dashboards/database/database_test.go
+++ b/pkg/services/dashboards/database/database_test.go
@@ -257,7 +257,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
AccessToken: "an-access-token",
},
}
- err := publicDashboardStore.Save(context.Background(), cmd)
+ _, err := publicDashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token")
require.NotNil(t, pubdashConfig)
@@ -292,7 +292,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
AccessToken: "an-access-token",
},
}
- err := publicDashboardStore.Save(context.Background(), cmd)
+ _, err := publicDashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token")
require.NotNil(t, pubdashConfig)
diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go
index d0766c95634..61e867ad287 100644
--- a/pkg/services/publicdashboards/api/api.go
+++ b/pkg/services/publicdashboards/api/api.go
@@ -15,8 +15,8 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/publicdashboards"
+ "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
- "github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -73,10 +73,15 @@ func (api *Api) RegisterAPIEndpoints() {
auth(middleware.ReqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, uidScope)),
routing.Wrap(api.GetPublicDashboard))
- // Create/Update Public Dashboard
+ // Create Public Dashboard
api.RouteRegister.Post("/api/dashboards/uid/:dashboardUid/public-dashboards",
auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)),
- routing.Wrap(api.SavePublicDashboard))
+ routing.Wrap(api.CreatePublicDashboard))
+
+ // Update Public Dashboard
+ api.RouteRegister.Put("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid",
+ auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)),
+ routing.Wrap(api.UpdatePublicDashboard))
// Delete Public dashboard
api.RouteRegister.Delete("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid",
@@ -94,59 +99,103 @@ func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response {
return response.JSON(http.StatusOK, resp)
}
-// GetPublicDashboard Gets public dashboard configuration for dashboard
-// GET /api/dashboards/uid/:uid/public-config
+// GetPublicDashboard Gets public dashboard for dashboard
+// GET /api/dashboards/uid/:uid/public-dashboards
func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
// exit if we don't have a valid dashboardUid
dashboardUid := web.Params(c.Req)[":dashboardUid"]
- if dashboardUid == "" || !util.IsValidShortUID(dashboardUid) {
+ if !tokens.IsValidShortUID(dashboardUid) {
api.handleError(c.Req.Context(), http.StatusBadRequest, "GetPublicDashboard: no valid dashboardUid", dashboards.ErrDashboardIdentifierNotSet)
}
- pdc, err := api.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, web.Params(c.Req)[":dashboardUid"])
+ pd, err := api.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, web.Params(c.Req)[":dashboardUid"])
+
if err != nil {
- return api.handleError(c.Req.Context(), http.StatusInternalServerError, "GetPublicDashboardConfig: failed to get public dashboard config", err)
+ return api.handleError(c.Req.Context(), http.StatusInternalServerError, "GetPublicDashboard: failed to get public dashboard ", err)
}
- return response.JSON(http.StatusOK, pdc)
+
+ if pd == nil {
+ return api.handleError(c.Req.Context(), http.StatusNotFound, "GetPublicDashboard: public dashboard not found", ErrPublicDashboardNotFound)
+ }
+
+ return response.JSON(http.StatusOK, pd)
}
-// SavePublicDashboard Sets public dashboard configuration for dashboard
-// POST /api/dashboards/uid/:uid/public-config
-func (api *Api) SavePublicDashboard(c *models.ReqContext) response.Response {
+// CreatePublicDashboard Sets public dashboard for dashboard
+// POST /api/dashboards/uid/:uid/public-dashboards
+func (api *Api) CreatePublicDashboard(c *models.ReqContext) response.Response {
// exit if we don't have a valid dashboardUid
dashboardUid := web.Params(c.Req)[":dashboardUid"]
- if dashboardUid == "" || !util.IsValidShortUID(dashboardUid) {
- api.handleError(c.Req.Context(), http.StatusBadRequest, "SavePublicDashboard: invalid dashboardUid", dashboards.ErrDashboardIdentifierNotSet)
+ if !tokens.IsValidShortUID(dashboardUid) {
+ return api.handleError(c.Req.Context(), http.StatusBadRequest, "CreatePublicDashboard: invalid dashboardUid", dashboards.ErrDashboardIdentifierInvalid)
}
- pubdash := &PublicDashboard{}
- if err := web.Bind(c.Req, pubdash); err != nil {
- return response.Error(http.StatusBadRequest, "SavePublicDashboard: bad request data", err)
+ pd := &PublicDashboard{}
+ if err := web.Bind(c.Req, pd); err != nil {
+ return api.handleError(c.Req.Context(), http.StatusBadRequest, "CreatePublicDashboard: bad request data", err)
}
// Always set the orgID and userID from the session
- pubdash.OrgId = c.OrgID
+ pd.OrgId = c.OrgID
dto := SavePublicDashboardDTO{
UserId: c.UserID,
OrgId: c.OrgID,
DashboardUid: dashboardUid,
- PublicDashboard: pubdash,
+ PublicDashboard: pd,
+ }
+
+ //Create the public dashboard
+ pd, err := api.PublicDashboardService.Create(c.Req.Context(), c.SignedInUser, &dto)
+ if err != nil {
+ return api.handleError(c.Req.Context(), http.StatusInternalServerError, "CreatePublicDashboard: failed to create public dashboard", err)
+ }
+
+ return response.JSON(http.StatusOK, pd)
+}
+
+// UpdatePublicDashboard Sets public dashboard for dashboard
+// PUT /api/dashboards/uid/:uid/public-dashboards
+func (api *Api) UpdatePublicDashboard(c *models.ReqContext) response.Response {
+ // exit if we don't have a valid dashboardUid
+ dashboardUid := web.Params(c.Req)[":dashboardUid"]
+ if !tokens.IsValidShortUID(dashboardUid) {
+ return api.handleError(c.Req.Context(), http.StatusBadRequest, "UpdatePublicDashboard: invalid dashboardUid", dashboards.ErrDashboardIdentifierInvalid)
+ }
+
+ uid := web.Params(c.Req)[":uid"]
+ if !tokens.IsValidShortUID(uid) {
+ return api.handleError(c.Req.Context(), http.StatusBadRequest, "UpdatePublicDashboard: invalid public dashboard uid", ErrPublicDashboardIdentifierNotSet)
+ }
+
+ pd := &PublicDashboard{}
+ if err := web.Bind(c.Req, pd); err != nil {
+ return api.handleError(c.Req.Context(), http.StatusBadRequest, "UpdatePublicDashboard: bad request data", err)
+ }
+
+ // Always set the orgID and userID from the session
+ pd.OrgId = c.OrgID
+ pd.Uid = uid
+ dto := SavePublicDashboardDTO{
+ UserId: c.UserID,
+ OrgId: c.OrgID,
+ DashboardUid: dashboardUid,
+ PublicDashboard: pd,
}
// Save the public dashboard
- pubdash, err := api.PublicDashboardService.Save(c.Req.Context(), c.SignedInUser, &dto)
+ pd, err := api.PublicDashboardService.Update(c.Req.Context(), c.SignedInUser, &dto)
if err != nil {
- return api.handleError(c.Req.Context(), http.StatusInternalServerError, "SavePublicDashboardConfig: failed to save public dashboard configuration", err)
+ return api.handleError(c.Req.Context(), http.StatusInternalServerError, "UpdatePublicDashboard: failed to update public dashboard", err)
}
- return response.JSON(http.StatusOK, pubdash)
+ return response.JSON(http.StatusOK, pd)
}
// Delete a public dashboard
// DELETE /api/dashboards/uid/:dashboardUid/public-dashboards/:uid
func (api *Api) DeletePublicDashboard(c *models.ReqContext) response.Response {
uid := web.Params(c.Req)[":uid"]
- if uid == "" || !util.IsValidShortUID(uid) {
+ if !tokens.IsValidShortUID(uid) {
return api.handleError(c.Req.Context(), http.StatusBadRequest, "DeletePublicDashboard: invalid dashboard uid", dashboards.ErrDashboardIdentifierNotSet)
}
@@ -171,7 +220,6 @@ func (api *Api) handleError(ctx context.Context, code int, message string, err e
return response.Error(publicDashboardErr.StatusCode, publicDashboardErr.Error(), publicDashboardErr)
}
- // handle dashboard errors as well
var dashboardErr dashboards.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr)
diff --git a/pkg/services/publicdashboards/api/api_test.go b/pkg/services/publicdashboards/api/api_test.go
index faffc2071f2..25b279cf978 100644
--- a/pkg/services/publicdashboards/api/api_test.go
+++ b/pkg/services/publicdashboards/api/api_test.go
@@ -57,10 +57,20 @@ func TestAPIFeatureFlag(t *testing.T) {
Path: "/api/dashboards/uid/abc123/public-dashboards",
},
{
- Name: "API: Save Public Dashboard",
+ Name: "API: Create Public Dashboard",
Method: http.MethodPost,
Path: "/api/dashboards/uid/abc123/public-dashboards",
},
+ {
+ Name: "API: Update Public Dashboard",
+ Method: http.MethodPut,
+ Path: "/api/dashboards/uid/abc123/public-dashboards",
+ },
+ {
+ Name: "API: Delete Public Dashboard",
+ Method: http.MethodDelete,
+ Path: "/api/dashboards/uid/:dashboardUid/public-dashboards/:uid",
+ },
}
for _, test := range testCases {
@@ -148,6 +158,354 @@ func TestAPIListPublicDashboard(t *testing.T) {
}
}
+func TestAPIGetPublicDashboard(t *testing.T) {
+ pubdash := &PublicDashboard{IsEnabled: true}
+
+ testCases := []struct {
+ Name string
+ DashboardUid string
+ ExpectedHttpResponse int
+ PublicDashboardResult *PublicDashboard
+ PublicDashboardErr error
+ User *user.SignedInUser
+ AccessControlEnabled bool
+ ShouldCallService bool
+ }{
+ {
+ Name: "retrieves public dashboard when dashboard is found",
+ DashboardUid: "1",
+ ExpectedHttpResponse: http.StatusOK,
+ PublicDashboardResult: pubdash,
+ PublicDashboardErr: nil,
+ User: userViewer,
+ AccessControlEnabled: false,
+ ShouldCallService: true,
+ },
+ {
+ Name: "returns 404 when dashboard not found",
+ DashboardUid: "77777",
+ ExpectedHttpResponse: http.StatusNotFound,
+ PublicDashboardResult: nil,
+ PublicDashboardErr: dashboards.ErrDashboardNotFound,
+ User: userViewer,
+ AccessControlEnabled: false,
+ ShouldCallService: true,
+ },
+ {
+ Name: "returns 500 when internal server error",
+ DashboardUid: "1",
+ ExpectedHttpResponse: http.StatusInternalServerError,
+ PublicDashboardResult: nil,
+ PublicDashboardErr: errors.New("database broken"),
+ User: userViewer,
+ AccessControlEnabled: false,
+ ShouldCallService: true,
+ },
+ {
+ Name: "retrieves public dashboard when dashboard is found RBAC on",
+ DashboardUid: "1",
+ ExpectedHttpResponse: http.StatusOK,
+ PublicDashboardResult: pubdash,
+ PublicDashboardErr: nil,
+ User: userViewerRBAC,
+ AccessControlEnabled: true,
+ ShouldCallService: true,
+ },
+ {
+ Name: "returns 403 when no permissions RBAC on",
+ ExpectedHttpResponse: http.StatusForbidden,
+ PublicDashboardResult: pubdash,
+ PublicDashboardErr: nil,
+ User: userViewer,
+ AccessControlEnabled: true,
+ ShouldCallService: false,
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.Name, func(t *testing.T) {
+ service := publicdashboards.NewFakePublicDashboardService(t)
+
+ if test.ShouldCallService {
+ service.On("FindByDashboardUid", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
+ Return(test.PublicDashboardResult, test.PublicDashboardErr)
+ }
+
+ cfg := setting.NewCfg()
+ cfg.RBACEnabled = test.AccessControlEnabled
+
+ testServer := setupTestServer(
+ t,
+ cfg,
+ featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
+ service,
+ nil,
+ test.User,
+ )
+
+ response := callAPI(
+ testServer,
+ http.MethodGet,
+ "/api/dashboards/uid/1/public-dashboards",
+ nil,
+ t,
+ )
+
+ assert.Equal(t, test.ExpectedHttpResponse, response.Code)
+
+ if response.Code == http.StatusOK {
+ var pdcResp PublicDashboard
+ err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
+ require.NoError(t, err)
+ assert.Equal(t, test.PublicDashboardResult, &pdcResp)
+ }
+ })
+ }
+}
+
+func TestApiCreatePublicDashboard(t *testing.T) {
+ testCases := []struct {
+ Name string
+ DashboardUid string
+ publicDashboard *PublicDashboard
+ ExpectedHttpResponse int
+ SaveDashboardErr error
+ User *user.SignedInUser
+ AccessControlEnabled bool
+ ShouldCallService bool
+ }{
+ {
+ Name: "returns 200 when update persists",
+ DashboardUid: "1",
+ publicDashboard: &PublicDashboard{IsEnabled: true},
+ ExpectedHttpResponse: http.StatusOK,
+ SaveDashboardErr: nil,
+ User: userAdmin,
+ AccessControlEnabled: false,
+ ShouldCallService: true,
+ },
+ {
+ Name: "returns 500 when not persisted",
+ ExpectedHttpResponse: http.StatusInternalServerError,
+ publicDashboard: &PublicDashboard{},
+ SaveDashboardErr: errors.New("backend failed to save"),
+ User: userAdmin,
+ AccessControlEnabled: false,
+ ShouldCallService: true,
+ },
+ {
+ Name: "returns 404 when dashboard not found",
+ ExpectedHttpResponse: http.StatusNotFound,
+ publicDashboard: &PublicDashboard{},
+ SaveDashboardErr: dashboards.ErrDashboardNotFound,
+ User: userAdmin,
+ AccessControlEnabled: false,
+ ShouldCallService: true,
+ },
+ {
+ Name: "returns 200 when update persists RBAC on",
+ DashboardUid: "1",
+ publicDashboard: &PublicDashboard{IsEnabled: true},
+ ExpectedHttpResponse: http.StatusOK,
+ SaveDashboardErr: nil,
+ User: userAdminRBAC,
+ AccessControlEnabled: true,
+ ShouldCallService: true,
+ },
+ {
+ Name: "returns 403 when no permissions",
+ ExpectedHttpResponse: http.StatusForbidden,
+ publicDashboard: &PublicDashboard{IsEnabled: true},
+ SaveDashboardErr: nil,
+ User: userViewer,
+ AccessControlEnabled: false,
+ ShouldCallService: false,
+ },
+ {
+ Name: "returns 403 when no permissions RBAC on",
+ ExpectedHttpResponse: http.StatusForbidden,
+ publicDashboard: &PublicDashboard{IsEnabled: true},
+ SaveDashboardErr: nil,
+ User: userAdmin,
+ AccessControlEnabled: true,
+ ShouldCallService: false,
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.Name, func(t *testing.T) {
+ service := publicdashboards.NewFakePublicDashboardService(t)
+
+ // this is to avoid AssertExpectations fail at t.Cleanup when the middleware returns before calling the service
+ if test.ShouldCallService {
+ service.On("Create", mock.Anything, mock.Anything, mock.AnythingOfType("*models.SavePublicDashboardDTO")).
+ Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr)
+ }
+
+ cfg := setting.NewCfg()
+ cfg.RBACEnabled = test.AccessControlEnabled
+
+ testServer := setupTestServer(
+ t,
+ cfg,
+ featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
+ service,
+ nil,
+ test.User,
+ )
+
+ response := callAPI(
+ testServer,
+ http.MethodPost,
+ "/api/dashboards/uid/1/public-dashboards",
+ strings.NewReader(`{ "isPublic": true }`),
+ t,
+ )
+
+ assert.Equal(t, test.ExpectedHttpResponse, response.Code)
+
+ //check the result if it's a 200
+ if response.Code == http.StatusOK {
+ val, err := json.Marshal(test.publicDashboard)
+ require.NoError(t, err)
+ assert.Equal(t, string(val), response.Body.String())
+ }
+ })
+ }
+}
+
+func TestAPIUpdatePublicDashboard(t *testing.T) {
+ dashboardUid := "abc1234"
+ publicDashboardUid := "1234asdfasdf"
+
+ adminUser := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {dashboards.ScopeDashboardsAll}}}}
+
+ userEditorPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {fmt.Sprintf("dashboards:uid:%s", dashboardUid)}}}}
+
+ userEditorAnotherPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {"another-uid"}}}}
+
+ testCases := []struct {
+ Name string
+ User *user.SignedInUser
+ DashboardUid string
+ PublicDashboardUid string
+ PublicDashboardRes *PublicDashboard
+ PublicDashboardErr error
+ ExpectedHttpResponse int
+ ShouldCallService bool
+ }{
+ {
+ Name: "Invalid dashboardUid",
+ User: adminUser,
+ DashboardUid: "",
+ PublicDashboardUid: "",
+ PublicDashboardRes: nil,
+ PublicDashboardErr: dashboards.ErrDashboardIdentifierInvalid,
+ ExpectedHttpResponse: http.StatusNotFound,
+ ShouldCallService: false,
+ },
+ {
+ Name: "Invalid public dashboard uid",
+ User: adminUser,
+ DashboardUid: dashboardUid,
+ PublicDashboardUid: "",
+ PublicDashboardRes: nil,
+ PublicDashboardErr: ErrPublicDashboardNotFound,
+ ExpectedHttpResponse: http.StatusNotFound,
+ ShouldCallService: false,
+ },
+ {
+ Name: "Service Error",
+ User: adminUser,
+ DashboardUid: dashboardUid,
+ PublicDashboardUid: publicDashboardUid,
+ PublicDashboardRes: nil,
+ PublicDashboardErr: dashboards.ErrDashboardNotFound,
+ ExpectedHttpResponse: http.StatusNotFound,
+ ShouldCallService: true,
+ },
+ {
+ Name: "Success",
+ User: adminUser,
+ DashboardUid: dashboardUid,
+ PublicDashboardUid: publicDashboardUid,
+ PublicDashboardRes: &PublicDashboard{Uid: "success"},
+ PublicDashboardErr: nil,
+ ExpectedHttpResponse: http.StatusOK,
+ ShouldCallService: true,
+ },
+
+ // permissions
+ {
+ Name: "User can update this public dashboard",
+ User: userEditorPublicDashboard,
+ DashboardUid: dashboardUid,
+ PublicDashboardUid: publicDashboardUid,
+ PublicDashboardRes: &PublicDashboard{Uid: "success"},
+ PublicDashboardErr: nil,
+ ExpectedHttpResponse: http.StatusOK,
+ ShouldCallService: true,
+ },
+ {
+ Name: "User has permissions on another dashboard",
+ User: userEditorAnotherPublicDashboard,
+ PublicDashboardUid: publicDashboardUid,
+ ExpectedHttpResponse: http.StatusForbidden,
+ ShouldCallService: false,
+ },
+ {
+ Name: "Viewer cannot update any dashboard",
+ User: userViewer,
+ PublicDashboardUid: publicDashboardUid,
+ ExpectedHttpResponse: http.StatusForbidden,
+ ShouldCallService: false,
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.Name, func(t *testing.T) {
+ service := publicdashboards.NewFakePublicDashboardService(t)
+
+ if test.ShouldCallService {
+ service.On("Update", mock.Anything, mock.Anything, mock.Anything).
+ Return(test.PublicDashboardRes, test.PublicDashboardErr)
+ }
+
+ cfg := setting.NewCfg()
+ features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
+ testServer := setupTestServer(t, cfg, features, service, nil, test.User)
+ url := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", test.DashboardUid, test.PublicDashboardUid)
+ body := strings.NewReader(fmt.Sprintf(`{ "uid": "%s"}`, test.PublicDashboardUid))
+
+ response := callAPI(testServer, http.MethodPut, url, body, t)
+ assert.Equal(t, test.ExpectedHttpResponse, response.Code)
+
+ // check whether service called
+ if !test.ShouldCallService {
+ service.AssertNotCalled(t, "Update")
+ }
+
+ fmt.Println(response.Body.String())
+
+ // check response
+ if response.Code == http.StatusOK {
+ val, err := json.Marshal(test.PublicDashboardRes)
+ require.NoError(t, err)
+ assert.Equal(t, string(val), response.Body.String())
+
+ // verify 4XXs except 403 && 404
+ } else if test.ExpectedHttpResponse > 200 &&
+ test.ExpectedHttpResponse != 403 &&
+ test.ExpectedHttpResponse != 404 {
+ var errResp JsonErrResponse
+ err := json.Unmarshal(response.Body.Bytes(), &errResp)
+ require.NoError(t, err)
+ assert.Equal(t, test.PublicDashboardErr.Error(), errResp.Error)
+ }
+ })
+ }
+}
+
func TestAPIDeletePublicDashboard(t *testing.T) {
dashboardUid := "abc1234"
publicDashboardUid := "1234asdfasdf"
@@ -275,219 +633,3 @@ func TestAPIDeletePublicDashboard(t *testing.T) {
})
}
}
-
-func TestAPIGetPublicDashboard(t *testing.T) {
- pubdash := &PublicDashboard{IsEnabled: true}
-
- testCases := []struct {
- Name string
- DashboardUid string
- ExpectedHttpResponse int
- PublicDashboardResult *PublicDashboard
- PublicDashboardErr error
- User *user.SignedInUser
- AccessControlEnabled bool
- ShouldCallService bool
- }{
- {
- Name: "retrieves public dashboard when dashboard is found",
- DashboardUid: "1",
- ExpectedHttpResponse: http.StatusOK,
- PublicDashboardResult: pubdash,
- PublicDashboardErr: nil,
- User: userViewer,
- AccessControlEnabled: false,
- ShouldCallService: true,
- },
- {
- Name: "returns 404 when dashboard not found",
- DashboardUid: "77777",
- ExpectedHttpResponse: http.StatusNotFound,
- PublicDashboardResult: nil,
- PublicDashboardErr: dashboards.ErrDashboardNotFound,
- User: userViewer,
- AccessControlEnabled: false,
- ShouldCallService: true,
- },
- {
- Name: "returns 500 when internal server error",
- DashboardUid: "1",
- ExpectedHttpResponse: http.StatusInternalServerError,
- PublicDashboardResult: nil,
- PublicDashboardErr: errors.New("database broken"),
- User: userViewer,
- AccessControlEnabled: false,
- ShouldCallService: true,
- },
- {
- Name: "retrieves public dashboard when dashboard is found RBAC on",
- DashboardUid: "1",
- ExpectedHttpResponse: http.StatusOK,
- PublicDashboardResult: pubdash,
- PublicDashboardErr: nil,
- User: userViewerRBAC,
- AccessControlEnabled: true,
- ShouldCallService: true,
- },
- {
- Name: "returns 403 when no permissions RBAC on",
- ExpectedHttpResponse: http.StatusForbidden,
- PublicDashboardResult: pubdash,
- PublicDashboardErr: nil,
- User: userViewer,
- AccessControlEnabled: true,
- ShouldCallService: false,
- },
- }
-
- for _, test := range testCases {
- t.Run(test.Name, func(t *testing.T) {
- service := publicdashboards.NewFakePublicDashboardService(t)
-
- if test.ShouldCallService {
- service.On("FindByDashboardUid", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
- Return(test.PublicDashboardResult, test.PublicDashboardErr)
- }
-
- cfg := setting.NewCfg()
- cfg.RBACEnabled = test.AccessControlEnabled
-
- testServer := setupTestServer(
- t,
- cfg,
- featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
- service,
- nil,
- test.User,
- )
-
- response := callAPI(
- testServer,
- http.MethodGet,
- "/api/dashboards/uid/1/public-dashboards",
- nil,
- t,
- )
-
- assert.Equal(t, test.ExpectedHttpResponse, response.Code)
-
- if response.Code == http.StatusOK {
- var pdcResp PublicDashboard
- err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
- require.NoError(t, err)
- assert.Equal(t, test.PublicDashboardResult, &pdcResp)
- }
- })
- }
-}
-
-func TestApiSavePublicDashboard(t *testing.T) {
- testCases := []struct {
- Name string
- DashboardUid string
- publicDashboard *PublicDashboard
- ExpectedHttpResponse int
- SaveDashboardErr error
- User *user.SignedInUser
- AccessControlEnabled bool
- ShouldCallService bool
- }{
- {
- Name: "returns 200 when update persists",
- DashboardUid: "1",
- publicDashboard: &PublicDashboard{IsEnabled: true},
- ExpectedHttpResponse: http.StatusOK,
- SaveDashboardErr: nil,
- User: userAdmin,
- AccessControlEnabled: false,
- ShouldCallService: true,
- },
- {
- Name: "returns 500 when not persisted",
- ExpectedHttpResponse: http.StatusInternalServerError,
- publicDashboard: &PublicDashboard{},
- SaveDashboardErr: errors.New("backend failed to save"),
- User: userAdmin,
- AccessControlEnabled: false,
- ShouldCallService: true,
- },
- {
- Name: "returns 404 when dashboard not found",
- ExpectedHttpResponse: http.StatusNotFound,
- publicDashboard: &PublicDashboard{},
- SaveDashboardErr: dashboards.ErrDashboardNotFound,
- User: userAdmin,
- AccessControlEnabled: false,
- ShouldCallService: true,
- },
- {
- Name: "returns 200 when update persists RBAC on",
- DashboardUid: "1",
- publicDashboard: &PublicDashboard{IsEnabled: true},
- ExpectedHttpResponse: http.StatusOK,
- SaveDashboardErr: nil,
- User: userAdminRBAC,
- AccessControlEnabled: true,
- ShouldCallService: true,
- },
- {
- Name: "returns 403 when no permissions",
- ExpectedHttpResponse: http.StatusForbidden,
- publicDashboard: &PublicDashboard{IsEnabled: true},
- SaveDashboardErr: nil,
- User: userViewer,
- AccessControlEnabled: false,
- ShouldCallService: false,
- },
- {
- Name: "returns 403 when no permissions RBAC on",
- ExpectedHttpResponse: http.StatusForbidden,
- publicDashboard: &PublicDashboard{IsEnabled: true},
- SaveDashboardErr: nil,
- User: userAdmin,
- AccessControlEnabled: true,
- ShouldCallService: false,
- },
- }
-
- for _, test := range testCases {
- t.Run(test.Name, func(t *testing.T) {
- service := publicdashboards.NewFakePublicDashboardService(t)
-
- // this is to avoid AssertExpectations fail at t.Cleanup when the middleware returns before calling the service
- if test.ShouldCallService {
- service.On("Save", mock.Anything, mock.Anything, mock.AnythingOfType("*models.SavePublicDashboardDTO")).
- Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr)
- }
-
- cfg := setting.NewCfg()
- cfg.RBACEnabled = test.AccessControlEnabled
-
- testServer := setupTestServer(
- t,
- cfg,
- featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
- service,
- nil,
- test.User,
- )
-
- response := callAPI(
- testServer,
- http.MethodPost,
- "/api/dashboards/uid/1/public-dashboards",
- strings.NewReader(`{ "isPublic": true }`),
- t,
- )
-
- assert.Equal(t, test.ExpectedHttpResponse, response.Code)
-
- //check the result if it's a 200
- if response.Code == http.StatusOK {
- val, err := json.Marshal(test.publicDashboard)
- require.NoError(t, err)
- assert.Equal(t, string(val), response.Body.String())
- }
- })
- }
-}
diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go
index 4f6173eec8e..a586f514e0d 100644
--- a/pkg/services/publicdashboards/api/query_test.go
+++ b/pkg/services/publicdashboards/api/query_test.go
@@ -316,7 +316,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
ac := acmock.New()
cfg.RBACEnabled = false
service := publicdashboardsService.ProvideService(cfg, store, qds, annotationsService, ac)
- pubdash, err := service.Save(context.Background(), &user.SignedInUser{}, savePubDashboardCmd)
+ pubdash, err := service.Create(context.Background(), &user.SignedInUser{}, savePubDashboardCmd)
require.NoError(t, err)
// setup test server
diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go
index 10f2f1bb313..2f9b8de65d4 100644
--- a/pkg/services/publicdashboards/database/database.go
+++ b/pkg/services/publicdashboards/database/database.go
@@ -66,11 +66,15 @@ func (d *PublicDashboardStoreImpl) FindDashboard(ctx context.Context, orgId int6
return err
})
+ if err != nil {
+ return nil, err
+ }
+
if !found {
return nil, nil
}
- return dashboard, err
+ return dashboard, nil
}
// Find Returns public dashboard by Uid or nil if not found
@@ -80,10 +84,10 @@ func (d *PublicDashboardStoreImpl) Find(ctx context.Context, uid string) (*Publi
}
var found bool
- pdRes := &PublicDashboard{Uid: uid}
+ publicDashboard := &PublicDashboard{Uid: uid}
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
var err error
- found, err = sess.Get(pdRes)
+ found, err = sess.Get(publicDashboard)
return err
})
@@ -95,7 +99,7 @@ func (d *PublicDashboardStoreImpl) Find(ctx context.Context, uid string) (*Publi
return nil, nil
}
- return pdRes, err
+ return publicDashboard, nil
}
// FindByAccessToken Returns public dashboard by access token or nil if not found
@@ -105,10 +109,10 @@ func (d *PublicDashboardStoreImpl) FindByAccessToken(ctx context.Context, access
}
var found bool
- pdRes := &PublicDashboard{AccessToken: accessToken}
+ publicDashboard := &PublicDashboard{AccessToken: accessToken}
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
var err error
- found, err = sess.Get(pdRes)
+ found, err = sess.Get(publicDashboard)
return err
})
@@ -120,7 +124,7 @@ func (d *PublicDashboardStoreImpl) FindByAccessToken(ctx context.Context, access
return nil, nil
}
- return pdRes, err
+ return publicDashboard, nil
}
// FindByDashboardUid Retrieves public dashboard by dashboard uid or nil if not found
@@ -128,7 +132,6 @@ func (d *PublicDashboardStoreImpl) FindByDashboardUid(ctx context.Context, orgId
if dashboardUid == "" || orgId == 0 {
return nil, nil
}
-
var found bool
publicDashboard := &PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid}
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
@@ -149,67 +152,7 @@ func (d *PublicDashboardStoreImpl) FindByDashboardUid(ctx context.Context, orgId
return nil, nil
}
- return publicDashboard, err
-}
-
-// Save Persists public dashboard
-func (d *PublicDashboardStoreImpl) Save(ctx context.Context, cmd SavePublicDashboardCommand) error {
- if cmd.PublicDashboard.DashboardUid == "" {
- return dashboards.ErrDashboardIdentifierNotSet
- }
-
- err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
- _, err := sess.UseBool("is_enabled").Insert(&cmd.PublicDashboard)
- if err != nil {
- return err
- }
-
- return nil
- })
-
- return err
-}
-
-// Update updates existing public dashboard
-func (d *PublicDashboardStoreImpl) Update(ctx context.Context, cmd SavePublicDashboardCommand) error {
- err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
- timeSettingsJSON, err := json.Marshal(cmd.PublicDashboard.TimeSettings)
- if err != nil {
- return err
- }
-
- _, err = sess.Exec("UPDATE dashboard_public SET is_enabled = ?, annotations_enabled = ?, time_settings = ?, updated_by = ?, updated_at = ? WHERE uid = ?",
- cmd.PublicDashboard.IsEnabled,
- cmd.PublicDashboard.AnnotationsEnabled,
- string(timeSettingsJSON),
- cmd.PublicDashboard.UpdatedBy,
- cmd.PublicDashboard.UpdatedAt.UTC().Format("2006-01-02 15:04:05"),
- cmd.PublicDashboard.Uid)
-
- if err != nil {
- return err
- }
-
- return nil
- })
-
- return err
-}
-
-func (d *PublicDashboardStoreImpl) Delete(ctx context.Context, orgId int64, uid string) (int64, error) {
- dashboard := &PublicDashboard{OrgId: orgId, Uid: uid}
- var affectedRows int64
- err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
- var err error
- affectedRows, err = sess.Delete(dashboard)
-
- if err != nil {
- return err
- }
- return nil
- })
-
- return affectedRows, err
+ return publicDashboard, nil
}
// ExistsEnabledByDashboardUid Responds true if there is an enabled public dashboard for a dashboard uid
@@ -264,3 +207,62 @@ func (d *PublicDashboardStoreImpl) GetOrgIdByAccessToken(ctx context.Context, ac
return orgId, err
}
+
+// Creates a public dashboard
+func (d *PublicDashboardStoreImpl) Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) {
+ if cmd.PublicDashboard.DashboardUid == "" {
+ return 0, dashboards.ErrDashboardIdentifierNotSet
+ }
+
+ var affectedRows int64
+ err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
+ var err error
+ affectedRows, err = sess.UseBool("is_enabled").Insert(&cmd.PublicDashboard)
+ return err
+ })
+
+ return affectedRows, err
+}
+
+// Updates existing public dashboard
+func (d *PublicDashboardStoreImpl) Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) {
+ var affectedRows int64
+ err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
+ timeSettingsJSON, err := json.Marshal(cmd.PublicDashboard.TimeSettings)
+ if err != nil {
+ return err
+ }
+
+ sqlResult, err := sess.Exec("UPDATE dashboard_public SET is_enabled = ?, annotations_enabled = ?, time_settings = ?, updated_by = ?, updated_at = ? WHERE uid = ?",
+ cmd.PublicDashboard.IsEnabled,
+ cmd.PublicDashboard.AnnotationsEnabled,
+ string(timeSettingsJSON),
+ cmd.PublicDashboard.UpdatedBy,
+ cmd.PublicDashboard.UpdatedAt.UTC().Format("2006-01-02 15:04:05"),
+ cmd.PublicDashboard.Uid)
+
+ if err != nil {
+ return err
+ }
+
+ affectedRows, err = sqlResult.RowsAffected()
+
+ return err
+ })
+
+ return affectedRows, err
+}
+
+// Deletes a public dashboard
+func (d *PublicDashboardStoreImpl) Delete(ctx context.Context, orgId int64, uid string) (int64, error) {
+ dashboard := &PublicDashboard{OrgId: orgId, Uid: uid}
+ var affectedRows int64
+ err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
+ var err error
+ affectedRows, err = sess.Delete(dashboard)
+
+ return err
+ })
+
+ return affectedRows, err
+}
diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go
index b39c86f4278..4a505a76117 100644
--- a/pkg/services/publicdashboards/database/database_test.go
+++ b/pkg/services/publicdashboards/database/database_test.go
@@ -103,7 +103,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) {
t.Run("ExistsEnabledByAccessToken will return true when at least one public dashboard has a matching access token", func(t *testing.T) {
setup()
- err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ _, err := publicdashboardStore.Create(context.Background(), SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "abc123",
@@ -125,7 +125,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) {
t.Run("ExistsEnabledByAccessToken will return false when IsEnabled=false", func(t *testing.T) {
setup()
- err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ _, err := publicdashboardStore.Create(context.Background(), SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: false,
Uid: "abc123",
@@ -171,7 +171,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) {
t.Run("ExistsEnabledByDashboardUid Will return true when dashboard has at least one enabled public dashboard", func(t *testing.T) {
setup()
- err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ _, err := publicdashboardStore.Create(context.Background(), SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "abc123",
@@ -193,7 +193,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) {
t.Run("ExistsEnabledByDashboardUid will return false when dashboard has public dashboards but they are not enabled", func(t *testing.T) {
setup()
- err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ _, err := publicdashboardStore.Create(context.Background(), SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: false,
Uid: "abc123",
@@ -257,7 +257,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) {
}
// insert test public dashboard
- err := publicdashboardStore.Save(context.Background(), cmd)
+ _, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
// retrieve from db
@@ -320,7 +320,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) {
}
// insert test public dashboard
- err := publicdashboardStore.Save(context.Background(), cmd)
+ _, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
// retrieve from db
@@ -338,7 +338,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) {
})
}
-func TestIntegrationSavePublicDashboard(t *testing.T) {
+func TestIntegrationCreatePublicDashboard(t *testing.T) {
var sqlStore db.DB
var cfg *setting.Cfg
var dashboardStore *dashboardsDB.DashboardStore
@@ -357,7 +357,7 @@ func TestIntegrationSavePublicDashboard(t *testing.T) {
t.Run("saves new public dashboard", func(t *testing.T) {
setup()
- err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
AnnotationsEnabled: true,
@@ -369,14 +369,14 @@ func TestIntegrationSavePublicDashboard(t *testing.T) {
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
- })
+ }
+ affectedRows, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
+ assert.EqualValues(t, affectedRows, 1)
pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
-
- // verify we have a valid uid
- assert.True(t, util.IsValidShortUID(pubdash.Uid))
+ assert.Equal(t, pubdash.AccessToken, "NOTAREALUUID")
// verify we didn't update all dashboards
pubdash2, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid)
@@ -386,7 +386,7 @@ func TestIntegrationSavePublicDashboard(t *testing.T) {
t.Run("guards from saving without dashboardUid", func(t *testing.T) {
setup()
- err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "pubdash-uid",
@@ -397,9 +397,11 @@ func TestIntegrationSavePublicDashboard(t *testing.T) {
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
- })
+ }
+ affectedRows, err := publicdashboardStore.Create(context.Background(), cmd)
require.Error(t, err)
assert.Equal(t, err, dashboards.ErrDashboardIdentifierNotSet)
+ assert.EqualValues(t, affectedRows, 0)
})
}
@@ -423,7 +425,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
setup()
pdUid := "asdf1234"
- err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
@@ -434,12 +436,14 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
- })
+ }
+ affectedRows, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
+ assert.EqualValues(t, affectedRows, 1)
// inserting two different public dashboards to test update works and only affect the desired pd by uid
anotherPdUid := "anotherUid"
- err = publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ cmd = SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
Uid: anotherPdUid,
DashboardUid: anotherSavedDashboard.Uid,
@@ -450,8 +454,11 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
CreatedBy: 7,
AccessToken: "fakeaccesstoken",
},
- })
+ }
+
+ affectedRows, err = publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
+ assert.EqualValues(t, affectedRows, 1)
updatedPublicDashboard := PublicDashboard{
Uid: pdUid,
@@ -463,11 +470,12 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
UpdatedAt: time.Now().UTC().Round(time.Second),
UpdatedBy: 8,
}
+
// update initial record
- err = publicdashboardStore.Update(context.Background(), SavePublicDashboardCommand{
- PublicDashboard: updatedPublicDashboard,
- })
+ cmd = SavePublicDashboardCommand{PublicDashboard: updatedPublicDashboard}
+ rowsAffected, err := publicdashboardStore.Update(context.Background(), cmd)
require.NoError(t, err)
+ assert.EqualValues(t, rowsAffected, 1)
// updated dashboard should have changed
pdRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
@@ -503,8 +511,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
}
t.Run("GetOrgIdByAccessToken will OrgId when enabled", func(t *testing.T) {
setup()
-
- err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "abc123",
@@ -514,7 +521,8 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
CreatedBy: 7,
AccessToken: "accessToken",
},
- })
+ }
+ _, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken")
@@ -525,8 +533,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
t.Run("GetOrgIdByAccessToken will return 0 when IsEnabled=false", func(t *testing.T) {
setup()
-
- err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
+ cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: false,
Uid: "abc123",
@@ -536,8 +543,11 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
CreatedBy: 7,
AccessToken: "accessToken",
},
- })
+ }
+
+ _, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
+
orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken")
require.NoError(t, err)
assert.NotEqual(t, savedDashboard.OrgId, orgId)
@@ -634,8 +644,9 @@ func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardSt
},
}
- err = publicdashboardStore.Save(ctx, cmd)
+ affectedRows, err := publicdashboardStore.Create(ctx, cmd)
require.NoError(t, err)
+ assert.EqualValues(t, affectedRows, 1)
pubdash, err := publicdashboardStore.Find(ctx, uid)
require.NoError(t, err)
diff --git a/pkg/services/publicdashboards/internal/tokens/tokens.go b/pkg/services/publicdashboards/internal/tokens/tokens.go
index 48e241872fa..4d4f3accf0d 100644
--- a/pkg/services/publicdashboards/internal/tokens/tokens.go
+++ b/pkg/services/publicdashboards/internal/tokens/tokens.go
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/google/uuid"
+ "github.com/grafana/grafana/pkg/util"
)
// GenerateAccessToken generates an uuid formatted without dashes to use as access token
@@ -20,3 +21,9 @@ func IsValidAccessToken(token string) bool {
_, err := uuid.Parse(token)
return err == nil
}
+
+// IsValidShortUID checks that the uid is not blank and contains valid
+// characters. Wraps utils.IsValidShortUID
+func IsValidShortUID(uid string) bool {
+ return uid != "" && util.IsValidShortUID(uid)
+}
diff --git a/pkg/services/publicdashboards/internal/tokens/tokens_test.go b/pkg/services/publicdashboards/internal/tokens/tokens_test.go
index fdf0da97a9f..b04ba221f10 100644
--- a/pkg/services/publicdashboards/internal/tokens/tokens_test.go
+++ b/pkg/services/publicdashboards/internal/tokens/tokens_test.go
@@ -36,3 +36,19 @@ func TestValidAccessToken(t *testing.T) {
assert.False(t, IsValidAccessToken("0123456789012345678901234567890123456789"))
})
}
+
+// we just check base cases since this wraps utils.IsValidShortUID which has
+// test coverage
+func TestValidUid(t *testing.T) {
+ t.Run("true", func(t *testing.T) {
+ assert.True(t, IsValidShortUID("afqrz7jZZ"))
+ })
+
+ t.Run("false when blank", func(t *testing.T) {
+ assert.False(t, IsValidShortUID(""))
+ })
+
+ t.Run("false when invalid chars", func(t *testing.T) {
+ assert.False(t, IsValidShortUID("afqrz7j%%"))
+ })
+}
diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go
index a6b7d51e853..afee7d78e5b 100644
--- a/pkg/services/publicdashboards/public_dashboard_service_mock.go
+++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go
@@ -25,6 +25,29 @@ type FakePublicDashboardService struct {
mock.Mock
}
+// Create provides a mock function with given fields: ctx, u, dto
+func (_m *FakePublicDashboardService) Create(ctx context.Context, u *user.SignedInUser, dto *models.SavePublicDashboardDTO) (*models.PublicDashboard, error) {
+ ret := _m.Called(ctx, u, dto)
+
+ var r0 *models.PublicDashboard
+ if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, *models.SavePublicDashboardDTO) *models.PublicDashboard); ok {
+ r0 = rf(ctx, u, dto)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*models.PublicDashboard)
+ }
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, *models.SavePublicDashboardDTO) error); ok {
+ r1 = rf(ctx, u, dto)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
// Delete provides a mock function with given fields: ctx, orgId, uid
func (_m *FakePublicDashboardService) Delete(ctx context.Context, orgId int64, uid string) error {
ret := _m.Called(ctx, orgId, uid)
@@ -312,8 +335,8 @@ func (_m *FakePublicDashboardService) NewPublicDashboardUid(ctx context.Context)
return r0, r1
}
-// Save provides a mock function with given fields: ctx, u, dto
-func (_m *FakePublicDashboardService) Save(ctx context.Context, u *user.SignedInUser, dto *models.SavePublicDashboardDTO) (*models.PublicDashboard, error) {
+// Update provides a mock function with given fields: ctx, u, dto
+func (_m *FakePublicDashboardService) Update(ctx context.Context, u *user.SignedInUser, dto *models.SavePublicDashboardDTO) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, u, dto)
var r0 *models.PublicDashboard
diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go
index eafc1b92a50..a664cb9cc2c 100644
--- a/pkg/services/publicdashboards/public_dashboard_store_mock.go
+++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go
@@ -18,6 +18,27 @@ type FakePublicDashboardStore struct {
mock.Mock
}
+// Create provides a mock function with given fields: ctx, cmd
+func (_m *FakePublicDashboardStore) Create(ctx context.Context, cmd models.SavePublicDashboardCommand) (int64, error) {
+ ret := _m.Called(ctx, cmd)
+
+ var r0 int64
+ if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardCommand) int64); ok {
+ r0 = rf(ctx, cmd)
+ } else {
+ r0 = ret.Get(0).(int64)
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardCommand) error); ok {
+ r1 = rf(ctx, cmd)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
// Delete provides a mock function with given fields: ctx, orgId, uid
func (_m *FakePublicDashboardStore) Delete(ctx context.Context, orgId int64, uid string) (int64, error) {
ret := _m.Called(ctx, orgId, uid)
@@ -217,32 +238,25 @@ func (_m *FakePublicDashboardStore) GetOrgIdByAccessToken(ctx context.Context, a
return r0, r1
}
-// Save provides a mock function with given fields: ctx, cmd
-func (_m *FakePublicDashboardStore) Save(ctx context.Context, cmd models.SavePublicDashboardCommand) error {
- ret := _m.Called(ctx, cmd)
-
- var r0 error
- if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardCommand) error); ok {
- r0 = rf(ctx, cmd)
- } else {
- r0 = ret.Error(0)
- }
-
- return r0
-}
-
// Update provides a mock function with given fields: ctx, cmd
-func (_m *FakePublicDashboardStore) Update(ctx context.Context, cmd models.SavePublicDashboardCommand) error {
+func (_m *FakePublicDashboardStore) Update(ctx context.Context, cmd models.SavePublicDashboardCommand) (int64, error) {
ret := _m.Called(ctx, cmd)
- var r0 error
- if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardCommand) error); ok {
+ var r0 int64
+ if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardCommand) int64); ok {
r0 = rf(ctx, cmd)
} else {
- r0 = ret.Error(0)
+ r0 = ret.Get(0).(int64)
}
- return r0
+ var r1 error
+ if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardCommand) error); ok {
+ r1 = rf(ctx, cmd)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
}
// NewFakePublicDashboardStore creates a new instance of FakePublicDashboardStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go
index 6c1163e0fc5..cf019141255 100644
--- a/pkg/services/publicdashboards/publicdashboard.go
+++ b/pkg/services/publicdashboards/publicdashboard.go
@@ -20,7 +20,8 @@ type Service interface {
FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error)
FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error)
FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error)
- Save(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
+ Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
+ Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
Delete(ctx context.Context, orgId int64, uid string) error
GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error)
@@ -40,8 +41,8 @@ type Store interface {
FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error)
FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error)
- Save(ctx context.Context, cmd SavePublicDashboardCommand) error
- Update(ctx context.Context, cmd SavePublicDashboardCommand) error
+ Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error)
+ Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error)
Delete(ctx context.Context, orgId int64, uid string) (int64, error)
GetOrgIdByAccessToken(ctx context.Context, accessToken string) (int64, error)
diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go
index 80e81d8da95..366fe40c259 100644
--- a/pkg/services/publicdashboards/service/query_test.go
+++ b/pkg/services/publicdashboards/service/query_test.go
@@ -399,7 +399,7 @@ func TestGetQueryDataResponse(t *testing.T) {
TimeSettings: timeSettings,
},
}
- pubdashDto, err := service.Save(context.Background(), SignedInUser, dto)
+ pubdashDto, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
resp, _ := service.GetQueryDataResponse(context.Background(), true, publicDashboardQueryDTO, 1, pubdashDto.AccessToken)
@@ -840,7 +840,7 @@ func TestBuildMetricRequest(t *testing.T) {
},
}
- publicDashboardPD, err := service.Save(context.Background(), SignedInUser, dto)
+ publicDashboardPD, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
nonPublicDto := &SavePublicDashboardDTO{
@@ -854,7 +854,7 @@ func TestBuildMetricRequest(t *testing.T) {
},
}
- _, err = service.Save(context.Background(), SignedInUser, nonPublicDto)
+ _, err = service.Create(context.Background(), SignedInUser, nonPublicDto)
require.NoError(t, err)
t.Run("extracts queries from provided dashboard", func(t *testing.T) {
diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go
index 766008e43ef..29685c36bc4 100644
--- a/pkg/services/publicdashboards/service/service.go
+++ b/pkg/services/publicdashboards/service/service.go
@@ -121,9 +121,76 @@ func (pd *PublicDashboardServiceImpl) FindByDashboardUid(ctx context.Context, or
return pubdash, nil
}
-// Save is a helper method to persist the sharing config
-// to the database. It handles validations for sharing config and persistence
-func (pd *PublicDashboardServiceImpl) Save(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) {
+// Creates and validates the public dashboard and saves it to the database
+func (pd *PublicDashboardServiceImpl) Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) {
+ // ensure dashboard exists
+ dashboard, err := pd.FindDashboard(ctx, u.OrgID, dto.DashboardUid)
+ if err != nil {
+ return nil, err
+ }
+
+ // set default value for time settings
+ if dto.PublicDashboard.TimeSettings == nil {
+ dto.PublicDashboard.TimeSettings = &TimeSettings{}
+ }
+
+ // validate fields
+ err = validation.ValidatePublicDashboard(dto, dashboard)
+ if err != nil {
+ return nil, err
+ }
+
+ // verify public dashboard does not exist and that we didn't get one from the
+ // request
+ existingPubdash, err := pd.store.Find(ctx, dto.PublicDashboard.Uid)
+ if err != nil {
+ return nil, err
+ } else if existingPubdash != nil {
+ return nil, ErrPublicDashboardBadRequest
+ }
+
+ uid, err := pd.NewPublicDashboardUid(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ accessToken, err := pd.NewPublicDashboardAccessToken(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ cmd := SavePublicDashboardCommand{
+ PublicDashboard: PublicDashboard{
+ Uid: uid,
+ DashboardUid: dto.DashboardUid,
+ OrgId: dto.OrgId,
+ IsEnabled: dto.PublicDashboard.IsEnabled,
+ AnnotationsEnabled: dto.PublicDashboard.AnnotationsEnabled,
+ TimeSettings: dto.PublicDashboard.TimeSettings,
+ CreatedBy: dto.UserId,
+ CreatedAt: time.Now(),
+ AccessToken: accessToken,
+ },
+ }
+
+ _, err = pd.store.Create(ctx, cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ //Get latest public dashboard to return
+ newPubdash, err := pd.store.Find(ctx, uid)
+ if err != nil {
+ return nil, err
+ }
+
+ pd.logIsEnabledChanged(existingPubdash, newPubdash, u)
+
+ return newPubdash, err
+}
+
+// Updates an existing public dashboard based on publicdashboard.Uid
+func (pd *PublicDashboardServiceImpl) Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) {
// validate if the dashboard exists
dashboard, err := pd.FindDashboard(ctx, u.OrgID, dto.DashboardUid)
if err != nil {
@@ -143,25 +210,41 @@ func (pd *PublicDashboardServiceImpl) Save(ctx context.Context, u *user.SignedIn
existingPubdash, err := pd.store.Find(ctx, dto.PublicDashboard.Uid)
if err != nil {
return nil, err
+ } else if existingPubdash == nil {
+ return nil, ErrPublicDashboardNotFound
}
- // save changes
- var pubdashUid string
- if existingPubdash == nil {
- err = validation.ValidateSavePublicDashboard(dto, dashboard)
- if err != nil {
- return nil, err
- }
- pubdashUid, err = pd.savePublicDashboard(ctx, dto)
- } else {
- pubdashUid, err = pd.updatePublicDashboard(ctx, dto)
- }
+ // validate dashboard
+ err = validation.ValidatePublicDashboard(dto, dashboard)
if err != nil {
return nil, err
}
- //Get latest public dashboard to return
- newPubdash, err := pd.store.Find(ctx, pubdashUid)
+ // set values to update
+ cmd := SavePublicDashboardCommand{
+ PublicDashboard: PublicDashboard{
+ Uid: existingPubdash.Uid,
+ IsEnabled: dto.PublicDashboard.IsEnabled,
+ AnnotationsEnabled: dto.PublicDashboard.AnnotationsEnabled,
+ TimeSettings: dto.PublicDashboard.TimeSettings,
+ UpdatedBy: dto.UserId,
+ UpdatedAt: time.Now(),
+ },
+ }
+
+ // persist
+ affectedRows, err := pd.store.Update(ctx, cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ // 404 if not found
+ if affectedRows == 0 {
+ return nil, ErrPublicDashboardNotFound
+ }
+
+ // get latest public dashboard to return
+ newPubdash, err := pd.store.Find(ctx, existingPubdash.Uid)
if err != nil {
return nil, err
}
@@ -203,58 +286,6 @@ func (pd *PublicDashboardServiceImpl) NewPublicDashboardAccessToken(ctx context.
return "", ErrPublicDashboardFailedGenerateAccessToken
}
-// Called by Save this handles business logic
-// to generate token and calls create at the database layer
-func (pd *PublicDashboardServiceImpl) savePublicDashboard(ctx context.Context, dto *SavePublicDashboardDTO) (string, error) {
- uid, err := pd.NewPublicDashboardUid(ctx)
- if err != nil {
- return "", err
- }
-
- accessToken, err := pd.NewPublicDashboardAccessToken(ctx)
- if err != nil {
- return "", err
- }
-
- cmd := SavePublicDashboardCommand{
- PublicDashboard: PublicDashboard{
- Uid: uid,
- DashboardUid: dto.DashboardUid,
- OrgId: dto.OrgId,
- IsEnabled: dto.PublicDashboard.IsEnabled,
- AnnotationsEnabled: dto.PublicDashboard.AnnotationsEnabled,
- TimeSettings: dto.PublicDashboard.TimeSettings,
- CreatedBy: dto.UserId,
- CreatedAt: time.Now(),
- AccessToken: accessToken,
- },
- }
-
- err = pd.store.Save(ctx, cmd)
- if err != nil {
- return "", err
- }
-
- return uid, nil
-}
-
-// Called by Save this handles business logic for updating a
-// dashboard and calls update at the database layer
-func (pd *PublicDashboardServiceImpl) updatePublicDashboard(ctx context.Context, dto *SavePublicDashboardDTO) (string, error) {
- cmd := SavePublicDashboardCommand{
- PublicDashboard: PublicDashboard{
- Uid: dto.PublicDashboard.Uid,
- IsEnabled: dto.PublicDashboard.IsEnabled,
- AnnotationsEnabled: dto.PublicDashboard.AnnotationsEnabled,
- TimeSettings: dto.PublicDashboard.TimeSettings,
- UpdatedBy: dto.UserId,
- UpdatedAt: time.Now(),
- },
- }
-
- return dto.PublicDashboard.Uid, pd.store.Update(ctx, cmd)
-}
-
// FindAll Returns a list of public dashboards by orgId
func (pd *PublicDashboardServiceImpl) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) {
publicDashboards, err := pd.store.FindAll(ctx, orgId)
diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go
index 36d37601377..82dec54da3d 100644
--- a/pkg/services/publicdashboards/service/service_test.go
+++ b/pkg/services/publicdashboards/service/service_test.go
@@ -120,8 +120,10 @@ func TestGetPublicDashboard(t *testing.T) {
}
}
-func TestSavePublicDashboard(t *testing.T) {
- t.Run("Saving public dashboard", func(t *testing.T) {
+// We're using sqlite here because testing all of the behaviors with mocks in
+// the correct order is convoluted.
+func TestCreatePublicDashboard(t *testing.T) {
+ t.Run("Create public dashboard", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
publicdashboardStore := database.ProvideStore(sqlStore)
@@ -145,7 +147,7 @@ func TestSavePublicDashboard(t *testing.T) {
},
}
- _, err := service.Save(context.Background(), SignedInUser, dto)
+ _, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid)
@@ -189,7 +191,7 @@ func TestSavePublicDashboard(t *testing.T) {
},
}
- _, err := service.Save(context.Background(), SignedInUser, dto)
+ _, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid)
@@ -220,11 +222,11 @@ func TestSavePublicDashboard(t *testing.T) {
},
}
- _, err := service.Save(context.Background(), SignedInUser, dto)
+ _, err := service.Create(context.Background(), SignedInUser, dto)
require.Error(t, err)
})
- t.Run("Pubdash access token generation throws an error and pubdash is not persisted", func(t *testing.T) {
+ t.Run("Throws an error when pubdash with generated access token already exists", func(t *testing.T) {
dashboard := models.NewDashboard("testDashie")
pubdash := &PublicDashboard{
IsEnabled: true,
@@ -238,7 +240,6 @@ func TestSavePublicDashboard(t *testing.T) {
publicDashboardStore.On("FindDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil)
publicDashboardStore.On("Find", mock.Anything, mock.Anything).Return(nil, nil)
publicDashboardStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(pubdash, nil)
- publicDashboardStore.On("NewPublicDashboardUid", mock.Anything).Return("an-uid", nil)
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
@@ -256,11 +257,59 @@ func TestSavePublicDashboard(t *testing.T) {
},
}
- _, err := service.Save(context.Background(), SignedInUser, dto)
+ _, err := service.Create(context.Background(), SignedInUser, dto)
require.Error(t, err)
require.Equal(t, err, ErrPublicDashboardFailedGenerateAccessToken)
- publicDashboardStore.AssertNotCalled(t, "Save")
+ publicDashboardStore.AssertNotCalled(t, "Create")
+ })
+
+ t.Run("Returns error if public dashboard exists", func(t *testing.T) {
+ sqlStore := db.InitTestDB(t)
+ dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
+ publicdashboardStore := database.ProvideStore(sqlStore)
+ dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
+
+ service := &PublicDashboardServiceImpl{
+ log: log.New("test.logger"),
+ store: publicdashboardStore,
+ }
+
+ dto := &SavePublicDashboardDTO{
+ DashboardUid: dashboard.Uid,
+ OrgId: dashboard.OrgId,
+ UserId: 7,
+ PublicDashboard: &PublicDashboard{
+ AnnotationsEnabled: false,
+ IsEnabled: true,
+ TimeSettings: timeSettings,
+ },
+ }
+
+ savedPubdash, err := service.Create(context.Background(), SignedInUser, dto)
+ require.NoError(t, err)
+
+ // attempt to overwrite settings
+ dto = &SavePublicDashboardDTO{
+ DashboardUid: dashboard.Uid,
+ OrgId: dashboard.OrgId,
+ UserId: 8,
+ PublicDashboard: &PublicDashboard{
+ Uid: savedPubdash.Uid,
+ OrgId: 9,
+ DashboardUid: "abc1234",
+ CreatedBy: 9,
+ CreatedAt: time.Time{},
+
+ IsEnabled: true,
+ AnnotationsEnabled: true,
+ TimeSettings: timeSettings,
+ AccessToken: "NOTAREALUUID",
+ },
+ }
+
+ _, err = service.Create(context.Background(), SignedInUser, dto)
+ assert.Equal(t, ErrPublicDashboardBadRequest, err)
})
}
@@ -287,7 +336,8 @@ func TestUpdatePublicDashboard(t *testing.T) {
},
}
- savedPubdash, err := service.Save(context.Background(), SignedInUser, dto)
+ // insert initial pubdash
+ savedPubdash, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
// attempt to overwrite settings
@@ -308,10 +358,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
AccessToken: "NOTAREALUUID",
},
}
-
- // Since the dto.PublicDashboard has a uid, this will call
- // service.updatePublicDashboard
- updatedPubdash, err := service.Save(context.Background(), SignedInUser, dto)
+ updatedPubdash, err := service.Update(context.Background(), SignedInUser, dto)
require.NoError(t, err)
// don't get updated
@@ -350,9 +397,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
},
}
- // Since the dto.PublicDashboard has a uid, this will call
- // service.updatePublicDashboard
- savedPubdash, err := service.Save(context.Background(), SignedInUser, dto)
+ savedPubdash, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
// attempt to overwrite settings
@@ -372,7 +417,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
},
}
- updatedPubdash, err := service.Save(context.Background(), SignedInUser, dto)
+ updatedPubdash, err := service.Update(context.Background(), SignedInUser, dto)
require.NoError(t, err)
assert.Equal(t, &TimeSettings{}, updatedPubdash.TimeSettings)
@@ -422,81 +467,6 @@ func TestDeletePublicDashboard(t *testing.T) {
}
}
-func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
- folderId int64, isFolder bool, templateVars []map[string]interface{}, customPanels []interface{}, tags ...interface{}) *models.Dashboard {
- t.Helper()
-
- var dashboardPanels []interface{}
- if customPanels != nil {
- dashboardPanels = customPanels
- } else {
- dashboardPanels = []interface{}{
- map[string]interface{}{
- "id": 1,
- "datasource": map[string]interface{}{
- "uid": "ds1",
- },
- "targets": []interface{}{
- map[string]interface{}{
- "datasource": map[string]interface{}{
- "type": "mysql",
- "uid": "ds1",
- },
- "refId": "A",
- },
- map[string]interface{}{
- "datasource": map[string]interface{}{
- "type": "prometheus",
- "uid": "ds2",
- },
- "refId": "B",
- },
- },
- },
- map[string]interface{}{
- "id": 2,
- "datasource": map[string]interface{}{
- "uid": "ds3",
- },
- "targets": []interface{}{
- map[string]interface{}{
- "datasource": map[string]interface{}{
- "type": "mysql",
- "uid": "ds3",
- },
- "refId": "C",
- },
- },
- },
- }
- }
-
- cmd := models.SaveDashboardCommand{
- OrgId: orgId,
- FolderId: folderId,
- IsFolder: isFolder,
- Dashboard: simplejson.NewFromAny(map[string]interface{}{
- "id": nil,
- "title": title,
- "tags": tags,
- "panels": dashboardPanels,
- "templating": map[string]interface{}{
- "list": templateVars,
- },
- "time": map[string]interface{}{
- "from": "2022-09-01T00:00:00.000Z",
- "to": "2022-09-01T12:00:00.000Z",
- },
- }),
- }
- dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
- require.NoError(t, err)
- require.NotNil(t, dash)
- dash.Data.Set("id", dash.Id)
- dash.Data.Set("uid", dash.Uid)
- return dash
-}
-
func TestPublicDashboardServiceImpl_getSafeIntervalAndMaxDataPoints(t *testing.T) {
type args struct {
reqDTO PublicDashboardQueryDTO
@@ -596,36 +566,6 @@ func TestDashboardEnabledChanged(t *testing.T) {
})
}
-func CreateDatasource(dsType string, uid string) struct {
- Type *string `json:"type,omitempty"`
- Uid *string `json:"uid,omitempty"`
-} {
- return struct {
- Type *string `json:"type,omitempty"`
- Uid *string `json:"uid,omitempty"`
- }{
- Type: &dsType,
- Uid: &uid,
- }
-}
-
-func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations []DashAnnotation) *models.Dashboard {
- type annotationsDto struct {
- List []DashAnnotation `json:"list"`
- }
- annos := annotationsDto{}
- annos.List = annotations
- annoJSON, err := json.Marshal(annos)
- require.NoError(t, err)
-
- dashAnnos, err := simplejson.NewJson(annoJSON)
- require.NoError(t, err)
-
- dash.Data.Set("annotations", dashAnnos)
-
- return dash
-}
-
func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
type args struct {
ctx context.Context
@@ -962,3 +902,108 @@ func TestPublicDashboardServiceImpl_NewPublicDashboardAccessToken(t *testing.T)
})
}
}
+
+func CreateDatasource(dsType string, uid string) struct {
+ Type *string `json:"type,omitempty"`
+ Uid *string `json:"uid,omitempty"`
+} {
+ return struct {
+ Type *string `json:"type,omitempty"`
+ Uid *string `json:"uid,omitempty"`
+ }{
+ Type: &dsType,
+ Uid: &uid,
+ }
+}
+
+func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations []DashAnnotation) *models.Dashboard {
+ type annotationsDto struct {
+ List []DashAnnotation `json:"list"`
+ }
+ annos := annotationsDto{}
+ annos.List = annotations
+ annoJSON, err := json.Marshal(annos)
+ require.NoError(t, err)
+
+ dashAnnos, err := simplejson.NewJson(annoJSON)
+ require.NoError(t, err)
+
+ dash.Data.Set("annotations", dashAnnos)
+
+ return dash
+}
+
+func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
+ folderId int64, isFolder bool, templateVars []map[string]interface{}, customPanels []interface{}, tags ...interface{}) *models.Dashboard {
+ t.Helper()
+
+ var dashboardPanels []interface{}
+ if customPanels != nil {
+ dashboardPanels = customPanels
+ } else {
+ dashboardPanels = []interface{}{
+ map[string]interface{}{
+ "id": 1,
+ "datasource": map[string]interface{}{
+ "uid": "ds1",
+ },
+ "targets": []interface{}{
+ map[string]interface{}{
+ "datasource": map[string]interface{}{
+ "type": "mysql",
+ "uid": "ds1",
+ },
+ "refId": "A",
+ },
+ map[string]interface{}{
+ "datasource": map[string]interface{}{
+ "type": "prometheus",
+ "uid": "ds2",
+ },
+ "refId": "B",
+ },
+ },
+ },
+ map[string]interface{}{
+ "id": 2,
+ "datasource": map[string]interface{}{
+ "uid": "ds3",
+ },
+ "targets": []interface{}{
+ map[string]interface{}{
+ "datasource": map[string]interface{}{
+ "type": "mysql",
+ "uid": "ds3",
+ },
+ "refId": "C",
+ },
+ },
+ },
+ }
+ }
+
+ cmd := models.SaveDashboardCommand{
+ OrgId: orgId,
+ FolderId: folderId,
+ IsFolder: isFolder,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "id": nil,
+ "title": title,
+ "tags": tags,
+ "panels": dashboardPanels,
+ "templating": map[string]interface{}{
+ "list": templateVars,
+ },
+ "time": map[string]interface{}{
+ "from": "2022-09-01T00:00:00.000Z",
+ "to": "2022-09-01T12:00:00.000Z",
+ },
+ }),
+ }
+ dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
+ require.NoError(t, err)
+ require.NotNil(t, dash)
+ dash.Data.Set("id", dash.Id)
+ dash.Data.Set("uid", dash.Uid)
+ return dash
+}
diff --git a/pkg/services/publicdashboards/validation/validation.go b/pkg/services/publicdashboards/validation/validation.go
index 5eda57dfcc4..d0b4936625e 100644
--- a/pkg/services/publicdashboards/validation/validation.go
+++ b/pkg/services/publicdashboards/validation/validation.go
@@ -7,7 +7,7 @@ import (
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
)
-func ValidateSavePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dashboard) error {
+func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dashboard) error {
if hasTemplateVariables(dashboard) {
return ErrPublicDashboardHasTemplateVariables
}
diff --git a/pkg/services/publicdashboards/validation/validation_test.go b/pkg/services/publicdashboards/validation/validation_test.go
index 5d189f99e9d..1273ca3191f 100644
--- a/pkg/services/publicdashboards/validation/validation_test.go
+++ b/pkg/services/publicdashboards/validation/validation_test.go
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
)
-func TestValidateSavePublicDashboard(t *testing.T) {
+func TestValidatePublicDashboard(t *testing.T) {
t.Run("Returns validation error when dashboard has template variables", func(t *testing.T) {
templateVars := []byte(`{
"templating": {
@@ -24,7 +24,7 @@ func TestValidateSavePublicDashboard(t *testing.T) {
dashboard := models.NewDashboardFromJson(dashboardData)
dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil}
- err := ValidateSavePublicDashboard(dto, dashboard)
+ err := ValidatePublicDashboard(dto, dashboard)
require.ErrorContains(t, err, ErrPublicDashboardHasTemplateVariables.Reason)
})
@@ -38,7 +38,7 @@ func TestValidateSavePublicDashboard(t *testing.T) {
dashboard := models.NewDashboardFromJson(dashboardData)
dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil}
- err := ValidateSavePublicDashboard(dto, dashboard)
+ err := ValidatePublicDashboard(dto, dashboard)
require.NoError(t, err)
})
}
diff --git a/public/app/features/dashboard/api/publicDashboardApi.ts b/public/app/features/dashboard/api/publicDashboardApi.ts
index 6c4b6ca625d..460739f7581 100644
--- a/public/app/features/dashboard/api/publicDashboardApi.ts
+++ b/public/app/features/dashboard/api/publicDashboardApi.ts
@@ -35,10 +35,10 @@ const getConfigError = (err: { status: number }) => ({ error: err.status !== 404
export const publicDashboardApi = createApi({
reducerPath: 'publicDashboardApi',
baseQuery: retry(backendSrvBaseQuery({ baseUrl: '/api/dashboards' }), { maxRetries: 0 }),
- tagTypes: ['Config', 'PublicDashboards'],
+ tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard'],
keepUnusedDataFor: 0,
endpoints: (builder) => ({
- getConfig: builder.query({
+ getPublicDashboard: builder.query({
query: (dashboardUid) => ({
url: `/uid/${dashboardUid}/public-dashboards`,
manageError: getConfigError,
@@ -53,9 +53,9 @@ export const publicDashboardApi = createApi({
dispatch(notifyApp(createErrorNotification(customError?.error?.data?.message)));
}
},
- providesTags: ['Config'],
+ providesTags: ['PublicDashboard'],
}),
- saveConfig: builder.mutation({
+ createPublicDashboard: builder.mutation({
query: (params) => ({
url: `/uid/${params.dashboard.uid}/public-dashboards`,
method: 'POST',
@@ -63,21 +63,42 @@ export const publicDashboardApi = createApi({
}),
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
const { data } = await queryFulfilled;
- dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved')));
+ dispatch(notifyApp(createSuccessNotification('Public dashboard created!')));
// Update runtime meta flag
dashboard.updateMeta({
+ hasPublicDashboard: true,
publicDashboardUid: data.uid,
publicDashboardEnabled: data.isEnabled,
});
},
- invalidatesTags: ['Config'],
+ invalidatesTags: ['PublicDashboard'],
+ }),
+ updatePublicDashboard: builder.mutation({
+ query: (params) => ({
+ url: `/uid/${params.dashboard.uid}/public-dashboards/${params.payload.uid}`,
+ method: 'PUT',
+ data: params.payload,
+ }),
+ extraOptions: { maxRetries: 0 },
+ async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
+ const { data } = await queryFulfilled;
+ dispatch(notifyApp(createSuccessNotification('Public dashboard updated!')));
+
+ // Update runtime meta flag
+ dashboard.updateMeta({
+ hasPublicDashboard: true,
+ publicDashboardUid: data.uid,
+ publicDashboardEnabled: data.isEnabled,
+ });
+ },
+ invalidatesTags: ['PublicDashboard'],
}),
listPublicDashboards: builder.query({
query: () => ({
url: '/public-dashboards',
}),
- providesTags: ['PublicDashboards'],
+ providesTags: ['AuditTablePublicDashboard'],
}),
deletePublicDashboard: builder.mutation({
query: (params) => ({
@@ -97,14 +118,15 @@ export const publicDashboardApi = createApi({
)
);
},
- invalidatesTags: ['PublicDashboards'],
+ invalidatesTags: ['AuditTablePublicDashboard'],
}),
}),
});
export const {
- useGetConfigQuery,
- useSaveConfigMutation,
+ useGetPublicDashboardQuery,
+ useCreatePublicDashboardMutation,
+ useUpdatePublicDashboardMutation,
useDeletePublicDashboardMutation,
useListPublicDashboardsQuery,
} = publicDashboardApi;
diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx
index 06a3fe59eef..15cdbec7a7d 100644
--- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx
+++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx
@@ -17,20 +17,7 @@ import { configureStore } from 'app/store/configureStore';
import { ShareModal } from '../ShareModal';
-const server = setupServer(
- rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (_, res, ctx) => {
- return res(
- ctx.status(200),
- ctx.json({
- isEnabled: false,
- annotationsEnabled: false,
- uid: undefined,
- dashboardUid: undefined,
- accessToken: 'an-access-token',
- })
- );
- })
-);
+const server = setupServer();
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
@@ -147,6 +134,7 @@ describe('SharePublic', () => {
expect(screen.getByText('2022-08-30 00:00:00 to 2022-09-04 01:59:59')).toBeInTheDocument();
});
it('when modal is opened, then loader spinner appears and inputs are disabled', async () => {
+ mockDashboard.meta.hasPublicDashboard = true;
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
@@ -158,6 +146,7 @@ describe('SharePublic', () => {
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
});
it('when fetch errors happen, then all inputs remain disabled', async () => {
+ mockDashboard.meta.hasPublicDashboard = true;
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(ctx.status(500));
@@ -165,7 +154,7 @@ describe('SharePublic', () => {
);
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
- await waitForElementToBeRemoved(screen.getByTestId('Spinner'), { timeout: 7000 });
+ await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
@@ -178,13 +167,16 @@ describe('SharePublic', () => {
});
describe('SharePublic - New config setup', () => {
+ beforeEach(() => {
+ mockDashboard.meta.hasPublicDashboard = false;
+ });
it('when modal is opened, then save button is disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
});
- it('when fetch is done, then loader spinner is gone, inputs are enabled and save button is disabled', async () => {
+ it('when fetch is done, then no loader spinner appears, inputs are enabled and save button is disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
- await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
+ expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeEnabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeEnabled();
@@ -196,7 +188,7 @@ describe('SharePublic - New config setup', () => {
});
it('when checkboxes are filled, then save button remains disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
- await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
+ expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
@@ -206,7 +198,7 @@ describe('SharePublic - New config setup', () => {
});
it('when checkboxes and switch are filled, then save button is enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
- await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
+ expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
@@ -219,6 +211,7 @@ describe('SharePublic - New config setup', () => {
describe('SharePublic - Already persisted', () => {
beforeEach(() => {
+ mockDashboard.meta.hasPublicDashboard = true;
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(
diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx
index 36c801ea48a..4af0c3d7ff8 100644
--- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx
+++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx
@@ -6,7 +6,11 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime/src';
import { Alert, Button, ClipboardButton, Field, HorizontalGroup, Input, useStyles2, Spinner } from '@grafana/ui/src';
import { contextSrv } from 'app/core/services/context_srv';
-import { useGetConfigQuery, useSaveConfigMutation } from 'app/features/dashboard/api/publicDashboardApi';
+import {
+ useGetPublicDashboardQuery,
+ useCreatePublicDashboardMutation,
+ useUpdatePublicDashboardMutation,
+} from 'app/features/dashboard/api/publicDashboardApi';
import { AcknowledgeCheckboxes } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes';
import { Configuration } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration';
import { Description } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Description';
@@ -27,13 +31,19 @@ export const SharePublicDashboard = (props: Props) => {
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
const styles = useStyles2(getStyles);
+ const [hasPublicDashboard, setHasPublicDashboard] = useState(props.dashboard.meta.hasPublicDashboard);
+
const {
isLoading: isFetchingLoading,
data: publicDashboard,
isError: isFetchingError,
- } = useGetConfigQuery(props.dashboard.uid);
+ } = useGetPublicDashboardQuery(props.dashboard.uid, {
+ // if we don't have a public dashboard, don't try to load public dashboard
+ skip: !hasPublicDashboard,
+ });
- const [saveConfig, { isLoading: isSaveLoading }] = useSaveConfigMutation();
+ const [createPublicDashboard, { isLoading: isSaveLoading }] = useCreatePublicDashboardMutation();
+ const [updatePublicDashboard, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
const [acknowledgements, setAcknowledgements] = useState({
public: false,
@@ -63,7 +73,7 @@ export const SharePublicDashboard = (props: Props) => {
setEnabledSwitch((prevState) => ({ ...prevState, isEnabled: !!publicDashboard?.isEnabled }));
}, [publicDashboard]);
- const isLoading = isFetchingLoading || isSaveLoading;
+ const isLoading = isFetchingLoading || isSaveLoading || isUpdateLoading;
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
const isSaveEnabled = useMemo(
@@ -77,13 +87,23 @@ export const SharePublicDashboard = (props: Props) => {
[hasWritePermissions, acknowledged, props.dashboard, isLoading, isFetchingError, enabledSwitch, publicDashboard]
);
- const onSavePublicConfig = () => {
+ const onSavePublicConfig = async () => {
reportInteraction('grafana_dashboards_public_create_clicked');
- saveConfig({
+ const req = {
dashboard: props.dashboard,
payload: { ...publicDashboard!, isEnabled: enabledSwitch.isEnabled, annotationsEnabled },
- });
+ };
+
+ // create or update based on whether we have existing uid
+
+ if (hasPublicDashboard) {
+ await updatePublicDashboard(req).unwrap();
+ setHasPublicDashboard(true);
+ } else {
+ await createPublicDashboard(req).unwrap();
+ setHasPublicDashboard(true);
+ }
};
const onAcknowledge = (field: string, checked: boolean) => {
diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts
index 2958f366dbe..1db125255cc 100644
--- a/public/app/types/dashboard.ts
+++ b/public/app/types/dashboard.ts
@@ -44,6 +44,7 @@ export interface DashboardMeta {
publicDashboardAccessToken?: string;
publicDashboardUid?: string;
publicDashboardEnabled?: boolean;
+ hasPublicDashboard?: boolean;
dashboardNotFound?: boolean;
}
From 376f4b0cc76d24dbfab8fc13cad7ec30abbcd2be Mon Sep 17 00:00:00 2001
From: Levente Balogh
Date: Thu, 3 Nov 2022 21:19:42 +0100
Subject: [PATCH 026/926] Navigation: Add `pluginId` to standalone plugin page
NavLinks (#57769)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(Navigation): add `pluginId` to NavLink and override sibling navlinks with the same URL
* test replacing page from plugin
* chore: fix go lint issues
* fix(NavLink): change `PluginId` to `PluginID`
Co-authored-by: Torkel Ödegaard
* fix(NavLink): make the `PluginId` -> `PluginID` change everywhere
* chore(navModel.ts): update explanatory comment for `pluginId`
Co-authored-by: Miklós Tolnai
Co-authored-by: Torkel Ödegaard
---
packages/grafana-data/src/types/navModel.ts | 2 +
pkg/services/navtree/models.go | 1 +
pkg/services/navtree/navtreeimpl/applinks.go | 32 ++++++++++++---
.../navtree/navtreeimpl/applinks_test.go | 41 ++++++++++++++++++-
4 files changed, 70 insertions(+), 6 deletions(-)
diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts
index 2b2e1ceece6..dce924e8d9b 100644
--- a/packages/grafana-data/src/types/navModel.ts
+++ b/packages/grafana-data/src/types/navModel.ts
@@ -26,6 +26,8 @@ export interface NavLinkDTO {
children?: NavLinkDTO[];
highlightText?: string;
emptyMessageId?: string;
+ // The ID of the plugin that registered the page (in case it was registered by a plugin, otherwise left empty)
+ pluginId?: string;
}
export interface NavModelItem extends NavLinkDTO {
diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go
index dc579ffbe29..00358200547 100644
--- a/pkg/services/navtree/models.go
+++ b/pkg/services/navtree/models.go
@@ -67,6 +67,7 @@ type NavLink struct {
HighlightText string `json:"highlightText,omitempty"`
HighlightID string `json:"highlightId,omitempty"`
EmptyMessageId string `json:"emptyMessageId,omitempty"`
+ PluginID string `json:"pluginId,omitempty"` // (Optional) The ID of the plugin that registered nav link (e.g. as a standalone plugin page)
}
func (node *NavLink) Sort() {
diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go
index e8bab53a813..b67bd8f0208 100644
--- a/pkg/services/navtree/navtreeimpl/applinks.go
+++ b/pkg/services/navtree/navtreeimpl/applinks.go
@@ -72,6 +72,7 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo
Section: navtree.NavSectionPlugin,
SortWeight: navtree.WeightPlugin,
IsSection: true,
+ PluginID: plugin.ID,
}
if topNavEnabled {
@@ -87,8 +88,9 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo
if include.Type == "page" && include.AddToNav {
link := &navtree.NavLink{
- Text: include.Name,
- Icon: include.Icon,
+ Text: include.Name,
+ Icon: include.Icon,
+ PluginID: plugin.ID,
}
if len(include.Path) > 0 {
@@ -100,11 +102,30 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo
link.Url = s.cfg.AppSubURL + "/plugins/" + plugin.ID + "/page/" + include.Slug
}
+ // Register standalone plugin pages to certain sections using the Grafana config
if pathConfig, ok := s.navigationAppPathConfig[include.Path]; ok {
if sectionForPage := treeRoot.FindById(pathConfig.SectionID); sectionForPage != nil {
link.Id = "standalone-plugin-page-" + include.Path
link.SortWeight = pathConfig.SortWeight
- sectionForPage.Children = append(sectionForPage.Children, link)
+
+ // Check if the section already has a page with the same URL, and in that case override it
+ // (This only happens if it is explicitly set by `navigation.app_standalone_pages` in the INI config)
+ isOverridingCorePage := false
+ for _, child := range sectionForPage.Children {
+ if child.Url == link.Url {
+ child.Id = link.Id
+ child.SortWeight = link.SortWeight
+ child.PluginID = link.PluginID
+ child.Children = []*navtree.NavLink{}
+ isOverridingCorePage = true
+ break
+ }
+ }
+
+ // Append the page to the section
+ if !isOverridingCorePage {
+ sectionForPage.Children = append(sectionForPage.Children, link)
+ }
}
} else {
appLink.Children = append(appLink.Children, link)
@@ -115,8 +136,9 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo
dboardURL := include.DashboardURLPath()
if dboardURL != "" {
link := &navtree.NavLink{
- Url: path.Join(s.cfg.AppSubURL, dboardURL),
- Text: include.Name,
+ Url: path.Join(s.cfg.AppSubURL, dboardURL),
+ Text: include.Name,
+ PluginID: plugin.ID,
}
appLink.Children = append(appLink.Children, link)
}
diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go
index d6397d680d7..82b791090ad 100644
--- a/pkg/services/navtree/navtreeimpl/applinks_test.go
+++ b/pkg/services/navtree/navtreeimpl/applinks_test.go
@@ -65,9 +65,27 @@ func TestAddAppLinks(t *testing.T) {
},
}
+ testApp3 := plugins.PluginDTO{
+ JSONData: plugins.JSONData{
+ ID: "test-app3",
+ Name: "Test app3 name",
+ Type: plugins.App,
+ Includes: []*plugins.Includes{
+ {
+ Name: "Hello",
+ Path: "/connections/connect-data",
+ Type: "page",
+ AddToNav: true,
+ DefaultNav: true,
+ },
+ },
+ },
+ }
+
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
testApp1.ID: {ID: 0, OrgID: 1, PluginID: testApp1.ID, PluginVersion: "1.0.0", Enabled: true},
testApp2.ID: {ID: 0, OrgID: 1, PluginID: testApp2.ID, PluginVersion: "1.0.0", Enabled: true},
+ testApp3.ID: {ID: 0, OrgID: 1, PluginID: testApp3.ID, PluginVersion: "1.0.0", Enabled: true},
}}
service := ServiceImpl{
@@ -77,7 +95,7 @@ func TestAddAppLinks(t *testing.T) {
pluginSettings: &pluginSettings,
features: featuremgmt.WithFeatures(),
pluginStore: plugins.FakePluginStore{
- PluginList: []plugins.PluginDTO{testApp1, testApp2},
+ PluginList: []plugins.PluginDTO{testApp1, testApp2, testApp3},
},
}
@@ -172,6 +190,27 @@ func TestAddAppLinks(t *testing.T) {
require.Equal(t, "Test app2 name", treeRoot.Children[0].Children[0].Text)
require.Equal(t, "Test app1 name", treeRoot.Children[0].Children[1].Text)
})
+
+ t.Run("Should replace page from plugin", func(t *testing.T) {
+ service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav, featuremgmt.FlagDataConnectionsConsole)
+ service.navigationAppPathConfig = map[string]NavigationAppConfig{
+ "/connections/connect-data": {SectionID: "connections"},
+ }
+
+ treeRoot := navtree.NavTreeRoot{}
+ treeRoot.AddSection(service.buildDataConnectionsNavLink(reqCtx))
+ require.Equal(t, "Connections", treeRoot.Children[0].Text)
+ require.Equal(t, "Connect Data", treeRoot.Children[0].Children[1].Text)
+ require.Equal(t, "connections-connect-data", treeRoot.Children[0].Children[1].Id)
+ require.Equal(t, "", treeRoot.Children[0].Children[1].PluginID)
+
+ err := service.addAppLinks(&treeRoot, reqCtx)
+ require.NoError(t, err)
+ require.Equal(t, "Connections", treeRoot.Children[0].Text)
+ require.Equal(t, "Connect Data", treeRoot.Children[0].Children[1].Text)
+ require.Equal(t, "standalone-plugin-page-/connections/connect-data", treeRoot.Children[0].Children[1].Id)
+ require.Equal(t, "test-app3", treeRoot.Children[0].Children[1].PluginID)
+ })
}
func TestReadingNavigationSettings(t *testing.T) {
From 3dfa49b37654469a53952934c10317ab781636ea Mon Sep 17 00:00:00 2001
From: Ryan McKinley
Date: Thu, 3 Nov 2022 16:35:20 -0700
Subject: [PATCH 027/926] Playlist: cleanup object store implementation
(#58201)
---
pkg/services/export/object_store.go | 4 +-
.../playlist/playlistimpl/object_store.go | 47 ++++++++++---------
pkg/services/store/auth.go | 6 +++
public/app/features/playlist/PlaylistForm.tsx | 17 +++++--
4 files changed, 48 insertions(+), 26 deletions(-)
diff --git a/pkg/services/export/object_store.go b/pkg/services/export/object_store.go
index 360021ed8ea..990c4aca0c3 100644
--- a/pkg/services/export/object_store.go
+++ b/pkg/services/export/object_store.go
@@ -113,6 +113,8 @@ func (e *objectStoreJob) start() {
e.status.Status = "error: " + err.Error()
return
}
+ e.status.Last = fmt.Sprintf("export %d dashboards", len(dashInfo))
+ e.broadcaster(e.status)
for _, dash := range dashInfo {
rowUser.OrgID = dash.OrgID
@@ -261,7 +263,7 @@ func (e *objectStoreJob) getDashboards(ctx context.Context) ([]dashInfo, error)
e.broadcaster(e.status)
dash := make([]dashInfo, 0)
- rows, err := e.sess.Query(ctx, "SELECT org_id,uid,data,updated_by FROM dashboard WHERE is_folder=0")
+ rows, err := e.sess.Query(ctx, "SELECT org_id,uid,data,updated_by FROM dashboard WHERE is_folder=false")
if err != nil {
return nil, err
}
diff --git a/pkg/services/playlist/playlistimpl/object_store.go b/pkg/services/playlist/playlistimpl/object_store.go
index 45a3903eec1..ad7b867c0a0 100644
--- a/pkg/services/playlist/playlistimpl/object_store.go
+++ b/pkg/services/playlist/playlistimpl/object_store.go
@@ -27,7 +27,12 @@ type objectStoreImpl struct {
var _ playlist.Service = &objectStoreImpl{}
func (s *objectStoreImpl) sync() {
- rows, err := s.sess.Query(context.Background(), "SELECT org_id,uid FROM playlist ORDER BY org_id asc")
+ type Info struct {
+ OrgID int64 `db:"org_id"`
+ UID string `db:"uid"`
+ }
+ results := []Info{}
+ err := s.sess.Select(context.Background(), &results, "SELECT org_id,uid FROM playlist ORDER BY org_id asc")
if err != nil {
fmt.Printf("error loading playlists")
return
@@ -35,22 +40,15 @@ func (s *objectStoreImpl) sync() {
// Change the org_id with each row
rowUser := &user.SignedInUser{
- Login: "?",
- OrgID: 0, // gets filled in from each row
- UserID: 0,
+ OrgID: 0, // gets filled in from each row
+ UserID: 0, // Admin user
+ IsGrafanaAdmin: true,
}
ctx := objectstore.ContextWithUser(context.Background(), rowUser)
- uid := ""
- for rows.Next() {
- err = rows.Scan(&rowUser.OrgID, &uid)
- if err != nil {
- fmt.Printf("error loading playlists: %v", err)
- return
- }
-
+ for _, info := range results {
dto, err := s.sqlimpl.Get(ctx, &playlist.GetPlaylistByUidQuery{
- OrgId: rowUser.OrgID,
- UID: uid,
+ OrgId: info.OrgID,
+ UID: info.UID,
})
if err != nil {
fmt.Printf("error loading playlist: %v", err)
@@ -59,8 +57,10 @@ func (s *objectStoreImpl) sync() {
body, _ := json.Marshal(dto)
_, _ = s.objectstore.Write(ctx, &object.WriteObjectRequest{
GRN: &object.GRN{
- UID: uid,
- Kind: models.StandardKindPlaylist,
+ TenantId: info.OrgID,
+ UID: info.UID,
+ Kind: models.StandardKindPlaylist,
+ Scope: models.ObjectStoreScopeEntity,
},
Body: body,
})
@@ -98,8 +98,9 @@ func (s *objectStoreImpl) Update(ctx context.Context, cmd *playlist.UpdatePlayli
}
_, err = s.objectstore.Write(ctx, &object.WriteObjectRequest{
GRN: &object.GRN{
- UID: rsp.Uid,
- Kind: models.StandardKindPlaylist,
+ UID: rsp.Uid,
+ Kind: models.StandardKindPlaylist,
+ Scope: models.ObjectStoreScopeEntity,
},
Body: body,
})
@@ -115,8 +116,9 @@ func (s *objectStoreImpl) Delete(ctx context.Context, cmd *playlist.DeletePlayli
if err == nil {
_, err = s.objectstore.Delete(ctx, &object.DeleteObjectRequest{
GRN: &object.GRN{
- UID: cmd.UID,
- Kind: models.StandardKindPlaylist,
+ UID: cmd.UID,
+ Kind: models.StandardKindPlaylist,
+ Scope: models.ObjectStoreScopeEntity,
},
})
if err != nil {
@@ -146,8 +148,9 @@ func (s *objectStoreImpl) GetWithoutItems(ctx context.Context, q *playlist.GetPl
func (s *objectStoreImpl) Get(ctx context.Context, q *playlist.GetPlaylistByUidQuery) (*playlist.PlaylistDTO, error) {
rsp, err := s.objectstore.Read(ctx, &object.ReadObjectRequest{
GRN: &object.GRN{
- UID: q.UID,
- Kind: models.StandardKindPlaylist,
+ UID: q.UID,
+ Kind: models.StandardKindPlaylist,
+ Scope: models.ObjectStoreScopeEntity,
},
WithBody: true,
})
diff --git a/pkg/services/store/auth.go b/pkg/services/store/auth.go
index d7258280f89..b9d7cb8bf9b 100644
--- a/pkg/services/store/auth.go
+++ b/pkg/services/store/auth.go
@@ -44,6 +44,12 @@ func GetUserIDString(user *user.SignedInUser) string {
if user == nil {
return ""
}
+ if user.IsAnonymous {
+ return "anon"
+ }
+ if user.ApiKeyID > 0 {
+ return fmt.Sprintf("key:%d", user.UserID)
+ }
if user.IsRealUser() {
return fmt.Sprintf("user:%d:%s", user.UserID, user.Login)
}
diff --git a/public/app/features/playlist/PlaylistForm.tsx b/public/app/features/playlist/PlaylistForm.tsx
index c930acf4ef1..9e667d5183f 100644
--- a/public/app/features/playlist/PlaylistForm.tsx
+++ b/public/app/features/playlist/PlaylistForm.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo } from 'react';
+import React, { useMemo, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
@@ -18,6 +18,7 @@ interface Props {
}
export const PlaylistForm = ({ onSubmit, playlist }: Props) => {
+ const [saving, setSaving] = useState(false);
const { name, interval, items: propItems } = playlist;
const tagOptions = useMemo(() => {
return () => getGrafanaSearcher().tags({ kind: ['dashboard'] });
@@ -25,9 +26,14 @@ export const PlaylistForm = ({ onSubmit, playlist }: Props) => {
const { items, addById, addByTag, deleteItem, moveItem } = usePlaylistItems(propItems);
+ const doSubmit = (list: Playlist) => {
+ setSaving(true);
+ onSubmit({ ...list, items });
+ };
+
return (
-
-