Chore: remove querylibrary feature toggle (#65021)

* chore: remove querylibrary

* chore: remove querylibrary

* chore: remove querylibrary
This commit is contained in:
Artur Wierzbicki
2023-03-20 20:00:14 +04:00
committed by GitHub
parent 68551ac9ca
commit 4274b9414f
53 changed files with 6 additions and 3527 deletions

View File

@@ -1,72 +0,0 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { DataQuery } from '@grafana/data/src';
import { SavedQueryUpdateOpts } from '../components/QueryEditorDrawer';
import { getSavedQuerySrv } from './SavedQueriesSrv';
export type SavedQueryRef = {
uid?: string;
};
export type Variable = {
name: string;
type?: string;
current: {
value: string | number;
};
};
type SavedQueryMeta = {
title: string;
description?: string;
tags?: string[];
schemaVersion?: number;
variables: Variable[];
};
type SavedQueryData<TQuery extends DataQuery = DataQuery> = {
queries: TQuery[];
};
export type SavedQuery<TQuery extends DataQuery = DataQuery> = SavedQueryMeta & SavedQueryData<TQuery> & SavedQueryRef;
export const isQueryWithMixedDatasource = (savedQuery: SavedQuery): boolean => {
if (!savedQuery?.queries?.length) {
return false;
}
const firstDs = savedQuery.queries[0].datasource;
return savedQuery.queries.some((q) => q.datasource?.uid !== firstDs?.uid || q.datasource?.type !== firstDs?.type);
};
const api = createApi({
reducerPath: 'savedQueries',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getSavedQueryByUids: build.query<SavedQuery[] | null, SavedQueryRef[]>({
async queryFn(arg, queryApi, extraOptions, baseQuery) {
return { data: await getSavedQuerySrv().getSavedQueries(arg) };
},
}),
deleteSavedQuery: build.mutation<null, SavedQueryRef>({
async queryFn(arg) {
await getSavedQuerySrv().deleteSavedQuery(arg);
return {
data: null,
};
},
}),
updateSavedQuery: build.mutation<null, { query: SavedQuery; opts: SavedQueryUpdateOpts }>({
async queryFn(arg) {
await getSavedQuerySrv().updateSavedQuery(arg.query, arg.opts);
return {
data: null,
};
},
}),
}),
});
export const { useUpdateSavedQueryMutation } = api;

View File

@@ -1,26 +0,0 @@
import { getBackendSrv } from 'app/core/services/backend_srv';
import { SavedQueryUpdateOpts } from 'app/features/query-library/components/QueryEditorDrawer';
import { SavedQuery, SavedQueryRef } from './SavedQueriesApi';
export class SavedQuerySrv {
getSavedQueries = async (refs: SavedQueryRef[]): Promise<SavedQuery[]> => {
if (!refs.length) {
return [];
}
const uidParams = refs.map((r) => `uid=${r.uid}`).join('&');
return getBackendSrv().get<SavedQuery[]>(`/api/query-library?${uidParams}`);
};
deleteSavedQuery = async (ref: SavedQueryRef): Promise<void> => {
return getBackendSrv().delete(`/api/query-library?uid=${ref.uid}`);
};
updateSavedQuery = async (query: SavedQuery, options: SavedQueryUpdateOpts): Promise<void> => {
return getBackendSrv().post(`/api/query-library`, query);
};
}
const savedQuerySrv = new SavedQuerySrv();
export const getSavedQuerySrv = () => savedQuerySrv;

View File

@@ -1,123 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, CodeEditor, useStyles2 } from '@grafana/ui';
import { SavedQuery, useUpdateSavedQueryMutation } from '../api/SavedQueriesApi';
import { SavedQueryUpdateOpts } from './QueryEditorDrawer';
type Props = {
options: SavedQueryUpdateOpts;
onDismiss: () => void;
updateComponent?: () => void;
};
interface QueryForm {
val: SavedQuery;
}
const initialForm: QueryForm = {
val: {
title: 'ds-variables',
tags: [],
description: 'example description',
schemaVersion: 1,
time: {
from: 'now-6h',
to: 'now',
},
variables: [
{
name: 'var1',
type: 'text',
current: {
value: 'hello world',
},
},
],
queries: [
{
// @ts-ignore
channel: 'plugin/testdata/random-flakey-stream',
datasource: {
type: 'datasource',
uid: 'grafana',
},
filter: {
fields: ['Time', 'Value'],
},
queryType: 'measurements',
refId: 'A',
search: {
query: '',
},
},
{
// @ts-ignore
alias: 'my-alias',
datasource: {
type: 'testdata',
uid: 'PD8C576611E62080A',
},
drop: 11,
hide: false,
max: 1000,
min: 10,
noise: 5,
refId: 'B',
scenarioId: 'random_walk',
startValue: 10,
},
],
},
};
export const CreateNewQuery = ({ onDismiss, updateComponent, options }: Props) => {
const styles = useStyles2(getStyles);
const [updateSavedQuery] = useUpdateSavedQueryMutation();
const [query, setQuery] = useState(initialForm);
return (
<>
<CodeEditor
containerStyles={styles.editor}
width="80%"
height="70vh"
language="json"
showLineNumbers={false}
showMiniMap={true}
value={JSON.stringify(query.val, null, 2)}
onBlur={(val) => setQuery(() => ({ val: JSON.parse(val) }))}
onSave={(val) => setQuery(() => ({ val: JSON.parse(val) }))}
readOnly={false}
/>
<Button
type="submit"
className={styles.submitButton}
onClick={async () => {
await updateSavedQuery({ query: query.val, opts: options });
onDismiss();
updateComponent?.();
}}
>
Save query
</Button>
</>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
editor: css``,
submitButton: css`
align-self: flex-end;
margin-bottom: 25px;
margin-top: 25px;
`,
};
};

View File

@@ -1,113 +0,0 @@
// Libraries
import { uniqBy } from 'lodash';
import React from 'react';
// Components
import { DataSourceInstanceSettings, isUnsignedPluginSignature } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { getDataSourceSrv } from '@grafana/runtime/src';
import { HorizontalGroup, PluginSignatureBadge, Select } from '@grafana/ui';
export type DatasourceTypePickerProps = {
onChange: (ds: string | null) => void;
current: string | null; // type
hideTextValue?: boolean;
onBlur?: () => void;
autoFocus?: boolean;
openMenuOnFocus?: boolean;
placeholder?: string;
tracing?: boolean;
mixed?: boolean;
dashboard?: boolean;
metrics?: boolean;
type?: string | string[];
annotations?: boolean;
variables?: boolean;
alerting?: boolean;
pluginId?: string;
/** If true,we show only DSs with logs; and if true, pluginId shouldnt be passed in */
logs?: boolean;
width?: number;
inputId?: string;
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
onClear?: () => void;
};
const getDataSourceTypeOptions = (props: DatasourceTypePickerProps) => {
const { alerting, tracing, metrics, mixed, dashboard, variables, annotations, pluginId, type, filter, logs } = props;
return uniqBy(
getDataSourceSrv()
.getList({
alerting,
tracing,
metrics,
logs,
dashboard,
mixed,
variables,
annotations,
pluginId,
filter,
type,
})
.map((ds) => {
if (ds.type === 'datasource') {
return {
value: ds.type,
label: ds.type,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
};
}
return {
value: ds.type,
label: ds.type,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
};
}),
(opt) => opt.value
);
};
export const DatasourceTypePicker = (props: DatasourceTypePickerProps) => {
const { autoFocus, onBlur, onChange, current, openMenuOnFocus, placeholder, width, inputId } = props;
const options = getDataSourceTypeOptions(props);
return (
<div aria-label={selectors.components.DataSourcePicker.container}>
<Select
aria-label={selectors.components.DataSourcePicker.inputV2}
inputId={inputId || 'data-source-picker'}
className="ds-picker select-container"
isMulti={false}
isClearable={true}
backspaceRemovesValue={true}
options={options}
autoFocus={autoFocus}
onBlur={onBlur}
width={width}
value={current}
onChange={(newValue) => {
onChange(newValue?.value ?? null);
}}
openMenuOnFocus={openMenuOnFocus}
maxMenuHeight={500}
placeholder={placeholder ?? 'Select datasource type'}
noOptionsMessage="No datasources found"
getOptionLabel={(o) => {
if (o.meta && isUnsignedPluginSignature(o.meta.signature)) {
return (
<HorizontalGroup align="center" justify="space-between">
<span>{o.label}</span> <PluginSignatureBadge status={o.meta.signature} />
</HorizontalGroup>
);
}
return o.label || '';
}}
/>
</div>
);
};

