Chore: Remove unused files (#57515)

pull/57640/head
kay delaney 3 years ago committed by GitHub
parent 069a4cb21b
commit b49713d323
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      .betterer.results
  2. 1
      public/app/core/components/AppChrome/constants.ts
  3. 26
      public/app/core/components/TagFilter/TagValue.tsx
  4. 12
      public/app/core/hooks/useRefMounted.ts
  5. 32
      public/app/core/reducers/processsAclItems.ts
  6. 16
      public/app/features/alerting/unified/components/TimeToNow.tsx
  7. 143
      public/app/features/alerting/unified/components/silences/SilenceTableRow.tsx
  8. 33
      public/app/features/dashboard/components/DashboardSettings/DashboardSettingsHeader.tsx
  9. 1
      public/app/features/dashboard/components/PanelEditor/index.ts
  10. 77
      public/app/features/dashboard/dashgrid/PanelResizer.tsx
  11. 32
      public/app/features/datasources/passwordHandlers.test.ts
  12. 43
      public/app/features/datasources/passwordHandlers.ts
  13. 52
      public/app/features/explore/utils/set.test.ts
  14. 35
      public/app/features/explore/utils/set.ts
  15. 17
      public/app/features/panel/state/getOptionSuggestions.ts
  16. 37
      public/app/features/plugins/admin/components/PluginTypeIcon.tsx
  17. 6
      public/app/features/plugins/admin/guards.ts
  18. 33
      public/app/features/plugins/admin/pages/NotEnabed.tsx
  19. 132
      public/app/features/plugins/components/AppPluginLoader.test.tsx
  20. 44
      public/app/features/plugins/components/AppPluginLoader.tsx
  21. 150
      public/app/features/plugins/hooks/tests/useImportAppPlugin.test.tsx
  22. 26
      public/app/features/plugins/hooks/useImportAppPlugin.ts
  23. 27
      public/app/features/query/components/HelpToggle.tsx
  24. 5
      public/app/features/query/state/types.ts
  25. 25
      public/app/features/variables/editor/VariableSectionHeader.tsx
  26. 1
      public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/index.tsx
  27. 7
      public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/SectionFill.tsx
  28. 50
      public/app/plugins/datasource/prometheus/querybuilder/shared/FeedbackLink.tsx
  29. 52
      public/app/plugins/panel/timeseries/LayoutBuilder.ts
  30. 420
      public/app/plugins/panel/timeseries/overrides/hideSeriesConfigFactory.test.ts
  31. 175
      public/app/plugins/panel/timeseries/overrides/hideSeriesConfigFactory.ts
  32. 1
      public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx
  33. 1
      public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx
  34. 2
      public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx
  35. 1
      public/app/plugins/panel/timeseries/plugins/annotations/AnnotationMarker.tsx
  36. 2
      public/app/plugins/panel/timeseries/plugins/annotations/AnnotationTooltip.tsx
  37. 2
      public/app/plugins/panel/timeseries/plugins/styles.ts
  38. 2
      public/app/plugins/panel/timeseries/plugins/types.ts

@ -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;

Loading…
Cancel
Save