mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Data Trails: Cleanup and explore+share buttons (#81062)
* Share button works now. Removed add to dashboard button for now * WIP explore link * Remove settings dropdown for now * Use getExploreUrl to generate explore link * Fix conflicts * Update betterer * Navigate to a new trail when the recent trails list is empty * Address PR comments
This commit is contained in:
parent
9167d67c05
commit
2c7e95a680
@ -4206,6 +4206,10 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "11"],
|
[0, 0, 0, "Do not use any type assertions.", "11"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "12"]
|
[0, 0, 0, "Do not use any type assertions.", "12"]
|
||||||
],
|
],
|
||||||
|
"public/app/features/trails/MetricScene.tsx:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||||
|
],
|
||||||
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [
|
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
@ -167,7 +167,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Component = ({ model }: SceneComponentProps<DataTrail>) => {
|
static Component = ({ model }: SceneComponentProps<DataTrail>) => {
|
||||||
const { controls, topScene, history, settings } = model.useState();
|
const { controls, topScene, history } = model.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -178,7 +178,6 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
|||||||
{controls.map((control) => (
|
{controls.map((control) => (
|
||||||
<control.Component key={control.state.key} model={control} />
|
<control.Component key={control.state.key} model={control} />
|
||||||
))}
|
))}
|
||||||
<settings.Component model={settings} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.body}>{topScene && <topScene.Component model={topScene} />}</div>
|
<div className={styles.body}>{topScene && <topScene.Component model={topScene} />}</div>
|
||||||
|
@ -2,12 +2,12 @@ import { css } from '@emotion/css';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
|
||||||
import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes';
|
import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes';
|
||||||
import { useStyles2, Stack, Tooltip, Button } from '@grafana/ui';
|
import { useStyles2, Stack, Tooltip, Button } from '@grafana/ui';
|
||||||
|
|
||||||
import { DataTrail } from './DataTrail';
|
import { DataTrail } from './DataTrail';
|
||||||
import { LOGS_METRIC, VAR_DATASOURCE_EXPR, VAR_FILTERS } from './shared';
|
import { LOGS_METRIC, VAR_FILTERS } from './shared';
|
||||||
|
import { getDataSource, getDataSourceName } from './utils';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
trail: DataTrail;
|
trail: DataTrail;
|
||||||
@ -67,14 +67,6 @@ function getMetricName(metric?: string) {
|
|||||||
return metric;
|
return metric;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDataSource(trail: DataTrail) {
|
|
||||||
return sceneGraph.interpolate(trail, VAR_DATASOURCE_EXPR);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDataSourceName(dataSourceUid: string) {
|
|
||||||
return getDataSourceSrv().getInstanceSettings(dataSourceUid)?.name || dataSourceUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
return {
|
return {
|
||||||
container: css({
|
container: css({
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
||||||
@ -10,7 +11,7 @@ import { DataTrail } from './DataTrail';
|
|||||||
import { DataTrailCard } from './DataTrailCard';
|
import { DataTrailCard } from './DataTrailCard';
|
||||||
import { DataTrailsApp } from './DataTrailsApp';
|
import { DataTrailsApp } from './DataTrailsApp';
|
||||||
import { getTrailStore } from './TrailStore/TrailStore';
|
import { getTrailStore } from './TrailStore/TrailStore';
|
||||||
import { getDatasourceForNewTrail, newMetricsTrail } from './utils';
|
import { getDatasourceForNewTrail, getUrlForTrail, newMetricsTrail } from './utils';
|
||||||
|
|
||||||
export interface DataTrailsHomeState extends SceneObjectState {}
|
export interface DataTrailsHomeState extends SceneObjectState {}
|
||||||
|
|
||||||
@ -43,6 +44,13 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
|
|||||||
setLastDelete(Date.now()); // trigger re-render
|
setLastDelete(Date.now()); // trigger re-render
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If there are no recent trails, don't show home page and create a new trail
|
||||||
|
if (!getTrailStore().recent.length) {
|
||||||
|
const trail = newMetricsTrail(getDatasourceForNewTrail());
|
||||||
|
getTrailStore().setRecentTrail(trail);
|
||||||
|
return <Redirect to={getUrlForTrail(trail)} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Stack direction="column" gap={1}>
|
<Stack direction="column" gap={1}>
|
||||||
|
@ -17,11 +17,14 @@ import {
|
|||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { ToolbarButton, Box, Stack, Icon, TabsBar, Tab, useStyles2 } from '@grafana/ui';
|
import { ToolbarButton, Box, Stack, Icon, TabsBar, Tab, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { getExploreUrl } from '../../core/utils/explore';
|
||||||
|
|
||||||
import { buildBreakdownActionScene } from './ActionTabs/BreakdownScene';
|
import { buildBreakdownActionScene } from './ActionTabs/BreakdownScene';
|
||||||
import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene';
|
import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene';
|
||||||
import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene';
|
import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene';
|
||||||
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
|
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
|
||||||
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
|
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
|
||||||
|
import { ShareTrailButton } from './ShareTrailButton';
|
||||||
import { getTrailStore } from './TrailStore/TrailStore';
|
import { getTrailStore } from './TrailStore/TrailStore';
|
||||||
import {
|
import {
|
||||||
ActionViewDefinition,
|
ActionViewDefinition,
|
||||||
@ -33,7 +36,7 @@ import {
|
|||||||
VAR_GROUP_BY,
|
VAR_GROUP_BY,
|
||||||
VAR_METRIC_EXPR,
|
VAR_METRIC_EXPR,
|
||||||
} from './shared';
|
} from './shared';
|
||||||
import { getTrailFor } from './utils';
|
import { getDataSource, getTrailFor } from './utils';
|
||||||
|
|
||||||
export interface MetricSceneState extends SceneObjectState {
|
export interface MetricSceneState extends SceneObjectState {
|
||||||
body: SceneFlexLayout;
|
body: SceneFlexLayout;
|
||||||
@ -113,6 +116,32 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
|
|||||||
this.publishEvent(new OpenEmbeddedTrailEvent(), true);
|
this.publishEvent(new OpenEmbeddedTrailEvent(), true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getLinkToExplore = async () => {
|
||||||
|
const metricScene = sceneGraph.getAncestor(this, MetricScene);
|
||||||
|
const trail = getTrailFor(this);
|
||||||
|
const dsValue = getDataSource(trail);
|
||||||
|
|
||||||
|
const flexItem = metricScene.state.body.state.children[0] as SceneFlexItem;
|
||||||
|
const autoVizPanel = flexItem.state.body as AutoVizPanel;
|
||||||
|
const queries = autoVizPanel.state.queryDef?.queries || [];
|
||||||
|
const timeRange = sceneGraph.getTimeRange(autoVizPanel);
|
||||||
|
|
||||||
|
return getExploreUrl({
|
||||||
|
queries,
|
||||||
|
dsRef: { uid: dsValue },
|
||||||
|
timeRange: timeRange.state.value,
|
||||||
|
scopedVars: { __sceneObject: { value: metricScene } },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public openExploreLink = async () => {
|
||||||
|
this.getLinkToExplore().then((link) => {
|
||||||
|
// We use window.open instead of a Link or <a> because we want to compute the explore link when clicking,
|
||||||
|
// if we precompute it we have to keep track of a lot of dependencies
|
||||||
|
window.open(link, '_blank');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
public static Component = ({ model }: SceneComponentProps<MetricActionBar>) => {
|
public static Component = ({ model }: SceneComponentProps<MetricActionBar>) => {
|
||||||
const metricScene = sceneGraph.getAncestor(model, MetricScene);
|
const metricScene = sceneGraph.getAncestor(model, MetricScene);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@ -129,11 +158,13 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
|
|||||||
<Box paddingY={1}>
|
<Box paddingY={1}>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
<ToolbarButton variant={'canvas'} icon="compass" tooltip="Open in explore (todo)" disabled>
|
<ToolbarButton
|
||||||
Explore
|
variant={'canvas'}
|
||||||
</ToolbarButton>
|
icon="compass"
|
||||||
<ToolbarButton variant={'canvas'}>Add to dashboard</ToolbarButton>
|
tooltip="Open in explore"
|
||||||
<ToolbarButton variant={'canvas'} icon="share-alt" tooltip="Copy url (todo)" disabled />
|
onClick={model.openExploreLink}
|
||||||
|
></ToolbarButton>
|
||||||
|
<ShareTrailButton trail={trail} />
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
variant={'canvas'}
|
variant={'canvas'}
|
||||||
icon={
|
icon={
|
||||||
|
28
public/app/features/trails/ShareTrailButton.tsx
Normal file
28
public/app/features/trails/ShareTrailButton.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
|
import { ToolbarButton } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { DataTrail } from './DataTrail';
|
||||||
|
import { getUrlForTrail } from './utils';
|
||||||
|
|
||||||
|
interface ShareTrailButtonState {
|
||||||
|
trail: DataTrail;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShareTrailButton = ({ trail }: ShareTrailButtonState) => {
|
||||||
|
const { origin } = useLocation();
|
||||||
|
const [tooltip, setTooltip] = useState('Copy url');
|
||||||
|
|
||||||
|
const onShare = () => {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(origin + getUrlForTrail(trail));
|
||||||
|
setTooltip('Copied!');
|
||||||
|
setTimeout(() => {
|
||||||
|
setTooltip('Copy url');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <ToolbarButton variant={'canvas'} icon={'share-alt'} tooltip={tooltip} onClick={onShare} />;
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { urlUtil } from '@grafana/data';
|
import { urlUtil } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { getUrlSyncManager, sceneGraph, SceneObject, SceneObjectUrlValues, SceneTimeRange } from '@grafana/scenes';
|
import { getUrlSyncManager, sceneGraph, SceneObject, SceneObjectUrlValues, SceneTimeRange } from '@grafana/scenes';
|
||||||
|
|
||||||
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
||||||
@ -50,6 +50,14 @@ export function getMetricSceneFor(model: SceneObject): MetricScene {
|
|||||||
throw new Error('Unable to find trail');
|
throw new Error('Unable to find trail');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDataSource(trail: DataTrail) {
|
||||||
|
return sceneGraph.interpolate(trail, VAR_DATASOURCE_EXPR);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDataSourceName(dataSourceUid: string) {
|
||||||
|
return getDataSourceSrv().getInstanceSettings(dataSourceUid)?.name || dataSourceUid;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDatasourceForNewTrail(): string | undefined {
|
export function getDatasourceForNewTrail(): string | undefined {
|
||||||
const prevTrail = getTrailStore().recent[0];
|
const prevTrail = getTrailStore().recent[0];
|
||||||
if (prevTrail) {
|
if (prevTrail) {
|
||||||
|
Loading…
Reference in New Issue
Block a user