View File

@@ -1,27 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
export const HistoryTab = () => {
const styles = useStyles2(getStyles);
// @TODO Implement history
return (
<div className={styles.wrap}>
<p className={styles.tabDescription}>No history.</p>
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
wrap: css`
padding: 20px 5px 5px 5px;
`,
tabDescription: css`
color: ${theme.colors.text.secondary};
`,
};
};

View File

@@ -1,25 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { useStyles2 } from '@grafana/ui/src';
import QueryLibrarySearchTable from './QueryLibrarySearchTable';
export const Queries = () => {
const styles = useStyles2(getStyles);
return (
<div className={styles.tableWrapper}>
<QueryLibrarySearchTable />
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
tableWrapper: css`
height: 100%;
`,
};
};

View File

@@ -1,118 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Card, Drawer, Icon, ModalsController, useStyles2 } from '@grafana/ui';
import { SavedQuery } from '../api/SavedQueriesApi';
import { QueryEditorDrawer, SavedQueryUpdateOpts } from './QueryEditorDrawer';
import { QueryImportDrawer } from './QueryImportDrawer';
type Props = {
onDismiss: () => void;
updateComponent: () => void;
};
export const QueryCreateDrawer = ({ onDismiss, updateComponent }: Props) => {
const styles = useStyles2(getStyles);
const type: SavedQueryUpdateOpts['type'] = 'create-new';
const closeDrawer = () => {
onDismiss();
updateComponent();
};
return (
<Drawer
title="Add new query"
subtitle="You can create a new query from builder or import from file"
onClose={onDismiss}
width={'1000px'}
expandable
scrollableContent
>
<div>
<Card>
<Card.Heading>Create by query builder</Card.Heading>
<Card.Description></Card.Description>
<Card.Figure>
<Icon name={'list-ui-alt'} className={styles.cardIcon} />
</Card.Figure>
<Card.Tags>
<ModalsController>
{({ showModal, hideModal }) => {
return (
<Button
icon="plus"
size="md"
onClick={() => {
const savedQuery: SavedQuery = {
title: 'New Query',
variables: [],
queries: [
{
refId: 'A',
datasource: {
type: 'datasource',
uid: 'grafana',
},
queryType: 'randomWalk',
},
],
};
showModal(QueryEditorDrawer, {
onDismiss: closeDrawer,
options: { type },
savedQuery,
});
}}
>
Create query
</Button>
);
}}
</ModalsController>
</Card.Tags>
</Card>
<Card>
<Card.Heading>Import from file</Card.Heading>
<Card.Description>Supported formats: JSON</Card.Description>
<Card.Figure>
<Icon name={'import'} className={styles.cardIcon} />
</Card.Figure>
<Card.Tags>
<ModalsController>
{({ showModal, hideModal }) => {
return (
<Button
icon="arrow-right"
size="md"
onClick={() => {
showModal(QueryImportDrawer, {
onDismiss: closeDrawer,
options: { type },
});
}}
>
Next
</Button>
);
}}
</ModalsController>
</Card.Tags>
</Card>
</div>
</Drawer>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
cardIcon: css`
width: 30px;
height: 30px;
`,
};
};

View File

