mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Remove unused files (#57515)
This commit is contained in:
@@ -2511,10 +2511,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/core/components/TagFilter/TagValue.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/core/components/connectWithCleanUp.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
@@ -2554,9 +2550,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/core/reducers/processsAclItems.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/core/reducers/root.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
@@ -3799,12 +3792,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/datasources/components/ButtonRow.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/datasources/passwordHandlers.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/datasources/passwordHandlers.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/datasources/state/actions.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
@@ -4428,9 +4415,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/plugins/admin/components/SearchField.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/guards.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/helpers.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
@@ -4497,16 +4481,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "24"]
|
||||
],
|
||||
"public/app/features/plugins/hooks/tests/useImportAppPlugin.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"]
|
||||
],
|
||||
"public/app/features/plugins/importPanelPlugin.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
@@ -8392,10 +8366,6 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/panel/timeseries/FillBelowToEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/timeseries/LayoutBuilder.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/timeseries/LineStyleEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@@ -8455,11 +8425,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"]
|
||||
],
|
||||
"public/app/plugins/panel/timeseries/overrides/hideSeriesConfigFactory.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||
],
|
||||
"public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const NEWS_FEED = 'https://grafana.com/blog/news.xml';
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { TagBadge } from './TagBadge';
|
||||
|
||||
export interface Props {
|
||||
value: any;
|
||||
className: string;
|
||||
onClick: React.MouseEventHandler<HTMLDivElement>;
|
||||
onRemove: (value: any, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
export class TagValue extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
onClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
this.props.onRemove(this.props.value, event);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value } = this.props;
|
||||
return <TagBadge label={value.label} removeIcon={false} count={0} onClick={this.onClick} />;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
export const useRefMounted = () => {
|
||||
const refMounted = useRef(false);
|
||||
useEffect(() => {
|
||||
refMounted.current = true;
|
||||
return () => {
|
||||
refMounted.current = false;
|
||||
};
|
||||
});
|
||||
return refMounted;
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { DashboardAcl, DashboardAclDTO } from 'app/types/acl';
|
||||
|
||||
export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] {
|
||||
return items.map(processAclItem).sort((a, b) => b.sortRank! - a.sortRank! || a.name!.localeCompare(b.name!));
|
||||
}
|
||||
|
||||
function processAclItem(dto: DashboardAclDTO): DashboardAcl {
|
||||
const item = dto as DashboardAcl;
|
||||
|
||||
item.sortRank = 0;
|
||||
|
||||
if (item.userId! > 0) {
|
||||
item.name = item.userLogin;
|
||||
item.sortRank = 10;
|
||||
} else if (item.teamId! > 0) {
|
||||
item.name = item.team;
|
||||
item.sortRank = 20;
|
||||
} else if (item.role) {
|
||||
item.icon = 'fa fa-fw fa-street-view';
|
||||
item.name = item.role;
|
||||
item.sortRank = 30;
|
||||
if (item.role === 'Editor') {
|
||||
item.sortRank += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.inherited) {
|
||||
item.sortRank += 100;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
|
||||
import { dateTimeFormatTimeAgo, DateTimeInput } from '@grafana/data';
|
||||
|
||||
export interface Props {
|
||||
date: DateTimeInput;
|
||||
}
|
||||
|
||||
export const TimeToNow: FC<Props> = ({ date }) => {
|
||||
const setRandom = useState(0)[1];
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setRandom(Math.random()), 1000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
return <span title={String(date)}>{dateTimeFormatTimeAgo(date)}</span>;
|
||||
};
|
||||
@@ -1,143 +0,0 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { FC, Fragment, useState } from 'react';
|
||||
|
||||
import { dateMath, GrafanaTheme, intervalToAbbreviatedDurationString } from '@grafana/data';
|
||||
import { useStyles, Link } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { Silence, AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { expireSilenceAction } from '../../state/actions';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
import { ActionButton } from '../rules/ActionButton';
|
||||
import { ActionIcon } from '../rules/ActionIcon';
|
||||
|
||||
import { Matchers } from './Matchers';
|
||||
import { SilenceStateTag } from './SilenceStateTag';
|
||||
import SilencedAlertsTable from './SilencedAlertsTable';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
silence: Silence;
|
||||
silencedAlerts: AlertmanagerAlert[];
|
||||
alertManagerSourceName: string;
|
||||
}
|
||||
|
||||
const SilenceTableRow: FC<Props> = ({ silence, className, silencedAlerts, alertManagerSourceName }) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(true);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const styles = useStyles(getStyles);
|
||||
const { status, matchers = [], startsAt, endsAt, comment, createdBy } = silence;
|
||||
|
||||
const dateDisplayFormat = 'YYYY-MM-DD HH:mm';
|
||||
const startsAtDate = dateMath.parse(startsAt);
|
||||
const endsAtDate = dateMath.parse(endsAt);
|
||||
const duration = intervalToAbbreviatedDurationString({ start: new Date(startsAt), end: new Date(endsAt) });
|
||||
|
||||
const handleExpireSilenceClick = () => {
|
||||
dispatch(expireSilenceAction(alertManagerSourceName, silence.id));
|
||||
};
|
||||
|
||||
const detailsColspan = contextSrv.isEditor ? 4 : 3;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<tr className={className} data-testid="silence-table-row">
|
||||
<td>
|
||||
<CollapseToggle isCollapsed={isCollapsed} onToggle={(value) => setIsCollapsed(value)} />
|
||||
</td>
|
||||
<td>
|
||||
<SilenceStateTag state={status.state} />
|
||||
</td>
|
||||
<td className={styles.matchersCell}>
|
||||
<Matchers matchers={matchers} />
|
||||
</td>
|
||||
<td data-testid="silenced-alerts">{silencedAlerts.length}</td>
|
||||
<td>
|
||||
{startsAtDate?.format(dateDisplayFormat)} {'-'}
|
||||
<br />
|
||||
{endsAtDate?.format(dateDisplayFormat)}
|
||||
</td>
|
||||
{contextSrv.isEditor && (
|
||||
<td className={styles.actionsCell}>
|
||||
{status.state === 'expired' ? (
|
||||
<Link href={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}>
|
||||
<ActionButton icon="sync">Recreate</ActionButton>
|
||||
</Link>
|
||||
) : (
|
||||
<ActionButton icon="bell" onClick={handleExpireSilenceClick}>
|
||||
Unsilence
|
||||
</ActionButton>
|
||||
)}
|
||||
{status.state !== 'expired' && (
|
||||
<ActionIcon
|
||||
to={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}
|
||||
icon="pen"
|
||||
tooltip="edit"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<tr className={className}>
|
||||
<td />
|
||||
<td>Comment</td>
|
||||
<td colSpan={detailsColspan}>{comment}</td>
|
||||
</tr>
|
||||
<tr className={className}>
|
||||
<td />
|
||||
<td>Schedule</td>
|
||||
<td colSpan={detailsColspan}>{`${startsAtDate?.format(dateDisplayFormat)} - ${endsAtDate?.format(
|
||||
dateDisplayFormat
|
||||
)}`}</td>
|
||||
</tr>
|
||||
<tr className={className}>
|
||||
<td />
|
||||
<td>Duration</td>
|
||||
<td colSpan={detailsColspan}>{duration}</td>
|
||||
</tr>
|
||||
<tr className={className}>
|
||||
<td />
|
||||
<td>Created by</td>
|
||||
<td colSpan={detailsColspan}>{createdBy}</td>
|
||||
</tr>
|
||||
{!!silencedAlerts.length && (
|
||||
<tr className={cx(className, styles.alertRulesCell)}>
|
||||
<td />
|
||||
<td>Affected alerts</td>
|
||||
<td colSpan={detailsColspan}>
|
||||
<SilencedAlertsTable silencedAlerts={silencedAlerts} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
matchersCell: css`
|
||||
& > * + * {
|
||||
margin-left: ${theme.spacing.xs};
|
||||
}
|
||||
`,
|
||||
actionsCell: css`
|
||||
text-align: right;
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
|
||||
& > * + * {
|
||||
margin-left: ${theme.spacing.sm};
|
||||
}
|
||||
`,
|
||||
alertRulesCell: css`
|
||||
vertical-align: top;
|
||||
`,
|
||||
});
|
||||
|
||||
export default SilenceTableRow;
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Icon, HorizontalGroup } from '@grafana/ui';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
onGoBack: () => void;
|
||||
isEditing: boolean;
|
||||
};
|
||||
|
||||
export const DashboardSettingsHeader: React.FC<Props> = ({ onGoBack, isEditing, title }) => {
|
||||
if (config.featureToggles.topnav) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard-settings__header">
|
||||
<HorizontalGroup align="center" justify="space-between">
|
||||
<h3>
|
||||
<span onClick={onGoBack} className={isEditing ? 'pointer' : ''}>
|
||||
{title}
|
||||
</span>
|
||||
{isEditing && (
|
||||
<span>
|
||||
<Icon name="angle-right" /> Edit
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { PanelEditor } from './PanelEditor';
|
||||
@@ -1,77 +0,0 @@
|
||||
import { throttle } from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import Draggable, { DraggableEventHandler } from 'react-draggable';
|
||||
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
|
||||
interface Props {
|
||||
isEditing: boolean;
|
||||
render: (styles: object) => JSX.Element;
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
interface State {
|
||||
editorHeight: number;
|
||||
}
|
||||
|
||||
export class PanelResizer extends PureComponent<Props, State> {
|
||||
initialHeight: number = Math.floor(document.documentElement.scrollHeight * 0.3);
|
||||
prevEditorHeight?: number;
|
||||
throttledChangeHeight: (height: number) => void;
|
||||
throttledResizeDone?: () => void;
|
||||
noStyles: object = {};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
editorHeight: this.initialHeight,
|
||||
};
|
||||
|
||||
this.throttledChangeHeight = throttle(this.changeHeight, 20, { trailing: true });
|
||||
}
|
||||
|
||||
get largestHeight() {
|
||||
return document.documentElement.scrollHeight * 0.9;
|
||||
}
|
||||
get smallestHeight() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
changeHeight = (height: number) => {
|
||||
const sh = this.smallestHeight;
|
||||
const lh = this.largestHeight;
|
||||
height = height < sh ? sh : height;
|
||||
height = height > lh ? lh : height;
|
||||
|
||||
this.prevEditorHeight = this.state.editorHeight;
|
||||
this.setState({
|
||||
editorHeight: height,
|
||||
});
|
||||
};
|
||||
|
||||
onDrag: DraggableEventHandler = (evt, data) => {
|
||||
const newHeight = this.state.editorHeight + data.y;
|
||||
this.throttledChangeHeight(newHeight);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { render, isEditing } = this.props;
|
||||
const { editorHeight } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{render(isEditing ? { height: editorHeight } : this.noStyles)}
|
||||
{isEditing && (
|
||||
<div className="panel-editor-container__resizer">
|
||||
<Draggable axis="y" grid={[100, 1]} onDrag={this.onDrag} position={{ x: 0, y: 0 }}>
|
||||
<div className="panel-editor-resizer">
|
||||
<div className="panel-editor-resizer__handle" />
|
||||
</div>
|
||||
</Draggable>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createResetHandler, PasswordFieldEnum, Ctrl } from './passwordHandlers';
|
||||
describe('createResetHandler', () => {
|
||||
Object.values(PasswordFieldEnum).forEach((field) => {
|
||||
it(`should reset existing ${field} field`, () => {
|
||||
const event: any = {
|
||||
preventDefault: () => {},
|
||||
};
|
||||
const ctrl: Ctrl = {
|
||||
current: {
|
||||
[field]: 'set',
|
||||
secureJsonData: {
|
||||
[field]: 'set',
|
||||
},
|
||||
secureJsonFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
createResetHandler(ctrl, field)(event);
|
||||
expect(ctrl).toEqual({
|
||||
current: {
|
||||
[field]: undefined,
|
||||
secureJsonData: {
|
||||
[field]: '',
|
||||
},
|
||||
secureJsonFields: {
|
||||
[field]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Set of handlers for secure password field in Angular components. They handle backward compatibility with
|
||||
* passwords stored in plain text fields.
|
||||
*/
|
||||
|
||||
import { SyntheticEvent } from 'react';
|
||||
|
||||
export enum PasswordFieldEnum {
|
||||
Password = 'password',
|
||||
BasicAuthPassword = 'basicAuthPassword',
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic shape for settings controllers in at the moment mostly angular data source plugins.
|
||||
*/
|
||||
export type Ctrl = {
|
||||
current: {
|
||||
secureJsonFields: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
secureJsonData?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
password?: string;
|
||||
basicAuthPassword?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const createResetHandler =
|
||||
(ctrl: Ctrl, field: PasswordFieldEnum) => (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
// Reset also normal plain text password to remove it and only save it in secureJsonData.
|
||||
ctrl.current[field] = undefined;
|
||||
ctrl.current.secureJsonFields[field] = false;
|
||||
ctrl.current.secureJsonData = ctrl.current.secureJsonData || {};
|
||||
ctrl.current.secureJsonData[field] = '';
|
||||
};
|
||||
|
||||
export const createChangeHandler =
|
||||
(ctrl: any, field: PasswordFieldEnum) => (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
ctrl.current.secureJsonData = ctrl.current.secureJsonData || {};
|
||||
ctrl.current.secureJsonData[field] = event.currentTarget.value;
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import { equal, intersect } from './set';
|
||||
|
||||
describe('equal', () => {
|
||||
it('returns false for two sets of differing sizes', () => {
|
||||
const s1 = new Set([1, 2, 3]);
|
||||
const s2 = new Set([4, 5, 6, 7]);
|
||||
expect(equal(s1, s2)).toBe(false);
|
||||
});
|
||||
it('returns false for two sets where one is a subset of the other', () => {
|
||||
const s1 = new Set([1, 2, 3]);
|
||||
const s2 = new Set([1, 2, 3, 4]);
|
||||
expect(equal(s1, s2)).toBe(false);
|
||||
});
|
||||
it('returns false for two sets with uncommon elements', () => {
|
||||
const s1 = new Set([1, 2, 3, 4]);
|
||||
const s2 = new Set([1, 2, 5, 6]);
|
||||
expect(equal(s1, s2)).toBe(false);
|
||||
});
|
||||
it('returns false for two deeply equivalent sets', () => {
|
||||
const s1 = new Set([{ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }]);
|
||||
const s2 = new Set([{ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }]);
|
||||
expect(equal(s1, s2)).toBe(false);
|
||||
});
|
||||
it('returns true for two sets with the same elements', () => {
|
||||
const s1 = new Set([1, 2, 3, 4]);
|
||||
const s2 = new Set([4, 3, 2, 1]);
|
||||
expect(equal(s1, s2)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('intersect', () => {
|
||||
it('returns an empty set for two sets without any common elements', () => {
|
||||
const s1 = new Set([1, 2, 3, 4]);
|
||||
const s2 = new Set([5, 6, 7, 8]);
|
||||
expect(intersect(s1, s2)).toEqual(new Set());
|
||||
});
|
||||
it('returns an empty set for two deeply equivalent sets', () => {
|
||||
const s1 = new Set([{ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }]);
|
||||
const s2 = new Set([{ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }]);
|
||||
expect(intersect(s1, s2)).toEqual(new Set());
|
||||
});
|
||||
it('returns a set containing common elements between two sets of the same size', () => {
|
||||
const s1 = new Set([1, 2, 3, 4]);
|
||||
const s2 = new Set([5, 2, 7, 4]);
|
||||
expect(intersect(s1, s2)).toEqual(new Set([2, 4]));
|
||||
});
|
||||
it('returns a set containing common elements between two sets of differing sizes', () => {
|
||||
const s1 = new Set([1, 2, 3, 4]);
|
||||
const s2 = new Set([5, 4, 3, 2, 1]);
|
||||
expect(intersect(s1, s2)).toEqual(new Set([1, 2, 3, 4]));
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Performs a shallow comparison of two sets with the same item type.
|
||||
*/
|
||||
export function equal<T>(a: Set<T>, b: Set<T>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
const it = a.values();
|
||||
while (true) {
|
||||
const { value, done } = it.next();
|
||||
if (done) {
|
||||
return true;
|
||||
}
|
||||
if (!b.has(value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new set with items in both sets using shallow comparison.
|
||||
*/
|
||||
export function intersect<T>(a: Set<T>, b: Set<T>): Set<T> {
|
||||
const result = new Set<T>();
|
||||
const it = b.values();
|
||||
while (true) {
|
||||
const { value, done } = it.next();
|
||||
if (done) {
|
||||
return result;
|
||||
}
|
||||
if (a.has(value)) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { VisualizationSuggestion, PanelModel, PanelPlugin, PanelData } from '@grafana/data';
|
||||
|
||||
export function getOptionSuggestions(
|
||||
plugin: PanelPlugin,
|
||||
panel: PanelModel,
|
||||
data?: PanelData
|
||||
): VisualizationSuggestion[] {
|
||||
// const supplier = plugin.getSuggestionsSupplier();
|
||||
|
||||
// if (supplier && supplier.getOptionSuggestions) {
|
||||
// const builder = new VisualizationSuggestionsBuilder(data, panel);
|
||||
// supplier.getOptionSuggestions(builder);
|
||||
// return builder.getList();
|
||||
// }
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { PluginTypeCode } from '../types';
|
||||
|
||||
interface PluginTypeIconProps {
|
||||
typeCode: PluginTypeCode;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export const PluginTypeIcon = ({ typeCode, size }: PluginTypeIconProps) => {
|
||||
const imageUrl = ((typeCode: string) => {
|
||||
switch (typeCode) {
|
||||
case 'panel':
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNSIgaGVpZ2h0PSIyNC4zMzEiIHZpZXdCb3g9Ii02MiA2My42NjkgMjUgMjQuMzMxIj48dGl0bGU+aWNvbl9kYXRhLXNvdXJjZTwvdGl0bGU+PHBhdGggZD0iTS00MS40MDUgNjMuNjczaC0xNi4xOUE0LjQxIDQuNDEgMCAwIDAtNjIgNjguMDc4djE1LjUxN0E0LjQxIDQuNDEgMCAwIDAtNTcuNTk1IDg4aDE2LjE5QTQuNDEgNC40MSAwIDAgMC0zNyA4My41OTVWNjguMDc4YTQuNDEgNC40MSAwIDAgMC00LjQwNS00LjQwNXptMy43MjcgMTkuOTIyYTMuNzMxIDMuNzMxIDAgMCAxLTMuNzI3IDMuNzI3aC0xNi4xOWEzLjczMSAzLjczMSAwIDAgMS0zLjcyNy0zLjcyN1Y2OC4wNzhhMy43MzEgMy43MzEgMCAwIDEgMy43MjctMy43MjdoMTYuMTlhMy43MzEgMy43MzEgMCAwIDEgMy43MjcgMy43Mjd2MTUuNTE3eiIgZmlsbD0iIzg5ODk4OSIvPjxnIGZpbGw9IiM4OTg5ODkiPjxwYXRoIGQ9Ik0tNTYuNDU3IDg1LjE0N2gxMy45MTRhMi4zNSAyLjM1IDAgMCAwIDIuMjctMS43NTloLTE4LjQ1NGEyLjM1MSAyLjM1MSAwIDAgMCAyLjI3IDEuNzU5em0uMDQ3LTguNzA2bDIuMDg3LjgzLjgxLS45NzdoLTUuMjk5djEuNjgzbDEuNjM2LTEuNDA4YS43NTEuNzUxIDAgMCAxIC43NjYtLjEyOHptNS44MzktMy42OTRoLTguMjQxdjIuODI4aDUuODk1em03Ljk0OSAyLjgyOGguNzM5bDEuNjk1LTEuMzA0di0xLjUyNGgtNC4yN3ptLTE2LjE5IDQuMzgxdjIuNzEzaDE4LjYyNHYtMi44MjhoLTE4LjQ5MXptOS43NjYtOS4wNDdhLjc0OC43NDggMCAwIDEgLjg5MS0uMjAybDIuODY5IDEuMzIyaDUuMDk5VjY5LjJoLTE4LjYyNXYyLjgyOGg4LjgzOGwuOTI4LTEuMTE5em02LjUwMy00LjM4N2gtMTMuOTE0YTIuMzUyIDIuMzUyIDAgMCAwLTIuMzE2IDEuOTZoMTguNTQ1YTIuMzUgMi4zNSAwIDAgMC0yLjMxNS0xLjk2em0tNC43NjggNi4yMjVoLTEuMzExbC0yLjM0NiAyLjgyOGg2LjU1OGwtMS4zODItMi4xMjh6Ii8+PHBhdGggZD0iTS00Mi4xMDUgNzcuNjM5YS43NDcuNzQ3IDAgMCAxLTEuMDg2LS4xODZsLS43NTItMS4xNThoLTcuNjIxbC0xLjk1MiAyLjM1NGEuNzUuNzUgMCAwIDEtLjg1NC4yMTlsLTIuMTcyLS44NjQtMS4zMDEgMS4xMmgxNy42NTd2LTIuODI4aC0uMTdsLTEuNzQ5IDEuMzQzeiIvPjwvZz48L3N2Zz4=';
|
||||
case 'datasource':
|
||||
return 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxOS4wLjEsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMjVweCIgaGVpZ2h0PSIyNC4zcHgiIHZpZXdCb3g9Ii0xODcgNzMuNyAyNSAyNC4zIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IC0xODcgNzMuNyAyNSAyNC4zOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSINCgk+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDojNWE1YTVhO30NCjwvc3R5bGU+DQo8Zz4NCgk8dGl0bGU+aWNvbl9kYXRhLXNvdXJjZTwvdGl0bGU+DQoJPGc+DQoJCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0tMTc0LjUsOTQuM2MtNS41LDAtMTAuMi0xLjYtMTIuMy00Yy0wLjEsMC4zLTAuMiwwLjYtMC4yLDFjMCwzLjIsNS43LDYsMTIuNSw2czEyLjUtMi43LDEyLjUtNg0KCQkJYzAtMC4zLTAuMS0wLjctMC4yLTFDLTE2NC40LDkyLjctMTY5LDk0LjMtMTc0LjUsOTQuM3oiLz4NCgkJPHBhdGggY2xhc3M9InN0MCIgZD0iTS0xNzQuNSw4OC45Yy01LjUsMC0xMC4yLTEuNi0xMi4zLTRjLTAuMSwwLjMtMC4yLDAuNi0wLjIsMWMwLDMuMiw1LjcsNiwxMi41LDZzMTIuNS0yLjcsMTIuNS02DQoJCQljMC0wLjMtMC4xLTAuNy0wLjItMUMtMTY0LjQsODcuMy0xNjksODguOS0xNzQuNSw4OC45eiIvPg0KCQk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNLTE4Nyw4MC40YzAsMy4yLDUuNyw2LDEyLjUsNnMxMi41LTIuNywxMi41LTZzLTUuNy02LTEyLjUtNlMtMTg3LDc3LjEtMTg3LDgwLjR6Ii8+DQoJPC9nPg0KPC9nPg0KPC9zdmc+DQo=';
|
||||
case 'app':
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMi45NzgiIGhlaWdodD0iMjUiIHZpZXdCb3g9IjAgMCAyMi45NzggMjUiPjx0aXRsZT5pY29uX2FwcHM8L3RpdGxlPjxwYXRoIGQ9Ik0yMS4yMjQgMTEuMjFhMS43NiAxLjc2IDAgMCAwLTEuNjgyIDEuMjU3SDE0Ljg5YTQuMjMgNC4yMyAwIDAgMC0uMy0xLjE0MmwzLjExNC0xLjhBMi4wNSAyLjA1IDAgMSAwIDE3LjEyIDguMWExLjk4NiAxLjk4NiAwIDAgMCAuMDguNTY1bC0zLjExOCAxLjhhNC4yNDMgNC4yNDMgMCAwIDAtLjgzNS0uODM1bC41ODYtMS4wMTVhMi4xNjUgMi4xNjUgMCAwIDAgLjU5My4wODYgMi4xMTYgMi4xMTYgMCAxIDAtMS40NS0uNThsLS41OCAxLjAxMmEzLjk1NSAzLjk1NSAwIDAgMC0xLjE0LS4zVjMuMDA4YTEuNTQ3IDEuNTQ3IDAgMSAwLTEgMHY1LjgxN2E0LjIzIDQuMjMgMCAwIDAtMS4xNDMuM2wtMi4wNi0zLjU2MkExLjY4NCAxLjY4NCAwIDAgMCA3LjUxIDQuNGExLjcxIDEuNzEgMCAxIDAtMS4zMiAxLjY2bDIuMDYgMy41N2E0LjMyMyA0LjMyMyAwIDAgMC0uODQzLjg0M2wtMy41NjYtMi4wNmExLjc2IDEuNzYgMCAwIDAgLjA0NS0uMzkgMS43IDEuNyAwIDEgMC0xLjcgMS43IDEuNjg2IDEuNjg2IDAgMCAwIDEuMTU1LS40NTNsMy41NyAyLjA2YTQuMDkgNC4wOSAwIDAgMC0uMyAxLjEzM0g1LjIwNmEyLjMwNSAyLjMwNSAwIDEgMCAwIDFoMS40MDdhNC4yMyA0LjIzIDAgMCAwIC4zIDEuMTQyTDMuMTAyIDE2LjhhMS44MjMgMS44MjMgMCAxIDAgLjU1IDEuMyAxLjc3NSAxLjc3NSAwIDAgMC0uMDYzLS40MzhsMy44MjItMi4yMDZhNC4yIDQuMiAwIDAgMCAuODQzLjg0bC0yLjk4IDUuMTkzYTEuNzI3IDEuNzI3IDAgMCAwLS40MTMtLjA1IDEuNzggMS43OCAwIDEgMCAxLjI3Ny41NGwyLjk4LTUuMTc4YTQuMDkgNC4wOSAwIDAgMCAxLjEzMy4zdjEuNDA4YTIuMDU1IDIuMDU1IDAgMSAwIC45OSAwVjE3LjFhNC4yMyA0LjIzIDAgMCAwIDEuMTQzLS4zbDIuNDYgNC4yNmExLjgyNCAxLjgyNCAwIDEgMCAxLjMwNi0uNTUyIDEuNzc4IDEuNzc4IDAgMCAwLS40NDYuMDU3bC0yLjQ2LTQuMjY1YTMuOTYgMy45NiAwIDAgMCAuODI2LS44MjdsLjQ0Ni4yNThhMi4zMjQgMi4zMjQgMCAwIDAtLjEyLjczOCAyLjQgMi40IDAgMSAwIC42Mi0xLjZsLS40NDMtLjI1NGE0LjE1NSA0LjE1NSAwIDAgMCAuMzEtMS4xNTRoNC42NmExLjc1MyAxLjc1MyAwIDEgMCAxLjY4LTIuMjV6bTAgMi43MWEuOTU4Ljk1OCAwIDEgMSAuOTU4LS45NTguOTYuOTYgMCAwIDEtLjk1OC45NnpNMTAuNzUgMTYuMTRhMy4xNzcgMy4xNzcgMCAxIDEgMy4xNzctMy4xNzggMy4xOCAzLjE4IDAgMCAxLTMuMTc3IDMuMTc3em03LjE2My04LjA0YTEuMjYgMS4yNiAwIDEgMSAxLjI2IDEuMjYgMS4yNiAxLjI2IDAgMCAxLTEuMjYtMS4yNnpNMTUuNzQgNi41OTRhMS4zMTQgMS4zMTQgMCAxIDEtMS4zMTUtMS4zMTQgMS4zMTUgMS4zMTUgMCAwIDEgMS4zMTQgMS4zMTR6TTkuOTk2IDEuNTQ4YS43NTMuNzUzIDAgMSAxIC43NTMuNzUzLjc1NC43NTQgMCAwIDEtLjc1My0uNzUyek00Ljg5NyA0LjRhLjkxLjkxIDAgMSAxIC45MS45MS45MS45MSAwIDAgMS0uOTEtLjkxek0yLjE5IDguOTM2YS45MS45MSAwIDEgMSAuOTA4LS45MS45MS45MSAwIDAgMS0uOTEuOTF6bS43NyA1LjUyNmExLjUwNiAxLjUwNiAwIDEgMSAxLjUwNS0xLjUwNiAxLjUwOCAxLjUwOCAwIDAgMS0xLjUwOCAxLjUwNnptLS4xIDMuNjQ2YTEuMDMyIDEuMDMyIDAgMSAxLTEuMDMzLTEuMDMyIDEuMDMzIDEuMDMzIDAgMCAxIDEuMDMyIDEuMDMyem0yLjk4NyA1LjExYS45ODYuOTg2IDAgMSAxLS45ODYtLjk4Ny45ODcuOTg3IDAgMCAxIC45ODcuOTg3em00LjktMS40NmExLjI2IDEuMjYgMCAxIDEgMS4yNi0xLjI2IDEuMjYgMS4yNiAwIDAgMS0xLjI1NyAxLjI2em02LjQ0Mi41N2ExLjAzMiAxLjAzMiAwIDEgMS0xLjAzMy0xLjAyOCAxLjAzMyAxLjAzMyAwIDAgMSAxLjAzMiAxLjAyOHptLS40LTcuNDU4YTEuNiAxLjYgMCAxIDEtMS42IDEuNiAxLjYgMS42IDAgMCAxIDEuNi0xLjZ6IiBmaWxsPSIjODk4OTg5Ii8+PC9zdmc+';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})(typeCode);
|
||||
|
||||
return imageUrl ? (
|
||||
<div
|
||||
className={css`
|
||||
display: inline-block;
|
||||
background-image: url(${imageUrl});
|
||||
background-size: ${size}px;
|
||||
background-repeat: no-repeat;
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
`}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { LocalPlugin } from './types';
|
||||
|
||||
export function isLocalPlugin(plugin: any): plugin is LocalPlugin {
|
||||
// super naive way of figuring out if this is a local plugin
|
||||
return 'category' in plugin;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { NavModel, NavModelItem } from '@grafana/data';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
const node: NavModelItem = {
|
||||
id: 'not-found',
|
||||
text: 'The plugin catalog is not enabled',
|
||||
icon: 'exclamation-triangle',
|
||||
url: 'not-found',
|
||||
};
|
||||
|
||||
const navModel: NavModel = { node, main: node };
|
||||
|
||||
export default function NotEnabled(): JSX.Element | null {
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
To enable installing plugins via catalog, please refer to the{' '}
|
||||
<a
|
||||
className={css`
|
||||
text-decoration: underline;
|
||||
`}
|
||||
href="https://grafana.com/docs/grafana/latest/plugins/catalog"
|
||||
>
|
||||
Plugin Catalog
|
||||
</a>{' '}
|
||||
instructions
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React, { Component } from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data';
|
||||
import { locationService, setEchoSrv } from '@grafana/runtime';
|
||||
import { Echo } from 'app/core/services/echo/Echo';
|
||||
|
||||
import { getMockPlugin } from '../__mocks__/pluginMocks';
|
||||
import { useImportAppPlugin } from '../hooks/useImportAppPlugin';
|
||||
|
||||
import { AppPluginLoader } from './AppPluginLoader';
|
||||
|
||||
jest.mock('../hooks/useImportAppPlugin', () => ({
|
||||
useImportAppPlugin: jest.fn(),
|
||||
}));
|
||||
|
||||
const useImportAppPluginMock = useImportAppPlugin as jest.Mock<
|
||||
ReturnType<typeof useImportAppPlugin>,
|
||||
Parameters<typeof useImportAppPlugin>
|
||||
>;
|
||||
|
||||
const TEXTS = {
|
||||
PLUGIN_TITLE: 'Amazing App',
|
||||
PLUGIN_CONTENT: 'This is my amazing app plugin!',
|
||||
PLUGIN_TAB_TITLE_A: 'Tab (A)',
|
||||
PLUGIN_TAB_TITLE_B: 'Tab (B)',
|
||||
};
|
||||
|
||||
describe('AppPluginLoader', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
AppPluginComponent.timesMounted = 0;
|
||||
setEchoSrv(new Echo());
|
||||
});
|
||||
|
||||
test('renders the app plugin correctly', async () => {
|
||||
useImportAppPluginMock.mockReturnValue({ value: getAppPluginMock(), loading: false, error: undefined });
|
||||
|
||||
renderAppPlugin();
|
||||
|
||||
expect(await screen.findByText(TEXTS.PLUGIN_TITLE)).toBeVisible();
|
||||
expect(await screen.findByText(TEXTS.PLUGIN_CONTENT)).toBeVisible();
|
||||
expect(await screen.findByLabelText(`Tab ${TEXTS.PLUGIN_TAB_TITLE_A}`)).toBeVisible();
|
||||
expect(await screen.findByLabelText(`Tab ${TEXTS.PLUGIN_TAB_TITLE_B}`)).toBeVisible();
|
||||
expect(screen.queryByText('Loading ...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the app plugin only once', async () => {
|
||||
useImportAppPluginMock.mockReturnValue({ value: getAppPluginMock(), loading: false, error: undefined });
|
||||
|
||||
renderAppPlugin();
|
||||
|
||||
expect(await screen.findByText(TEXTS.PLUGIN_TITLE)).toBeVisible();
|
||||
expect(AppPluginComponent.timesMounted).toEqual(1);
|
||||
});
|
||||
|
||||
test('renders a loader while the plugin is loading', async () => {
|
||||
useImportAppPluginMock.mockReturnValue({ value: undefined, loading: true, error: undefined });
|
||||
|
||||
renderAppPlugin();
|
||||
|
||||
expect(await screen.findByText('Loading ...')).toBeVisible();
|
||||
expect(screen.queryByText(TEXTS.PLUGIN_TITLE)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders an error message if there are any errors while importing the plugin', async () => {
|
||||
const errorMsg = 'Unable to find plugin';
|
||||
useImportAppPluginMock.mockReturnValue({ value: undefined, loading: false, error: new Error(errorMsg) });
|
||||
|
||||
renderAppPlugin();
|
||||
|
||||
expect(await screen.findByText(errorMsg)).toBeVisible();
|
||||
expect(screen.queryByText(TEXTS.PLUGIN_TITLE)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function renderAppPlugin() {
|
||||
render(
|
||||
<Router history={locationService.getHistory()}>
|
||||
<AppPluginLoader id="foo" />;
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
class AppPluginComponent extends Component<AppRootProps> {
|
||||
static timesMounted = 0;
|
||||
|
||||
componentDidMount() {
|
||||
AppPluginComponent.timesMounted += 1;
|
||||
|
||||
const node: NavModelItem = {
|
||||
text: TEXTS.PLUGIN_TITLE,
|
||||
children: [
|
||||
{
|
||||
text: TEXTS.PLUGIN_TAB_TITLE_A,
|
||||
url: '/tab-a',
|
||||
id: 'a',
|
||||
},
|
||||
{
|
||||
text: TEXTS.PLUGIN_TAB_TITLE_B,
|
||||
url: '/tab-b',
|
||||
id: 'b',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
this.props.onNavChanged({
|
||||
main: node,
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <p>{TEXTS.PLUGIN_CONTENT}</p>;
|
||||
}
|
||||
}
|
||||
|
||||
function getAppPluginMeta() {
|
||||
return getMockPlugin({
|
||||
type: PluginType.app,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
function getAppPluginMock() {
|
||||
const plugin = new AppPlugin();
|
||||
|
||||
plugin.root = AppPluginComponent;
|
||||
plugin.init(getAppPluginMeta());
|
||||
|
||||
return plugin;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { getWarningNav } from 'app/angular/services/nav_model_srv';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
|
||||
import { useImportAppPlugin } from '../hooks/useImportAppPlugin';
|
||||
|
||||
type AppPluginLoaderProps = {
|
||||
// The id of the app plugin to be loaded
|
||||
id: string;
|
||||
// The base URL path - defaults to the current path
|
||||
basePath?: string;
|
||||
};
|
||||
|
||||
// This component can be used to render an app-plugin based on its plugin ID.
|
||||
export const AppPluginLoader = ({ id, basePath }: AppPluginLoaderProps) => {
|
||||
const [nav, setNav] = useState<NavModel | null>(null);
|
||||
const { value: plugin, error, loading } = useImportAppPlugin(id);
|
||||
const queryParams = useParams();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
if (error) {
|
||||
return <Page.Header navItem={getWarningNav(error.message, error.stack).main} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <PageLoader />}
|
||||
{nav && <Page.Header navItem={nav.main} />}
|
||||
{!loading && plugin && plugin.root && (
|
||||
<plugin.root
|
||||
meta={plugin.meta}
|
||||
basename={basePath || pathname}
|
||||
onNavChanged={setNav}
|
||||
query={queryParams}
|
||||
path={pathname}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,150 +0,0 @@
|
||||
import { render, act, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AppPlugin, PluginType } from '@grafana/data';
|
||||
|
||||
import { getMockPlugin } from '../../__mocks__/pluginMocks';
|
||||
import { getPluginSettings } from '../../pluginSettings';
|
||||
import { importAppPlugin } from '../../plugin_loader';
|
||||
import { useImportAppPlugin } from '../useImportAppPlugin';
|
||||
|
||||
jest.mock('../../pluginSettings', () => ({
|
||||
getPluginSettings: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../plugin_loader', () => ({
|
||||
importAppPlugin: jest.fn(),
|
||||
}));
|
||||
|
||||
const importAppPluginMock = importAppPlugin as jest.Mock<
|
||||
ReturnType<typeof importAppPlugin>,
|
||||
Parameters<typeof importAppPlugin>
|
||||
>;
|
||||
|
||||
const getPluginSettingsMock = getPluginSettings as jest.Mock<
|
||||
ReturnType<typeof getPluginSettings>,
|
||||
Parameters<typeof getPluginSettings>
|
||||
>;
|
||||
|
||||
const PLUGIN_ID = 'sample-plugin';
|
||||
|
||||
describe('useImportAppPlugin()', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('returns the imported plugin in case it exists', async () => {
|
||||
let response: any;
|
||||
getPluginSettingsMock.mockResolvedValue(getAppPluginMeta());
|
||||
importAppPluginMock.mockResolvedValue(getAppPluginMock());
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error).toBeUndefined());
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns an error if the plugin does not exist', async () => {
|
||||
let response: any;
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toMatch(/unknown plugin/i));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns an error if the plugin is not an app', async () => {
|
||||
let response: any;
|
||||
getPluginSettingsMock.mockResolvedValue(getAppPluginMeta({ type: PluginType.panel }));
|
||||
importAppPluginMock.mockResolvedValue(getAppPluginMock());
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toMatch(/plugin must be an app/i));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns an error if the plugin is not enabled', async () => {
|
||||
let response: any;
|
||||
getPluginSettingsMock.mockResolvedValue(getAppPluginMeta({ enabled: false }));
|
||||
importAppPluginMock.mockResolvedValue(getAppPluginMock());
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toMatch(/is not enabled/i));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns errors that happen during fetching plugin settings', async () => {
|
||||
let response: any;
|
||||
const errorMsg = 'Error while fetching plugin data';
|
||||
getPluginSettingsMock.mockRejectedValue(new Error(errorMsg));
|
||||
importAppPluginMock.mockResolvedValue(getAppPluginMock());
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toBe(errorMsg));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns errors that happen during importing a plugin', async () => {
|
||||
let response: any;
|
||||
const errorMsg = 'Error while importing the plugin';
|
||||
getPluginSettingsMock.mockResolvedValue(getAppPluginMeta());
|
||||
importAppPluginMock.mockRejectedValue(new Error(errorMsg));
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toBe(errorMsg));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
});
|
||||
|
||||
function runHook(id: string): any {
|
||||
const returnVal = {};
|
||||
function TestComponent() {
|
||||
Object.assign(returnVal, useImportAppPlugin(id));
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
function getAppPluginMeta(overrides?: Record<string, any>) {
|
||||
return getMockPlugin({
|
||||
id: PLUGIN_ID,
|
||||
type: PluginType.app,
|
||||
enabled: true,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function getAppPluginMock() {
|
||||
const plugin = new AppPlugin();
|
||||
|
||||
plugin.init(getAppPluginMeta());
|
||||
|
||||
return plugin;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { PluginType } from '@grafana/data';
|
||||
|
||||
import { getPluginSettings } from '../pluginSettings';
|
||||
import { importAppPlugin } from '../plugin_loader';
|
||||
|
||||
export const useImportAppPlugin = (id: string) => {
|
||||
return useAsync(async () => {
|
||||
const pluginMeta = await getPluginSettings(id);
|
||||
|
||||
if (!pluginMeta) {
|
||||
throw new Error(`Unknown plugin: "${id}"`);
|
||||
}
|
||||
|
||||
if (pluginMeta.type !== PluginType.app) {
|
||||
throw new Error(`Plugin must be an app (currently "${pluginMeta.type}")`);
|
||||
}
|
||||
|
||||
if (!pluginMeta.enabled) {
|
||||
throw new Error(`Application "${id}" is not enabled`);
|
||||
}
|
||||
|
||||
return await importAppPlugin(pluginMeta);
|
||||
});
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { Icon, InfoBox, stylesFactory, useTheme } from '@grafana/ui';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
infoBox: css`
|
||||
margin-top: ${theme.spacing.xs};
|
||||
`,
|
||||
}));
|
||||
|
||||
export const HelpToggle = ({ children }: React.PropsWithChildren<{}>) => {
|
||||
const [isHelpVisible, setIsHelpVisible] = useState(false);
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="gf-form-label query-keyword pointer" onClick={(_) => setIsHelpVisible(!isHelpVisible)}>
|
||||
Help
|
||||
<Icon name={isHelpVisible ? 'angle-down' : 'angle-right'} />
|
||||
</button>
|
||||
{isHelpVisible && <InfoBox className={cx(styles.infoBox)}>{children}</InfoBox>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EventBusExtended } from '@grafana/data';
|
||||
|
||||
export interface PanelModelForLegacyQueryEditors {
|
||||
events: EventBusExtended;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { useStyles } from '@grafana/ui';
|
||||
|
||||
interface VariableSectionHeaderProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function VariableSectionHeader({ name }: PropsWithChildren<VariableSectionHeaderProps>): ReactElement {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
return <h5 className={styles.sectionHeading}>{name}</h5>;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme) {
|
||||
return {
|
||||
sectionHeading: css`
|
||||
label: sectionHeading;
|
||||
font-size: ${theme.typography.size.md};
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`,
|
||||
};
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { VariableQueryEditor } from './VariableQueryEditor';
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export const SectionFill = () => (
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form-label gf-form-label--grow"></label>
|
||||
</div>
|
||||
);
|
||||
@@ -1,50 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Icon, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
feedbackUrl?: string;
|
||||
}
|
||||
|
||||
export function FeedbackLink({ feedbackUrl }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!config.feedbackLinksEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={1}>
|
||||
<a
|
||||
href={feedbackUrl}
|
||||
className={styles.link}
|
||||
title="This query builder is new, please let us know how we can improve it"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
onClick={() =>
|
||||
reportInteraction('grafana_feedback_link_clicked', {
|
||||
link: feedbackUrl,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Icon name="comment-alt-message" /> Give feedback
|
||||
</a>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
link: css({
|
||||
color: theme.colors.text.secondary,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
':hover': {
|
||||
color: theme.colors.text.link,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface LayoutRendererComponentProps<T extends string> {
|
||||
slots: Partial<Record<T, React.ReactNode | null>>;
|
||||
refs: Record<T, (i: any) => void>;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type LayoutRendererComponent<T extends string> = React.ComponentType<LayoutRendererComponentProps<T>>;
|
||||
|
||||
// Fluent API for defining and rendering layout
|
||||
export class LayoutBuilder<T extends string> {
|
||||
private layout: Partial<Record<T, React.ReactNode | null>> = {};
|
||||
|
||||
constructor(
|
||||
private renderer: LayoutRendererComponent<T>,
|
||||
private refsMap: Record<T, (i: any) => void>,
|
||||
private width: number,
|
||||
private height: number
|
||||
) {}
|
||||
|
||||
getLayout() {
|
||||
return this.layout;
|
||||
}
|
||||
addSlot(id: T, node: React.ReactNode) {
|
||||
this.layout[id] = node;
|
||||
return this;
|
||||
}
|
||||
|
||||
clearSlot(id: T) {
|
||||
if (this.layout[id] && this.refsMap[id]) {
|
||||
delete this.layout[id];
|
||||
|
||||
this.refsMap[id](null);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.layout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.createElement(this.renderer, {
|
||||
slots: this.layout,
|
||||
refs: this.refsMap,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,420 +0,0 @@
|
||||
import {
|
||||
ByNamesMatcherMode,
|
||||
DataFrame,
|
||||
FieldConfigSource,
|
||||
FieldMatcherID,
|
||||
FieldType,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { GraphNGLegendEvent, SeriesVisibilityChangeMode } from '@grafana/ui';
|
||||
|
||||
import { hideSeriesConfigFactory } from './hideSeriesConfigFactory';
|
||||
|
||||
describe('hideSeriesConfigFactory', () => {
|
||||
it('should create config override matching one series', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.ToggleSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 0,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature'])],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create config override matching one series if selected with others', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.ToggleSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 0,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature', 'humidity'])],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'pressure', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature'])],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create config override that append series to existing override', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.AppendToSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 1,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature'])],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'pressure', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature', 'humidity'])],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create config override that hides all series if appending only existing series', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.AppendToSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 0,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature'])],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [createOverride([])],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create config override that removes series if appending existing field', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.AppendToSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 0,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature', 'humidity'])],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [createOverride(['humidity'])],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create config override replacing existing series', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.ToggleSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 1,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature'])],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [createOverride(['humidity'])],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create config override removing existing series', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.ToggleSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 0,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature'])],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove override if all fields are appended', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.AppendToSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 1,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature'])],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create config override hiding appended series if no previous override exists', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.AppendToSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 0,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'pressure', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [createOverride(['humidity', 'pressure'])],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return existing override if invalid index is passed', () => {
|
||||
const event: GraphNGLegendEvent = {
|
||||
mode: SeriesVisibilityChangeMode.ToggleSelection,
|
||||
fieldIndex: {
|
||||
frameIndex: 4,
|
||||
fieldIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const existingConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature'])],
|
||||
};
|
||||
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config = hideSeriesConfigFactory(event, existingConfig, data);
|
||||
|
||||
expect(config).toEqual({
|
||||
defaults: {},
|
||||
overrides: [createOverride(['temperature'])],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const createOverride = (matchers: string[]) => {
|
||||
return {
|
||||
__systemRef: 'hideSeriesFrom',
|
||||
matcher: {
|
||||
id: FieldMatcherID.byNames,
|
||||
options: {
|
||||
mode: ByNamesMatcherMode.exclude,
|
||||
names: matchers,
|
||||
prefix: 'All except:',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
id: 'custom.hideFrom',
|
||||
value: {
|
||||
graph: true,
|
||||
legend: false,
|
||||
tooltip: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -1,175 +0,0 @@
|
||||
import {
|
||||
ByNamesMatcherMode,
|
||||
DataFrame,
|
||||
DynamicConfigValue,
|
||||
FieldConfigSource,
|
||||
FieldMatcherID,
|
||||
FieldType,
|
||||
getFieldDisplayName,
|
||||
isSystemOverrideWithRef,
|
||||
SystemConfigOverrideRule,
|
||||
} from '@grafana/data';
|
||||
import { GraphNGLegendEvent, SeriesVisibilityChangeMode } from '@grafana/ui';
|
||||
|
||||
const displayOverrideRef = 'hideSeriesFrom';
|
||||
const isHideSeriesOverride = isSystemOverrideWithRef(displayOverrideRef);
|
||||
|
||||
export const hideSeriesConfigFactory = (
|
||||
event: GraphNGLegendEvent,
|
||||
fieldConfig: FieldConfigSource<any>,
|
||||
data: DataFrame[]
|
||||
): FieldConfigSource<any> => {
|
||||
const { fieldIndex, mode } = event;
|
||||
const { overrides } = fieldConfig;
|
||||
|
||||
const frame = data[fieldIndex.frameIndex];
|
||||
|
||||
if (!frame) {
|
||||
return fieldConfig;
|
||||
}
|
||||
|
||||
const field = frame.fields[fieldIndex.fieldIndex];
|
||||
|
||||
if (!field) {
|
||||
return fieldConfig;
|
||||
}
|
||||
|
||||
const displayName = getFieldDisplayName(field, frame, data);
|
||||
const currentIndex = overrides.findIndex(isHideSeriesOverride);
|
||||
|
||||
if (currentIndex < 0) {
|
||||
if (mode === SeriesVisibilityChangeMode.ToggleSelection) {
|
||||
const override = createOverride([displayName]);
|
||||
|
||||
return {
|
||||
...fieldConfig,
|
||||
overrides: [override, ...fieldConfig.overrides],
|
||||
};
|
||||
}
|
||||
|
||||
const displayNames = getDisplayNames(data, displayName);
|
||||
const override = createOverride(displayNames);
|
||||
|
||||
return {
|
||||
...fieldConfig,
|
||||
overrides: [override, ...fieldConfig.overrides],
|
||||
};
|
||||
}
|
||||
|
||||
const overridesCopy = Array.from(overrides);
|
||||
const [current] = overridesCopy.splice(currentIndex, 1) as SystemConfigOverrideRule[];
|
||||
|
||||
if (mode === SeriesVisibilityChangeMode.ToggleSelection) {
|
||||
const existing = getExistingDisplayNames(current);
|
||||
|
||||
if (existing[0] === displayName && existing.length === 1) {
|
||||
return {
|
||||
...fieldConfig,
|
||||
overrides: overridesCopy,
|
||||
};
|
||||
}
|
||||
|
||||
const override = createOverride([displayName]);
|
||||
|
||||
return {
|
||||
...fieldConfig,
|
||||
overrides: [override, ...overridesCopy],
|
||||
};
|
||||
}
|
||||
|
||||
const override = createExtendedOverride(current, displayName);
|
||||
|
||||
if (allFieldsAreExcluded(override, data)) {
|
||||
return {
|
||||
...fieldConfig,
|
||||
overrides: overridesCopy,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...fieldConfig,
|
||||
overrides: [override, ...overridesCopy],
|
||||
};
|
||||
};
|
||||
|
||||
const createExtendedOverride = (current: SystemConfigOverrideRule, displayName: string): SystemConfigOverrideRule => {
|
||||
const property = current.properties.find((p) => p.id === 'custom.hideFrom');
|
||||
const existing = getExistingDisplayNames(current);
|
||||
const index = existing.findIndex((name) => name === displayName);
|
||||
|
||||
if (index < 0) {
|
||||
existing.push(displayName);
|
||||
} else {
|
||||
existing.splice(index, 1);
|
||||
}
|
||||
|
||||
return createOverride(existing, property);
|
||||
};
|
||||
|
||||
const getExistingDisplayNames = (rule: SystemConfigOverrideRule): string[] => {
|
||||
const names = rule.matcher.options?.names;
|
||||
if (!Array.isArray(names)) {
|
||||
return [];
|
||||
}
|
||||
return names;
|
||||
};
|
||||
|
||||
const createOverride = (names: string[], property?: DynamicConfigValue): SystemConfigOverrideRule => {
|
||||
property = property ?? {
|
||||
id: 'custom.hideFrom',
|
||||
value: {
|
||||
graph: true,
|
||||
legend: false,
|
||||
tooltip: false,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
__systemRef: displayOverrideRef,
|
||||
matcher: {
|
||||
id: FieldMatcherID.byNames,
|
||||
options: {
|
||||
mode: ByNamesMatcherMode.exclude,
|
||||
names: names,
|
||||
prefix: 'All except:',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
...property,
|
||||
value: {
|
||||
graph: true,
|
||||
legend: false,
|
||||
tooltip: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const allFieldsAreExcluded = (override: SystemConfigOverrideRule, data: DataFrame[]): boolean => {
|
||||
return getExistingDisplayNames(override).length === getDisplayNames(data).length;
|
||||
};
|
||||
|
||||
const getDisplayNames = (data: DataFrame[], excludeName?: string): string[] => {
|
||||
const unique = new Set<string>();
|
||||
|
||||
for (const frame of data) {
|
||||
for (const field of frame.fields) {
|
||||
if (field.type !== FieldType.number) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = getFieldDisplayName(field, frame, data);
|
||||
|
||||
if (name === excludeName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unique.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(unique);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { colorManipulator, DataFrame, DataFrameFieldIndex, DataFrameView, TimeZo
|
||||
import { EventsCanvas, UPlotConfigBuilder, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { AnnotationMarker } from './annotations/AnnotationMarker';
|
||||
import { AnnotationsDataFrameViewDTO } from './types';
|
||||
|
||||
interface AnnotationsPluginProps {
|
||||
config: UPlotConfigBuilder;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { colorManipulator, DataFrame, getDisplayProcessor, GrafanaTheme2, TimeZo
|
||||
import { PlotSelection, useStyles2, useTheme2, Portal, DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
|
||||
|
||||
import { getCommonAnnotationStyles } from '../styles';
|
||||
import { AnnotationsDataFrameViewDTO } from '../types';
|
||||
|
||||
import { AnnotationEditorForm } from './AnnotationEditorForm';
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Button, Field, Form, HorizontalGroup, InputControl, TextArea, usePanelC
|
||||
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
|
||||
import { getAnnotationTags } from 'app/features/annotations/api';
|
||||
|
||||
import { AnnotationsDataFrameViewDTO } from '../types';
|
||||
|
||||
interface AnnotationEditFormDTO {
|
||||
description: string;
|
||||
tags: string[];
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Portal, useStyles2, usePanelContext } from '@grafana/ui';
|
||||
import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins';
|
||||
|
||||
import { getCommonAnnotationStyles } from '../styles';
|
||||
import { AnnotationsDataFrameViewDTO } from '../types';
|
||||
|
||||
import { AnnotationEditorForm } from './AnnotationEditorForm';
|
||||
import { AnnotationTooltip } from './AnnotationTooltip';
|
||||
|
||||
@@ -7,6 +7,8 @@ import config from 'app/core/config';
|
||||
import alertDef from 'app/features/alerting/state/alertDef';
|
||||
import { CommentManager } from 'app/features/comments/CommentManager';
|
||||
|
||||
import { AnnotationsDataFrameViewDTO } from '../types';
|
||||
|
||||
interface AnnotationTooltipProps {
|
||||
annotation: AnnotationsDataFrameViewDTO;
|
||||
timeFormatter: (v: number) => string;
|
||||
|
||||
@@ -3,6 +3,8 @@ import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
|
||||
|
||||
import { AnnotationsDataFrameViewDTO } from './types';
|
||||
|
||||
export const getCommonAnnotationStyles = (theme: GrafanaTheme2) => {
|
||||
return (annotation?: AnnotationsDataFrameViewDTO) => {
|
||||
const color = theme.visualization.getColorByName(annotation?.color || DEFAULT_ANNOTATION_COLOR);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface AnnotationsDataFrameViewDTO {
|
||||
export interface AnnotationsDataFrameViewDTO {
|
||||
id: string;
|
||||
/** @deprecate */
|
||||
dashboardId: number;
|
||||
|
||||
Reference in New Issue
Block a user