= ({ dashboard, onNew, onEd
{dataSourceSrv.getInstanceSettings(annotation.datasource)?.name || annotation.datasource?.uid}
|
- {idx !== 0 && (
- onMove(idx, -1)}
- />
- )}
+ {idx !== 0 && onMove(idx, -1)} />}
|
{dashboard.annotations.list.length > 1 && idx !== dashboard.annotations.list.length - 1 ? (
- onMove(idx, 1)}
- />
+ onMove(idx, 1)} />
) : null}
|
diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx
new file mode 100644
index 00000000000..a694be7c3f6
--- /dev/null
+++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx
@@ -0,0 +1,51 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { Provider } from 'react-redux';
+import { BrowserRouter } from 'react-router-dom';
+
+import { locationService, setBackendSrv } from '@grafana/runtime';
+import { configureStore } from 'app/store/configureStore';
+
+import { DashboardModel } from '../../state';
+
+import { DashboardSettings } from './DashboardSettings';
+
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ locationService: {
+ partial: jest.fn(),
+ },
+}));
+
+setBackendSrv({
+ get: jest.fn().mockResolvedValue({}),
+} as any);
+
+describe('DashboardSettings', () => {
+ it('pressing escape navigates away correctly', async () => {
+ jest.spyOn(locationService, 'partial');
+ const dashboard = new DashboardModel(
+ {
+ title: 'Foo',
+ },
+ {
+ folderId: 1,
+ }
+ );
+ const store = configureStore();
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('Foo / Settings')).toBeInTheDocument();
+
+ await userEvent.keyboard('{Escape}');
+
+ expect(locationService.partial).toHaveBeenCalledWith({ editview: null });
+ });
+});
diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx
index d8494fe6c59..363eae8acc6 100644
--- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx
+++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx
@@ -49,7 +49,13 @@ const MakeEditable = (props: { onMakeEditable: () => any }) => (
export function DashboardSettings({ dashboard, editview }: Props) {
const ref = useRef(null);
- const { overlayProps } = useOverlay({}, ref);
+ const { overlayProps } = useOverlay(
+ {
+ isOpen: true,
+ onClose,
+ },
+ ref
+ );
const { dialogProps } = useDialog(
{
'aria-label': 'Dashboard settings',
diff --git a/public/app/features/dashboard/components/LinksSettings/LinkSettingsList.tsx b/public/app/features/dashboard/components/LinksSettings/LinkSettingsList.tsx
index 4db46c6143f..5da57c62171 100644
--- a/public/app/features/dashboard/components/LinksSettings/LinkSettingsList.tsx
+++ b/public/app/features/dashboard/components/LinksSettings/LinkSettingsList.tsx
@@ -73,27 +73,15 @@ export const LinkSettingsList: React.FC = ({ dashboard, o
|
- {idx !== 0 && (
- moveLink(idx, -1)}
- />
- )}
+ {idx !== 0 && moveLink(idx, -1)} />}
|
{links.length > 1 && idx !== links.length - 1 ? (
- moveLink(idx, 1)}
- />
+ moveLink(idx, 1)} />
) : null}
|
- duplicateLink(link, idx)} />
+ duplicateLink(link, idx)} />
|
{
this.setState({ search: '' });
}}
@@ -251,7 +250,6 @@ class UnThemedTransformationsEditor extends React.PureComponent {
this.setState({ showPicker: false });
}}
diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx
index 83b73403dc8..7a450f373a0 100644
--- a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx
+++ b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx
@@ -18,6 +18,11 @@ const mockDS = mockDataSource({
type: DataSourceType.Alertmanager,
});
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ reportInteraction: jest.fn(),
+}));
+
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
return {
getDataSourceSrv: () => ({
diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.tsx
index 850c46f9ee8..df3efab5bde 100644
--- a/public/app/features/explore/RichHistory/RichHistoryCard.tsx
+++ b/public/app/features/explore/RichHistory/RichHistoryCard.tsx
@@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme, DataSourceApi, DataQuery } from '@grafana/data';
-import { getDataSourceSrv } from '@grafana/runtime';
+import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import appEvents from 'app/core/app_events';
@@ -176,12 +176,17 @@ export function RichHistoryCard(props: Props) {
const onRunQuery = async () => {
const queriesToRun = query.queries;
- if (query.datasourceName !== datasourceInstance?.name) {
+ const differentDataSource = query.datasourceName !== datasourceInstance?.name;
+ if (differentDataSource) {
await changeDatasource(exploreId, query.datasourceName, { importQueries: true });
setQueries(exploreId, queriesToRun);
} else {
setQueries(exploreId, queriesToRun);
}
+ reportInteraction('grafana_explore_query_history_run', {
+ queryHistoryEnabled: config.queryHistoryEnabled,
+ differentDataSource,
+ });
};
const onCopyQuery = () => {
@@ -196,6 +201,14 @@ export function RichHistoryCard(props: Props) {
};
const onDeleteQuery = () => {
+ const performDelete = (queryId: string) => {
+ deleteHistoryItem(queryId);
+ dispatch(notifyApp(createSuccessNotification('Query deleted')));
+ reportInteraction('grafana_explore_query_history_deleted', {
+ queryHistoryEnabled: config.queryHistoryEnabled,
+ });
+ };
+
// For starred queries, we want confirmation. For non-starred, we don't.
if (query.starred) {
appEvents.publish(
@@ -204,20 +217,20 @@ export function RichHistoryCard(props: Props) {
text: 'Are you sure you want to permanently delete your starred query?',
yesText: 'Delete',
icon: 'trash-alt',
- onConfirm: () => {
- deleteHistoryItem(query.id);
- dispatch(notifyApp(createSuccessNotification('Query deleted')));
- },
+ onConfirm: () => performDelete(query.id),
})
);
} else {
- deleteHistoryItem(query.id);
- dispatch(notifyApp(createSuccessNotification('Query deleted')));
+ performDelete(query.id);
}
};
const onStarrQuery = () => {
starHistoryItem(query.id, !query.starred);
+ reportInteraction('grafana_explore_query_history_starred', {
+ queryHistoryEnabled: config.queryHistoryEnabled,
+ newValue: !query.starred,
+ });
};
const toggleActiveUpdateComment = () => setActiveUpdateComment(!activeUpdateComment);
@@ -225,6 +238,9 @@ export function RichHistoryCard(props: Props) {
const onUpdateComment = () => {
commentHistoryItem(query.id, comment);
setActiveUpdateComment(false);
+ reportInteraction('grafana_explore_query_history_commented', {
+ queryHistoryEnabled: config.queryHistoryEnabled,
+ });
};
const onCancelUpdateComment = () => {
diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx
index dfe279eca66..84d1aca02a9 100644
--- a/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx
+++ b/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx
@@ -17,6 +17,7 @@ jest.mock('@grafana/runtime', () => ({
getList: () => [],
};
},
+ reportInteraction: jest.fn(),
}));
const setup = (propOverrides?: Partial) => {
diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx
index 099cea64304..72fb7c125f1 100644
--- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx
+++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx
@@ -2,6 +2,7 @@
import React, { useEffect, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
+import { config, reportInteraction } from '@grafana/runtime';
import { useTheme2 } from '@grafana/ui';
// Types
import { ExploreItemState, StoreState } from 'app/types';
@@ -86,6 +87,9 @@ export function RichHistoryContainer(props: Props) {
useEffect(() => {
initRichHistory();
+ reportInteraction('grafana_explore_query_history_opened', {
+ queryHistoryEnabled: config.queryHistoryEnabled,
+ });
}, [initRichHistory]);
if (!richHistorySettings) {
diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx
index e0434713f69..4cedf1e8e8f 100644
--- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx
+++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx
@@ -1,4 +1,4 @@
-import { mount } from 'enzyme';
+import { render } from '@testing-library/react';
import React from 'react';
import { SortOrder } from 'app/core/utils/richHistory';
@@ -44,27 +44,27 @@ const setup = (activeDatasourceOnly = false) => {
},
};
- const wrapper = mount();
- return wrapper;
+ const container = render();
+ return container;
};
describe('RichHistoryStarredTab', () => {
describe('sorter', () => {
it('should render sorter', () => {
- const wrapper = setup();
- expect(wrapper.find({ 'aria-label': 'Sort queries' })).toHaveLength(1);
+ const container = setup();
+ expect(container.queryByLabelText('Sort queries')).toBeInTheDocument();
});
});
describe('select datasource', () => {
it('should render select datasource if activeDatasourceOnly is false', () => {
- const wrapper = setup();
- expect(wrapper.find({ 'aria-label': 'Filter queries for data sources(s)' }).exists()).toBeTruthy();
+ const container = setup();
+ expect(container.queryByLabelText('Filter queries for data sources(s)')).toBeInTheDocument();
});
it('should not render select datasource if activeDatasourceOnly is true', () => {
- const wrapper = setup(true);
- expect(wrapper.find({ 'aria-label': 'Filter queries for data sources(s)' }).exists()).toBeFalsy();
+ const container = setup(true);
+ expect(container.queryByLabelText('Filter queries for data sources(s)')).not.toBeInTheDocument();
});
});
});
diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx
index 4133ce61da5..b2c28ebdf99 100644
--- a/public/app/features/explore/spec/queryHistory.test.tsx
+++ b/public/app/features/explore/spec/queryHistory.test.tsx
@@ -40,9 +40,13 @@ import {
const fetchMock = jest.fn();
const postMock = jest.fn();
const getMock = jest.fn();
+const reportInteractionMock = jest.fn();
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({ fetch: fetchMock, post: postMock, get: getMock }),
+ reportInteraction: (...args: object[]) => {
+ reportInteractionMock(...args);
+ },
}));
jest.mock('app/core/services/PreferencesService', () => ({
@@ -78,6 +82,7 @@ describe('Explore: Query History', () => {
fetchMock.mockClear();
postMock.mockClear();
getMock.mockClear();
+ reportInteractionMock.mockClear();
tearDown();
});
@@ -103,6 +108,11 @@ describe('Explore: Query History', () => {
// previously added query is in query history
await openQueryHistory();
await assertQueryHistoryExists(RAW_QUERY);
+
+ expect(reportInteractionMock).toBeCalledTimes(2);
+ expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_opened', {
+ queryHistoryEnabled: false,
+ });
});
it('adds recently added query if the query history panel is already open', async () => {
@@ -124,10 +134,7 @@ describe('Explore: Query History', () => {
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}']);
});
- /**
- * TODO: #47635 check why this test times out
- */
- it.skip('updates the state in both Explore panes', async () => {
+ it('updates the state in both Explore panes', async () => {
const urlParams = {
left: serializeStateToUrlParam({
datasource: 'loki',
@@ -156,10 +163,17 @@ describe('Explore: Query History', () => {
starQueryHistory(1, ExploreId.left);
await assertQueryHistoryIsStarred([false, true], ExploreId.left);
await assertQueryHistoryIsStarred([false, true], ExploreId.right);
+ expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_starred', {
+ queryHistoryEnabled: false,
+ newValue: true,
+ });
deleteQueryHistory(0, ExploreId.left);
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.left);
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.right);
+ expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_deleted', {
+ queryHistoryEnabled: false,
+ });
});
it('updates query history settings', async () => {
@@ -195,6 +209,9 @@ describe('Explore: Query History', () => {
await openQueryHistory();
expect(postMock).not.toBeCalledWith('/api/query-history/migrate', { queries: [] });
+ expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_opened', {
+ queryHistoryEnabled: false,
+ });
});
it('migrates query history from local storage', async () => {
@@ -222,6 +239,9 @@ describe('Explore: Query History', () => {
url: expect.stringMatching('/api/query-history/migrate'),
})
);
+ expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_opened', {
+ queryHistoryEnabled: true,
+ });
});
});
diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts
index 23aba3ee9e5..e0fe9e9ee19 100644
--- a/public/app/features/explore/state/history.ts
+++ b/public/app/features/explore/state/history.ts
@@ -1,7 +1,7 @@
import { AnyAction, createAction } from '@reduxjs/toolkit';
import { DataQuery, HistoryItem } from '@grafana/data';
-import { config } from '@grafana/runtime';
+import { config, logError } from '@grafana/runtime';
import { RICH_HISTORY_SETTING_KEYS } from 'app/core/history/richHistoryLocalStorageUtils';
import store from 'app/core/store';
import {
@@ -178,9 +178,10 @@ export const initRichHistory = (): ThunkResult => {
// the migration attempt happens only once per session, and the user is informed about the failure
// in a way that can help with potential investigation.
if (config.queryHistoryEnabled && !queriesMigrated && !migrationFailedDuringThisSession) {
- const migrationStatus = await migrateQueryHistoryFromLocalStorage();
- if (migrationStatus === LocalStorageMigrationStatus.Failed) {
+ const migrationResult = await migrateQueryHistoryFromLocalStorage();
+ if (migrationResult.status === LocalStorageMigrationStatus.Failed) {
dispatch(richHistoryMigrationFailedAction());
+ logError(migrationResult.error!, { explore: { event: 'QueryHistoryMigrationFailed' } });
} else {
store.set(RICH_HISTORY_SETTING_KEYS.migrated, true);
}
diff --git a/public/app/features/expressions/ExpressionDatasource.ts b/public/app/features/expressions/ExpressionDatasource.ts
index 096d8a988dc..5376e396377 100644
--- a/public/app/features/expressions/ExpressionDatasource.ts
+++ b/public/app/features/expressions/ExpressionDatasource.ts
@@ -43,7 +43,7 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend;
export const NotificationsPage = ({ navModel }: Props) => {
- if (!config.featureToggles.persistNotifications) {
- return null;
- }
-
return (
diff --git a/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts b/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts
index 4eadff44e59..05b5bf7a3bc 100644
--- a/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts
+++ b/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts
@@ -15,7 +15,7 @@ export class AnnotationsQueryRunner implements AnnotationQueryRunner {
return false;
}
- return !Boolean(datasource.annotationQuery && !datasource.annotations);
+ return Boolean(!datasource.annotationQuery || datasource.annotations);
}
run({ annotation, datasource, dashboard, range }: AnnotationQueryRunnerOptions): Observable {
diff --git a/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts b/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts
index 5d94911b9ab..f383897f5d9 100644
--- a/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts
+++ b/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts
@@ -2,6 +2,7 @@ import { from, Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AnnotationEvent, DataSourceApi } from '@grafana/data';
+import { shouldUseLegacyRunner } from 'app/features/annotations/standardAnnotationSupport';
import { AnnotationQueryRunner, AnnotationQueryRunnerOptions } from './types';
import { handleAnnotationQueryRunnerError } from './utils';
@@ -12,6 +13,10 @@ export class LegacyAnnotationQueryRunner implements AnnotationQueryRunner {
return false;
}
+ if (shouldUseLegacyRunner(datasource)) {
+ return true;
+ }
+
return Boolean(datasource.annotationQuery && !datasource.annotations);
}
diff --git a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.test.ts b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.test.ts
index f2d1c4eaf8c..744885d9526 100644
--- a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.test.ts
+++ b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.test.ts
@@ -2,7 +2,9 @@ import { lastValueFrom } from 'rxjs';
import { AlertState, getDefaultTimeRange, TimeRange } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv';
+import { disableRBAC, enableRBAC, grantUserPermissions } from 'app/features/alerting/unified/mocks';
import { Annotation } from 'app/features/alerting/unified/utils/constants';
+import { AccessControlAction } from 'app/types/accessControl';
import { PromAlertingRuleState, PromRuleDTO, PromRulesResponse, PromRuleType } from 'app/types/unified-alerting-dto';
import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
@@ -35,6 +37,10 @@ function getTestContext() {
describe('UnifiedAlertStatesWorker', () => {
const worker = new UnifiedAlertStatesWorker();
+ beforeAll(() => {
+ disableRBAC();
+ });
+
describe('when canWork is called with correct props', () => {
it('then it should return true', () => {
const options = getDefaultOptions();
@@ -200,3 +206,25 @@ describe('UnifiedAlertStatesWorker', () => {
});
});
});
+
+describe('UnifiedAlertStateWorker with RBAC', () => {
+ beforeAll(() => {
+ enableRBAC();
+ grantUserPermissions([]);
+ });
+
+ it('should not do work with insufficient permissions', () => {
+ const worker = new UnifiedAlertStatesWorker();
+ const options = getDefaultOptions();
+
+ expect(worker.canWork(options)).toBe(false);
+ });
+
+ it('should do work with correct permissions', () => {
+ grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]);
+ const workerWithPermissions = new UnifiedAlertStatesWorker();
+
+ const options = getDefaultOptions();
+ expect(workerWithPermissions.canWork(options)).toBe(true);
+ });
+});
diff --git a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts
index a9100ce3e60..335b1cd20ca 100644
--- a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts
+++ b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts
@@ -3,8 +3,10 @@ import { catchError, map } from 'rxjs/operators';
import { AlertState, AlertStateInfo } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
+import { contextSrv } from 'app/core/services/context_srv';
import { Annotation } from 'app/features/alerting/unified/utils/constants';
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
+import { AccessControlAction } from 'app/types';
import { PromAlertingRuleState, PromRulesResponse } from 'app/types/unified-alerting-dto';
import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorker, DashboardQueryRunnerWorkerResult } from './types';
@@ -29,6 +31,14 @@ export class UnifiedAlertStatesWorker implements DashboardQueryRunnerWorker {
return false;
}
+ const hasRuleReadPermission =
+ contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) &&
+ contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead);
+
+ if (!hasRuleReadPermission) {
+ return false;
+ }
+
return true;
}
diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts
index f7ceea18895..dcd7ef65c2a 100644
--- a/public/app/features/query/state/PanelQueryRunner.ts
+++ b/public/app/features/query/state/PanelQueryRunner.ts
@@ -236,9 +236,11 @@ export class PanelQueryRunner {
try {
const ds = await getDataSource(datasource, request.scopedVars);
- // Attach the data source name to each query
+ // Attach the data source to each query
request.targets = request.targets.map((query) => {
- if (!query.datasource) {
+ // When using a data source variable, the panel might have the incorrect datasource
+ // stored, so when running the query make sure it is done with the correct one
+ if (!query.datasource || (query.datasource.uid !== ds.uid && !ds.meta.mixed)) {
query.datasource = ds.getRef();
}
return query;
diff --git a/public/app/features/search/components/ActionRow.tsx b/public/app/features/search/components/ActionRow.tsx
index ef9c5d13c65..12b5c0a67cd 100644
--- a/public/app/features/search/components/ActionRow.tsx
+++ b/public/app/features/search/components/ActionRow.tsx
@@ -1,9 +1,9 @@
import { css } from '@emotion/css';
import React, { FC, ChangeEvent, FormEvent } from 'react';
-import { GrafanaTheme, SelectableValue } from '@grafana/data';
+import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
-import { HorizontalGroup, RadioButtonGroup, stylesFactory, useTheme, Checkbox, InlineSwitch } from '@grafana/ui';
+import { HorizontalGroup, RadioButtonGroup, Checkbox, InlineSwitch, useStyles2 } from '@grafana/ui';
import { SortPicker } from 'app/core/components/Select/SortPicker';
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
import { SearchSrv } from 'app/core/services/search_srv';
@@ -40,8 +40,7 @@ export const ActionRow: FC = ({
hideLayout,
showPreviews,
}) => {
- const theme = useTheme();
- const styles = getStyles(theme);
+ const styles = useStyles2(getStyles);
const previewsEnabled = config.featureToggles.dashboardPreviews;
return (
@@ -78,21 +77,21 @@ export const ActionRow: FC = ({
ActionRow.displayName = 'ActionRow';
-const getStyles = stylesFactory((theme: GrafanaTheme) => {
+export const getStyles = (theme: GrafanaTheme2) => {
return {
actionRow: css`
display: none;
- @media only screen and (min-width: ${theme.breakpoints.md}) {
+ ${theme.breakpoints.up('md')} {
display: flex;
justify-content: space-between;
align-items: center;
- padding: ${theme.spacing.lg} 0;
+ padding-bottom: ${theme.spacing(2)};
width: 100%;
}
`,
rowContainer: css`
- margin-right: ${theme.spacing.md};
+ margin-right: ${theme.spacing(1)};
`,
checkboxWrapper: css`
label {
@@ -100,4 +99,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
}
`,
};
-});
+};
diff --git a/public/app/features/search/components/DashboardListPage.tsx b/public/app/features/search/components/DashboardListPage.tsx
index ffb8219067e..3ececc199ca 100644
--- a/public/app/features/search/components/DashboardListPage.tsx
+++ b/public/app/features/search/components/DashboardListPage.tsx
@@ -46,8 +46,7 @@ export const DashboardListPage: FC = memo(({ navModel, match, location })
return (
- {/*Todo: remove the false to test, or when we feel confident with thsi approach */}
- {Boolean(config.featureToggles.panelTitleSearch && !window.location.search?.includes('index=sql')) ? (
+ {Boolean(config.featureToggles.panelTitleSearch) ? (
;
+ return ;
}
return ;
}
-function DashbaordSearchNEW({ onCloseSearch }: Props) {
+function DashboardSearchNew({ onCloseSearch }: Props) {
const styles = useStyles2(getStyles);
const { query, onQueryChange } = useSearchQuery({});
@@ -56,7 +56,7 @@ function DashbaordSearchNEW({ onCloseSearch }: Props) {
-
+
@@ -82,7 +82,7 @@ export const DashboardSearchOLD: FC = memo(({ onCloseSearch }) => {
@@ -137,14 +137,19 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
}
`,
container: css`
+ display: flex;
+ flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: ${theme.spacing(2)};
+ background: ${theme.colors.background.primary};
+ border: 1px solid ${theme.components.panel.borderColor};
+ margin-top: ${theme.spacing(4)};
height: 100%;
${theme.breakpoints.up('md')} {
- padding: ${theme.spacing(4)};
+ padding: ${theme.spacing(3)};
}
`,
closeBtn: css`
@@ -159,8 +164,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
search: css`
display: flex;
flex-direction: column;
+ overflow: hidden;
height: 100%;
- padding-bottom: ${theme.spacing(3)};
+ padding: ${theme.spacing(2, 0, 3, 0)};
`,
input: css`
box-sizing: border-box;
diff --git a/public/app/features/search/components/ManageDashboardsNew.tsx b/public/app/features/search/components/ManageDashboardsNew.tsx
index 9756f9bf99e..22468b9fc1f 100644
--- a/public/app/features/search/components/ManageDashboardsNew.tsx
+++ b/public/app/features/search/components/ManageDashboardsNew.tsx
@@ -1,4 +1,4 @@
-import { css } from '@emotion/css';
+import { css, cx } from '@emotion/css';
import React, { useState } from 'react';
import { useDebounce } from 'react-use';
@@ -38,8 +38,8 @@ export const ManageDashboardsNew = React.memo(({ folder }: Props) => {
return (
<>
-
-
+
+
({
+ actionBar: css`
+ ${theme.breakpoints.down('sm')} {
+ flex-wrap: wrap;
+ }
+ `,
+ inputWrapper: css`
+ ${theme.breakpoints.down('sm')} {
+ margin-right: 0 !important;
+ }
+ `,
searchInput: css`
margin-bottom: 6px;
min-height: ${theme.spacing(4)};
diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx
index c6a86240f05..091a057d2a2 100644
--- a/public/app/features/search/page/components/ActionRow.tsx
+++ b/public/app/features/search/page/components/ActionRow.tsx
@@ -9,8 +9,6 @@ import { TagFilter, TermCount } from 'app/core/components/TagFilter/TagFilter';
import { DashboardQuery, SearchLayout } from '../../types';
-import { getSortOptions } from './sorting';
-
export const layoutOptions = [
{ value: SearchLayout.Folders, icon: 'folder', ariaLabel: 'View by folders' },
{ value: SearchLayout.List, icon: 'list-ul', ariaLabel: 'View as list' },
@@ -26,6 +24,7 @@ interface Props {
onStarredFilterChange?: (event: FormEvent ) => void;
onTagFilterChange: (tags: string[]) => void;
getTagOptions: () => Promise;
+ getSortOptions: () => Promise;
onDatasourceChange: (ds?: string) => void;
query: DashboardQuery;
showStarredFilter?: boolean;
@@ -54,6 +53,7 @@ export const ActionRow: FC = ({
onStarredFilterChange = () => {},
onTagFilterChange,
getTagOptions,
+ getSortOptions,
onDatasourceChange,
query,
showStarredFilter,
@@ -104,11 +104,11 @@ export const getStyles = (theme: GrafanaTheme2) => {
actionRow: css`
display: none;
- @media only screen and (min-width: ${theme.v1.breakpoints.md}) {
+ ${theme.breakpoints.up('md')} {
display: flex;
justify-content: space-between;
align-items: center;
- padding: ${theme.v1.spacing.lg} 0;
+ padding-bottom: ${theme.spacing(2)};
width: 100%;
}
`,
diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx
index 39b508b17ab..167cc37afbc 100644
--- a/public/app/features/search/page/components/FolderSection.tsx
+++ b/public/app/features/search/page/components/FolderSection.tsx
@@ -3,9 +3,7 @@ import React, { FC } from 'react';
import { useAsync, useLocalStorage } from 'react-use';
import { GrafanaTheme } from '@grafana/data';
-import { getBackendSrv } from '@grafana/runtime';
import { Card, Checkbox, CollapsableSection, Icon, Spinner, stylesFactory, useTheme } from '@grafana/ui';
-import impressionSrv from 'app/core/services/impression_srv';
import { getSectionStorageKey } from 'app/features/search/utils';
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
@@ -58,23 +56,14 @@ export const FolderSection: FC = ({
location: section.uid,
sort: 'name_sort',
};
- if (section.title === 'Starred') {
+ if (section.itemsUIDs) {
query = {
uid: section.itemsUIDs, // array of UIDs
};
folderUid = undefined;
folderTitle = undefined;
- } else if (section.title === 'Recent') {
- const ids = impressionSrv.getDashboardOpened();
- const uids = await getBackendSrv().get(`/api/dashboards/ids/${ids.slice(0, 30).join(',')}`);
- if (uids?.length) {
- query = {
- uid: uids,
- };
- }
- folderUid = undefined;
- folderTitle = undefined;
}
+
const raw = await getGrafanaSearcher().search({ ...query, tags });
const v = raw.view.map(
(item) =>
diff --git a/public/app/features/search/page/components/FolderView.tsx b/public/app/features/search/page/components/FolderView.tsx
index 1ba64cffc0e..ee5c4a53a4b 100644
--- a/public/app/features/search/page/components/FolderView.tsx
+++ b/public/app/features/search/page/components/FolderView.tsx
@@ -6,6 +6,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { getBackendSrv } from '@grafana/runtime';
import { Spinner, useStyles2 } from '@grafana/ui';
+import { contextSrv } from 'app/core/core';
+import impressionSrv from 'app/core/services/impression_srv';
import { GENERAL_FOLDER_UID } from '../../constants';
import { getGrafanaSearcher } from '../../service';
@@ -23,11 +25,20 @@ export const FolderView = ({ selection, selectionToggle, onTagSelected, tags, hi
const results = useAsync(async () => {
const folders: DashboardSection[] = [];
if (!hidePseudoFolders) {
- const stars = await getBackendSrv().get('api/user/stars');
- if (stars.length > 0) {
- folders.push({ title: 'Starred', icon: 'star', kind: 'query-star', uid: '__starred', itemsUIDs: stars });
+ if (contextSrv.isSignedIn) {
+ const stars = await getBackendSrv().get('api/user/stars');
+ if (stars.length > 0) {
+ folders.push({ title: 'Starred', icon: 'star', kind: 'query-star', uid: '__starred', itemsUIDs: stars });
+ }
+ }
+
+ const ids = impressionSrv.getDashboardOpened();
+ if (ids.length) {
+ const itemsUIDs = await getBackendSrv().get(`/api/dashboards/ids/${ids.slice(0, 30).join(',')}`);
+ if (itemsUIDs.length) {
+ folders.push({ title: 'Recent', icon: 'clock', kind: 'query-recent', uid: '__recent', itemsUIDs });
+ }
}
- folders.push({ title: 'Recent', icon: 'clock', kind: 'query-recent', uid: '__recent' });
}
folders.push({ title: 'General', url: '/dashboards', kind: 'folder', uid: GENERAL_FOLDER_UID });
@@ -97,7 +108,10 @@ const getStyles = (theme: GrafanaTheme2) => {
display: flex;
flex-direction: column;
background: ${theme.v1.colors.panelBg};
- border-bottom: solid 1px ${theme.v1.colors.border2};
+
+ &:not(:last-child) {
+ border-bottom: solid 1px ${theme.v1.colors.border2};
+ }
`,
sectionItems: css`
margin: 0 24px 0 32px;
diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx
index 43f540bf4ee..45e0ac95f3a 100644
--- a/public/app/features/search/page/components/SearchView.tsx
+++ b/public/app/features/search/page/components/SearchView.tsx
@@ -205,6 +205,7 @@ export const SearchView = ({ showManage, folderDTO, queryText, hidePseudoFolders
onSortChange={onSortChange}
onTagFilterChange={onTagFilterChange}
getTagOptions={getTagOptions}
+ getSortOptions={getGrafanaSearcher().getSortOptions}
onDatasourceChange={onDatasourceChange}
query={query}
/>
diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx
index c38a472a704..b13f716db4b 100644
--- a/public/app/features/search/page/components/columns.tsx
+++ b/public/app/features/search/page/components/columns.tsx
@@ -3,7 +3,7 @@ import { isNumber } from 'lodash';
import React from 'react';
import SVG from 'react-inlinesvg';
-import { Field } from '@grafana/data';
+import { Field, getFieldDisplayName } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { Checkbox, Icon, IconButton, IconName, TagList } from '@grafana/ui';
@@ -11,7 +11,6 @@ import { QueryResponse, SearchResultMeta } from '../../service';
import { SelectionChecker, SelectionToggle } from '../selection';
import { TableColumn } from './SearchResultsTable';
-import { getSortFieldDisplayName } from './sorting';
const TYPE_COLUMN_WIDTH = 250;
const DATASOURCE_COLUMN_WIDTH = 200;
@@ -172,7 +171,7 @@ export const generateColumns = (
if (sortField) {
columns.push({
- Header: () => {getSortFieldDisplayName(sortField.name)} ,
+ Header: () => {getFieldDisplayName(sortField)} ,
Cell: (p) => {
let value = sortField.values.get(p.row.index);
try {
@@ -182,7 +181,7 @@ export const generateColumns = (
} catch {}
return (
- {value}
+ {`${value}`}
);
},
@@ -296,7 +295,18 @@ function makeTypeColumn(
}
txt = info.name;
} else {
- icon = `public/img/icons/unicons/question.svg`; // plugin not found
+ switch (type) {
+ case 'row':
+ txt = 'Row';
+ icon = `public/img/icons/unicons/bars.svg`;
+ break;
+ case 'singlestat': // auto-migration
+ txt = 'Singlestat';
+ icon = `public/app/plugins/panel/stat/img/icn-singlestat-panel.svg`;
+ break;
+ default:
+ icon = `public/img/icons/unicons/question.svg`; // plugin not found
+ }
}
}
break;
diff --git a/public/app/features/search/page/components/sorting.ts b/public/app/features/search/page/components/sorting.ts
deleted file mode 100644
index 0046d1b2b55..00000000000
--- a/public/app/features/search/page/components/sorting.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { SelectableValue } from '@grafana/data';
-import { config } from '@grafana/runtime';
-
-// Enterprise only sort field values for dashboards
-const sortFields = [
- { name: 'views_total', display: 'Views total' },
- { name: 'views_last_30_days', display: 'Views 30 days' },
- { name: 'errors_total', display: 'Errors total' },
- { name: 'errors_last_30_days', display: 'Errors 30 days' },
-];
-
-// This should eventually be filled by an API call, but hardcoded is a good start
-export async function getSortOptions(): Promise {
- const opts: SelectableValue[] = [
- { value: 'name_sort', label: 'Alphabetically (A-Z)' },
- { value: '-name_sort', label: 'Alphabetically (Z-A)' },
- ];
-
- if (config.licenseInfo.enabledFeatures.analytics) {
- for (const sf of sortFields) {
- opts.push({ value: `-${sf.name}`, label: `${sf.display} (most)` });
- opts.push({ value: `${sf.name}`, label: `${sf.display} (least)` });
- }
- }
-
- return opts;
-}
-
-/** Given the internal field name, this gives a reasonable display name for the table colum header */
-export function getSortFieldDisplayName(name: string) {
- for (const sf of sortFields) {
- if (sf.name === name) {
- return sf.display;
- }
- }
- return name;
-}
diff --git a/public/app/features/search/service/bluge.ts b/public/app/features/search/service/bluge.ts
index 27814551442..38886ba369b 100644
--- a/public/app/features/search/service/bluge.ts
+++ b/public/app/features/search/service/bluge.ts
@@ -1,6 +1,6 @@
import { lastValueFrom } from 'rxjs';
-import { ArrayVector, DataFrame, DataFrameView, getDisplayProcessor } from '@grafana/data';
+import { ArrayVector, DataFrame, DataFrameView, getDisplayProcessor, SelectableValue } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
import { GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
@@ -16,10 +16,6 @@ export class BlugeSearcher implements GrafanaSearcher {
return doSearchQuery(query);
}
- async list(location: string): Promise {
- return doSearchQuery({ query: `list:${location ?? ''}` });
- }
-
async tags(query: SearchQuery): Promise {
const ds = (await getDataSourceSrv().get('-- Grafana --')) as GrafanaDatasource;
const target = {
@@ -46,12 +42,29 @@ export class BlugeSearcher implements GrafanaSearcher {
}
return [];
}
+
+ // This should eventually be filled by an API call, but hardcoded is a good start
+ getSortOptions(): Promise {
+ const opts: SelectableValue[] = [
+ { value: 'name_sort', label: 'Alphabetically (A-Z)' },
+ { value: '-name_sort', label: 'Alphabetically (Z-A)' },
+ ];
+
+ if (config.licenseInfo.enabledFeatures.analytics) {
+ for (const sf of sortFields) {
+ opts.push({ value: `-${sf.name}`, label: `${sf.display} (most)` });
+ opts.push({ value: `${sf.name}`, label: `${sf.display} (least)` });
+ }
+ }
+
+ return Promise.resolve(opts);
+ }
}
const firstPageSize = 50;
const nextPageSizes = 100;
-export async function doSearchQuery(query: SearchQuery): Promise {
+async function doSearchQuery(query: SearchQuery): Promise {
const ds = (await getDataSourceSrv().get('-- Grafana --')) as GrafanaDatasource;
const target = {
...query,
@@ -84,8 +97,19 @@ export async function doSearchQuery(query: SearchQuery): Promise
const meta = first.meta.custom as SearchResultMeta;
if (!meta.locationInfo) {
- meta.locationInfo = {};
+ meta.locationInfo = {}; // always set it so we can append
}
+
+ // Set the field name to a better display name
+ if (meta.sortBy?.length) {
+ const field = first.fields.find((f) => f.name === meta.sortBy);
+ if (field) {
+ const name = getSortFieldDisplayName(field.name);
+ meta.sortBy = name;
+ field.name = name; // make it look nicer
+ }
+ }
+
const view = new DataFrameView(first);
return {
totalRows: meta.count ?? first.length,
@@ -146,3 +170,21 @@ function getTermCountsFrom(frame: DataFrame): TermCount[] {
}
return counts;
}
+
+// Enterprise only sort field values for dashboards
+const sortFields = [
+ { name: 'views_total', display: 'Views total' },
+ { name: 'views_last_30_days', display: 'Views 30 days' },
+ { name: 'errors_total', display: 'Errors total' },
+ { name: 'errors_last_30_days', display: 'Errors 30 days' },
+];
+
+/** Given the internal field name, this gives a reasonable display name for the table colum header */
+function getSortFieldDisplayName(name: string) {
+ for (const sf of sortFields) {
+ if (sf.name === name) {
+ return sf.display;
+ }
+ }
+ return name;
+}
diff --git a/public/app/features/search/service/searcher.ts b/public/app/features/search/service/searcher.ts
index ff6675339d6..c61ed5bc552 100644
--- a/public/app/features/search/service/searcher.ts
+++ b/public/app/features/search/service/searcher.ts
@@ -1,11 +1,17 @@
+import { config } from '@grafana/runtime';
+
import { BlugeSearcher } from './bluge';
+import { SQLSearcher } from './sql';
import { GrafanaSearcher } from './types';
let searcher: GrafanaSearcher | undefined = undefined;
export function getGrafanaSearcher(): GrafanaSearcher {
if (!searcher) {
- searcher = new BlugeSearcher();
+ const useBluge =
+ config.featureToggles.panelTitleSearch && // set in system configs
+ window.location.search.indexOf('index=sql') < 0; // or URL override
+ searcher = useBluge ? new BlugeSearcher() : new SQLSearcher();
}
return searcher!;
}
diff --git a/public/app/features/search/service/sql.ts b/public/app/features/search/service/sql.ts
new file mode 100644
index 00000000000..96e68ac6946
--- /dev/null
+++ b/public/app/features/search/service/sql.ts
@@ -0,0 +1,198 @@
+import { ArrayVector, DataFrame, DataFrameView, FieldType, getDisplayProcessor, SelectableValue } from '@grafana/data';
+import { config } from '@grafana/runtime';
+import { TermCount } from 'app/core/components/TagFilter/TagFilter';
+import { backendSrv } from 'app/core/services/backend_srv';
+
+import { DashboardSearchHit } from '../types';
+
+import { LocationInfo } from './types';
+
+import { DashboardQueryResult, GrafanaSearcher, QueryResponse, SearchQuery } from '.';
+
+interface APIQuery {
+ query?: string;
+ tag?: string[];
+ limit?: number;
+ page?: number;
+ type?: string;
+ // DashboardIds []int64
+ folderIds?: number[];
+ sort?: string;
+
+ // NEW!!!! TODO TODO: needs backend support?
+ dashboardUIDs?: string[];
+}
+
+// Internal object to hold folderId
+interface LocationInfoEXT extends LocationInfo {
+ folderId?: number;
+}
+
+export class SQLSearcher implements GrafanaSearcher {
+ locationInfo: Record = {
+ general: {
+ kind: 'folder',
+ name: 'General',
+ url: '/dashboards',
+ folderId: 0,
+ },
+ }; // share location info with everyone
+
+ async search(query: SearchQuery): Promise {
+ if (query.facet?.length) {
+ throw 'facets not supported!';
+ }
+ const q: APIQuery = {
+ limit: 1000, // 1k max values
+ tag: query.tags,
+ sort: query.sort,
+ };
+
+ if (query.query === '*') {
+ if (query.kind?.length === 1 && query.kind[0] === 'folder') {
+ q.type = 'dash-folder';
+ }
+ } else if (query.query?.length) {
+ q.query = query.query;
+ }
+
+ if (query.uid) {
+ q.query = query.uid.join(', '); // TODO! this will return nothing
+ q.dashboardUIDs = query.uid;
+ } else if (query.location?.length) {
+ let info = this.locationInfo[query.location];
+ if (!info) {
+ // This will load all folder folders
+ await this.doAPIQuery({ type: 'dash-folder', limit: 999 });
+ info = this.locationInfo[query.location];
+ }
+ q.folderIds = [info.folderId ?? 0];
+ }
+ return this.doAPIQuery(q);
+ }
+
+ // returns the appropriate sorting options
+ async getSortOptions(): Promise {
+ // {
+ // "sortOptions": [
+ // {
+ // "description": "Sort results in an alphabetically ascending order",
+ // "displayName": "Alphabetically (A–Z)",
+ // "meta": "",
+ // "name": "alpha-asc"
+ // },
+ // {
+ // "description": "Sort results in an alphabetically descending order",
+ // "displayName": "Alphabetically (Z–A)",
+ // "meta": "",
+ // "name": "alpha-desc"
+ // }
+ // ]
+ // }
+ const opts = await backendSrv.get('/api/search/sorting');
+ return opts.sortOptions.map((v: any) => ({
+ value: v.name,
+ label: v.displayName,
+ }));
+ }
+
+ // NOTE: the bluge query will find tags within the current results, the SQL based one does not
+ async tags(query: SearchQuery): Promise {
+ const terms = (await backendSrv.get('/api/dashboards/tags')) as TermCount[];
+ return terms.sort((a, b) => b.count - a.count);
+ }
+
+ async doAPIQuery(query: APIQuery): Promise {
+ const rsp = (await backendSrv.get('/api/search', query)) as DashboardSearchHit[];
+
+ // Field values (columnar)
+ const kind: string[] = [];
+ const name: string[] = [];
+ const uid: string[] = [];
+ const url: string[] = [];
+ const tags: string[][] = [];
+ const location: string[] = [];
+ const sortBy: number[] = [];
+ let sortMetaName: string | undefined;
+
+ for (let hit of rsp) {
+ const k = hit.type === 'dash-folder' ? 'folder' : 'dashboard';
+ kind.push(k);
+ name.push(hit.title);
+ uid.push(hit.uid!);
+ url.push(hit.url);
+ tags.push(hit.tags);
+ sortBy.push(hit.sortMeta!);
+
+ let v = hit.folderUid;
+ if (!v && k === 'dashboard') {
+ v = 'general';
+ }
+ location.push(v!);
+
+ if (hit.sortMetaName?.length) {
+ sortMetaName = hit.sortMetaName;
+ }
+
+ if (hit.folderUid && hit.folderTitle) {
+ this.locationInfo[hit.folderUid] = {
+ kind: 'folder',
+ name: hit.folderTitle,
+ url: hit.folderUrl!,
+ folderId: hit.folderId,
+ };
+ } else if (k === 'folder') {
+ this.locationInfo[hit.uid!] = {
+ kind: k,
+ name: hit.title!,
+ url: hit.url,
+ folderId: hit.id,
+ };
+ }
+ }
+
+ const data: DataFrame = {
+ fields: [
+ { name: 'kind', type: FieldType.string, config: {}, values: new ArrayVector(kind) },
+ { name: 'name', type: FieldType.string, config: {}, values: new ArrayVector(name) },
+ { name: 'uid', type: FieldType.string, config: {}, values: new ArrayVector(uid) },
+ { name: 'url', type: FieldType.string, config: {}, values: new ArrayVector(url) },
+ { name: 'tags', type: FieldType.other, config: {}, values: new ArrayVector(tags) },
+ { name: 'location', type: FieldType.string, config: {}, values: new ArrayVector(location) },
+ ],
+ length: name.length,
+ meta: {
+ custom: {
+ count: name.length,
+ max_score: 1,
+ locationInfo: this.locationInfo,
+ },
+ },
+ };
+
+ // Add enterprise sort fields as a field in the frame
+ if (sortMetaName?.length && sortBy.length) {
+ data.meta!.custom!.sortBy = sortMetaName;
+ data.fields.push({
+ name: sortMetaName, // Used in display
+ type: FieldType.number,
+ config: {},
+ values: new ArrayVector(sortBy),
+ });
+ }
+
+ for (const field of data.fields) {
+ field.display = getDisplayProcessor({ field, theme: config.theme2 });
+ }
+
+ const view = new DataFrameView(data);
+ return {
+ totalRows: data.length,
+ view,
+
+ // Paging not supported with this version
+ loadMoreItems: async (startIndex: number, stopIndex: number): Promise => {},
+ isItemLoaded: (index: number): boolean => true,
+ };
+ }
+}
diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts
index adff98d3467..1b0d79b9d2a 100644
--- a/public/app/features/search/service/types.ts
+++ b/public/app/features/search/service/types.ts
@@ -1,4 +1,4 @@
-import { DataFrameView } from '@grafana/data';
+import { DataFrameView, SelectableValue } from '@grafana/data';
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
export interface FacetField {
@@ -32,7 +32,6 @@ export interface DashboardQueryResult {
tags: string[];
location: string; // url that can be split
ds_uid: string[];
- score?: number;
}
export interface LocationInfo {
@@ -45,6 +44,7 @@ export interface SearchResultMeta {
count: number;
max_score: number;
locationInfo: Record;
+ sortBy?: string;
}
export interface QueryResponse {
@@ -62,6 +62,6 @@ export interface QueryResponse {
export interface GrafanaSearcher {
search: (query: SearchQuery) => Promise;
- list: (location: string) => Promise;
tags: (query: SearchQuery) => Promise;
+ getSortOptions: () => Promise;
}
diff --git a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx
index e1ab18f7739..ee3bb166da1 100644
--- a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx
+++ b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx
@@ -73,7 +73,7 @@ export function ServiceAccountProfile({
<>
-
+
= ({
className={styles.toggle}
size="md"
name={visible ? 'eye' : 'eye-slash'}
- surface="header"
onClick={() => onToggleVisibility(fieldName, visible)}
/>
diff --git a/public/app/features/transformers/extractFields/extractFields.ts b/public/app/features/transformers/extractFields/extractFields.ts
index 81628b595ed..ede77024b40 100644
--- a/public/app/features/transformers/extractFields/extractFields.ts
+++ b/public/app/features/transformers/extractFields/extractFields.ts
@@ -41,7 +41,8 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da
}
const source = findField(frame, options.source);
if (!source) {
- throw new Error('json field not found');
+ // this case can happen when there are multiple queries
+ return frame;
}
const ext = fieldExtractors.getIfExists(options.format ?? FieldExtractorID.Auto);
diff --git a/public/app/features/users/UsersListPage.test.tsx b/public/app/features/users/UsersListPage.test.tsx
index bc334b6287a..e5a2837e98f 100644
--- a/public/app/features/users/UsersListPage.test.tsx
+++ b/public/app/features/users/UsersListPage.test.tsx
@@ -12,6 +12,12 @@ jest.mock('../../core/app_events', () => ({
emit: jest.fn(),
}));
+jest.mock('app/core/services/context_srv', () => ({
+ contextSrv: {
+ user: { orgId: 1 },
+ },
+}));
+
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {
diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx
index f53b3f47b9f..803deead884 100644
--- a/public/app/features/users/UsersListPage.tsx
+++ b/public/app/features/users/UsersListPage.tsx
@@ -5,6 +5,7 @@ import { renderMarkdown } from '@grafana/data';
import { HorizontalGroup, Pagination, VerticalGroup } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
+import { contextSrv } from 'app/core/services/context_srv';
import { OrgUser, OrgRole, StoreState } from 'app/types';
import InviteesTable from '../invites/InviteesTable';
@@ -106,6 +107,7 @@ export class UsersListPage extends PureComponent {
this.onRoleChange(role, user)}
onRemoveUser={(user) => this.props.removeUser(user.userId)}
/>
diff --git a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap
index 08b3ba0ec22..01d1dbeeba2 100644
--- a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap
+++ b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap
@@ -26,6 +26,7 @@ exports[`Render should render List page 1`] = `
{
});
});
+ describe('interpolateMetricsQueryVariables', () => {
+ it('interpolates dimensions correctly', () => {
+ const testQuery = {
+ id: 'a',
+ refId: 'a',
+ region: 'us-east-2',
+ namespace: '',
+ dimensions: { InstanceId: '$dimension' },
+ };
+ const ds = setupMockedDataSource({ variables: [dimensionVariable], mockGetVariableName: false });
+ const result = ds.datasource.interpolateMetricsQueryVariables(testQuery, {
+ dimension: { text: 'foo', value: 'foo' },
+ });
+ expect(result).toStrictEqual({
+ alias: '',
+ metricName: '',
+ namespace: '',
+ period: '',
+ sqlExpression: '',
+ dimensions: { InstanceId: ['foo'] },
+ });
+ });
+ });
+
describe('convertMultiFiltersFormat', () => {
const ds = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable], mockGetVariableName: false });
it('converts keys and values correctly', () => {
diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts
index 15761c0d7de..582d3016959 100644
--- a/public/app/plugins/datasource/cloudwatch/datasource.ts
+++ b/public/app/plugins/datasource/cloudwatch/datasource.ts
@@ -958,13 +958,7 @@ export class CloudWatchDatasource
namespace: this.replace(query.namespace, scopedVars),
period: this.replace(query.period, scopedVars),
sqlExpression: this.replace(query.sqlExpression, scopedVars),
- dimensions: Object.entries(query.dimensions ?? {}).reduce((prev, [key, value]) => {
- if (Array.isArray(value)) {
- return { ...prev, [key]: value };
- }
-
- return { ...prev, [this.replace(key, scopedVars)]: this.replace(value, scopedVars) };
- }, {}),
+ dimensions: this.convertDimensionFormat(query.dimensions ?? {}, scopedVars),
};
}
}
diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
index df2004957bc..6855148d22a 100644
--- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
+++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
@@ -585,7 +585,11 @@ describe('CloudWatchDatasource', () => {
});
it('should replace correct variables in CloudWatchMetricsQuery', () => {
- const templateSrv: any = { replace: jest.fn(), getVariables: () => [] };
+ const templateSrv: any = {
+ replace: jest.fn(),
+ getVariables: () => [],
+ getVariableName: jest.fn((name: string) => name),
+ };
const { ds } = getTestContext({ templateSrv });
const variableName = 'someVar';
const logQuery: CloudWatchMetricsQuery = {
@@ -608,9 +612,12 @@ describe('CloudWatchDatasource', () => {
ds.interpolateVariablesInQueries([logQuery], {});
- // We interpolate `expression`, `region`, `period`, `alias`, `metricName`, `nameSpace` and `dimensions` in CloudWatchMetricsQuery
+ // We interpolate `expression`, `region`, `period`, `alias`, `metricName`, and `nameSpace` in CloudWatchMetricsQuery
expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
- expect(templateSrv.replace).toHaveBeenCalledTimes(8);
+ expect(templateSrv.replace).toHaveBeenCalledTimes(7);
+
+ expect(templateSrv.getVariableName).toHaveBeenCalledWith(`$${variableName}`);
+ expect(templateSrv.getVariableName).toHaveBeenCalledTimes(1);
});
});
diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorRow.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorRow.tsx
index 155ed2942bc..0ee4faf69ae 100644
--- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorRow.tsx
+++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorRow.tsx
@@ -31,7 +31,6 @@ export const QueryEditorRow = ({
= ({
+const ArgQueryEditor: React.FC = ({
query,
datasource,
subscriptionId,
@@ -52,12 +54,26 @@ const ArgQueryEditor: React.FC = ({
.catch((err) => setError(ERROR_SOURCE, err));
}, [datasource, onChange, query, setError]);
- return (
-
-
-
+
+
+
+
+
+
+
+ = ({
onQueryChange={onChange}
setError={setError}
/>
-
+
+ );
+ } else {
+ return (
+
+
+
+
-
-
- );
+
+
+ );
+ }
};
export default ArgQueryEditor;
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ArgQueryEditor/QueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ArgQueryEditor/QueryField.tsx
index 9ce83b50797..4a88a301c7d 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ArgQueryEditor/QueryField.tsx
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ArgQueryEditor/QueryField.tsx
@@ -23,7 +23,7 @@ const QueryField: React.FC = ({ query, onQueryChange
value={query.azureResourceGraph?.query ?? ''}
language="kusto"
height={200}
- width={1000}
+ width="100%"
showMiniMap={false}
onBlur={onChange}
onSave={onChange}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx
index 1e77997c5c4..83d858ac5b6 100644
--- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx
@@ -1,5 +1,7 @@
import React from 'react';
+import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental';
+import { config } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
import Datasource from '../../datasource';
@@ -33,36 +35,78 @@ const LogsQueryEditor: React.FC = ({
}) => {
const migrationError = useMigrations(datasource, query, onChange);
- return (
-
-
+ if (config.featureToggles.azureMonitorExperimentalUI) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {!hideFormatAs && (
+
+ )}
-
+ {migrationError && {migrationError.message}}
+
+
+
+
+ );
+ } else {
+ return (
+
+
- {!hideFormatAs && (
- = ({
onQueryChange={onChange}
setError={setError}
/>
- )}
- {migrationError && {migrationError.message}}
-
- );
+ {!hideFormatAs && (
+
+ )}
+
+ {migrationError && {migrationError.message}}
+
+ );
+ }
};
export default LogsQueryEditor;
diff --git a/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx b/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx
index bb593a1edb0..8babee6d2a2 100644
--- a/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx
+++ b/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx
@@ -20,7 +20,7 @@ export const MappingsConfiguration = (props: Props): JSX.Element => {
Label mappings
{!props.showHelp && (
-
diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx
index 1354458a7ac..2ea26ae1f60 100644
--- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx
+++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx
@@ -1,16 +1,33 @@
import { css } from '@emotion/css';
import React from 'react';
-import { GrafanaTheme2 } from '@grafana/data';
+import { CoreApp, GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { testIds } from '../../components/LokiQueryEditor';
import { LokiQueryField } from '../../components/LokiQueryField';
import { LokiQueryEditorProps } from '../../components/types';
-export function LokiQueryCodeEditor({ query, datasource, range, onRunQuery, onChange, data }: LokiQueryEditorProps) {
+export function LokiQueryCodeEditor({
+ query,
+ datasource,
+ range,
+ onRunQuery,
+ onChange,
+ data,
+ app,
+}: LokiQueryEditorProps) {
const styles = useStyles2(getStyles);
+ // the inner QueryField works like this when a blur event happens:
+ // - if it has an onBlur prop, it calls it
+ // - else it calls onRunQuery (some extra conditions apply)
+ //
+ // we want it to not do anything when a blur event happens in explore mode,
+ // so we set an empty-function in such case. otherwise we set `undefined`,
+ // which will cause it to run the query when blur happens.
+ const onBlur = app === CoreApp.Explore ? () => undefined : undefined;
+
return (
((props)
- {editorMode === QueryEditorMode.Code && }
+ {editorMode === QueryEditorMode.Code && }
{editorMode === QueryEditorMode.Builder && (
{
);
});
+ it('parses query with with unwrap and error filter', () => {
+ expect(
+ buildVisualQueryFromString('sum_over_time({app="frontend"} | logfmt | unwrap duration | __error__="" [1m])')
+ ).toEqual(
+ noErrors({
+ labels: [
+ {
+ op: '=',
+ value: 'frontend',
+ label: 'app',
+ },
+ ],
+ operations: [
+ { id: 'logfmt', params: [] },
+ { id: 'unwrap', params: ['duration'] },
+ { id: '__label_filter_no_errors', params: [] },
+ { id: 'sum_over_time', params: ['1m'] },
+ ],
+ })
+ );
+ });
+
+ it('parses query with with unwrap and label filter', () => {
+ expect(
+ buildVisualQueryFromString('sum_over_time({app="frontend"} | logfmt | unwrap duration | label="value" [1m])')
+ ).toEqual(
+ noErrors({
+ labels: [
+ {
+ op: '=',
+ value: 'frontend',
+ label: 'app',
+ },
+ ],
+ operations: [
+ { id: 'logfmt', params: [] },
+ { id: 'unwrap', params: ['duration'] },
+ { id: '__label_filter', params: ['label', '=', 'value'] },
+ { id: 'sum_over_time', params: ['1m'] },
+ ],
+ })
+ );
+ });
+
it('returns error for query with unwrap and conversion operation', () => {
const context = buildVisualQueryFromString(
'sum_over_time({app="frontend"} | logfmt | unwrap duration(label) [5m])'
diff --git a/public/app/plugins/datasource/loki/querybuilder/parsing.ts b/public/app/plugins/datasource/loki/querybuilder/parsing.ts
index 39b38c0f848..f46bb546d4c 100644
--- a/public/app/plugins/datasource/loki/querybuilder/parsing.ts
+++ b/public/app/plugins/datasource/loki/querybuilder/parsing.ts
@@ -120,7 +120,7 @@ export function handleExpression(expr: string, node: SyntaxNode, context: Contex
}
case 'UnwrapExpr': {
- const { operation, error } = getUnwrap(expr, node);
+ const { operation, error } = handleUnwrapExpr(expr, node, context);
if (operation) {
visQuery.operations.push(operation);
}
@@ -297,25 +297,40 @@ function getLabelFormat(expr: string, node: SyntaxNode): QueryBuilderOperation {
};
}
-function getUnwrap(expr: string, node: SyntaxNode): { operation?: QueryBuilderOperation; error?: string } {
- // Check for nodes not supported in visual builder and return error
- if (node.getChild('ConvOp')) {
+function handleUnwrapExpr(
+ expr: string,
+ node: SyntaxNode,
+ context: Context
+): { operation?: QueryBuilderOperation; error?: string } {
+ const unwrapExprChild = node.getChild('UnwrapExpr');
+ const labelFilterChild = node.getChild('LabelFilter');
+ const unwrapChild = node.getChild('Unwrap');
+
+ if (unwrapExprChild) {
+ handleExpression(expr, unwrapExprChild, context);
+ }
+
+ if (labelFilterChild) {
+ handleExpression(expr, labelFilterChild, context);
+ }
+
+ if (unwrapChild) {
+ if (unwrapChild?.nextSibling?.type.name === 'ConvOp') {
+ return {
+ error: 'Unwrap with conversion operator not supported in query builder',
+ };
+ }
+
return {
- error: 'Unwrap with conversion operator not supported in query builder',
+ operation: {
+ id: 'unwrap',
+ params: [getString(expr, unwrapChild?.nextSibling)],
+ },
};
}
- const id = 'unwrap';
- const string = getString(expr, node.getChild('Identifier'));
-
- return {
- operation: {
- id,
- params: [string],
- },
- };
+ return {};
}
-
function handleRangeAggregation(expr: string, node: SyntaxNode, context: Context) {
const nameNode = node.getChild('RangeOp');
const funcName = getString(expr, nameNode);
diff --git a/public/app/plugins/datasource/loki/syntax.ts b/public/app/plugins/datasource/loki/syntax.ts
index 94a5e54dab4..e203e0b218f 100644
--- a/public/app/plugins/datasource/loki/syntax.ts
+++ b/public/app/plugins/datasource/loki/syntax.ts
@@ -72,6 +72,13 @@ export const PIPE_PARSERS: CompletionItem[] = [
insertText: 'pattern',
documentation: 'Extracting labels from the log line using pattern parser. Only available in Loki 2.3+.',
},
+ {
+ label: 'unpack',
+ insertText: 'unpack',
+ detail: 'unpack identifier',
+ documentation:
+ 'Parses a JSON log line, unpacking all embedded labels in the pack stage. A special property "_entry" will also be used to replace the original log line. Only available in Loki 2.2+.',
+ },
];
export const PIPE_OPERATORS: CompletionItem[] = [
@@ -82,13 +89,6 @@ export const PIPE_OPERATORS: CompletionItem[] = [
documentation:
'Take labels and use the values as sample data for metric aggregations. Only available in Loki 2.0+.',
},
- {
- label: 'unpack',
- insertText: 'unpack',
- detail: 'unpack identifier',
- documentation:
- 'Parses a JSON log line, unpacking all embedded labels in the pack stage. A special property "_entry" will also be used to replace the original log line. Only available in Loki 2.0+.',
- },
{
label: 'label_format',
insertText: 'label_format',
@@ -122,6 +122,18 @@ export const RANGE_VEC_FUNCTIONS = [
detail: 'max_over_time(range-vector)',
documentation: 'The maximum of all values in the specified interval. Only available in Loki 2.0+.',
},
+ {
+ insertText: 'first_over_time',
+ label: 'first_over_time',
+ detail: 'first_over_time(range-vector)',
+ documentation: 'The first of all values in the specified interval. Only available in Loki 2.3+.',
+ },
+ {
+ insertText: 'last_over_time',
+ label: 'last_over_time',
+ detail: 'last_over_time(range-vector)',
+ documentation: 'The last of all values in the specified interval. Only available in Loki 2.3+.',
+ },
{
insertText: 'sum_over_time',
label: 'sum_over_time',
diff --git a/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx
new file mode 100644
index 00000000000..3eed372d2d5
--- /dev/null
+++ b/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx
@@ -0,0 +1,133 @@
+import React from 'react';
+
+import { AnnotationQuery } from '@grafana/data';
+import { EditorRow, EditorField, EditorSwitch, Space, EditorRows } from '@grafana/experimental';
+import { Input } from '@grafana/ui';
+
+import { PromQueryCodeEditor } from '../querybuilder/components/PromQueryCodeEditor';
+import { AutoSizeInput } from '../querybuilder/shared/AutoSizeInput';
+import { PromQuery } from '../types';
+
+import { PromQueryEditorProps } from './types';
+
+type Props = PromQueryEditorProps & {
+ annotation?: AnnotationQuery;
+ onAnnotationChange?: (annotation: AnnotationQuery) => void;
+};
+
+export function AnnotationQueryEditor(props: Props) {
+ // This is because of problematic typing. See AnnotationQueryEditorProps in grafana-data/annotations.ts.
+ const annotation = props.annotation!;
+ const onAnnotationChange = props.onAnnotationChange!;
+ const query = { expr: annotation.expr, refId: annotation.name, interval: annotation.step };
+
+ return (
+ <>
+
+ {
+ onAnnotationChange({
+ ...annotation,
+ expr: query.expr,
+ });
+ }}
+ />
+
+
+ An additional lower limit for the step parameter of the Prometheus query and for the{' '}
+ $__interval and $__rate_interval variables.
+ >
+ }
+ >
+ {
+ onAnnotationChange({
+ ...annotation,
+ step: ev.currentTarget.value,
+ });
+ }}
+ defaultValue={query.interval}
+ />
+
+
+
+
+
+
+ {
+ onAnnotationChange({
+ ...annotation,
+ titleFormat: event.currentTarget.value,
+ });
+ }}
+ />
+
+
+ {
+ onAnnotationChange({
+ ...annotation,
+ tagKeys: event.currentTarget.value,
+ });
+ }}
+ />
+
+
+ {
+ onAnnotationChange({
+ ...annotation,
+ textFormat: event.currentTarget.value,
+ });
+ }}
+ />
+
+
+ {
+ onAnnotationChange({
+ ...annotation,
+ useValueForTime: event.currentTarget.value,
+ });
+ }}
+ />
+
+
+ >
+ );
+}
diff --git a/public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx
index 1108dff3a9c..412e63f9389 100644
--- a/public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx
+++ b/public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx
@@ -1,7 +1,7 @@
-import React, { FunctionComponent, useMemo } from 'react';
+import React, { FunctionComponent, FormEvent, useMemo, useState } from 'react';
import { config } from '@grafana/runtime';
-import { InlineFormLabel, Input } from '@grafana/ui';
+import { InlineField, InlineFieldRow, InlineSwitch, Input } from '@grafana/ui';
import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types';
import { KnownAzureClouds, AzureCredentials } from './AzureCredentials';
@@ -11,12 +11,38 @@ import { AzureCredentialsForm } from './AzureCredentialsForm';
export const AzureAuthSettings: FunctionComponent = (props: HttpSettingsBaseProps) => {
const { dataSourceConfig, onChange } = props;
+ const [overrideAudienceAllowed] = useState(
+ config.featureToggles.prometheusAzureOverrideAudience || !!dataSourceConfig.jsonData.azureEndpointResourceId
+ );
+ const [overrideAudienceChecked, setOverrideAudienceChecked] = useState(
+ !!dataSourceConfig.jsonData.azureEndpointResourceId
+ );
+
const credentials = useMemo(() => getCredentials(dataSourceConfig), [dataSourceConfig]);
const onCredentialsChange = (credentials: AzureCredentials): void => {
onChange(updateCredentials(dataSourceConfig, credentials));
};
+ const onOverrideAudienceChange = (ev: FormEvent): void => {
+ setOverrideAudienceChecked(ev.currentTarget.checked);
+ if (!ev.currentTarget.checked) {
+ onChange({
+ ...dataSourceConfig,
+ jsonData: { ...dataSourceConfig.jsonData, azureEndpointResourceId: undefined },
+ });
+ }
+ };
+
+ const onResourceIdChange = (ev: FormEvent): void => {
+ if (overrideAudienceChecked) {
+ onChange({
+ ...dataSourceConfig,
+ jsonData: { ...dataSourceConfig.jsonData, azureEndpointResourceId: ev.currentTarget.value },
+ });
+ }
+ };
+
return (
<>
Azure Authentication
@@ -26,26 +52,29 @@ export const AzureAuthSettings: FunctionComponent = (prop
azureCloudOptions={KnownAzureClouds}
onCredentialsChange={onCredentialsChange}
/>
- Azure Configuration
-
-
-
- AAD resource ID
-
-
- onChange({
- ...dataSourceConfig,
- jsonData: { ...dataSourceConfig.jsonData, azureEndpointResourceId: event.currentTarget.value },
- })
- }
- />
-
+ {overrideAudienceAllowed && (
+ <>
+ Azure Configuration
+
+
+
+
+
+
+ {overrideAudienceChecked && (
+
+
+
+
+
+ )}
-
-
+ >
+ )}
>
);
};
diff --git a/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx
index 692e7cfe24d..82d80b12a65 100644
--- a/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx
+++ b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx
@@ -18,7 +18,7 @@ export const ConfigEditor = (props: Props) => {
const alertmanagers = getAllAlertmanagerDataSources();
const azureAuthSettings = {
- azureAuthSupported: config.featureToggles['prometheus_azure_auth'] ?? false,
+ azureAuthSupported: !!config.featureToggles.prometheus_azure_auth,
getAzureAuthEnabled: (config: DataSourceSettings ): boolean => hasCredentials(config),
setAzureAuthEnabled: (config: DataSourceSettings, enabled: boolean) =>
enabled ? setDefaultCredentials(config) : resetCredentials(config),
diff --git a/public/app/plugins/datasource/prometheus/datasource.tsx b/public/app/plugins/datasource/prometheus/datasource.tsx
index 420d203c817..9c5426548d7 100644
--- a/public/app/plugins/datasource/prometheus/datasource.tsx
+++ b/public/app/plugins/datasource/prometheus/datasource.tsx
@@ -40,6 +40,7 @@ import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_sr
import { PromApplication, PromApiFeatures } from 'app/types/unified-alerting-dto';
import { addLabelToQuery } from './add_label_to_query';
+import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
import PrometheusLanguageProvider from './language_provider';
import { expandRecordingRules } from './language_utils';
import { renderLegendFormat } from './legend';
@@ -61,7 +62,7 @@ import {
} from './types';
import { PrometheusVariableSupport } from './variables';
-export const ANNOTATION_QUERY_STEP_DEFAULT = '60s';
+const ANNOTATION_QUERY_STEP_DEFAULT = '60s';
const GET_AND_POST_METADATA_ENDPOINTS = ['api/v1/query', 'api/v1/query_range', 'api/v1/series', 'api/v1/labels'];
export class PrometheusDatasource
@@ -119,6 +120,14 @@ export class PrometheusDatasource
this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters);
this.variables = new PrometheusVariableSupport(this, this.templateSrv, this.timeSrv);
this.exemplarsAvailable = true;
+
+ // This needs to be here and cannot be static because of how annotations typing affects casting of data source
+ // objects to DataSourceApi types.
+ // We don't use the default processing for prometheus.
+ // See standardAnnotationSupport.ts/[shouldUseMappingUI|shouldUseLegacyRunner]
+ this.annotations = {
+ QueryEditor: AnnotationQueryEditor,
+ };
}
init = async () => {
diff --git a/public/app/plugins/datasource/prometheus/module.test.ts b/public/app/plugins/datasource/prometheus/module.test.ts
index af031a9ee84..171fd491637 100644
--- a/public/app/plugins/datasource/prometheus/module.test.ts
+++ b/public/app/plugins/datasource/prometheus/module.test.ts
@@ -1,13 +1,7 @@
-import { ANNOTATION_QUERY_STEP_DEFAULT } from './datasource';
import { plugin as PrometheusDatasourcePlugin } from './module';
describe('module', () => {
it('should have metrics query field in panels and Explore', () => {
expect(PrometheusDatasourcePlugin.components.QueryEditor).toBeDefined();
});
- it('should have stepDefaultValuePlaceholder set in annotations ctrl', () => {
- expect(PrometheusDatasourcePlugin.components.AnnotationsQueryCtrl).toBeDefined();
- const annotationsCtrl = new PrometheusDatasourcePlugin.components.AnnotationsQueryCtrl();
- expect(annotationsCtrl.stepDefaultValuePlaceholder).toEqual(ANNOTATION_QUERY_STEP_DEFAULT);
- });
});
diff --git a/public/app/plugins/datasource/prometheus/module.ts b/public/app/plugins/datasource/prometheus/module.ts
index 06c047eeaf2..ba33b7f90cb 100644
--- a/public/app/plugins/datasource/prometheus/module.ts
+++ b/public/app/plugins/datasource/prometheus/module.ts
@@ -3,15 +3,9 @@ import { DataSourcePlugin } from '@grafana/data';
import PromCheatSheet from './components/PromCheatSheet';
import PromQueryEditorByApp from './components/PromQueryEditorByApp';
import { ConfigEditor } from './configuration/ConfigEditor';
-import { ANNOTATION_QUERY_STEP_DEFAULT, PrometheusDatasource } from './datasource';
-
-class PrometheusAnnotationsQueryCtrl {
- static templateUrl = 'partials/annotations.editor.html';
- stepDefaultValuePlaceholder = ANNOTATION_QUERY_STEP_DEFAULT;
-}
+import { PrometheusDatasource } from './datasource';
export const plugin = new DataSourcePlugin(PrometheusDatasource)
.setQueryEditor(PromQueryEditorByApp)
.setConfigEditor(ConfigEditor)
- .setAnnotationQueryCtrl(PrometheusAnnotationsQueryCtrl)
.setQueryEditorHelp(PromCheatSheet);
diff --git a/public/app/plugins/datasource/prometheus/partials/annotations.editor.html b/public/app/plugins/datasource/prometheus/partials/annotations.editor.html
deleted file mode 100644
index c7c54e816e1..00000000000
--- a/public/app/plugins/datasource/prometheus/partials/annotations.editor.html
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx
index ab95e2798e4..941bb1e11f3 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx
+++ b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx
@@ -3,7 +3,6 @@ import React, { useState } from 'react';
import { DataSourceApi, SelectableValue, toOption } from '@grafana/data';
import { Select } from '@grafana/ui';
-import { PrometheusDatasource } from '../../datasource';
import { promQueryModeller } from '../PromQueryModeller';
import { getOperationParamId } from '../shared/operationUtils';
import { QueryBuilderLabelFilter, QueryBuilderOperationParamEditorProps } from '../shared/types';
@@ -49,8 +48,8 @@ async function loadGroupByLabels(
): Promise>> {
let labels: QueryBuilderLabelFilter[] = query.labels;
- // This function is used by both Prometheus and Loki and this the only difference
- if (datasource instanceof PrometheusDatasource) {
+ // This function is used by both Prometheus and Loki and this the only difference.
+ if (datasource.type === 'prometheus') {
labels = [{ label: '__name__', op: '=', value: query.metric }, ...query.labels];
}
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx
index a4df0c00284..9fc5fe05d38 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx
+++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx
@@ -78,6 +78,14 @@ function setup(queryOverrides: Partial = {}) {
},
onRunQuery: jest.fn(),
onChange: jest.fn(),
+ uiOptions: {
+ exemplars: true,
+ type: true,
+ format: true,
+ minStep: true,
+ legend: true,
+ resolution: true,
+ },
};
const { container } = render();
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx
index 67ef8b17e7b..a071fe5230f 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx
+++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx
@@ -12,6 +12,15 @@ import { QueryOptionGroup } from '../shared/QueryOptionGroup';
import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor';
+export interface UIOptions {
+ exemplars: boolean;
+ type: boolean;
+ format: boolean;
+ minStep: boolean;
+ legend: boolean;
+ resolution: boolean;
+}
+
export interface Props {
query: PromQuery;
app?: CoreApp;
@@ -118,11 +127,7 @@ function getCollapsedInfo(query: PromQuery, formatOption: string, queryType: str
items.push(`Legend: ${getLegendModeLabel(query.legendFormat)}`);
items.push(`Format: ${formatOption}`);
-
- if (query.interval) {
- items.push(`Step ${query.interval}`);
- }
-
+ items.push(`Step ${query.interval}`);
items.push(`Type: ${queryType}`);
if (query.exemplar) {
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx
index 1c0d61b519a..cbb5f0801bc 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx
+++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx
@@ -20,7 +20,9 @@ import { PromQueryBuilderExplained } from './PromQueryBuilderExplained';
import { PromQueryBuilderOptions } from './PromQueryBuilderOptions';
import { PromQueryCodeEditor } from './PromQueryCodeEditor';
-export const PromQueryEditorSelector = React.memo((props) => {
+type Props = PromQueryEditorProps;
+
+export const PromQueryEditorSelector = React.memo((props) => {
const { onChange, onRunQuery, data, app } = props;
const [parseModalOpen, setParseModalOpen] = useState(false);
const [dataIsStale, setDataIsStale] = useState(false);
diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
index da0feae77c7..194b7ceea09 100644
--- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
+++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
@@ -4,7 +4,8 @@ import React, { useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { GrafanaTheme2, PanelProps } from '@grafana/data';
-import { CustomScrollbar, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
+import { Alert, CustomScrollbar, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
+import { contextSrv } from 'app/core/services/context_srv';
import alertDef from 'app/features/alerting/state/alertDef';
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions';
@@ -17,6 +18,7 @@ import {
} from 'app/features/alerting/unified/utils/datasource';
import { flattenRules, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
+import { AccessControlAction } from 'app/types';
import { PromRuleWithLocation } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
@@ -60,6 +62,15 @@ export function UnifiedAlertList(props: PanelProps) {
const noAlertsMessage = rules.length ? '' : 'No alerts';
+ if (
+ !contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) &&
+ !contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead)
+ ) {
+ return (
+ Sorry, you do not have the required permissions to read alert rules
+ );
+ }
+
return (
diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx
index 7176739fa1f..c42b7a2fdf2 100644
--- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx
+++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx
@@ -316,7 +316,7 @@ export const CandlestickPanel: React.FC = ({
/>
)}
-
+
>
);
}}
diff --git a/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx b/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx
index 1080cd38179..81cbc37e423 100644
--- a/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx
+++ b/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx
@@ -48,6 +48,12 @@ export const PlacementEditor: FC {
+ setTimeout(() => {
+ settings.scene.select({ targets: [element.div!] });
+ });
+ };
+
const onHorizontalConstraintSelect = (h: SelectableValue) => {
onHorizontalConstraintChange(h.value!);
};
@@ -57,6 +63,7 @@ export const PlacementEditor: FC) => {
@@ -68,16 +75,14 @@ export const PlacementEditor: FC {
element.options.placement![placement] = value ?? element.options.placement![placement];
element.applyLayoutStylesToDiv();
settings.scene.clearCurrentSelection(true);
- // TODO: This needs to have a better sync method with where div is
- setTimeout(() => {
- settings.scene.select({ targets: [element.div!] });
- }, 100);
+ reselectElementAfterChange();
};
const constraint = element.tempConstraint ?? layout ?? {};
diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts
index 51d5e068f62..b0db3635b80 100644
--- a/public/app/plugins/panel/graph/data_processor.ts
+++ b/public/app/plugins/panel/graph/data_processor.ts
@@ -1,15 +1,6 @@
import { find } from 'lodash';
-import {
- DataFrame,
- dateTime,
- Field,
- FieldType,
- getColorForTheme,
- getFieldDisplayName,
- getTimeField,
- TimeRange,
-} from '@grafana/data';
+import { DataFrame, dateTime, Field, FieldType, getFieldDisplayName, getTimeField, TimeRange } from '@grafana/data';
import { colors } from '@grafana/ui';
import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold';
import config from 'app/core/config';
@@ -89,7 +80,7 @@ export class DataProcessor {
const series = new TimeSeries({
datapoints: datapoints || [],
alias: alias,
- color: getColorForTheme(color, config.theme),
+ color: config.theme.visualization.getColorByName(color),
unit: field.config ? field.config.unit : undefined,
dataFrameIndex,
fieldIndex,
diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts
index 4d04007096a..7c2d7ade2e9 100644
--- a/public/app/plugins/panel/graph/module.ts
+++ b/public/app/plugins/panel/graph/module.ts
@@ -8,7 +8,7 @@ import './event_editor';
import { auto } from 'angular';
import { defaults, find, without } from 'lodash';
-import { DataFrame, FieldConfigProperty, getColorForTheme, PanelEvents, PanelPlugin } from '@grafana/data';
+import { DataFrame, FieldConfigProperty, PanelEvents, PanelPlugin } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { MetricsPanelCtrl } from 'app/angular/panel/metrics_panel_ctrl';
import config from 'app/core/config';
@@ -297,7 +297,7 @@ export class GraphCtrl extends MetricsPanelCtrl {
}
onColorChange = (series: any, color: string) => {
- series.setColor(getColorForTheme(color, config.theme));
+ series.setColor(config.theme.visualization.getColorByName(color));
this.panel.aliasColors[series.alias] = color;
this.render();
};
diff --git a/public/app/plugins/panel/graph/threshold_manager.ts b/public/app/plugins/panel/graph/threshold_manager.ts
index 533ef94392b..599976a8bfb 100644
--- a/public/app/plugins/panel/graph/threshold_manager.ts
+++ b/public/app/plugins/panel/graph/threshold_manager.ts
@@ -2,7 +2,6 @@ import 'vendor/flot/jquery.flot';
import $ from 'jquery';
import { isNumber } from 'lodash';
-import { getColorForTheme } from '@grafana/data';
import { PanelCtrl } from 'app/angular/panel/panel_ctrl';
import { config } from 'app/core/config';
import { CoreEvents } from 'app/types';
@@ -235,12 +234,12 @@ export class ThresholdManager {
if (threshold.yaxis === 'right' && this.hasSecondYAxis) {
options.grid.markings.push({
y2axis: { from: threshold.value, to: limit },
- color: getColorForTheme(fillColor, config.theme),
+ color: config.theme.visualization.getColorByName(fillColor),
});
} else {
options.grid.markings.push({
yaxis: { from: threshold.value, to: limit },
- color: getColorForTheme(fillColor, config.theme),
+ color: config.theme.visualization.getColorByName(fillColor),
});
}
}
@@ -248,12 +247,12 @@ export class ThresholdManager {
if (threshold.yaxis === 'right' && this.hasSecondYAxis) {
options.grid.markings.push({
y2axis: { from: threshold.value, to: threshold.value },
- color: getColorForTheme(lineColor, config.theme),
+ color: config.theme.visualization.getColorByName(lineColor),
});
} else {
options.grid.markings.push({
yaxis: { from: threshold.value, to: threshold.value },
- color: getColorForTheme(lineColor, config.theme),
+ color: config.theme.visualization.getColorByName(lineColor),
});
}
}
diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts
index cd83a083871..8c5272e52cd 100644
--- a/public/app/plugins/panel/graph/time_region_manager.ts
+++ b/public/app/plugins/panel/graph/time_region_manager.ts
@@ -1,7 +1,7 @@
import 'vendor/flot/jquery.flot';
import { map } from 'lodash';
-import { getColorForTheme, dateTime, DateTime, AbsoluteTimeRange, GrafanaTheme } from '@grafana/data';
+import { dateTime, DateTime, AbsoluteTimeRange, GrafanaTheme } from '@grafana/data';
import { config } from 'app/core/config';
type TimeRegionColorDefinition = {
@@ -51,8 +51,8 @@ function getColor(timeRegion: any, theme: GrafanaTheme): TimeRegionColorDefiniti
if (timeRegion.colorMode === 'custom') {
return {
- fill: timeRegion.fill && timeRegion.fillColor ? getColorForTheme(timeRegion.fillColor, theme) : null,
- line: timeRegion.line && timeRegion.lineColor ? getColorForTheme(timeRegion.lineColor, theme) : null,
+ fill: timeRegion.fill && timeRegion.fillColor ? theme.visualization.getColorByName(timeRegion.fillColor) : null,
+ line: timeRegion.line && timeRegion.lineColor ? theme.visualization.getColorByName(timeRegion.lineColor) : null,
};
}
@@ -63,8 +63,8 @@ function getColor(timeRegion: any, theme: GrafanaTheme): TimeRegionColorDefiniti
}
return {
- fill: timeRegion.fill ? getColorForTheme(colorMode.color.fill, theme) : null,
- line: timeRegion.fill ? getColorForTheme(colorMode.color.line, theme) : null,
+ fill: timeRegion.fill ? theme.visualization.getColorByName(colorMode.color.fill) : null,
+ line: timeRegion.fill ? theme.visualization.getColorByName(colorMode.color.line) : null,
};
}
diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx
index 09040bb6836..01ce263be9b 100644
--- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx
+++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx
@@ -115,6 +115,7 @@ export const HeatmapPanel: React.FC = ({
cellGap: options.cellGap,
hideThreshold: options.hideThreshold,
exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)',
+ yAxisReverse: options.yAxisReverse,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options, data.structureRev]);
diff --git a/public/app/plugins/panel/heatmap-new/migrations.test.ts b/public/app/plugins/panel/heatmap-new/migrations.test.ts
index 276eb7fdb4d..8d8c5bf8018 100644
--- a/public/app/plugins/panel/heatmap-new/migrations.test.ts
+++ b/public/app/plugins/panel/heatmap-new/migrations.test.ts
@@ -43,7 +43,7 @@ describe('Heatmap Migrations', () => {
"mode": "scheme",
"scale": "exponential",
"scheme": "BuGn",
- "steps": 256,
+ "steps": 128,
},
"exemplars": Object {
"color": "rgba(255,0,255,0.7)",
diff --git a/public/app/plugins/panel/heatmap-new/migrations.ts b/public/app/plugins/panel/heatmap-new/migrations.ts
index 9a1530f38d5..7864f8ff4ae 100644
--- a/public/app/plugins/panel/heatmap-new/migrations.ts
+++ b/public/app/plugins/panel/heatmap-new/migrations.ts
@@ -53,7 +53,7 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
calculate,
color: {
...defaultPanelOptions.color,
- steps: 256, // best match with existing colors
+ steps: 128, // best match with existing colors
},
cellGap: asNumber(angular.cards?.cardPadding),
cellSize: asNumber(angular.cards?.cardRound),
diff --git a/public/app/plugins/panel/heatmap-new/utils.ts b/public/app/plugins/panel/heatmap-new/utils.ts
index 3197cfab7ac..15b4cb6e5a1 100644
--- a/public/app/plugins/panel/heatmap-new/utils.ts
+++ b/public/app/plugins/panel/heatmap-new/utils.ts
@@ -52,6 +52,7 @@ interface PrepConfigOpts {
exemplarColor: string;
cellGap?: number | null; // in css pixels
hideThreshold?: number;
+ yAxisReverse?: boolean;
}
export function prepConfig(opts: PrepConfigOpts) {
@@ -67,6 +68,7 @@ export function prepConfig(opts: PrepConfigOpts) {
palette,
cellGap,
hideThreshold,
+ yAxisReverse,
} = opts;
const pxRatio = devicePixelRatio;
@@ -210,14 +212,18 @@ export function prepConfig(opts: PrepConfigOpts) {
isTime: false,
// distribution: ScaleDistribution.Ordinal, // does not work with facets/scatter yet
orientation: ScaleOrientation.Vertical,
- direction: ScaleDirection.Up,
+ direction: yAxisReverse ? ScaleDirection.Down : ScaleDirection.Up,
// should be tweakable manually
distribution: shouldUseLogScale ? ScaleDistribution.Log : ScaleDistribution.Linear,
log: 2,
range: shouldUseLogScale
? undefined
: (u, dataMin, dataMax) => {
- const bucketSize = dataRef.current?.yBucketSize;
+ let bucketSize = dataRef.current?.yBucketSize;
+
+ if (bucketSize === 0) {
+ bucketSize = 1;
+ }
if (bucketSize) {
if (dataRef.current?.yLayout === BucketLayout.le) {
@@ -302,7 +308,11 @@ export function prepConfig(opts: PrepConfigOpts) {
gap: cellGap,
hideThreshold,
xAlign: dataRef.current?.xLayout === BucketLayout.le ? -1 : dataRef.current?.xLayout === BucketLayout.ge ? 1 : 0,
- yAlign: dataRef.current?.yLayout === BucketLayout.le ? -1 : dataRef.current?.yLayout === BucketLayout.ge ? 1 : 0,
+ yAlign: ((dataRef.current?.yLayout === BucketLayout.le
+ ? -1
+ : dataRef.current?.yLayout === BucketLayout.ge
+ ? 1
+ : 0) * (yAxisReverse ? -1 : 1)) as -1 | 0 | 1,
disp: {
fill: {
values: (u, seriesIdx) => {
@@ -437,7 +447,7 @@ export function heatmapPathsDense(opts: PathbuilderOpts) {
// detect x and y bin qtys by detecting layout repetition in x & y data
let yBinQty = dlen - ys.lastIndexOf(ys[0]);
let xBinQty = dlen / yBinQty;
- let yBinIncr = ys[1] - ys[0];
+ let yBinIncr = ys[1] - ys[0] || scaleY.max! - scaleY.min!;
let xBinIncr = xs[yBinQty] - xs[0];
// uniform tile sizes based on zoom level
diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts
index d6eb9892d21..71df88abe40 100644
--- a/public/app/plugins/panel/heatmap/color_legend.ts
+++ b/public/app/plugins/panel/heatmap/color_legend.ts
@@ -2,7 +2,7 @@ import * as d3 from 'd3';
import $ from 'jquery';
import { find, isEmpty, isNil, sortBy, uniq } from 'lodash';
-import { PanelEvents, getColorForTheme } from '@grafana/data';
+import { PanelEvents } from '@grafana/data';
import coreModule from 'app/angular/core_module';
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/core';
@@ -273,7 +273,7 @@ function drawSimpleOpacityLegend(elem: JQuery, options: { colorScale: string; ex
.attr('width', rangeStep)
.attr('height', legendHeight)
.attr('stroke-width', 0)
- .attr('fill', getColorForTheme(options.cardColor, config.theme))
+ .attr('fill', config.theme.visualization.getColorByName(options.cardColor))
.style('opacity', (d) => legendOpacityScale(d));
}
}
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts
index a397249286c..906224b24e2 100644
--- a/public/app/plugins/panel/heatmap/rendering.ts
+++ b/public/app/plugins/panel/heatmap/rendering.ts
@@ -5,7 +5,6 @@ import { find, isEmpty, isNaN, isNil, isString, map, max, min, toNumber } from '
import {
dateTimeFormat,
formattedValueToString,
- getColorForTheme,
getValueFormat,
LegacyGraphHoverClearEvent,
LegacyGraphHoverEvent,
@@ -660,7 +659,7 @@ export class HeatmapRenderer {
getCardColor(d: { count: any }) {
if (this.panel.color.mode === 'opacity') {
- return getColorForTheme(this.panel.color.cardColor, config.theme);
+ return config.theme.visualization.getColorByName(this.panel.color.cardColor);
} else {
return this.colorScale(d.count);
}
diff --git a/public/app/plugins/panel/nodeGraph/Legend.tsx b/public/app/plugins/panel/nodeGraph/Legend.tsx
index f2f7184b16e..350eaf6b62f 100644
--- a/public/app/plugins/panel/nodeGraph/Legend.tsx
+++ b/public/app/plugins/panel/nodeGraph/Legend.tsx
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { identity } from 'lodash';
import React, { useCallback } from 'react';
-import { Field, FieldColorModeId, getColorForTheme, GrafanaTheme } from '@grafana/data';
+import { Field, FieldColorModeId, GrafanaTheme } from '@grafana/data';
import { LegendDisplayMode } from '@grafana/schema';
import { Icon, useStyles, useTheme, VizLegend, VizLegendItem, VizLegendListItem } from '@grafana/ui';
@@ -97,14 +97,14 @@ function getColorLegendItems(nodes: NodeDatum[], theme: GrafanaTheme): Array>(props: Props
{allowConfiguration && (
- setShowConfig((showConfig) => !showConfig)}>
+ setShowConfig((showConfig) => !showConfig)}>
{showConfig ? 'Hide config' : 'Show config'}
)}
diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx
index 54d5ea2f0b0..3a0f899c06a 100644
--- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx
+++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx
@@ -118,7 +118,7 @@ export const StateTimelinePanel: React.FC = ({
timeZone={timeZone}
renderTooltip={renderCustomTooltip}
/>
-
+
>
);
}}
diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx
index 3cfea9dc0cb..8a1fcbc2a78 100644
--- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx
+++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx
@@ -72,7 +72,7 @@ export const StatusHistoryPanel: React.FC = ({
<>
-
+
>
);
}}
diff --git a/public/app/plugins/panel/table-old/renderer.ts b/public/app/plugins/panel/table-old/renderer.ts
index 550ad34adc9..e79a7439b94 100644
--- a/public/app/plugins/panel/table-old/renderer.ts
+++ b/public/app/plugins/panel/table-old/renderer.ts
@@ -12,7 +12,6 @@ import {
TimeZone,
dateTimeFormatISO,
dateTimeFormat,
- getColorForTheme,
GrafanaTheme,
} from '@grafana/data';
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
@@ -77,10 +76,10 @@ export class TableRenderer {
}
for (let i = style.thresholds.length; i > 0; i--) {
if (value >= style.thresholds[i - 1]) {
- return getColorForTheme(style.colors[i], this.theme);
+ return this.theme.visualization.getColorByName(style.colors[i]);
}
}
- return getColorForTheme(first(style.colors), this.theme);
+ return this.theme.visualization.getColorByName(first(style.colors));
}
defaultCellFormatter(v: any, style: ColumnStyle) {
diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx
index d80d4234842..9e35be29ec8 100644
--- a/public/app/plugins/panel/table/TablePanel.tsx
+++ b/public/app/plugins/panel/table/TablePanel.tsx
@@ -145,7 +145,7 @@ export class TablePanel extends Component {
return (
- {this.renderTable(data.series[currentIndex], width, height - inputHeight + padding)}
+ {this.renderTable(data.series[currentIndex], width, height - inputHeight - padding)}
diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx
index 9e143d4f976..e727764fa16 100644
--- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx
+++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx
@@ -138,7 +138,7 @@ export const TimeSeriesPanel: React.FC = ({
/>
)}
-
+
>
);
}}
diff --git a/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap b/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap
index 54174fbd013..243f9f77801 100644
--- a/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap
+++ b/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap
@@ -12,7 +12,7 @@ Object {
"lineInterpolation": "stepAfter",
"lineWidth": 5,
"showPoints": "never",
- "spanNulls": true,
+ "spanNulls": false,
},
"nullValueMode": "null",
"unit": "short",
@@ -120,7 +120,7 @@ Object {
"lineInterpolation": "stepAfter",
"lineWidth": 1,
"showPoints": "never",
- "spanNulls": true,
+ "spanNulls": false,
},
"nullValueMode": "null",
"unit": "short",
@@ -141,6 +141,21 @@ Object {
},
],
},
+ Object {
+ "matcher": Object {
+ "id": "byName",
+ "options": "B-series",
+ },
+ "properties": Array [
+ Object {
+ "id": "color",
+ "value": Object {
+ "fixedColor": "rgba(16, 72, 170, 0.77)",
+ "mode": "fixed",
+ },
+ },
+ ],
+ },
],
},
"options": Object {
@@ -176,7 +191,7 @@ Object {
"lineInterpolation": "stepAfter",
"lineWidth": 1,
"showPoints": "never",
- "spanNulls": true,
+ "spanNulls": false,
},
"nullValueMode": "null",
"unit": "short",
@@ -197,6 +212,21 @@ Object {
},
],
},
+ Object {
+ "matcher": Object {
+ "id": "byRegexp",
+ "options": "/.*Status: 2[0-9]+.*/i",
+ },
+ "properties": Array [
+ Object {
+ "id": "color",
+ "value": Object {
+ "fixedColor": "rgba(16, 72, 170, 0.77)",
+ "mode": "fixed",
+ },
+ },
+ ],
+ },
],
},
"options": Object {
@@ -259,7 +289,7 @@ Object {
"lineInterpolation": "stepAfter",
"lineWidth": 5,
"showPoints": "never",
- "spanNulls": true,
+ "spanNulls": false,
"stacking": Object {
"group": "A",
"mode": "normal",
@@ -346,7 +376,7 @@ Object {
"lineInterpolation": "stepAfter",
"lineWidth": 5,
"showPoints": "never",
- "spanNulls": true,
+ "spanNulls": false,
"stacking": Object {
"group": "A",
"mode": "normal",
@@ -404,7 +434,7 @@ Object {
"lineInterpolation": "stepAfter",
"lineWidth": 1,
"showPoints": "never",
- "spanNulls": true,
+ "spanNulls": false,
},
"displayName": "DISPLAY NAME",
"nullValueMode": "null",
@@ -444,7 +474,7 @@ Object {
"lineInterpolation": "stepAfter",
"lineWidth": 5,
"showPoints": "never",
- "spanNulls": true,
+ "spanNulls": false,
},
"nullValueMode": "null",
"unit": "short",
@@ -554,7 +584,7 @@ Object {
"type": "log",
},
"showPoints": "never",
- "spanNulls": true,
+ "spanNulls": false,
},
"decimals": 3,
"max": 1000,
diff --git a/public/app/plugins/panel/timeseries/migrations.test.ts b/public/app/plugins/panel/timeseries/migrations.test.ts
index 767c528464a..14310ce9fb6 100644
--- a/public/app/plugins/panel/timeseries/migrations.test.ts
+++ b/public/app/plugins/panel/timeseries/migrations.test.ts
@@ -1,6 +1,6 @@
import { cloneDeep } from 'lodash';
-import { PanelModel, FieldConfigSource } from '@grafana/data';
+import { PanelModel, FieldConfigSource, FieldMatcherID } from '@grafana/data';
import { TooltipDisplayMode, SortOrder } from '@grafana/schema';
import { graphPanelChangedHandler } from './migrations';
@@ -78,6 +78,8 @@ describe('Graph Migrations', () => {
const panel = {} as PanelModel;
panel.options = graphPanelChangedHandler(panel, 'graph', old, prevFieldConfig);
expect(panel).toMatchSnapshot();
+ expect(panel.fieldConfig.overrides[0].matcher.id).toBe(FieldMatcherID.byRegexp);
+ expect(panel.fieldConfig.overrides[1].matcher.id).toBe(FieldMatcherID.byRegexp);
});
describe('legend', () => {
@@ -426,6 +428,29 @@ describe('Graph Migrations', () => {
expect(panel.fieldConfig).toMatchSnapshot();
});
});
+
+ describe('null values', () => {
+ test('nullPointMode = null', () => {
+ const old: any = {
+ angular: {
+ nullPointMode: 'null',
+ },
+ };
+ const panel = {} as PanelModel;
+ panel.options = graphPanelChangedHandler(panel, 'graph', old, prevFieldConfig);
+ expect(panel.fieldConfig.defaults.custom.spanNulls).toBeFalsy();
+ });
+ test('nullPointMode = connected', () => {
+ const old: any = {
+ angular: {
+ nullPointMode: 'connected',
+ },
+ };
+ const panel = {} as PanelModel;
+ panel.options = graphPanelChangedHandler(panel, 'graph', old, prevFieldConfig);
+ expect(panel.fieldConfig.defaults.custom.spanNulls).toBeTruthy();
+ });
+ });
});
const customColor = {
@@ -456,6 +481,11 @@ const customColor = {
alias: 'A-series',
color: 'rgba(165, 72, 170, 0.77)',
},
+ {
+ $$hashKey: 'object:13',
+ alias: 'B-series',
+ color: 'rgba(16, 72, 170, 0.77)',
+ },
],
spaceLength: 10,
steppedLine: true,
@@ -513,6 +543,7 @@ const customColor = {
const customColorRegex = cloneDeep(customColor);
customColorRegex.seriesOverrides[0].alias = '/^A-/';
+customColorRegex.seriesOverrides[1].alias = '/.*Status: 2[0-9]+.*/i';
const stairscase = {
aliasColors: {},
diff --git a/public/app/plugins/panel/timeseries/migrations.ts b/public/app/plugins/panel/timeseries/migrations.ts
index 56beb024450..e27b75fc8b1 100644
--- a/public/app/plugins/panel/timeseries/migrations.ts
+++ b/public/app/plugins/panel/timeseries/migrations.ts
@@ -119,7 +119,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
if (!seriesOverride.alias) {
continue; // the matcher config
}
- const aliasIsRegex = seriesOverride.alias.startsWith('/') && seriesOverride.alias.endsWith('/');
+ const aliasIsRegex = /^([/~@;%#'])(.*?)\1([gimsuy]*)$/.test(seriesOverride.alias);
const rule: ConfigOverrideRule = {
matcher: {
id: aliasIsRegex ? FieldMatcherID.byRegexp : FieldMatcherID.byName,
@@ -296,7 +296,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
graph.fillOpacity = angular.fillGradient * 10; // fill is 0-10
}
- graph.spanNulls = angular.nullPointMode === NullValueMode.Null;
+ graph.spanNulls = angular.nullPointMode === NullValueMode.Ignore;
if (angular.steppedLine) {
graph.lineInterpolation = LineInterpolation.StepAfter;
diff --git a/public/app/plugins/panel/timeseries/plugins/OutsideRangePlugin.tsx b/public/app/plugins/panel/timeseries/plugins/OutsideRangePlugin.tsx
index 6cf3cd30532..721c9f45bb3 100644
--- a/public/app/plugins/panel/timeseries/plugins/OutsideRangePlugin.tsx
+++ b/public/app/plugins/panel/timeseries/plugins/OutsideRangePlugin.tsx
@@ -1,34 +1,43 @@
-import React, { useLayoutEffect, useRef } from 'react';
-import uPlot from 'uplot';
+import React, { useLayoutEffect, useRef, useState } from 'react';
+import uPlot, { TypedArray, Scale } from 'uplot';
-import { TimeRange, AbsoluteTimeRange } from '@grafana/data';
+import { AbsoluteTimeRange } from '@grafana/data';
import { UPlotConfigBuilder, Button } from '@grafana/ui';
interface ThresholdControlsPluginProps {
config: UPlotConfigBuilder;
- range: TimeRange;
onChangeTimeRange: (timeRange: AbsoluteTimeRange) => void;
}
-export const OutsideRangePlugin: React.FC = ({ config, range, onChangeTimeRange }) => {
+export const OutsideRangePlugin: React.FC = ({ config, onChangeTimeRange }) => {
const plotInstance = useRef();
+ const [timevalues, setTimeValues] = useState([]);
+ const [timeRange, setTimeRange] = useState();
useLayoutEffect(() => {
config.addHook('init', (u) => {
plotInstance.current = u;
});
+
+ config.addHook('setScale', (u) => {
+ setTimeValues(u.data?.[0] ?? []);
+ setTimeRange(u.scales['x'] ?? undefined);
+ });
}, [config]);
- const timevalues = plotInstance.current?.data?.[0];
- if (!timevalues || !plotInstance.current || timevalues.length < 2 || !onChangeTimeRange) {
+ if (timevalues.length < 2 || !onChangeTimeRange) {
+ return null;
+ }
+
+ if (!timeRange || !timeRange.time || !timeRange.min || !timeRange.max!) {
return null;
}
// Time values are always sorted for uPlot to work
const first = timevalues[0];
const last = timevalues[timevalues.length - 1];
- const fromX = range.from.valueOf();
- const toX = range.to.valueOf();
+ const fromX = timeRange.min;
+ const toX = timeRange.max;
// (StartA <= EndB) and (EndA >= StartB)
if (first <= toX && last >= fromX) {
diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts
index 22bcf3d14c0..d98c1127f4c 100644
--- a/public/app/types/accessControl.ts
+++ b/public/app/types/accessControl.ts
@@ -71,14 +71,14 @@ export enum AccessControlAction {
DashboardsDelete = 'dashboards:delete',
DashboardsCreate = 'dashboards:create',
DashboardsPermissionsRead = 'dashboards.permissions:read',
- DashboardsPermissionsWrite = 'dashboards.permissions:read',
+ DashboardsPermissionsWrite = 'dashboards.permissions:write',
FoldersRead = 'folders:read',
- FoldersWrite = 'folders:read',
+ FoldersWrite = 'folders:write',
FoldersDelete = 'folders:delete',
FoldersCreate = 'folders:create',
FoldersPermissionsRead = 'folders.permissions:read',
- FoldersPermissionsWrite = 'folders.permissions:read',
+ FoldersPermissionsWrite = 'folders.permissions:write',
// Alerting rules
AlertingRuleCreate = 'alert.rules:create',
diff --git a/public/app/types/sanitize-url.d.ts b/public/app/types/sanitize-url.d.ts
deleted file mode 100644
index 9f6af49c606..00000000000
--- a/public/app/types/sanitize-url.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-declare module '@braintree/sanitize-url' {
- function sanitizeUrl(url: string): string;
-}
diff --git a/public/sass/components/_dropdown.scss b/public/sass/components/_dropdown.scss
index f3613f6b7da..ee69ba95656 100644
--- a/public/sass/components/_dropdown.scss
+++ b/public/sass/components/_dropdown.scss
@@ -207,8 +207,8 @@
// Right aligned dropdowns
// ---------------------------
.pull-right > .dropdown-menu {
- right: 0;
- left: auto;
+ left: 100%;
+ right: unset;
}
// Allow for dropdowns to go bottom up (aka, dropup-menu)
@@ -243,7 +243,6 @@
left: 100%;
margin-top: 0px;
margin-left: -1px;
- @include border-radius(0 6px 6px 6px);
}
.dropdown-submenu:hover > .dropdown-menu {
display: block;
@@ -255,7 +254,6 @@
bottom: 0;
margin-top: 0;
margin-bottom: -2px;
- @include border-radius(5px 5px 5px 0);
}
// .dropdown-submenu > a::after {
// position: absolute;
@@ -284,7 +282,6 @@
left: -100%;
width: 100%;
margin-left: 2px;
- @include border-radius(6px 0 6px 6px);
}
}
diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss
index b7e15c0701d..ad87d3621c1 100644
--- a/public/sass/components/_view_states.scss
+++ b/public/sass/components/_view_states.scss
@@ -56,7 +56,7 @@
}
@include media-breakpoint-down(sm) {
- div.page-toolbar {
+ nav.page-toolbar {
padding-left: 53px;
&--fullscreen {
diff --git a/public/sass/theme.dark.generated.json b/public/sass/theme.dark.generated.json
index 0952f01c4a5..178b38b2614 100644
--- a/public/sass/theme.dark.generated.json
+++ b/public/sass/theme.dark.generated.json
@@ -152,6 +152,10 @@
"menuTabs": {
"height": 41
},
+ "textHighlight": {
+ "text": "#000000",
+ "background": "#F5B73D"
+ },
"horizontalDrawer": {
"defaultHeight": 400
}
diff --git a/public/sass/theme.light.generated.json b/public/sass/theme.light.generated.json
index 9e1b08ec313..ddd149cd45c 100644
--- a/public/sass/theme.light.generated.json
+++ b/public/sass/theme.light.generated.json
@@ -152,6 +152,10 @@
"menuTabs": {
"height": 41
},
+ "textHighlight": {
+ "text": "#000000",
+ "background": "#FAD34A"
+ },
"horizontalDrawer": {
"defaultHeight": 400
}
diff --git a/public/vendor/angular-other/angular-strap.js b/public/vendor/angular-other/angular-strap.js
deleted file mode 100644
index 3670f3f8ac4..00000000000
--- a/public/vendor/angular-other/angular-strap.js
+++ /dev/null
@@ -1,198 +0,0 @@
-/**
- * AngularStrap - Twitter Bootstrap directives for AngularJS
- * @version v0.7.5 - 2013-07-21
- * @link http://mgcrea.github.com/angular-strap
- * @author Olivier Louvignes
- * @license MIT License, http://www.opensource.org/licenses/MIT
- */
-angular.module('$strap.config', []).value('$strapConfig', {});
-angular.module('$strap.filters', ['$strap.config']);
-angular.module('$strap.directives', ['$strap.config']);
-angular.module('$strap', [
- '$strap.filters',
- '$strap.directives',
- '$strap.config'
-]);
-'use strict';
-angular.module('$strap.directives').factory('$modal', [
- '$rootScope',
- '$compile',
- '$http',
- '$timeout',
- '$q',
- '$templateCache',
- '$strapConfig',
- function ($rootScope, $compile, $http, $timeout, $q, $templateCache, $strapConfig) {
- var ModalFactory = function ModalFactory(config) {
- function Modal(config) {
- var options = angular.extend({ show: true }, $strapConfig.modal, config);
- var scope = options.scope ? options.scope : $rootScope.$new()
- var templateUrl = options.template;
- return $q.when(options.templateHtml || $templateCache.get(templateUrl) || $http.get(templateUrl, { cache: true }).then(function (res) {
- return res.data;
- })).then(function onSuccess(template) {
- var id = scope.$id;
- if (templateUrl) {
- id += templateUrl.replace('.html', '').replace(/[\/|\.|:]/g, '-');
- }
- // grafana change, removed fade
- var $modal = $('').attr('id', id).html(template);
- if (options.modalClass)
- $modal.addClass(options.modalClass);
- $('body').append($modal);
- $timeout(function () {
- $compile($modal)(scope);
- });
- scope.$modal = function (name) {
- $modal.modal(name);
- };
- angular.forEach([
- 'show',
- 'hide'
- ], function (name) {
- scope[name] = function () {
- $modal.modal(name);
- };
- });
- scope.dismiss = scope.hide;
- angular.forEach([
- 'show',
- 'shown',
- 'hide',
- 'hidden'
- ], function (name) {
- $modal.on(name, function (ev) {
- scope.$emit('modal-' + name, ev);
- });
- });
- $modal.on('shown', function (ev) {
- $('input[autofocus], textarea[autofocus]', $modal).first().trigger('focus');
- });
- $modal.on('hidden', function (ev) {
- if (!options.persist)
- scope.$destroy();
- });
- scope.$on('$destroy', function () {
- $modal.remove();
- });
- $modal.modal(options);
- return $modal;
- });
- }
- return new Modal(config);
- };
- return ModalFactory;
- }
-])
-
-'use strict';
-angular.module('$strap.directives').directive('bsTooltip', [
- '$parse',
- '$compile',
- function ($parse, $compile) {
- return {
- restrict: 'A',
- scope: true,
- link: function postLink(scope, element, attrs, ctrl) {
- var getter = $parse(attrs.bsTooltip), setter = getter.assign, value = getter(scope);
- scope.$watch(attrs.bsTooltip, function (newValue, oldValue) {
- if (newValue !== oldValue) {
- value = newValue;
- }
- });
- // Grafana change, always hide other tooltips
- if (true) {
- element.on('show', function (ev) {
- $('.tooltip.in').each(function () {
- var $this = $(this), tooltip = $this.data('tooltip');
- if (tooltip && !tooltip.$element.is(element)) {
- $this.tooltip('hide');
- }
- });
- });
- }
- element.tooltip({
- title: function () {
- return angular.isFunction(value) ? value.apply(null, arguments) : value;
- },
- html: true,
- container: 'body', // Grafana change
- });
- var tooltip = element.data('tooltip');
- tooltip.show = function () {
- var r = $.fn.tooltip.Constructor.prototype.show.apply(this, arguments);
- this.tip().data('tooltip', this);
- return r;
- };
- scope._tooltip = function (event) {
- element.tooltip(event);
- };
- scope.hide = function () {
- element.tooltip('hide');
- };
- scope.show = function () {
- element.tooltip('show');
- };
- scope.dismiss = scope.hide;
- }
- };
- }
-]);
-
-'use strict';
-angular.module('$strap.directives').directive('bsTypeahead', [
- '$parse',
- function ($parse) {
- return {
- restrict: 'A',
- require: '?ngModel',
- link: function postLink(scope, element, attrs, controller) {
- var getter = $parse(attrs.bsTypeahead), setter = getter.assign, value = getter(scope);
- scope.$watch(attrs.bsTypeahead, function (newValue, oldValue) {
- if (newValue !== oldValue) {
- value = newValue;
- }
- });
- element.attr('data-provide', 'typeahead');
- element.typeahead({
- source: function (query) {
- return angular.isFunction(value) ? value.apply(null, arguments) : value;
- },
- minLength: attrs.minLength || 1,
- items: attrs.items,
- updater: function (value) {
- if (controller) {
- scope.$apply(function () {
- controller.$setViewValue(value);
- });
- }
- scope.$emit('typeahead-updated', value);
- return value;
- }
- });
- var typeahead = element.data('typeahead');
- typeahead.lookup = function (ev) {
- var items;
- this.query = this.$element.val() || '';
- if (this.query.length < this.options.minLength) {
- return this.shown ? this.hide() : this;
- }
- items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source;
- return items ? this.process(items) : this;
- };
- if (!!attrs.matchAll) {
- typeahead.matcher = function (item) {
- return true;
- };
- }
- if (attrs.minLength === '0') {
- setTimeout(function () {
- element.on('focus', function () {
- element.val().length === 0 && setTimeout(element.typeahead.bind(element, 'lookup'), 200);
- });
- });
- }
- }
- };
- }
-]);
diff --git a/public/vendor/angular-other/datepicker.js b/public/vendor/angular-other/datepicker.js
deleted file mode 100644
index 0f5f4bad610..00000000000
--- a/public/vendor/angular-other/datepicker.js
+++ /dev/null
@@ -1,1046 +0,0 @@
-/* =========================================================
- * bootstrap-datepicker.js
- * http://www.eyecon.ro/bootstrap-datepicker
- * =========================================================
- * Copyright 2012 Stefan Petre
- * Improvements by Andrew Rowls
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ========================================================= */
-
-!function( $ ) {
-
- function UTCDate(){
- return new Date(Date.UTC.apply(Date, arguments));
- }
- function UTCToday(){
- var today = new Date();
- return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
- }
-
- // Picker object
-
- var Datepicker = function(element, options) {
- var that = this;
-
- this.element = $(element);
- this.language = options.language||this.element.data('date-language')||"en";
- this.language = this.language in dates ? this.language : this.language.split('-')[0]; //Check if "de-DE" style date is available, if not language should fallback to 2 letter code eg "de"
- this.language = this.language in dates ? this.language : "en";
- this.isRTL = dates[this.language].rtl||false;
- this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||dates[this.language].format||'mm/dd/yyyy');
- this.isInline = false;
- this.isInput = this.element.is('input');
- this.component = this.element.is('.date') ? this.element.find('.add-on, .btn') : false;
- this.hasInput = this.component && this.element.find('input').length;
- if(this.component && this.component.length === 0)
- this.component = false;
-
- this.forceParse = true;
- if ('forceParse' in options) {
- this.forceParse = options.forceParse;
- } else if ('dateForceParse' in this.element.data()) {
- this.forceParse = this.element.data('date-force-parse');
- }
-
- this.picker = $(DPGlobal.template);
- this._buildEvents();
- this._attachEvents();
-
- if(this.isInline) {
- this.picker.addClass('datepicker-inline').appendTo(this.element);
- } else {
- this.picker.addClass('datepicker-dropdown dropdown-menu');
- }
- if (this.isRTL){
- this.picker.addClass('datepicker-rtl');
- this.picker.find('.prev i, .next i')
- .toggleClass('icon-arrow-left icon-arrow-right');
- }
-
- this.autoclose = false;
- if ('autoclose' in options) {
- this.autoclose = options.autoclose;
- } else if ('dateAutoclose' in this.element.data()) {
- this.autoclose = this.element.data('date-autoclose');
- }
-
- this.keyboardNavigation = true;
- if ('keyboardNavigation' in options) {
- this.keyboardNavigation = options.keyboardNavigation;
- } else if ('dateKeyboardNavigation' in this.element.data()) {
- this.keyboardNavigation = this.element.data('date-keyboard-navigation');
- }
-
- this.viewMode = this.startViewMode = 0;
- switch(options.startView || this.element.data('date-start-view')){
- case 2:
- case 'decade':
- this.viewMode = this.startViewMode = 2;
- break;
- case 1:
- case 'year':
- this.viewMode = this.startViewMode = 1;
- break;
- }
-
- this.minViewMode = options.minViewMode||this.element.data('date-min-view-mode')||0;
- if (typeof this.minViewMode === 'string') {
- switch (this.minViewMode) {
- case 'months':
- this.minViewMode = 1;
- break;
- case 'years':
- this.minViewMode = 2;
- break;
- default:
- this.minViewMode = 0;
- break;
- }
- }
-
- this.viewMode = this.startViewMode = Math.max(this.startViewMode, this.minViewMode);
-
- this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false);
- this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false);
-
- this.calendarWeeks = false;
- if ('calendarWeeks' in options) {
- this.calendarWeeks = options.calendarWeeks;
- } else if ('dateCalendarWeeks' in this.element.data()) {
- this.calendarWeeks = this.element.data('date-calendar-weeks');
- }
- if (this.calendarWeeks)
- this.picker.find('tfoot th.today')
- .attr('colspan', function(i, val){
- return parseInt(val) + 1;
- });
-
- this._allow_update = false;
-
- this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7);
- this.weekEnd = ((this.weekStart + 6) % 7);
- this.startDate = -Infinity;
- this.endDate = Infinity;
- this.daysOfWeekDisabled = [];
- this.setStartDate(options.startDate||this.element.data('date-startdate'));
- this.setEndDate(options.endDate||this.element.data('date-enddate'));
- this.setDaysOfWeekDisabled(options.daysOfWeekDisabled||this.element.data('date-days-of-week-disabled'));
- this.fillDow();
- this.fillMonths();
-
- this._allow_update = true;
-
- this.update();
- this.showMode();
-
- if(this.isInline) {
- this.show();
- }
- };
-
- Datepicker.prototype = {
- constructor: Datepicker,
-
- _events: [],
- _secondaryEvents: [],
- _applyEvents: function(evs){
- for (var i=0, el, ev; i this.endDate) {
- this.viewDate = new Date(this.endDate);
- } else {
- this.viewDate = new Date(this.date);
- }
- this.fill();
- },
-
- fillDow: function(){
- var dowCnt = this.weekStart,
- html = '';
- if(this.calendarWeeks){
- var cell = '| | ';
- html += cell;
- this.picker.find('.datepicker-days thead tr:first-child').prepend(cell);
- }
- while (dowCnt < this.weekStart + 7) {
- html += ''+dates[this.language].daysMin[(dowCnt++)%7]+' | ';
- }
- html += ' ';
- this.picker.find('.datepicker-days thead').append(html);
- },
-
- fillMonths: function(){
- var html = '',
- i = 0;
- while (i < 12) {
- html += ''+dates[this.language].monthsShort[i++]+'';
- }
- this.picker.find('.datepicker-months td').html(html);
- },
-
- fill: function() {
- var d = new Date(this.viewDate),
- year = d.getUTCFullYear(),
- month = d.getUTCMonth(),
- startYear = this.startDate !== -Infinity ? this.startDate.getUTCFullYear() : -Infinity,
- startMonth = this.startDate !== -Infinity ? this.startDate.getUTCMonth() : -Infinity,
- endYear = this.endDate !== Infinity ? this.endDate.getUTCFullYear() : Infinity,
- endMonth = this.endDate !== Infinity ? this.endDate.getUTCMonth() : Infinity,
- currentDate = this.date && this.date.valueOf(),
- today = new Date();
- this.picker.find('.datepicker-days thead th.switch')
- .text(dates[this.language].months[month]+' '+year);
- this.picker.find('tfoot th.today')
- .text(dates[this.language].today)
- .toggle(this.todayBtn !== false);
- this.updateNavArrows();
- this.fillMonths();
- var prevMonth = UTCDate(year, month-1, 28,0,0,0,0),
- day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
- prevMonth.setUTCDate(day);
- prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.weekStart + 7)%7);
- var nextMonth = new Date(prevMonth);
- nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
- nextMonth = nextMonth.valueOf();
- var html = [];
- var clsName;
- while(prevMonth.valueOf() < nextMonth) {
- if (prevMonth.getUTCDay() == this.weekStart) {
- html.push('');
- if(this.calendarWeeks){
- // ISO 8601: First week contains first thursday.
- // ISO also states week starts on Monday, but we can be more abstract here.
- var
- // Start of current week: based on weekstart/current date
- ws = new Date(+prevMonth + (this.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5),
- // Thursday of this week
- th = new Date(+ws + (7 + 4 - ws.getUTCDay()) % 7 * 864e5),
- // First Thursday of year, year from thursday
- yth = new Date(+(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay())%7*864e5),
- // Calendar week: ms between thursdays, div ms per day, div 7 days
- calWeek = (th - yth) / 864e5 / 7 + 1;
- html.push('| '+ calWeek +' | ');
-
- }
- }
- clsName = '';
- if (prevMonth.getUTCFullYear() < year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() < month)) {
- clsName += ' old';
- } else if (prevMonth.getUTCFullYear() > year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() > month)) {
- clsName += ' new';
- }
- // Compare internal UTC date with local today, not UTC today
- if (this.todayHighlight &&
- prevMonth.getUTCFullYear() == today.getFullYear() &&
- prevMonth.getUTCMonth() == today.getMonth() &&
- prevMonth.getUTCDate() == today.getDate()) {
- clsName += ' today';
- }
- if (currentDate && prevMonth.valueOf() == currentDate) {
- clsName += ' active';
- }
- if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate ||
- $.inArray(prevMonth.getUTCDay(), this.daysOfWeekDisabled) !== -1) {
- clsName += ' disabled';
- }
- html.push(''+prevMonth.getUTCDate() + ' | ');
- if (prevMonth.getUTCDay() == this.weekEnd) {
- html.push(' ');
- }
- prevMonth.setUTCDate(prevMonth.getUTCDate()+1);
- }
- this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
- var currentYear = this.date && this.date.getUTCFullYear();
-
- var months = this.picker.find('.datepicker-months')
- .find('th:eq(1)')
- .text(year)
- .end()
- .find('span').removeClass('active');
- if (currentYear && currentYear == year) {
- months.eq(this.date.getUTCMonth()).addClass('active');
- }
- if (year < startYear || year > endYear) {
- months.addClass('disabled');
- }
- if (year == startYear) {
- months.slice(0, startMonth).addClass('disabled');
- }
- if (year == endYear) {
- months.slice(endMonth+1).addClass('disabled');
- }
-
- html = '';
- year = parseInt(year/10, 10) * 10;
- var yearCont = this.picker.find('.datepicker-years')
- .find('th:eq(1)')
- .text(year + '-' + (year + 9))
- .end()
- .find('td');
- year -= 1;
- for (var i = -1; i < 11; i++) {
- html += ''+year+'';
- year += 1;
- }
- yearCont.html(html);
- },
-
- updateNavArrows: function() {
- if (!this._allow_update) return;
-
- var d = new Date(this.viewDate),
- year = d.getUTCFullYear(),
- month = d.getUTCMonth();
- switch (this.viewMode) {
- case 0:
- if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear() && month <= this.startDate.getUTCMonth()) {
- this.picker.find('.prev').css({visibility: 'hidden'});
- } else {
- this.picker.find('.prev').css({visibility: 'visible'});
- }
- if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() && month >= this.endDate.getUTCMonth()) {
- this.picker.find('.next').css({visibility: 'hidden'});
- } else {
- this.picker.find('.next').css({visibility: 'visible'});
- }
- break;
- case 1:
- case 2:
- if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear()) {
- this.picker.find('.prev').css({visibility: 'hidden'});
- } else {
- this.picker.find('.prev').css({visibility: 'visible'});
- }
- if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear()) {
- this.picker.find('.next').css({visibility: 'hidden'});
- } else {
- this.picker.find('.next').css({visibility: 'visible'});
- }
- break;
- }
- },
-
- click: function(e) {
- e.preventDefault();
- var target = $(e.target).closest('span, td, th');
- if (target.length == 1) {
- switch(target[0].nodeName.toLowerCase()) {
- case 'th':
- switch(target[0].className) {
- case 'switch':
- this.showMode(1);
- break;
- case 'prev':
- case 'next':
- var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1);
- switch(this.viewMode){
- case 0:
- this.viewDate = this.moveMonth(this.viewDate, dir);
- break;
- case 1:
- case 2:
- this.viewDate = this.moveYear(this.viewDate, dir);
- break;
- }
- this.fill();
- break;
- case 'today':
- var date = new Date();
- date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
-
- this.showMode(-2);
- var which = this.todayBtn == 'linked' ? null : 'view';
- this._setDate(date, which);
- break;
- }
- break;
- case 'span':
- if (!target.is('.disabled')) {
- this.viewDate.setUTCDate(1);
- if (target.is('.month')) {
- var day = 1;
- var month = target.parent().find('span').index(target);
- var year = this.viewDate.getUTCFullYear();
- this.viewDate.setUTCMonth(month);
- this.element.trigger({
- type: 'changeMonth',
- date: this.viewDate
- });
- if ( this.minViewMode == 1 ) {
- this._setDate(UTCDate(year, month, day,0,0,0,0));
- }
- } else {
- var year = parseInt(target.text(), 10)||0;
- var day = 1;
- var month = 0;
- this.viewDate.setUTCFullYear(year);
- this.element.trigger({
- type: 'changeYear',
- date: this.viewDate
- });
- if ( this.minViewMode == 2 ) {
- this._setDate(UTCDate(year, month, day,0,0,0,0));
- }
- }
- this.showMode(-1);
- this.fill();
- }
- break;
- case 'td':
- if (target.is('.day') && !target.is('.disabled')){
- var day = parseInt(target.text(), 10)||1;
- var year = this.viewDate.getUTCFullYear(),
- month = this.viewDate.getUTCMonth();
- if (target.is('.old')) {
- if (month === 0) {
- month = 11;
- year -= 1;
- } else {
- month -= 1;
- }
- } else if (target.is('.new')) {
- if (month == 11) {
- month = 0;
- year += 1;
- } else {
- month += 1;
- }
- }
- this._setDate(UTCDate(year, month, day,0,0,0,0));
- }
- break;
- }
- }
- },
-
- _setDate: function(date, which){
- if (!which || which == 'date')
- this.date = date;
- if (!which || which == 'view')
- this.viewDate = date;
- this.fill();
- this.setValue();
- this.element.trigger({
- type: 'changeDate',
- date: this.date
- });
- var element;
- if (this.isInput) {
- element = this.element;
- } else if (this.component){
- element = this.element.find('input');
- }
- if (element) {
- element.change();
- if (this.autoclose && (!which || which == 'date')) {
- this.hide();
- }
- }
- },
-
- moveMonth: function(date, dir){
- if (!dir) return date;
- var new_date = new Date(date.valueOf()),
- day = new_date.getUTCDate(),
- month = new_date.getUTCMonth(),
- mag = Math.abs(dir),
- new_month, test;
- dir = dir > 0 ? 1 : -1;
- if (mag == 1){
- test = dir == -1
- // If going back one month, make sure month is not current month
- // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
- ? function(){ return new_date.getUTCMonth() == month; }
- // If going forward one month, make sure month is as expected
- // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
- : function(){ return new_date.getUTCMonth() != new_month; };
- new_month = month + dir;
- new_date.setUTCMonth(new_month);
- // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
- if (new_month < 0 || new_month > 11)
- new_month = (new_month + 12) % 12;
- } else {
- // For magnitudes >1, move one month at a time...
- for (var i=0; i= this.startDate && date <= this.endDate;
- },
-
- keydown: function(e){
- if (this.picker.is(':not(:visible)')){
- if (e.keyCode == 27) // allow escape to hide and re-show picker
- this.show();
- return;
- }
- var dateChanged = false,
- dir, day, month,
- newDate, newViewDate;
- switch(e.keyCode){
- case 27: // escape
- this.hide();
- e.preventDefault();
- break;
- case 37: // left
- case 39: // right
- if (!this.keyboardNavigation) break;
- dir = e.keyCode == 37 ? -1 : 1;
- if (e.ctrlKey){
- newDate = this.moveYear(this.date, dir);
- newViewDate = this.moveYear(this.viewDate, dir);
- } else if (e.shiftKey){
- newDate = this.moveMonth(this.date, dir);
- newViewDate = this.moveMonth(this.viewDate, dir);
- } else {
- newDate = new Date(this.date);
- newDate.setUTCDate(this.date.getUTCDate() + dir);
- newViewDate = new Date(this.viewDate);
- newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir);
- }
- if (this.dateWithinRange(newDate)){
- this.date = newDate;
- this.viewDate = newViewDate;
- this.setValue();
- this.update();
- e.preventDefault();
- dateChanged = true;
- }
- break;
- case 38: // up
- case 40: // down
- if (!this.keyboardNavigation) break;
- dir = e.keyCode == 38 ? -1 : 1;
- if (e.ctrlKey){
- newDate = this.moveYear(this.date, dir);
- newViewDate = this.moveYear(this.viewDate, dir);
- } else if (e.shiftKey){
- newDate = this.moveMonth(this.date, dir);
- newViewDate = this.moveMonth(this.viewDate, dir);
- } else {
- newDate = new Date(this.date);
- newDate.setUTCDate(this.date.getUTCDate() + dir * 7);
- newViewDate = new Date(this.viewDate);
- newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7);
- }
- if (this.dateWithinRange(newDate)){
- this.date = newDate;
- this.viewDate = newViewDate;
- this.setValue();
- this.update();
- e.preventDefault();
- dateChanged = true;
- }
- break;
- case 13: // enter
- this.hide();
- e.preventDefault();
- break;
- case 9: // tab
- this.hide();
- break;
- }
- if (dateChanged){
- this.element.trigger({
- type: 'changeDate',
- date: this.date
- });
- var element;
- if (this.isInput) {
- element = this.element;
- } else if (this.component){
- element = this.element.find('input');
- }
- if (element) {
- element.change();
- }
- }
- },
-
- showMode: function(dir) {
- if (dir) {
- this.viewMode = Math.max(this.minViewMode, Math.min(2, this.viewMode + dir));
- }
- /*
- vitalets: fixing bug of very special conditions:
- jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover.
- Method show() does not set display css correctly and datepicker is not shown.
- Changed to .css('display', 'block') solve the problem.
- See https://github.com/vitalets/x-editable/issues/37
-
- In jquery 1.7.2+ everything works fine.
- */
- //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
- this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block');
- this.updateNavArrows();
- }
- };
-
- $.fn.datepicker = function ( option ) {
- var args = Array.apply(null, arguments);
- args.shift();
- return this.each(function () {
- var $this = $(this),
- data = $this.data('datepicker'),
- options = typeof option == 'object' && option;
- if (!data) {
- $this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options))));
- }
- if (typeof option == 'string' && typeof data[option] == 'function') {
- data[option].apply(data, args);
- }
- });
- };
-
- $.fn.datepicker.defaults = {
- };
- $.fn.datepicker.Constructor = Datepicker;
- var dates = $.fn.datepicker.dates = {
- en: {
- days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
- daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
- daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
- months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
- monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
- today: "Today"
- }
- };
-
- var DPGlobal = {
- modes: [
- {
- clsName: 'days',
- navFnc: 'Month',
- navStep: 1
- },
- {
- clsName: 'months',
- navFnc: 'FullYear',
- navStep: 1
- },
- {
- clsName: 'years',
- navFnc: 'FullYear',
- navStep: 10
- }],
- isLeapYear: function (year) {
- return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
- },
- getDaysInMonth: function (year, month) {
- return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
- },
- validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
- nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,
- parseFormat: function(format){
- // IE treats \0 as a string end in inputs (truncating the value),
- // so it's a bad format delimiter, anyway
- var separators = format.replace(this.validParts, '\0').split('\0'),
- parts = format.match(this.validParts);
- if (!separators || !separators.length || !parts || parts.length === 0){
- throw new Error("Invalid date format.");
- }
- return {separators: separators, parts: parts};
- },
- parseDate: function(date, format, language) {
- if (date instanceof Date) return date;
- if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) {
- var part_re = /([\-+]\d+)([dmwy])/,
- parts = date.match(/([\-+]\d+)([dmwy])/g),
- part, dir;
- date = new Date();
- for (var i=0; i'+
- ''+
- ' | '+
- ' | '+
- ' | '+
- ' '+
- '',
- contTemplate: ' | ',
- footTemplate: ' | '
- };
- DPGlobal.template = ''+
- ' '+
- ' '+
- DPGlobal.headTemplate+
- ''+
- DPGlobal.footTemplate+
- ' '+
- ' '+
- ' '+
- ' '+
- DPGlobal.headTemplate+
- DPGlobal.contTemplate+
- DPGlobal.footTemplate+
- ' '+
- ' '+
- ' '+
- ' '+
- DPGlobal.headTemplate+
- DPGlobal.contTemplate+
- DPGlobal.footTemplate+
- ' '+
- ' '+
- ' ';
-
- $.fn.datepicker.DPGlobal = DPGlobal;
-
-}( window.jQuery );
\ No newline at end of file
diff --git a/public/vendor/angular-other/timepicker.js b/public/vendor/angular-other/timepicker.js
deleted file mode 100644
index e0d9f51b33a..00000000000
--- a/public/vendor/angular-other/timepicker.js
+++ /dev/null
@@ -1,888 +0,0 @@
-/*!
- * Timepicker Component for Twitter Bootstrap
- *
- * Copyright 2013 Joris de Wit
- *
- * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-(function($, window, document, undefined) {
- 'use strict';
-
- // TIMEPICKER PUBLIC CLASS DEFINITION
- var Timepicker = function(element, options) {
- this.widget = '';
- this.$element = $(element);
- this.defaultTime = options.defaultTime;
- this.disableFocus = options.disableFocus;
- this.isOpen = options.isOpen;
- this.minuteStep = options.minuteStep;
- this.modalBackdrop = options.modalBackdrop;
- this.secondStep = options.secondStep;
- this.showInputs = options.showInputs;
- this.showMeridian = options.showMeridian;
- this.showSeconds = options.showSeconds;
- this.template = options.template;
- this.appendWidgetTo = options.appendWidgetTo;
-
- this._init();
- };
-
- Timepicker.prototype = {
-
- constructor: Timepicker,
-
- _init: function() {
- var self = this;
-
- if (this.$element.parent().hasClass('input-append') || this.$element.parent().hasClass('input-prepend')) {
- this.$element.parent('.input-append, .input-prepend').find('.add-on').on({
- 'click.timepicker': $.proxy(this.showWidget, this)
- });
- this.$element.on({
- 'focus.timepicker': $.proxy(this.highlightUnit, this),
- 'click.timepicker': $.proxy(this.highlightUnit, this),
- 'keydown.timepicker': $.proxy(this.elementKeydown, this),
- 'blur.timepicker': $.proxy(this.blurElement, this)
- });
- } else {
- if (this.template) {
- this.$element.on({
- 'focus.timepicker': $.proxy(this.showWidget, this),
- 'click.timepicker': $.proxy(this.showWidget, this),
- 'blur.timepicker': $.proxy(this.blurElement, this)
- });
- } else {
- this.$element.on({
- 'focus.timepicker': $.proxy(this.highlightUnit, this),
- 'click.timepicker': $.proxy(this.highlightUnit, this),
- 'keydown.timepicker': $.proxy(this.elementKeydown, this),
- 'blur.timepicker': $.proxy(this.blurElement, this)
- });
- }
- }
-
- if (this.template !== false) {
- this.$widget = $(this.getTemplate()).prependTo(this.$element.parents(this.appendWidgetTo)).on('click', $.proxy(this.widgetClick, this));
- } else {
- this.$widget = false;
- }
-
- if (this.showInputs && this.$widget !== false) {
- this.$widget.find('input').each(function() {
- $(this).on({
- 'click.timepicker': function() { $(this).select(); },
- 'keydown.timepicker': $.proxy(self.widgetKeydown, self)
- });
- });
- }
-
- this.setDefaultTime(this.defaultTime);
- },
-
- blurElement: function() {
- this.highlightedUnit = undefined;
- this.updateFromElementVal();
- },
-
- decrementHour: function() {
- if (this.showMeridian) {
- if (this.hour === 1) {
- this.hour = 12;
- } else if (this.hour === 12) {
- this.hour--;
-
- return this.toggleMeridian();
- } else if (this.hour === 0) {
- this.hour = 11;
-
- return this.toggleMeridian();
- } else {
- this.hour--;
- }
- } else {
- if (this.hour === 0) {
- this.hour = 23;
- } else {
- this.hour--;
- }
- }
- this.update();
- },
-
- decrementMinute: function(step) {
- var newVal;
-
- if (step) {
- newVal = this.minute - step;
- } else {
- newVal = this.minute - this.minuteStep;
- }
-
- if (newVal < 0) {
- this.decrementHour();
- this.minute = newVal + 60;
- } else {
- this.minute = newVal;
- }
- this.update();
- },
-
- decrementSecond: function() {
- var newVal = this.second - this.secondStep;
-
- if (newVal < 0) {
- this.decrementMinute(true);
- this.second = newVal + 60;
- } else {
- this.second = newVal;
- }
- this.update();
- },
-
- elementKeydown: function(e) {
- switch (e.keyCode) {
- case 9: //tab
- this.updateFromElementVal();
-
- switch (this.highlightedUnit) {
- case 'hour':
- e.preventDefault();
- this.highlightNextUnit();
- break;
- case 'minute':
- if (this.showMeridian || this.showSeconds) {
- e.preventDefault();
- this.highlightNextUnit();
- }
- break;
- case 'second':
- if (this.showMeridian) {
- e.preventDefault();
- this.highlightNextUnit();
- }
- break;
- }
- break;
- case 27: // escape
- this.updateFromElementVal();
- break;
- case 37: // left arrow
- e.preventDefault();
- this.highlightPrevUnit();
- this.updateFromElementVal();
- break;
- case 38: // up arrow
- e.preventDefault();
- switch (this.highlightedUnit) {
- case 'hour':
- this.incrementHour();
- this.highlightHour();
- break;
- case 'minute':
- this.incrementMinute();
- this.highlightMinute();
- break;
- case 'second':
- this.incrementSecond();
- this.highlightSecond();
- break;
- case 'meridian':
- this.toggleMeridian();
- this.highlightMeridian();
- break;
- }
- break;
- case 39: // right arrow
- e.preventDefault();
- this.updateFromElementVal();
- this.highlightNextUnit();
- break;
- case 40: // down arrow
- e.preventDefault();
- switch (this.highlightedUnit) {
- case 'hour':
- this.decrementHour();
- this.highlightHour();
- break;
- case 'minute':
- this.decrementMinute();
- this.highlightMinute();
- break;
- case 'second':
- this.decrementSecond();
- this.highlightSecond();
- break;
- case 'meridian':
- this.toggleMeridian();
- this.highlightMeridian();
- break;
- }
- break;
- }
- },
-
- formatTime: function(hour, minute, second, meridian) {
- hour = hour < 10 ? '0' + hour : hour;
- minute = minute < 10 ? '0' + minute : minute;
- second = second < 10 ? '0' + second : second;
-
- return hour + ':' + minute + (this.showSeconds ? ':' + second : '') + (this.showMeridian ? ' ' + meridian : '');
- },
-
- getCursorPosition: function() {
- var input = this.$element.get(0);
-
- if ('selectionStart' in input) {// Standard-compliant browsers
-
- return input.selectionStart;
- } else if (document.selection) {// IE fix
- input.focus();
- var sel = document.selection.createRange(),
- selLen = document.selection.createRange().text.length;
-
- sel.moveStart('character', - input.value.length);
-
- return sel.text.length - selLen;
- }
- },
-
- getTemplate: function() {
- var template,
- hourTemplate,
- minuteTemplate,
- secondTemplate,
- meridianTemplate,
- templateContent;
-
- if (this.showInputs) {
- hourTemplate = '';
- minuteTemplate = '';
- secondTemplate = '';
- meridianTemplate = '';
- } else {
- hourTemplate = '';
- minuteTemplate = '';
- secondTemplate = '';
- meridianTemplate = '';
- }
-
- templateContent = ''+
- ''+
- ' | '+
- ' | '+
- ' | '+
- (this.showSeconds ?
- ' | '+
- ' | '
- : '') +
- (this.showMeridian ?
- ' | '+
- ' | '
- : '') +
- ' '+
- ''+
- '| '+ hourTemplate +' | '+
- ': | '+
- ''+ minuteTemplate +' | '+
- (this.showSeconds ?
- ': | '+
- ''+ secondTemplate +' | '
- : '') +
- (this.showMeridian ?
- ' | '+
- ''+ meridianTemplate +' | '
- : '') +
- ' '+
- ''+
- ' | '+
- ' | '+
- ' | '+
- (this.showSeconds ?
- ' | '+
- ' | '
- : '') +
- (this.showMeridian ?
- ' | '+
- ' | '
- : '') +
- ' '+
- ' ';
-
- switch(this.template) {
- case 'modal':
- template = '';
- break;
- case 'dropdown':
- template = '';
- break;
- }
-
- return template;
- },
-
- getTime: function() {
- return this.formatTime(this.hour, this.minute, this.second, this.meridian);
- },
-
- hideWidget: function() {
- if (this.isOpen === false) {
- return;
- }
-
- if (this.showInputs) {
- this.updateFromWidgetInputs();
- }
-
- this.$element.trigger({
- 'type': 'hide.timepicker',
- 'time': {
- 'value': this.getTime(),
- 'hours': this.hour,
- 'minutes': this.minute,
- 'seconds': this.second,
- 'meridian': this.meridian
- }
- });
-
- if (this.template === 'modal') {
- this.$widget.modal('hide');
- } else {
- this.$widget.removeClass('open');
- }
-
- $(document).off('mousedown.timepicker');
-
- this.isOpen = false;
- },
-
- highlightUnit: function() {
- this.position = this.getCursorPosition();
- if (this.position >= 0 && this.position <= 2) {
- this.highlightHour();
- } else if (this.position >= 3 && this.position <= 5) {
- this.highlightMinute();
- } else if (this.position >= 6 && this.position <= 8) {
- if (this.showSeconds) {
- this.highlightSecond();
- } else {
- this.highlightMeridian();
- }
- } else if (this.position >= 9 && this.position <= 11) {
- this.highlightMeridian();
- }
- },
-
- highlightNextUnit: function() {
- switch (this.highlightedUnit) {
- case 'hour':
- this.highlightMinute();
- break;
- case 'minute':
- if (this.showSeconds) {
- this.highlightSecond();
- } else if (this.showMeridian){
- this.highlightMeridian();
- } else {
- this.highlightHour();
- }
- break;
- case 'second':
- if (this.showMeridian) {
- this.highlightMeridian();
- } else {
- this.highlightHour();
- }
- break;
- case 'meridian':
- this.highlightHour();
- break;
- }
- },
-
- highlightPrevUnit: function() {
- switch (this.highlightedUnit) {
- case 'hour':
- this.highlightMeridian();
- break;
- case 'minute':
- this.highlightHour();
- break;
- case 'second':
- this.highlightMinute();
- break;
- case 'meridian':
- if (this.showSeconds) {
- this.highlightSecond();
- } else {
- this.highlightMinute();
- }
- break;
- }
- },
-
- highlightHour: function() {
- var $element = this.$element.get(0);
-
- this.highlightedUnit = 'hour';
-
- if ($element.setSelectionRange) {
- setTimeout(function() {
- $element.setSelectionRange(0,2);
- }, 0);
- }
- },
-
- highlightMinute: function() {
- var $element = this.$element.get(0);
-
- this.highlightedUnit = 'minute';
-
- if ($element.setSelectionRange) {
- setTimeout(function() {
- $element.setSelectionRange(3,5);
- }, 0);
- }
- },
-
- highlightSecond: function() {
- var $element = this.$element.get(0);
-
- this.highlightedUnit = 'second';
-
- if ($element.setSelectionRange) {
- setTimeout(function() {
- $element.setSelectionRange(6,8);
- }, 0);
- }
- },
-
- highlightMeridian: function() {
- var $element = this.$element.get(0);
-
- this.highlightedUnit = 'meridian';
-
- if ($element.setSelectionRange) {
- if (this.showSeconds) {
- setTimeout(function() {
- $element.setSelectionRange(9,11);
- }, 0);
- } else {
- setTimeout(function() {
- $element.setSelectionRange(6,8);
- }, 0);
- }
- }
- },
-
- incrementHour: function() {
- if (this.showMeridian) {
- if (this.hour === 11) {
- this.hour++;
- return this.toggleMeridian();
- } else if (this.hour === 12) {
- this.hour = 0;
- }
- }
- if (this.hour === 23) {
- this.hour = 0;
-
- return;
- }
- this.hour++;
- this.update();
- },
-
- incrementMinute: function(step) {
- var newVal;
-
- if (step) {
- newVal = this.minute + step;
- } else {
- newVal = this.minute + this.minuteStep - (this.minute % this.minuteStep);
- }
-
- if (newVal > 59) {
- this.incrementHour();
- this.minute = newVal - 60;
- } else {
- this.minute = newVal;
- }
- this.update();
- },
-
- incrementSecond: function() {
- var newVal = this.second + this.secondStep - (this.second % this.secondStep);
-
- if (newVal > 59) {
- this.incrementMinute(true);
- this.second = newVal - 60;
- } else {
- this.second = newVal;
- }
- this.update();
- },
-
- remove: function() {
- $('document').off('.timepicker');
- if (this.$widget) {
- this.$widget.remove();
- }
- delete this.$element.data().timepicker;
- },
-
- setDefaultTime: function(defaultTime){
- if (!this.$element.val()) {
- if (defaultTime === 'current') {
- var dTime = new Date(),
- hours = dTime.getHours(),
- minutes = Math.floor(dTime.getMinutes() / this.minuteStep) * this.minuteStep,
- seconds = Math.floor(dTime.getSeconds() / this.secondStep) * this.secondStep,
- meridian = 'AM';
-
- if (this.showMeridian) {
- if (hours === 0) {
- hours = 12;
- } else if (hours >= 12) {
- if (hours > 12) {
- hours = hours - 12;
- }
- meridian = 'PM';
- } else {
- meridian = 'AM';
- }
- }
-
- this.hour = hours;
- this.minute = minutes;
- this.second = seconds;
- this.meridian = meridian;
-
- this.update();
-
- } else if (defaultTime === false) {
- this.hour = 0;
- this.minute = 0;
- this.second = 0;
- this.meridian = 'AM';
- } else {
- this.setTime(defaultTime);
- }
- } else {
- this.updateFromElementVal();
- }
- },
-
- setTime: function(time) {
- var arr,
- timeArray;
-
- if (this.showMeridian) {
- arr = time.split(' ');
- timeArray = arr[0].split(':');
- this.meridian = arr[1];
- } else {
- timeArray = time.split(':');
- }
-
- this.hour = parseInt(timeArray[0], 10);
- this.minute = parseInt(timeArray[1], 10);
- this.second = parseInt(timeArray[2], 10);
-
- if (isNaN(this.hour)) {
- this.hour = 0;
- }
- if (isNaN(this.minute)) {
- this.minute = 0;
- }
-
- if (this.showMeridian) {
- if (this.hour > 12) {
- this.hour = 12;
- } else if (this.hour < 1) {
- this.hour = 12;
- }
-
- if (this.meridian === 'am' || this.meridian === 'a') {
- this.meridian = 'AM';
- } else if (this.meridian === 'pm' || this.meridian === 'p') {
- this.meridian = 'PM';
- }
-
- if (this.meridian !== 'AM' && this.meridian !== 'PM') {
- this.meridian = 'AM';
- }
- } else {
- if (this.hour >= 24) {
- this.hour = 23;
- } else if (this.hour < 0) {
- this.hour = 0;
- }
- }
-
- if (this.minute < 0) {
- this.minute = 0;
- } else if (this.minute >= 60) {
- this.minute = 59;
- }
-
- if (this.showSeconds) {
- if (isNaN(this.second)) {
- this.second = 0;
- } else if (this.second < 0) {
- this.second = 0;
- } else if (this.second >= 60) {
- this.second = 59;
- }
- }
-
- this.update();
- },
-
- showWidget: function() {
- if (this.isOpen) {
- return;
- }
-
- if (this.$element.is(':disabled')) {
- return;
- }
-
- var self = this;
- $(document).on('mousedown.timepicker', function (e) {
- // Clicked outside the timepicker, hide it
- if ($(e.target).closest('.bootstrap-timepicker-widget').length === 0) {
- self.hideWidget();
- }
- });
-
- this.$element.trigger({
- 'type': 'show.timepicker',
- 'time': {
- 'value': this.getTime(),
- 'hours': this.hour,
- 'minutes': this.minute,
- 'seconds': this.second,
- 'meridian': this.meridian
- }
- });
-
- if (this.disableFocus) {
- this.$element.blur();
- }
-
- this.updateFromElementVal();
-
- if (this.template === 'modal') {
- this.$widget.modal('show').on('hidden', $.proxy(this.hideWidget, this));
- } else {
- if (this.isOpen === false) {
- this.$widget.addClass('open');
- }
- }
-
- this.isOpen = true;
- },
-
- toggleMeridian: function() {
- this.meridian = this.meridian === 'AM' ? 'PM' : 'AM';
- this.update();
- },
-
- update: function() {
- this.$element.trigger({
- 'type': 'changeTime.timepicker',
- 'time': {
- 'value': this.getTime(),
- 'hours': this.hour,
- 'minutes': this.minute,
- 'seconds': this.second,
- 'meridian': this.meridian
- }
- });
-
- this.updateElement();
- this.updateWidget();
- },
-
- updateElement: function() {
- this.$element.val(this.getTime()).change();
- },
-
- updateFromElementVal: function() {
- var val = this.$element.val();
-
- if (val) {
- this.setTime(val);
- }
- },
-
- updateWidget: function() {
- if (this.$widget === false) {
- return;
- }
-
- var hour = this.hour < 10 ? '0' + this.hour : this.hour,
- minute = this.minute < 10 ? '0' + this.minute : this.minute,
- second = this.second < 10 ? '0' + this.second : this.second;
-
- if (this.showInputs) {
- this.$widget.find('input.bootstrap-timepicker-hour').val(hour);
- this.$widget.find('input.bootstrap-timepicker-minute').val(minute);
-
- if (this.showSeconds) {
- this.$widget.find('input.bootstrap-timepicker-second').val(second);
- }
- if (this.showMeridian) {
- this.$widget.find('input.bootstrap-timepicker-meridian').val(this.meridian);
- }
- } else {
- this.$widget.find('span.bootstrap-timepicker-hour').text(hour);
- this.$widget.find('span.bootstrap-timepicker-minute').text(minute);
-
- if (this.showSeconds) {
- this.$widget.find('span.bootstrap-timepicker-second').text(second);
- }
- if (this.showMeridian) {
- this.$widget.find('span.bootstrap-timepicker-meridian').text(this.meridian);
- }
- }
- },
-
- updateFromWidgetInputs: function() {
- if (this.$widget === false) {
- return;
- }
- var time = $('input.bootstrap-timepicker-hour', this.$widget).val() + ':' +
- $('input.bootstrap-timepicker-minute', this.$widget).val() +
- (this.showSeconds ? ':' + $('input.bootstrap-timepicker-second', this.$widget).val() : '') +
- (this.showMeridian ? ' ' + $('input.bootstrap-timepicker-meridian', this.$widget).val() : '');
-
- this.setTime(time);
- },
-
- widgetClick: function(e) {
- e.stopPropagation();
- e.preventDefault();
-
- var action = $(e.target).closest('a').data('action');
- if (action) {
- this[action]();
- }
- },
-
- widgetKeydown: function(e) {
- var $input = $(e.target).closest('input'),
- name = $input.attr('name');
-
- switch (e.keyCode) {
- case 9: //tab
- if (this.showMeridian) {
- if (name === 'meridian') {
- return this.hideWidget();
- }
- } else {
- if (this.showSeconds) {
- if (name === 'second') {
- return this.hideWidget();
- }
- } else {
- if (name === 'minute') {
- return this.hideWidget();
- }
- }
- }
-
- this.updateFromWidgetInputs();
- break;
- case 27: // escape
- this.hideWidget();
- break;
- case 38: // up arrow
- e.preventDefault();
- switch (name) {
- case 'hour':
- this.incrementHour();
- break;
- case 'minute':
- this.incrementMinute();
- break;
- case 'second':
- this.incrementSecond();
- break;
- case 'meridian':
- this.toggleMeridian();
- break;
- }
- break;
- case 40: // down arrow
- e.preventDefault();
- switch (name) {
- case 'hour':
- this.decrementHour();
- break;
- case 'minute':
- this.decrementMinute();
- break;
- case 'second':
- this.decrementSecond();
- break;
- case 'meridian':
- this.toggleMeridian();
- break;
- }
- break;
- }
- }
- };
-
-
- //TIMEPICKER PLUGIN DEFINITION
- $.fn.timepicker = function(option) {
- var args = Array.apply(null, arguments);
- args.shift();
- return this.each(function() {
- var $this = $(this),
- data = $this.data('timepicker'),
- options = typeof option === 'object' && option;
-
- if (!data) {
- $this.data('timepicker', (data = new Timepicker(this, $.extend({}, $.fn.timepicker.defaults, options, $(this).data()))));
- }
-
- if (typeof option === 'string') {
- data[option].apply(data, args);
- }
- });
- };
-
- $.fn.timepicker.defaults = {
- defaultTime: 'current',
- disableFocus: false,
- isOpen: false,
- minuteStep: 15,
- modalBackdrop: false,
- secondStep: 15,
- showSeconds: false,
- showInputs: true,
- showMeridian: true,
- template: 'dropdown',
- appendWidgetTo: '.bootstrap-timepicker'
- };
-
- $.fn.timepicker.Constructor = Timepicker;
-
-})(jQuery, window, document);
\ No newline at end of file
diff --git a/public/views/error-template.html b/public/views/error-template.html
index 0f785120ce1..28e828a1554 100644
--- a/public/views/error-template.html
+++ b/public/views/error-template.html
@@ -10,7 +10,11 @@
-
+ [[ if eq .Theme "light" ]]
+
+ [[ else ]]
+
+ [[ end ]]
diff --git a/public/views/index-template.html b/public/views/index-template.html
index 2923b1918f9..955a1b4fc4e 100644
--- a/public/views/index-template.html
+++ b/public/views/index-template.html
@@ -20,10 +20,12 @@
-
+
+ [[ if eq .Theme "light" ]]
+
+ [[ else ]]
+
+ [[ end ]]
|