@@ -1,143 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import {
CoreApp,
DataQuery,
DataSourceApi,
DataSourceInstanceSettings,
getDefaultTimeRange,
GrafanaTheme2,
LoadingState,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors/src';
import { DataSourcePicker, getDataSourceSrv } from '@grafana/runtime';
import { Button, HorizontalGroup, useStyles2 } from '@grafana/ui';
import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows';
import { addQuery } from '../../../core/utils/query';
import { dataSource as expressionDatasource } from '../../expressions/ExpressionDatasource';
import { updateQueries } from '../../query/state/updateQueries';
import { isQueryWithMixedDatasource, SavedQuery } from '../api/SavedQueriesApi';
import { defaultQuery } from '../utils';
type Props = {
savedQuery: SavedQuery;
onSavedQueryChange: (newQuery: SavedQuery) => void;
};
export const QueryEditor = ({ savedQuery, onSavedQueryChange }: Props) => {
const styles = useStyles2(getStyles);
const [queries, setQueries] = useState<DataQuery[]>(savedQuery.queries ?? [defaultQuery]);
const dsRef = isQueryWithMixedDatasource(savedQuery)
? { uid: '-- Mixed --', type: 'datasource' }
: queries[0].datasource;
const [dsSettings, setDsSettings] = useState(getDataSourceSrv().getInstanceSettings(dsRef));
const data = {
state: LoadingState.NotStarted,
series: [],
timeRange: getDefaultTimeRange(),
};
const onQueriesChange = (newQueries: DataQuery[]) => {
setQueries(newQueries);
onSavedQueryChange({
...savedQuery,
queries: newQueries,
});
};
const onDsChange = async (newDsSettings: DataSourceInstanceSettings) => {
const newDs = await getDataSourceSrv().get(newDsSettings.uid);
const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined;
const newQueries = await updateQueries(newDs, newDs.uid, queries, currentDS);
onQueriesChange(newQueries);
setDsSettings(newDsSettings);
};
const newQuery = async (): Promise<Partial<DataQuery>> => {
const ds: DataSourceApi = !dsSettings?.meta.mixed // TODO remove the asyncs and use prefetched ds apis
? await getDataSourceSrv().get(dsSettings!.uid)
: await getDataSourceSrv().get();
return {
...ds?.getDefaultQuery?.(CoreApp.PanelEditor),
datasource: { uid: ds?.uid, type: ds?.type },
};
};
const onAddQueryClick = async () => {
const newQ = await newQuery();
onQueriesChange(addQuery(queries, newQ));
};
const onAddExpressionClick = () => {
const newExpr = expressionDatasource.newQuery();
onQueriesChange(addQuery(queries, newExpr));
};
return (
<div>
<HorizontalGroup>
<div className={styles.dataSourceHeader}>Data source</div>
<div className={styles.dataSourcePickerWrapper}>
<DataSourcePicker
onChange={onDsChange}
current={dsSettings}
metrics={true}
mixed={true}
dashboard={true}
variables={true}
/>
</div>
</HorizontalGroup>
<QueryEditorRows
queries={queries}
dsSettings={dsSettings!}
onQueriesChange={onQueriesChange}
onAddQuery={onAddQueryClick}
onRunQueries={() => {}}
data={data}
/>
<HorizontalGroup spacing="md" align="flex-start">
{
<Button
disabled={false}
icon="plus"
onClick={onAddQueryClick}
variant="secondary"
aria-label={selectors.components.QueryTab.addQuery}
>
Query
</Button>
}
{(dsSettings?.meta.alerting || dsSettings?.meta.mixed) && (
<Button icon="plus" onClick={onAddExpressionClick} variant="secondary" className={styles.expressionButton}>
<span>Expression&nbsp;</span>
</Button>
)}
</HorizontalGroup>
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
dataSourceHeader: css`
font-size: ${theme.typography.size.sm};
margin-top: 5px;
margin-bottom: 20px;
`,
dataSourcePickerWrapper: css`
margin-top: 5px;
margin-bottom: 20px;
`,
expressionButton: css`
margin-right: ${theme.spacing(2)};
`,
};
};

View File

@@ -1,108 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { DataQuery } from '@grafana/data/src/types/query';
import { Drawer, IconName, Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
import { SavedQuery } from '../api/SavedQueriesApi';
import { HistoryTab } from './HistoryTab';
import { QueryEditor } from './QueryEditor';
import { QueryEditorDrawerHeader } from './QueryEditorDrawerHeader';
import { UsagesTab } from './UsagesTab';
import { VariablesTab } from './VariablesTab';
export type SavedQueryUpdateOpts = { message?: string } & (
| {
type: 'create-new';
}
| {
type: 'edit';
}
);
type Props = {
onDismiss: () => void;
savedQuery: SavedQuery<DataQuery>;
options: SavedQueryUpdateOpts;
};
type tab = {
label: string;
active: boolean;
icon: IconName;
};
const initialTabs: tab[] = [
{
label: 'Usages',
active: true,
icon: 'link',
},
{
label: 'Variables',
active: false,
icon: 'info-circle',
},
{
label: 'History',
active: false,
icon: 'history',
},
];
export const QueryEditorDrawer = (props: Props) => {
const { onDismiss, options } = props;
const styles = useStyles2(getStyles);
const [tabs, setTabs] = useState(initialTabs);
const [query, setSavedQuery] = useState(props.savedQuery);
return (
<Drawer onClose={onDismiss} width={'1000px'} expandable scrollableContent>
<div>
<QueryEditorDrawerHeader
options={options}
onSavedQueryChange={setSavedQuery}
savedQuery={query}
onDismiss={onDismiss}
/>
<div className={styles.queryWrapper}>
<QueryEditor onSavedQueryChange={setSavedQuery} savedQuery={query} />
</div>
<TabsBar>
{tabs.map((tab, index) => (
<Tab
key={index}
label={tab.label}
active={tab.active}
icon={tab.icon}
onChangeTab={() => setTabs(tabs.map((tab, idx) => ({ ...tab, active: idx === index })))}
/>
))}
</TabsBar>
<TabContent>
<div className={styles.tabWrapper}>
{tabs[0].active && <UsagesTab savedQuery={query} />}
{tabs[1].active && <VariablesTab savedQuery={query} options={options} />}
{tabs[2].active && <HistoryTab />}
</div>
</TabContent>
</div>
</Drawer>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
queryWrapper: css`
max-height: calc(60vh);
overflow-y: scroll;
margin-bottom: 50px;
`,
tabWrapper: css`
overflow-y: scroll;
max-height: calc(27vh);
`,
};
};

View File

@@ -1,198 +0,0 @@
import { css, cx } from '@emotion/css';
import React, { useEffect, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { Button, HorizontalGroup, Icon, IconName, useStyles2 } from '@grafana/ui';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import { useAppNotification } from '../../../core/copy/appNotification';
import { SavedQuery } from '../api/SavedQueriesApi';
import { getSavedQuerySrv } from '../api/SavedQueriesSrv';
import { implementationComingSoonAlert } from '../utils';
import { SavedQueryUpdateOpts } from './QueryEditorDrawer';
import { QueryName } from './QueryName';
type Props = {
onSavedQueryChange: (newQuery: SavedQuery) => void;
savedQuery: SavedQuery;
onDismiss: () => void;
options: SavedQueryUpdateOpts;
};
export const QueryEditorDrawerHeader = ({ savedQuery, onDismiss, onSavedQueryChange, options }: Props) => {
const notifyApp = useAppNotification();
const styles = useStyles2(getStyles);
const dropdownRef = useRef(null);
const [queryName, setQueryName] = useState(savedQuery.title);
const [showUseQueryOptions, setShowUseQueryOptions] = useState(false);
const nameEditingEnabled = !Boolean(savedQuery?.uid?.length);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current !== event.target) {
setShowUseQueryOptions(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
}, [dropdownRef]);
const deleteQuery = async () => {
await getSavedQuerySrv().deleteSavedQuery({ uid: savedQuery.uid });
onDismiss();
};
type queryOption = {
label: string;
value: string;
icon: IconName;
src?: string;
};
const useQueryOptions: queryOption[] = [
{ label: 'Add to dashboard', value: 'dashboard-panel', icon: 'apps' },
{ label: 'Create alert rule', value: 'alert-rule', icon: 'bell' },
{ label: 'View in explore', value: 'explore', icon: 'compass' },
{
label: 'Create recorded query',
value: 'recorded-query',
icon: 'record-audio',
},
{ label: 'Create SLO', value: 'slo', icon: 'crosshair' },
{
label: 'Add to incident in Grafana OnCall',
value: 'incident-oncall',
icon: 'record-audio',
src: 'public/app/features/query-library/img/grafana_incident.svg',
},
{
label: 'Create incident in Grafana Incident',
value: 'incident-grafana',
icon: 'heart-break',
src: 'public/app/features/query-library/img/grafana_oncall.svg',
},
{
label: 'Create forecast in Grafana ML',
value: 'grafana-ml',
icon: 'grafana-ml',
src: 'public/app/features/query-library/img/grafana_ml.svg',
},
];
const onQueryNameChange = (name: string) => {
setQueryName(name);
onSavedQueryChange({
...savedQuery,
title: name,
});
};
const onQuerySave = async (options: SavedQueryUpdateOpts) => {
await getSavedQuerySrv()
.updateSavedQuery(savedQuery, options)
.then(() => notifyApp.success('Query updated'))
.catch((err) => {
const msg = err.data?.message || err;
notifyApp.warning(msg);
});
onDismiss();
};
return (
<>
<div className={styles.header}>
<HorizontalGroup justify={'space-between'}>
<QueryName name={queryName} onChange={onQueryNameChange} editingEnabled={nameEditingEnabled} />
<HorizontalGroup>
<Button icon="times" size="md" variant={'secondary'} onClick={onDismiss} autoFocus={false}>
Close
</Button>
<Button
icon={'grafana'}
variant="secondary"
size="md"
onClick={() => {
setShowUseQueryOptions(!showUseQueryOptions);
}}
>
Use query
</Button>
<Button icon="sync" size="md" variant={'secondary'} onClick={implementationComingSoonAlert}>
Run
</Button>
{/*<Button icon="share-alt" size="sm" variant={'secondary'}>Export</Button>*/}
<Button icon="lock" size="md" variant={'secondary'} onClick={implementationComingSoonAlert} />
<Button size="md" variant={'primary'} onClick={() => onQuerySave(options)}>
Save
</Button>
<Button icon="trash-alt" size="md" variant={'destructive'} onClick={() => deleteQuery()} />
</HorizontalGroup>
</HorizontalGroup>
{/*@TODO Nicer submenu*/}
<HorizontalGroup>
{showUseQueryOptions && (
<div
className="panel-menu-container dropdown open"
style={{ height: 0 }}
ref={dropdownRef}
onClick={() => {
setShowUseQueryOptions(false);
}}
>
<ul className={cx('dropdown-menu dropdown-menu--menu panel-menu', styles.dropdown)}>
{useQueryOptions.map((option, key) => {
return (
<li key={key}>
{/*eslint-disable-next-line jsx-a11y/anchor-is-valid*/}
<a onClick={implementationComingSoonAlert}>
<div>
{option.src ? (
<SanitizedSVG src={option.src} className={styles.optionSvg} />
) : (
<Icon name={option.icon} className={styles.menuIconClassName} />
)}
</div>
<span className="dropdown-item-text">{option.label}</span>
<span className="dropdown-menu-item-shortcut" />
</a>
</li>
);
})}
</ul>
</div>
)}
</HorizontalGroup>
</div>
</>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
cascaderButton: css`
height: 24px;
`,
header: css`
padding-top: 5px;
padding-bottom: 15px;
`,
menuIconClassName: css`
margin-right: ${theme.v1.spacing.sm};
a::after {
display: none;
}
`,
optionSvg: css`
margin-right: 8px;
width: 16px;
height: 16px;
`,
dropdown: css`
left: 609px;
top: 2px;
`,
};
};

View File

@@ -1,55 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Drawer, FileDropzone, useStyles2 } from '@grafana/ui';
import { CreateNewQuery } from './CreateNewQuery';
import { SavedQueryUpdateOpts } from './QueryEditorDrawer';
type Props = {
options: SavedQueryUpdateOpts;
onDismiss: () => void;
};
export const QueryImportDrawer = ({ onDismiss, options }: Props) => {
const styles = useStyles2(getStyles);
const [file, setFile] = useState<File | undefined>(undefined);
return (
<Drawer title="Import query" onClose={onDismiss} width={'1000px'} expandable scrollableContent>
<FileDropzone
readAs="readAsBinaryString"
onFileRemove={() => {
setFile(undefined);
}}
options={{
accept: '.json',
multiple: false,
onDrop: (acceptedFiles: File[]) => {
setFile(acceptedFiles[0]);
},
}}
>
<div>Drag and drop here or browse</div>
</FileDropzone>
{Boolean(file) && (
<div className={styles.queryPreview}>
<CreateNewQuery options={options} onDismiss={onDismiss} />
</div>
)}
</Drawer>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
queryPreview: css`
margin-top: 20px;
margin-bottom: 20px;
margin-left: 170px;
`,
};
};

View File

@@ -1,46 +0,0 @@
import React, { useState } from 'react';
import { config } from '@grafana/runtime/src';
import { Alert, Tab, TabsBar, TabContent } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from '../../../core/hooks/useNavModel';
import { Queries } from './Queries';
const initialTabs = [
{
label: 'Queries',
active: true,
},
];
const QueryLibraryPage = () => {
const navModel = useNavModel('query');
const [tabs, setTabs] = useState(initialTabs);
if (!config.featureToggles.panelTitleSearch) {
return <Alert title="Missing feature toggle: panelTitleSearch">Query library requires searchV2</Alert>;
}
return (
<Page navModel={navModel}>
<Page.Contents>
<TabsBar>
{tabs.map((tab, index) => (
<Tab
key={index}
label={tab.label}
active={tab.active}
onChangeTab={() => setTabs(tabs.map((tab, idx) => ({ ...tab, active: idx === index })))}
/>
))}
</TabsBar>
<TabContent>{tabs[0].active && <Queries />}</TabContent>
</Page.Contents>
</Page>
);
};
export default QueryLibraryPage;

View File

@@ -1,219 +0,0 @@
import { css, cx } from '@emotion/css';
import { Global } from '@emotion/react';
import React, { useEffect, useMemo, useState } from 'react';
import { useAsync } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, FilterInput, HorizontalGroup, ModalsController, useStyles2, useTheme2 } from '@grafana/ui';
import { getGrafanaSearcher, SearchQuery } from '../../search/service';
import { getGlobalStyles } from '../globalStyles';
import { QueryItem } from '../types';
import { DatasourceTypePicker } from './DatasourceTypePicker';
import { QueryCreateDrawer } from './QueryCreateDrawer';
import { QueryListItem } from './QueryListItem';
const QueryLibrarySearchTable = () => {
const styles = useStyles2(getStyles);
const [datasourceType, setDatasourceType] = useState<string | null>(null);
const [searchQueryBy, setSearchByQuery] = useState<string>('');
const [reload, setReload] = useState(0);
const theme = useTheme2();
const globalCSS = getGlobalStyles(theme);
// @TODO update with real data
const authors = ['Artur Wierzbicki', 'Drew Slobodnjak', 'Nathan Marrs', 'Raphael Batyrbaev', 'Adela Almasan'];
const dates = [
'August 17, 2022, 2:32pm',
'August 17, 2022, 4:10pm',
'August 18, 2022, 1:00am',
'August 18, 2022, 12:00pm',
'August 19, 2022, 2:33pm',
];
const searchQuery = useMemo<SearchQuery>(() => {
const query: SearchQuery = {
query: '*',
sort: 'name_sort',
explain: true,
kind: ['query'],
};
if (datasourceType?.length) {
query.ds_type = datasourceType;
}
if (searchQueryBy) {
query.query = searchQueryBy;
}
return query;
}, [datasourceType, searchQueryBy]);
useEffect(() => {}, [reload]);
const results = useAsync(async () => {
const raw = await getGrafanaSearcher().search(searchQuery);
return raw.view.map<QueryItem>((item) => ({
uid: item.uid,
title: item.name,
url: item.url,
uri: item.url,
type: item.kind,
id: 123, // do not use me!
tags: item.tags ?? [],
ds_uid: item.ds_uid,
}));
}, [searchQuery, reload]);
const found = results.value;
return (
<>
<Global styles={globalCSS} />
<div className={styles.tableWrapper}>
<HorizontalGroup width="100%" justify="space-between" spacing={'md'} height={25}>
<HorizontalGroup>
<FilterInput
placeholder="Search queries by name, source, or variable"
autoFocus={true}
value={searchQueryBy}
onChange={setSearchByQuery}
width={50}
className={styles.searchBy}
/>
Filter by datasource type
<DatasourceTypePicker
current={datasourceType}
onChange={(newDsType) => {
setDatasourceType(() => newDsType);
}}
/>
</HorizontalGroup>
<ModalsController>
{({ showModal, hideModal }) => {
return (
<div className={styles.createQueryButton}>
<Button
icon="plus"
size="md"
onClick={() => {
showModal(QueryCreateDrawer, {
onDismiss: hideModal,
updateComponent: () => {
setReload(reload + 1);
},
});
}}
>
Create query
</Button>
</div>
);
}}
</ModalsController>
</HorizontalGroup>
<ModalsController>
{({ showModal, hideModal }) => {
return (
<AutoSizer className={styles.autosizer} style={{ width: '100%', height: '100%' }}>
{({ width, height }) => {
return (
<table className={cx('filter-table form-inline filter-table--hover', styles.table)}>
<thead>
<tr>
<th />
<th>Status</th>
<th>Name and raw query</th>
<th>Data Source</th>
<th>User</th>
<th>Date</th>
<th />
</tr>
</thead>
<tbody>
{!Boolean(found?.length) && (
<tr className={styles.transparentBg}>
<td />
<td />
<td />
<td>
<div className={styles.noData}>No data</div>
</td>
<td />
<td />
<th />
</tr>
)}
{Boolean(found?.length) &&
found!.map((item, key) => {
return (
<QueryListItem
query={item}
key={item.uid}
showModal={showModal}
hideModal={hideModal}
updateComponent={() => setReload(reload + 1)}
author={key < authors.length ? authors[key] : authors[key - authors.length]}
date={key < dates.length ? dates[key] : dates[key - dates.length]}
/>
);
})}
</tbody>
</table>
);
}}
</AutoSizer>
);
}}
</ModalsController>
</div>
</>
);
};
export default QueryLibrarySearchTable;
export const getStyles = (theme: GrafanaTheme2) => {
return {
tableWrapper: css`
height: 100%;
margin-top: 20px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
align-items: flex-start;
`,
autosizer: css`
margin-top: 40px;
`,
createQueryButton: css`
text-align: center;
`,
filtersGroup: css`
padding-top: 10px;
margin-top: 30px;
`,
searchBy: css`
margin-right: 15px;
`,
table: css`
font-size: 14px;
&tbody {
&tr: {
background: ${theme.colors.background.secondary};
}
}
`,
noData: css`
color: ${theme.colors.text.secondary};
`,
transparentBg: css`
background: transparent !important;
`,
};
};

