mirror of https://github.com/grafana/grafana
Chore: Remove unused files (#57515)
parent
069a4cb21b
commit
b49713d323
@ -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); |
||||
}; |
Loading…
Reference in new issue