View File

@@ -1,198 +0,0 @@
import { css, cx } from '@emotion/css';
import { uniq } from 'lodash';
import React, { memo, useEffect, useState } from 'react';
import { DataSourceApi, GrafanaTheme2 } from '@grafana/data/src';
import { getDataSourceSrv } from '@grafana/runtime/src';
import { Icon, Tooltip } from '@grafana/ui';
import { Badge, IconButton, useStyles2 } from '@grafana/ui/src';
import { useAppNotification } from '../../../core/copy/appNotification';
import { getSavedQuerySrv } from '../api/SavedQueriesSrv';
import { QueryItem } from '../types';
import { implementationComingSoonAlert } from '../utils';
import { QueryEditorDrawer } from './QueryEditorDrawer';
type QueryListItemProps = {
query: QueryItem;
showModal: <T>(component: React.ComponentType<T>, props: T) => void;
hideModal: () => void;
updateComponent: () => void;
author: string;
date: string;
};
const options = {
type: 'edit',
} as const;
export const QueryListItem = memo(
({ query, showModal, hideModal, updateComponent, author, date }: QueryListItemProps) => {
const notifyApp = useAppNotification();
const styles = useStyles2(getStyles);
const [dsInfo, setDsInfo] = useState<DataSourceApi[]>([]);
useEffect(() => {
const getQueryDsInstance = async () => {
const uniqueUids = uniq(query?.ds_uid ?? []);
setDsInfo((await Promise.all(uniqueUids.map((dsUid) => getDataSourceSrv().get(dsUid)))).filter(Boolean));
};
getQueryDsInstance();
}, [query.ds_uid]);
const closeDrawer = () => {
hideModal();
updateComponent();
};
const openDrawer = async () => {
const result = await getSavedQuerySrv().getSavedQueries([{ uid: query.uid }]);
const savedQuery = result[0];
showModal(QueryEditorDrawer, { onDismiss: closeDrawer, savedQuery: savedQuery, options });
};
const deleteQuery = async () => {
await getSavedQuerySrv().deleteSavedQuery({ uid: query.uid });
updateComponent();
};
const getDsType = () => {
const dsType = dsInfo?.length > 1 ? 'mixed' : dsInfo?.[0]?.type ?? 'datasource';
return startWithUpperCase(dsType);
};
const startWithUpperCase = (dsType: string) => {
return dsType.charAt(0).toUpperCase() + dsType.slice(1);
};
const getTooltip = () => {
return (
<div>
<ul className={styles.dsTooltipList}>
{dsInfo.map((dsI, key) => {
return (
<li key={key}>
<img className={styles.dsTooltipIcon} src={dsI?.meta?.info.logos.small} alt="datasource" />
&nbsp;
{startWithUpperCase(dsI.type)}
</li>
);
})}
</ul>
</div>
);
};
const copyToClipboard = async () => {
const models = await getSavedQuerySrv().getSavedQueries([{ uid: query.uid }]);
if (!models?.length) {
implementationComingSoonAlert();
return;
}
await navigator.clipboard.writeText(
JSON.stringify(
{
...models[0],
uid: undefined,
storageOptions: undefined,
},
null,
2
)
);
notifyApp.success('Query JSON copied to clipboard!');
};
return (
<tr key={query.uid} className={styles.row}>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={implementationComingSoonAlert}>
<Icon name={'lock'} className={styles.disabled} title={'Implementation coming soon!'} />
</td>
<td>
<Badge color={'green'} text={'1'} icon={'link'} tooltip={'Implementation coming soon!'} />
</td>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={openDrawer}>{query.title}</td>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={openDrawer}>
<img
className={styles.dsIcon}
src={getDsType() === 'Mixed' ? 'public/img/icn-datasource.svg' : dsInfo[0]?.meta?.info.logos.small}
alt="datasource"
style={{ width: '16px', height: '16px' }}
/>
&nbsp;&nbsp;{getDsType()}&nbsp;
{getDsType() === 'Mixed' && (
<Tooltip content={getTooltip()}>
<Icon name={'question-circle'} className={styles.infoIcon} />
</Tooltip>
)}
</td>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={openDrawer}>
<img
className={cx('filter-table__avatar', styles.dsIcon)}
src={'/avatar/46d229b033af06a191ff2267bca9ae56'}
alt={`Avatar for ${author}`}
/>
&nbsp;&nbsp;{author}
</td>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/}
<td onClick={openDrawer}>{date}</td>
<td className={styles.tableTr}>
<IconButton name="share-alt" tooltip={'Share'} onClick={implementationComingSoonAlert} />
<IconButton name="copy" tooltip={'Copy'} onClick={copyToClipboard} />
<IconButton name="upload" tooltip={'Upload'} onClick={implementationComingSoonAlert} />
<IconButton name="cog" tooltip={'Settings'} onClick={implementationComingSoonAlert} />
<IconButton name="trash-alt" tooltip={'Delete'} onClick={deleteQuery} />
</td>
</tr>
);
}
);
QueryListItem.displayName = 'QueryListItem';
const getStyles = (theme: GrafanaTheme2) => {
return {
row: css`
height: 70px;
cursor: pointer;
`,
tableTr: css`
display: flex;
justify-content: space-between;
margin-top: 22px;
`,
disabled: css`
color: ${theme.colors.text.secondary};
`,
gitIcon: css`
width: 30px;
height: 30px;
margin-left: 10px;
margin-top: 1px;
opacity: 0.8;
`,
infoIcon: css`
margin-top: -2px;
`,
dsTooltipIcon: css`
width: 11px;
height: 11px;
`,
dsIcon: css`
width: 16px !important;
height: 16px !important;
`,
dsTooltipList: css`
list-style-type: none;
`,
};
};

View File

@@ -1,119 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, Input, FieldValidationMessage, HorizontalGroup, useStyles2 } from '@grafana/ui';
export interface QueryNameProps {
name: string;
editingEnabled: boolean;
onChange: (v: string) => void;
}
export const QueryName = ({ name, onChange, editingEnabled }: QueryNameProps) => {
const styles = useStyles2(getStyles);
const [isEditing, setIsEditing] = useState<boolean>(false);
const [validationError, setValidationError] = useState<string | null>(null);
const onEditQueryName = (event: React.SyntheticEvent) => {
setIsEditing(true);
};
const onEndEditName = (newName: string) => {
setIsEditing(false);
if (validationError) {
setValidationError(null);
return;
}
if (name !== newName) {
onChange(newName);
}
};
const onInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const newName = event.currentTarget.value.trim();
if (newName.length === 0) {
setValidationError('An empty name is not allowed');
return;
}
if (validationError) {
setValidationError(null);
}
};
const onEditLayerBlur = (event: React.SyntheticEvent<HTMLInputElement>) => {
onEndEditName(event.currentTarget.value.trim());
};
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
};
const onKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
if (!(event.target instanceof HTMLInputElement)) {
return;
}
onEndEditName(event.target.value);
}
};
return (
<>
<div className={styles.wrapper}>
{!isEditing && (
<HorizontalGroup>
<h2 className={styles.h2Style}>{name}</h2>
{editingEnabled && <Icon name="pen" className={styles.nameEditIcon} size="md" onClick={onEditQueryName} />}
</HorizontalGroup>
)}
{isEditing && (
<>
<Input
type="text"
defaultValue={name}
onBlur={onEditLayerBlur}
onFocus={onFocus}
autoFocus={true}
onKeyDown={onKeyDown}
invalid={validationError !== null}
onChange={onInputChange}
className={styles.nameInput}
/>
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
</>
)}
</div>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css`
display: flex;
align-items: center;
margin-left: ${theme.v1.spacing.xs};
`,
nameEditIcon: css`
cursor: pointer;
color: ${theme.colors.text.secondary};
width: 12px;
height: 12px;
`,
nameInput: css`
max-width: 300px;
margin: -8px 0;
`,
h2Style: css`
margin-bottom: 0;
`,
};
};

View File

@@ -1,79 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Button, Form, Modal, VerticalGroup, TextArea } from '@grafana/ui';
import { WorkflowID } from '../../storage/types';
import { SavedQuery } from '../api/SavedQueriesApi';
interface FormDTO {
message: string;
}
export interface SaveQueryOptions {
savedQuery: SavedQuery;
workflow: WorkflowID;
message?: string;
}
export type SaveProps = {
onCancel: () => void;
onSuccess: () => void;
onSubmit?: (options: SaveQueryOptions) => Promise<{ success: boolean }>;
options: SaveQueryOptions;
onOptionsChange: (opts: SaveQueryOptions) => void;
};
export const SaveQueryWorkflowModal = ({ options, onSubmit, onCancel, onSuccess }: SaveProps) => {
const [saving, setSaving] = useState(false);
return (
<Modal
isOpen={true}
title={options.workflow === WorkflowID.PR ? 'Create a Pull Request' : 'Push changes'}
onDismiss={onCancel}
icon="exclamation-triangle"
className={css`
width: 500px;
`}
>
<Form
onSubmit={async (data: FormDTO) => {
console.log('hello submitting!');
if (!onSubmit) {
return;
}
setSaving(true);
options = { ...options, message: data.message };
const result = await onSubmit(options);
if (result.success) {
onSuccess();
} else {
setSaving(false);
}
}}
>
{({ register, errors }) => (
<VerticalGroup>
<TextArea {...register('message')} placeholder="Add a note to describe your changes." autoFocus rows={5} />
<VerticalGroup>
<Button variant="secondary" onClick={onCancel} fill="outline">
Cancel
</Button>
<Button
type="submit"
disabled={false}
icon={saving ? 'fa fa-spinner' : undefined}
aria-label={selectors.pages.SaveDashboardModal.save}
>
{options.workflow === WorkflowID.PR ? 'Submit PR' : 'Push'}
</Button>
</VerticalGroup>
</VerticalGroup>
)}
</Form>
</Modal>
);
};

View File

@@ -1,166 +0,0 @@
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { useAsync } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data/src';
import { Button, Card, Icon, IconName, Spinner, useStyles2 } from '@grafana/ui/src';
import { HorizontalGroup } from '../../plugins/admin/components/HorizontalGroup';
import { getGrafanaSearcher, SearchQuery } from '../../search/service';
import { SavedQuery } from '../api/SavedQueriesApi';
import { QueryItem } from '../types';
type Props = {
savedQuery: SavedQuery;
};
export const UsagesTab = ({ savedQuery }: Props) => {
const styles = useStyles2(getStyles);
const searchQuery = useMemo<SearchQuery>(() => {
const query: SearchQuery = {
query: '*',
kind: savedQuery.uid ? ['dashboard', 'alert'] : ['newQuery'], // workaround for new queries
saved_query_uid: savedQuery.uid,
};
return query;
}, [savedQuery.uid]);
const results = useAsync(async () => {
const raw = await getGrafanaSearcher().search(searchQuery);
return raw.view.map<QueryItem>((item) => ({
uid: item.uid,
title: item.name,
url: item.url,
uri: item.url,
type: item.kind,
id: 321, // do not use me!
tags: item.tags ?? [],
ds_uid: item.ds_uid,
location: item.location,
panel_type: item.panel_type,
}));
}, [searchQuery]);
if (results.loading) {
return <Spinner />;
}
const found = results.value;
const getIconForKind = (kind: string): IconName => {
let icon: IconName = 'question-circle';
switch (kind) {
case 'dashboard':
icon = 'apps';
break;
case 'folder':
icon = 'folder';
break;
case 'alert':
icon = 'bell';
break;
default:
icon = 'question-circle';
break;
}
return icon;
};
if (found?.length === 0) {
return (
<div className={styles.wrap}>
<p className={styles.usagesDescription}>This query is not used anywhere.</p>
</div>
);
}
return (
<div className={styles.wrap}>
<p className={styles.usagesDescription}>
This query is used in the places below. Modifying will affect all its usages.
</p>
{found?.map((item) => {
return (
<div key={item.uid}>
<Card>
<Card.Heading>
<span className={styles.cardHeading}>
{item.title}
<a
href={item.url}
title={'Open in new tab'}
target="_blank"
rel="noopener noreferrer"
className={styles.externalLink}
>
<Icon name="external-link-alt" className={styles.cardHeadingIcon} />
</a>
</span>
</Card.Heading>
<Card.Description>
<a href={'dashboards'} target="_blank" rel="noopener noreferrer" className={styles.externalLink}>
<Icon name="folder" className={styles.cardDescriptionIcon} />
</a>
{item.location}
</Card.Description>
<Card.Figure className={styles.cardFigure}>
<Icon name={getIconForKind(item.type)} />
</Card.Figure>
<Card.Tags>
<HorizontalGroup>
<Button icon="eye" size="sm" variant={'secondary'} />
<Button icon="link" size="sm" variant={'secondary'}>
Unlink
</Button>
</HorizontalGroup>
</Card.Tags>
</Card>
</div>
);
})}
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
wrap: css`
padding: 20px 5px 5px 5px;
`,
info: css`
padding-bottom: 30px;
`,
folderIcon: css`
margin-right: 5px;
`,
cardFigure: css`
margin-right: 0;
margin-top: 15px;
`,
externalLink: css`
margin-left: 5px;
`,
cardHeading: css`
display: flex;
`,
cardHeadingIcon: css`
width: 13px;
height: 13px;
color: ${theme.colors.text.secondary};
display: flex;
align-self: center;
`,
usagesDescription: css`
color: ${theme.colors.text.secondary};
`,
cardDescriptionIcon: css`
width: 16px;
height: 16px;
color: ${theme.colors.text.secondary};
margin-right: 5px;
`,
};
};

View File

@@ -1,166 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Card, HorizontalGroup, useStyles2 } from '@grafana/ui';
import { LayerName } from 'app/core/components/Layers/LayerName';
import { SavedQuery, useUpdateSavedQueryMutation, Variable } from '../api/SavedQueriesApi';
import { SavedQueryUpdateOpts } from './QueryEditorDrawer';
type Props = {
savedQuery: SavedQuery;
options: SavedQueryUpdateOpts;
};
export const VariablesTab = ({ savedQuery, options }: Props) => {
const styles = useStyles2(getStyles);
const [updateSavedQuery] = useUpdateSavedQueryMutation();
const onVariableNameChange = (variable: Variable, newName: string) => {
const newVariables = savedQuery.variables.map((v: Variable) => {
if (v.name === variable.name) {
v.name = newName;
}
return v;
});
updateSavedQuery({
query: {
...savedQuery,
variables: newVariables,
},
opts: options,
});
};
const onVariableValueChange = (variable: Variable, newValue: string) => {
const newVariables = savedQuery.variables.map((v: Variable) => {
if (v.name === variable.name) {
v.current.value = newValue;
}
return v;
});
updateSavedQuery({
query: {
...savedQuery,
variables: newVariables,
},
opts: options,
});
};
const onAddVariable = () => {
// NOTE: doing mutation to force re-render
savedQuery.variables.unshift({
name: 'New variable',
current: {
value: 'General',
},
});
updateSavedQuery({ query: savedQuery, opts: options });
};
const onRemoveVariable = (variable: Variable) => {
const varIndex = savedQuery.variables.map((v: Variable, index: number) => {
if (v.name === variable.name) {
return index;
}
return;
});
if (typeof varIndex === 'number') {
// NOTE: doing mutation vs filter to force re-render
savedQuery.variables.splice(varIndex, 1);
updateSavedQuery({ query: savedQuery, opts: options });
}
};
return (
<div className={styles.tabWrapper}>
<div className={styles.variablesHeader}>
<HorizontalGroup width="100%" justify="space-between" spacing={'md'} height={25}>
<div className={styles.tabDescription}>
Variables enable more interactive and dynamic queries. Instead of hard-coding things like server or sensor
names in your metric queries you can use variables in their place. <br />
<b>Variable support is coming soon!</b>
</div>
<Button icon="plus" size="md" className={styles.addVariableButton} onClick={onAddVariable}>
Add variable
</Button>
</HorizontalGroup>
</div>
<div className={styles.variableList}>
<ul>
{savedQuery &&
savedQuery.variables &&
savedQuery.variables.map((variable: Variable) => (
<li key={variable && variable.name} className={styles.variableListItem}>
<Card>
<Card.Heading>
<LayerName
name={variable && variable.name}
onChange={(v) => onVariableNameChange(variable, v)}
overrideStyles
/>
</Card.Heading>
<Card.Description>
<LayerName
name={variable && variable.current.value.toString()}
onChange={(v) => onVariableValueChange(variable, v)}
overrideStyles
/>
</Card.Description>
<Card.Tags>
<Button
icon="trash-alt"
size="sm"
variant={'secondary'}
tooltip="Delete this variable"
onClick={() => onRemoveVariable(variable)}
>
Delete
</Button>
</Card.Tags>
</Card>
</li>
))}
</ul>
</div>
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
tabWrapper: css`
flex: 1;
padding: 20px 5px 5px 5px;
`,
tabDescription: css`
color: ${theme.colors.text.secondary};
`,
variableList: css`
padding-bottom: 20px;
`,
variableListItem: css`
list-style: none;
`,
addVariableButton: css`
display: flex;
align-self: center;
margin: auto;
margin-bottom: 15px;
`,
variablesHeader: css`
margin-top: 15px;
margin-bottom: 20px;
`,
};
};

View File

@@ -1,28 +0,0 @@
import { css } from '@emotion/react';
import { GrafanaTheme2 } from '@grafana/data';
export function getGlobalStyles(theme: GrafanaTheme2) {
return css`
.filter-table {
border-collapse: separate;
border-spacing: 0 5px;
tbody {
tr:nth-child(odd) {
background: ${theme.colors.background.secondary};
}
tr {
background: ${theme.colors.background.secondary};
}
}
&--hover {
tbody tr:hover {
background: ${theme.colors.background.primary};
}
}
}
`;
}

View File

@@ -1,10 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.25 2.5V1M2.5 10H1m1.5-7.5L4 4m18 6h1.5M22 2.5 20.5 4" stroke="#F3C90E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.682 4.75h5.143a3.75 3.75 0 0 1 3.7 3.338l1.133 10.22A3.75 3.75 0 0 1 22.75 22a2.25 2.25 0 0 1-2.25 2.25H4A2.25 2.25 0 0 1 1.75 22a3.75 3.75 0 0 1 3.051-3.684L5.935 8.088A3.75 3.75 0 0 1 9.682 4.75Zm9.298 15H5.484A2.25 2.25 0 0 0 3.25 22a.75.75 0 0 0 .75.75h16.5a.75.75 0 0 0 .75-.75A2.25 2.25 0 0 0 19 19.75h-.02ZM17.035 8.253l1.107 9.997H6.318l1.107-9.997a2.25 2.25 0 0 1 2.25-2.003h5.142a2.25 2.25 0 0 1 2.218 2.003Zm-1.382 2.115a.75.75 0 0 0-1.306-.736l-3.145 5.574v.002l-1.603-2.123a.75.75 0 1 0-1.198.904l1.627 2.155c.144.193.334.36.564.47.227.11.48.156.737.128a1.41 1.41 0 0 0 .69-.253c.204-.143.366-.33.484-.536l.004-.006 3.146-5.579Z" fill="url(#a)"/>
<defs>
<linearGradient id="a" x1="12.25" y1="5" x2="12.25" y2="24.25" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAC00E"/>
<stop offset="1" stop-color="#F26526"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 135.46 118.24"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="67.73" y1="137.67" x2="67.73" y2="-0.38" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f9ea1c"/><stop offset="1" stop-color="#ed5a29"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M135.37,64.86l-5.6-26.43a4.05,4.05,0,0,0-.9-1.82L113.19,18.24a4.11,4.11,0,0,0-1-.87L83.71.57A4.18,4.18,0,0,0,81.62,0H58.54a4.19,4.19,0,0,0-1,.12L26,8a4.16,4.16,0,0,0-2,1.17L1.12,33.32A4.1,4.1,0,0,0,0,36.14v24.2A4.1,4.1,0,0,0,2.46,64.1l22.85,10L39,95.07a4.13,4.13,0,0,0,3.45,1.86H68.66L81.59,116.4A4.09,4.09,0,0,0,85,118.24h0l6,0a4.11,4.11,0,0,0,4.09-4.11V90.21H117a4.09,4.09,0,0,0,3.36-1.75l14.34-20.38A4.1,4.1,0,0,0,135.37,64.86ZM86.12,34.1a3.29,3.29,0,0,0-.67.77l0,.05H59.83L57,30.53A3.38,3.38,0,0,0,54.24,29a3.45,3.45,0,0,0-2.86,1.45l-3.15,4.51H10.92L21.64,23.58H98.3Zm-3,4.52L72.77,55.23,62.19,38.62ZM37.75,50H8.22V38.62H45.66ZM29.12,15.65,59.05,8.22H80.49l19.74,11.66H25.13Zm-20.9,38h27L26.73,65.78,8.22,57.65ZM114.87,82h-4V46.21a2.3,2.3,0,0,0-2.3-2.3h-1.39a2.3,2.3,0,0,0-2.3,2.3V82h-12V63.92a2.31,2.31,0,0,0-2.3-2.31H89.21a2.31,2.31,0,0,0-2.3,2.31v45.64L74.48,90.84V78.36a2.3,2.3,0,0,0-2.3-2.3H70.79a2.3,2.3,0,0,0-2.3,2.3V88.71H56.23V58.54a2.32,2.32,0,0,0-2.31-2.31H52.54a2.32,2.32,0,0,0-2.31,2.31V88.71H44.64l-12.39-19L53.93,38.62h.21l15.8,24.81A3.32,3.32,0,0,0,72.82,65a3.39,3.39,0,0,0,2.87-1.6L91,38.9l16.73-14.45,14.26,16.7,5,23.66Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,9 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.3009 29.8017H30.6991C32.0765 29.8017 32.9322 28.313 32.2435 27.1235L25.5443 15.52C24.8557 14.3304 23.1374 14.3304 22.4557 15.52L15.7565 27.1235C15.0678 28.313 15.9304 29.8017 17.3009 29.8017ZM24 0C10.7478 0 0 10.7478 0 24C0 37.2522 10.7478 48 24 48C37.2522 48 48 37.2522 48 24C48 10.7478 37.2522 0 24 0ZM24.0626 10.9774C31.2557 11.0122 37.0574 16.8696 37.0226 24.0626C36.9878 31.2557 31.1304 37.0574 23.9374 37.0226C16.7443 36.9878 10.9426 31.1304 10.9774 23.9374C11.0122 16.7443 16.8696 10.9426 24.0626 10.9774ZM6.94261 31.3809C6.79652 31.4296 6.65739 31.4504 6.5113 31.4504C5.92696 31.4504 5.37739 31.0748 5.18957 30.4904C4.53565 28.48 4.2087 26.3165 4.2087 24.0626C4.2087 22.8104 4.31304 21.5026 4.52174 20.1878C6.09391 12.2017 12.2296 6.03826 20.16 4.4313C20.9183 4.27826 21.6487 4.76522 21.8017 5.51652C21.9548 6.26783 21.4678 7.00522 20.7165 7.15826C13.8922 8.53565 8.61217 13.8435 7.26957 20.6678C7.09565 21.7878 7.00522 22.9496 7.00522 24.0557C7.00522 26.0104 7.29043 27.8887 7.85391 29.6209C8.09044 30.3513 7.69391 31.1374 6.95652 31.3739L6.94261 31.3809ZM39.1791 37.127C35.52 41.4817 30.2539 43.8748 24.3548 43.8748C18.4557 43.8748 12.8626 41.447 9.05739 37.2174C8.54261 36.647 8.5913 35.7635 9.16174 35.2557C9.73217 34.7409 10.6157 34.7896 11.1235 35.36C14.3513 38.9496 19.2904 41.0991 24.3478 41.0991C29.4052 41.0991 33.92 39.0539 37.0435 35.3391C37.5374 34.7548 38.4139 34.6713 39.0052 35.1722C39.5965 35.6661 39.6661 36.5426 39.1722 37.1339L39.1791 37.127ZM42.9426 30.4C42.7409 30.9774 42.2052 31.3322 41.6278 31.3322C41.4748 31.3322 41.3217 31.3043 41.1687 31.2557C40.4452 31.0052 40.0626 30.2122 40.313 29.4817C40.5565 28.7722 40.7791 28.0278 40.96 27.2626C41.1687 26.2122 41.28 25.0852 41.28 23.9722C41.28 15.8052 35.4574 8.73044 27.4296 7.14435C26.6783 6.99826 26.1565 6.26783 26.2957 5.51652C26.4348 4.76522 27.1304 4.26435 27.8748 4.39652C27.8887 4.39652 27.9513 4.41043 27.9652 4.41739C37.287 6.25391 44.0557 14.4835 44.0557 23.9722C44.0557 25.2661 43.9304 26.5739 43.673 27.8539C43.4574 28.7652 43.2139 29.6 42.9357 30.4H42.9426Z" fill="url(#paint0_linear_911_12416)"/>
<defs>
<linearGradient id="paint0_linear_911_12416" x1="24.3556" y1="47.7468" x2="24.3556" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAC10D"/>
<stop offset="1" stop-color="#F05A28"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,20 +0,0 @@
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
import { config } from 'app/core/config';
import { RouteDescriptor } from 'app/core/navigation/types';
export function getRoutes(): RouteDescriptor[] {
if (config.featureToggles.queryLibrary) {
return [
{
path: `/query-library`,
exact: true,
component: SafeDynamicImport(
() =>
import(/* webpackChunkName: "QueryLibraryPage" */ 'app/features/query-library/components/QueryLibraryPage')
),
},
];
}
return [];
}

View File

@@ -1,30 +0,0 @@
import { SavedQueryRef } from './api/SavedQueriesApi';
export interface QueryItem {
id: number;
selected?: boolean;
tags: string[];
title: string;
type: string;
uid: string;
ds_uid: string[];
uri: string;
url: string;
sortMeta?: number;
sortMetaName?: string;
location?: string;
}
type SavedQueryVariable<T = unknown> = {
type: 'text' | 'datasource' | string; // TODO: enumify
name: string;
current: {
// current.value follows the structure from dashboard variables
value: T;
};
};
export type SavedQueryLink = {
ref: SavedQueryRef;
variables: SavedQueryVariable[];
};

View File

@@ -1,14 +0,0 @@
import { DataQuery } from '@grafana/data/src';
export const defaultQuery: DataQuery = {
refId: 'A',
datasource: {
type: 'datasource',
uid: 'grafana',
},
queryType: 'measurements',
};
export const implementationComingSoonAlert = () => {
alert('Implementation coming soon!');
};