mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Transforms: Adds beta notice and updates transform descriptions (#24158)
* Transforms: Adds beta notice and updates transform descriptions * Rename organize fields * Webpack - enable images import * Introduce FeatureState type * Alow Container component grow/shrink config * Enable svg import in main app * Jest + webpack for svgs * InfoBox refactor (+ added feature info box), Badge component introduced * Update packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx Co-authored-by: Carl Bergquist <carl@grafana.com> * Minor fixes * Update packages/grafana-ui/src/components/TransformersUI/OrganizeFieldsTransformerEditor.tsx Co-authored-by: Carl Bergquist <carl@grafana.com> * Update packages/grafana-ui/src/components/TransformersUI/SeriesToFieldsTransformerEditor.tsx Co-authored-by: Carl Bergquist <carl@grafana.com> * fix typo * Build storybook fixed * Fix padding Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: Carl Bergquist <carl@grafana.com>
This commit is contained in:
parent
0fe9e7e242
commit
92a16d2e10
@ -14,4 +14,7 @@ module.exports = {
|
||||
setupFiles: ['jest-canvas-mock', './public/test/jest-shim.ts', './public/test/jest-setup.ts'],
|
||||
snapshotSerializers: ['enzyme-to-json/serializer'],
|
||||
globals: { 'ts-jest': { isolatedModules: true } },
|
||||
moduleNameMapper: {
|
||||
'\\.svg': '<rootDir>/public/test/mocks/svg.ts',
|
||||
},
|
||||
};
|
||||
|
@ -18,7 +18,7 @@ export interface ReduceTransformerOptions {
|
||||
export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> = {
|
||||
id: DataTransformerID.reduce,
|
||||
name: 'Reduce',
|
||||
description: 'Reduce all rows to a single row and concatenate all results',
|
||||
description: 'Reduce all rows or data points to a single value using a function like max, min, mean or last',
|
||||
defaultOptions: {
|
||||
reducers: [ReducerID.max],
|
||||
},
|
||||
|
@ -74,3 +74,12 @@ export class AppPlugin<T = KeyValue> extends GrafanaPlugin<AppPluginMeta<T>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines life cycle of a feature
|
||||
* @internal
|
||||
*/
|
||||
export enum FeatureState {
|
||||
alpha = 'alpha',
|
||||
beta = 'beta',
|
||||
}
|
||||
|
9
packages/grafana-data/src/utils/docs.ts
Normal file
9
packages/grafana-data/src/utils/docs.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Enumeration of documentation topics
|
||||
* @internal
|
||||
*/
|
||||
export enum DocsId {
|
||||
Transformations,
|
||||
FieldConfig,
|
||||
FieldConfigOverrides,
|
||||
}
|
@ -16,3 +16,4 @@ export { getFlotPairs, getFlotPairsConstant } from './flotPairs';
|
||||
export { locationUtil } from './location';
|
||||
export { urlUtil, UrlQueryMap, UrlQueryValue } from './url';
|
||||
export { DataLinkBuiltInVars } from './dataLinks';
|
||||
export { DocsId } from './docs';
|
||||
|
@ -11,5 +11,10 @@
|
||||
},
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"extends": "@grafana/tsconfig",
|
||||
"include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts", "../../public/app/types/sanitize-url.d.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts*",
|
||||
"../../public/app/types/jquery/*.ts",
|
||||
"../../public/app/types/sanitize-url.d.ts",
|
||||
"../../public/app/types/svg.d.ts"
|
||||
]
|
||||
}
|
||||
|
@ -6,5 +6,10 @@
|
||||
},
|
||||
"exclude": ["../dist", "../node_modules"],
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["../src/**/*.ts", "../src/**/*.tsx", "../../../public/app/types/sanitize-url.d.ts"]
|
||||
"include": [
|
||||
"../src/**/*.ts",
|
||||
"../src/**/*.tsx",
|
||||
"../../../public/app/types/sanitize-url.d.ts",
|
||||
"../../../public/app/types/svg.d.ts"
|
||||
]
|
||||
}
|
||||
|
35
packages/grafana-ui/src/components/Badge/Badge.story.tsx
Normal file
35
packages/grafana-ui/src/components/Badge/Badge.story.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { boolean, text, select } from '@storybook/addon-knobs';
|
||||
import { Badge, BadgeColor } from './Badge';
|
||||
|
||||
export default {
|
||||
title: 'Other/Badge',
|
||||
component: Badge,
|
||||
decorators: [],
|
||||
parameters: {
|
||||
docs: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const basic = () => {
|
||||
const badgeColor = select<BadgeColor>(
|
||||
'Badge color',
|
||||
{
|
||||
Red: 'red',
|
||||
Green: 'green',
|
||||
Blue: 'blue',
|
||||
Orange: 'orange',
|
||||
},
|
||||
'blue'
|
||||
);
|
||||
const withIcon = boolean('With icon', true);
|
||||
const tooltipText = text('Tooltip text', '');
|
||||
return (
|
||||
<Badge
|
||||
text={'Badge label'}
|
||||
color={badgeColor}
|
||||
icon={withIcon ? 'rocket' : undefined}
|
||||
tooltip={tooltipText.trim() === '' ? undefined : tooltipText}
|
||||
/>
|
||||
);
|
||||
};
|
90
packages/grafana-ui/src/components/Badge/Badge.tsx
Normal file
90
packages/grafana-ui/src/components/Badge/Badge.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { useTheme } from '../../themes/ThemeContext';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
import { IconName } from '../../types';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
import { getColorFromHexRgbOrName, GrafanaTheme } from '@grafana/data';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { css } from 'emotion';
|
||||
import { HorizontalGroup } from '..';
|
||||
|
||||
export type BadgeColor = 'blue' | 'red' | 'green' | 'orange';
|
||||
|
||||
export interface BadgeProps {
|
||||
text: string;
|
||||
color: BadgeColor;
|
||||
icon?: IconName;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export const Badge = React.memo<BadgeProps>(({ icon, color, text, tooltip }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, color);
|
||||
const badge = (
|
||||
<div className={styles.wrapper}>
|
||||
<HorizontalGroup align="center" spacing="xs">
|
||||
{icon && <Icon name={icon} size="sm" />}
|
||||
<span>{text}</span>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<Tooltip content={tooltip} placement="auto">
|
||||
{badge}
|
||||
</Tooltip>
|
||||
) : (
|
||||
badge
|
||||
);
|
||||
});
|
||||
|
||||
Badge.displayName = 'Badge';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, color: BadgeColor) => {
|
||||
let sourceColor = getColorFromHexRgbOrName(color);
|
||||
let borderColor = '';
|
||||
let bgColor = '';
|
||||
let textColor = '';
|
||||
|
||||
if (theme.isDark) {
|
||||
bgColor = tinycolor(sourceColor)
|
||||
.darken(38)
|
||||
.toString();
|
||||
borderColor = tinycolor(sourceColor)
|
||||
.darken(25)
|
||||
.toString();
|
||||
textColor = tinycolor(sourceColor)
|
||||
.lighten(45)
|
||||
.toString();
|
||||
} else {
|
||||
bgColor = tinycolor(sourceColor)
|
||||
.lighten(30)
|
||||
.toString();
|
||||
borderColor = tinycolor(sourceColor)
|
||||
.lighten(15)
|
||||
.toString();
|
||||
textColor = tinycolor(sourceColor)
|
||||
.darken(40)
|
||||
.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
display: inline-flex;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
margin-top: 6px;
|
||||
background: ${bgColor};
|
||||
border: 1px solid ${borderColor};
|
||||
color: ${textColor};
|
||||
|
||||
> span {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { InfoBox, InfoBoxProps } from './InfoBox';
|
||||
import { FeatureState, GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { Badge, BadgeProps } from '../Badge/Badge';
|
||||
import { css } from 'emotion';
|
||||
|
||||
interface FeatureInfoBox extends Omit<InfoBoxProps, 'branded' | 'title' | 'urlTitle'> {
|
||||
title: string;
|
||||
featureState?: FeatureState;
|
||||
}
|
||||
export const FeatureInfoBox = React.memo(
|
||||
React.forwardRef<HTMLDivElement, FeatureInfoBox>(({ title, featureState, ...otherProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
const styles = getFeatureInfoBoxStyles(theme);
|
||||
|
||||
const titleEl = featureState ? (
|
||||
<>
|
||||
<div className={styles.badge}>
|
||||
<FeatureBadge featureState={featureState} />
|
||||
</div>
|
||||
<h3>{title}</h3>
|
||||
</>
|
||||
) : (
|
||||
<h3>{title}</h3>
|
||||
);
|
||||
return <InfoBox branded title={titleEl} urlTitle="Read documentation" {...otherProps} />;
|
||||
})
|
||||
);
|
||||
|
||||
const getFeatureInfoBoxStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
badge: css`
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface FeatureBadgeProps {
|
||||
featureState: FeatureState;
|
||||
}
|
||||
|
||||
export const FeatureBadge: React.FC<FeatureBadgeProps> = ({ featureState }) => {
|
||||
const display = getPanelStateBadgeDisplayModel(featureState);
|
||||
return <Badge text={display.text} color={display.color} icon={display.icon} />;
|
||||
};
|
||||
|
||||
function getPanelStateBadgeDisplayModel(featureState: FeatureState): BadgeProps {
|
||||
switch (featureState) {
|
||||
case FeatureState.alpha:
|
||||
return {
|
||||
text: 'Alpha',
|
||||
icon: 'exclamation-triangle',
|
||||
color: 'orange',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: 'Beta',
|
||||
icon: 'rocket',
|
||||
color: 'blue',
|
||||
};
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { number } from '@storybook/addon-knobs';
|
||||
import { InfoBox } from './InfoBox';
|
||||
import { FeatureInfoBox } from './FeatureInfoBox';
|
||||
import { FeatureState } from '@grafana/data';
|
||||
|
||||
export default {
|
||||
title: 'Layout/InfoBox',
|
||||
@ -35,16 +37,11 @@ export const basic = () => {
|
||||
return (
|
||||
<div style={{ width: containerWidth }}>
|
||||
<InfoBox
|
||||
header="User Permission"
|
||||
footer={
|
||||
<>
|
||||
Checkout the{' '}
|
||||
<a className="external-link" target="_blank" href="http://docs.grafana.org/features/datasources/mysql/">
|
||||
MySQL Data Source Docs
|
||||
</a>{' '}
|
||||
for more information.,
|
||||
</>
|
||||
}
|
||||
title="User Permission"
|
||||
url={'http://docs.grafana.org/features/datasources/mysql/'}
|
||||
onDismiss={() => {
|
||||
alert('onDismiss clicked');
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
The database user should only be granted SELECT permissions on the specified database & tables you want to
|
||||
@ -57,3 +54,26 @@ export const basic = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const featureInfoBox = () => {
|
||||
const { containerWidth } = getKnobs();
|
||||
|
||||
return (
|
||||
<div style={{ width: containerWidth }}>
|
||||
<FeatureInfoBox
|
||||
title="Transformations"
|
||||
url={'http://www.grafana.com'}
|
||||
featureState={FeatureState.beta}
|
||||
onDismiss={() => {
|
||||
alert('onDismiss clicked');
|
||||
}}
|
||||
>
|
||||
Transformations allow you to join, calculate, re-order, hide and rename your query results before being
|
||||
visualized. <br />
|
||||
Many transforms are not suitable if your using the Graph visualisation as it currently only supports time
|
||||
series. <br />
|
||||
It can help to switch to Table visualisation to understand what a transformation is doing.
|
||||
</FeatureInfoBox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,10 +2,19 @@ import React from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { HorizontalGroup } from '../Layout/Layout';
|
||||
import panelArtDark from './panelArt_dark.svg';
|
||||
import panelArtLight from './panelArt_light.svg';
|
||||
|
||||
export interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
header?: string | JSX.Element;
|
||||
footer?: string | JSX.Element;
|
||||
export interface InfoBoxProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
||||
children: React.ReactNode;
|
||||
title?: string | JSX.Element;
|
||||
url?: string;
|
||||
urlTitle?: string;
|
||||
branded?: boolean;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -14,33 +23,39 @@ export interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
* @Alpha
|
||||
*/
|
||||
export const InfoBox = React.memo(
|
||||
React.forwardRef<HTMLDivElement, Props>(({ header, footer, className, children, ...otherProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
const css = getInfoBoxStyles(theme);
|
||||
React.forwardRef<HTMLDivElement, InfoBoxProps>(
|
||||
({ title, className, children, branded, url, urlTitle, onDismiss, ...otherProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
const styles = getInfoBoxStyles(theme);
|
||||
const wrapperClassName = branded ? cx(styles.wrapperBranded, className) : cx(styles.wrapper, className);
|
||||
|
||||
return (
|
||||
<div className={cx([css.wrapper, className])} {...otherProps} ref={ref}>
|
||||
{header && (
|
||||
<div className={css.header}>
|
||||
<h5>{header}</h5>
|
||||
return (
|
||||
<div className={wrapperClassName} {...otherProps} ref={ref}>
|
||||
<div>
|
||||
<HorizontalGroup justify={'space-between'} align={'flex-start'}>
|
||||
<div>{typeof title === 'string' ? <h4>{title}</h4> : title}</div>
|
||||
{onDismiss && <IconButton name={'times'} onClick={onDismiss} />}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{footer && <div className={css.footer}>{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
<div>{children}</div>
|
||||
{url && (
|
||||
<a href={url} className={styles.docsLink} target="_blank">
|
||||
<Icon name="book" /> {urlTitle || 'Read more'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const getInfoBoxStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
wrapper: css`
|
||||
position: relative;
|
||||
padding: ${theme.spacing.lg};
|
||||
padding: ${theme.spacing.md};
|
||||
background-color: ${theme.colors.bg2};
|
||||
border-top: 3px solid ${theme.palette.blue80};
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
margin-right: ${theme.spacing.xs};
|
||||
box-shadow: ${theme.shadows.listItem};
|
||||
flex-grow: 1;
|
||||
|
||||
ul {
|
||||
@ -60,18 +75,39 @@ const getInfoBoxStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
@extend .external-link;
|
||||
}
|
||||
|
||||
&--max-lg {
|
||||
max-width: ${theme.breakpoints.lg};
|
||||
}
|
||||
`,
|
||||
header: css`
|
||||
margin-bottom: ${theme.spacing.d};
|
||||
wrapperBranded: css`
|
||||
padding: ${theme.spacing.md};
|
||||
border-radius: ${theme.border.radius.md};
|
||||
position: relative;
|
||||
box-shadow: 0 0 30px 10px rgba(0, 0, 0, ${theme.isLight ? 0.05 : 0.2});
|
||||
z-index: 0;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url(${theme.isLight ? panelArtLight : panelArtDark});
|
||||
border-radius: ${theme.border.radius.md};
|
||||
background-position: 50% 50%;
|
||||
background-size: cover;
|
||||
filter: saturate(80%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
footer: css`
|
||||
margin-top: ${theme.spacing.d};
|
||||
docsLink: css`
|
||||
display: inline-block;
|
||||
margin-top: ${theme.spacing.lg};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
}));
|
||||
|
58
packages/grafana-ui/src/components/InfoBox/panelArt_dark.svg
Normal file
58
packages/grafana-ui/src/components/InfoBox/panelArt_dark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 23 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 25 KiB |
@ -1,5 +1,5 @@
|
||||
import React, { HTMLProps } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
|
||||
@ -24,6 +24,8 @@ export interface LayoutProps extends Omit<HTMLProps<HTMLDivElement>, 'align' | '
|
||||
export interface ContainerProps {
|
||||
padding?: Spacing;
|
||||
margin?: Spacing;
|
||||
grow?: number;
|
||||
shrink?: number;
|
||||
}
|
||||
|
||||
export const Layout: React.FC<LayoutProps> = ({
|
||||
@ -84,10 +86,26 @@ export const VerticalGroup: React.FC<Omit<LayoutProps, 'orientation' | 'wrap'>>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export const Container: React.FC<ContainerProps> = ({ children, padding, margin }) => {
|
||||
export const Container: React.FC<ContainerProps> = ({ children, padding, margin, grow, shrink }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getContainerStyles(theme, padding, margin);
|
||||
return <div className={styles.wrapper}>{children}</div>;
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
styles.wrapper,
|
||||
grow !== undefined &&
|
||||
css`
|
||||
flex-grow: ${grow};
|
||||
`,
|
||||
shrink !== undefined &&
|
||||
css`
|
||||
flex-shrink: ${shrink};
|
||||
`
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory(
|
||||
|
@ -190,5 +190,5 @@ export const filterFieldsByNameTransformRegistryItem: TransformerRegistyItem<Fil
|
||||
editor: FilterByNameTransformerEditor,
|
||||
transformation: standardTransformers.filterFieldsByNameTransformer,
|
||||
name: 'Filter by name',
|
||||
description: 'Filter fields by name',
|
||||
description: 'Removes part of the query results using a regex pattern. The pattern can be inclusive or exclusive.',
|
||||
};
|
||||
|
@ -133,6 +133,7 @@ export const filterFramesByRefIdTransformRegistryItem: TransformerRegistyItem<Fi
|
||||
id: DataTransformerID.filterByRefId,
|
||||
editor: FilterByRefIdTransformerEditor,
|
||||
transformation: standardTransformers.filterFramesByRefIdTransformer,
|
||||
name: 'Filter by refId',
|
||||
description: 'Filter results by refId',
|
||||
name: 'Filter data by query',
|
||||
description:
|
||||
'Filter data by query. This is useful if you are sharing the results from a different panel that has many queries and you want to only visualize a subset of that in this panel.',
|
||||
};
|
||||
|
@ -15,5 +15,6 @@ export const labelsToFieldsTransformerRegistryItem: TransformerRegistyItem<Label
|
||||
editor: LabelsAsFieldsTransformerEditor,
|
||||
transformation: standardTransformers.labelsToFieldsTransformer,
|
||||
name: 'Labels to fields',
|
||||
description: 'Groups series by time and return labels or tags as fields',
|
||||
description: `Groups series by time and return labels or tags as fields.
|
||||
Useful for showing time series with labels in a table where each label key becomes a seperate column`,
|
||||
};
|
||||
|
@ -221,6 +221,7 @@ export const organizeFieldsTransformRegistryItem: TransformerRegistyItem<Organiz
|
||||
id: DataTransformerID.organize,
|
||||
editor: OrganizeFieldsTransformerEditor,
|
||||
transformation: standardTransformers.organizeFieldsTransformer,
|
||||
name: 'Organize fields',
|
||||
description: 'Order, filter and rename fields',
|
||||
name: 'Change order, hide and rename',
|
||||
description:
|
||||
"Allows the user to re-order, hide, or rename columns. Useful when data source doesn't allow overrides for visualizing data.",
|
||||
};
|
||||
|
@ -42,6 +42,7 @@ export const seriesToFieldsTransformerRegistryItem: TransformerRegistyItem<Serie
|
||||
id: DataTransformerID.seriesToColumns,
|
||||
editor: SeriesToFieldsTransformerEditor,
|
||||
transformation: standardTransformers.seriesToColumnsTransformer,
|
||||
name: 'Join by field',
|
||||
description: 'Joins many time series / data frames by a field',
|
||||
name: 'Outer join',
|
||||
description:
|
||||
'Joins many time series/tables by a field. This can be used to outer join multiple time series on the _time_ field to show many time series in one table.',
|
||||
};
|
||||
|
@ -99,6 +99,7 @@ export { DataLinkInput } from './DataLinks/DataLinkInput';
|
||||
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
|
||||
export { SeriesIcon } from './Legend/SeriesIcon';
|
||||
export { InfoBox } from './InfoBox/InfoBox';
|
||||
export { FeatureInfoBox } from './InfoBox/FeatureInfoBox';
|
||||
|
||||
export { JSONFormatter } from './JSONFormatter/JSONFormatter';
|
||||
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
|
||||
@ -138,6 +139,7 @@ export * from './Select/Select';
|
||||
export { ButtonSelect } from './Select/ButtonSelect';
|
||||
|
||||
export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout';
|
||||
export { Badge, BadgeColor, BadgeProps } from './Badge/Badge';
|
||||
export { RadioButtonGroup } from './Forms/RadioButtonGroup/RadioButtonGroup';
|
||||
|
||||
export { Input } from './Input/Input';
|
||||
|
@ -11,5 +11,5 @@
|
||||
},
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"extends": "@grafana/tsconfig",
|
||||
"include": ["src/**/*.ts*", "../../public/app/types/sanitize-url.d.ts"]
|
||||
"include": ["src/**/*.ts*", "../../public/app/types/sanitize-url.d.ts", "../../public/app/types/svg.d.ts"]
|
||||
}
|
||||
|
10
public/app/core/utils/docsLinks.ts
Normal file
10
public/app/core/utils/docsLinks.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { DocsId } from '@grafana/data';
|
||||
|
||||
// TODO: Documentation links
|
||||
const DOCS_LINKS: Record<DocsId, string> = {
|
||||
[DocsId.Transformations]: 'https://docs.grafana.com',
|
||||
[DocsId.FieldConfig]: 'https://docs.grafana.com',
|
||||
[DocsId.FieldConfigOverrides]: 'https://docs.grafana.com',
|
||||
};
|
||||
|
||||
export const getDocsLink = (id: DocsId) => DOCS_LINKS[id];
|
@ -2,18 +2,20 @@ import React, { useCallback } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {
|
||||
DataFrame,
|
||||
FeatureState,
|
||||
FieldConfigPropertyItem,
|
||||
FieldConfigSource,
|
||||
PanelPlugin,
|
||||
SelectableValue,
|
||||
VariableSuggestionsScope,
|
||||
} from '@grafana/data';
|
||||
import { Container, Counter, Field, fieldMatchersUI, Label, ValuePicker } from '@grafana/ui';
|
||||
import { Container, Counter, FeatureInfoBox, Field, fieldMatchersUI, Label, useTheme, ValuePicker } from '@grafana/ui';
|
||||
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
|
||||
import { OverrideEditor } from './OverrideEditor';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { OptionsGroup } from './OptionsGroup';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { css } from 'emotion';
|
||||
|
||||
interface Props {
|
||||
plugin: PanelPlugin;
|
||||
@ -27,6 +29,8 @@ interface Props {
|
||||
* Expects the container div to have size set and will fill it 100%
|
||||
*/
|
||||
export const OverrideFieldConfigEditor: React.FC<Props> = props => {
|
||||
const theme = useTheme();
|
||||
const { config } = props;
|
||||
const onOverrideChange = (index: number, override: any) => {
|
||||
const { config } = props;
|
||||
let overrides = cloneDeep(config.overrides);
|
||||
@ -104,6 +108,19 @@ export const OverrideFieldConfigEditor: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<div aria-label={selectors.components.OverridesConfigEditor.content}>
|
||||
{config.overrides.length === 0 && (
|
||||
<FeatureInfoBox
|
||||
title="Overrides"
|
||||
featureState={FeatureState.beta}
|
||||
// url={getDocsLink(DocsId.FieldConfigOverrides)}
|
||||
className={css`
|
||||
margin: ${theme.spacing.md};
|
||||
`}
|
||||
>
|
||||
Field options overrides give you a fine grained control over how your data is displayed.
|
||||
</FeatureInfoBox>
|
||||
)}
|
||||
|
||||
{renderOverrides()}
|
||||
{renderAddOverride()}
|
||||
</div>
|
||||
|
@ -1,8 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Button, Container, CustomScrollbar, stylesFactory, useTheme, ValuePicker, VerticalGroup } from '@grafana/ui';
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
CustomScrollbar,
|
||||
FeatureInfoBox,
|
||||
stylesFactory,
|
||||
useTheme,
|
||||
ValuePicker,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import {
|
||||
DataFrame,
|
||||
DataTransformerConfig,
|
||||
FeatureState,
|
||||
GrafanaTheme,
|
||||
SelectableValue,
|
||||
standardTransformersRegistry,
|
||||
@ -114,13 +124,23 @@ export class TransformationsEditor extends React.PureComponent<Props> {
|
||||
|
||||
renderNoAddedTransformsState() {
|
||||
return (
|
||||
<>
|
||||
<p className="muted">
|
||||
Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being
|
||||
visualized. <br />
|
||||
Choose one of the transformations below to start with:
|
||||
</p>
|
||||
|
||||
<VerticalGroup spacing={'lg'}>
|
||||
<Container grow={1}>
|
||||
<FeatureInfoBox
|
||||
title="Transformations"
|
||||
featureState={FeatureState.beta}
|
||||
// url={getDocsLink(DocsId.Transformations)}
|
||||
>
|
||||
<p>
|
||||
Transformations allow you to join, calculate, re-order, hide and rename your query results before being
|
||||
visualized. <br />
|
||||
Many transforms are not suitable if your using the Graph visualisation as it currently only supports time
|
||||
series. <br />
|
||||
It can help to switch to Table visualisation to understand what a transformation is doing. <br />
|
||||
</p>
|
||||
<p>Select one of the transformations below to start.</p>
|
||||
</FeatureInfoBox>
|
||||
</Container>
|
||||
<VerticalGroup>
|
||||
{standardTransformersRegistry.list().map(t => {
|
||||
return (
|
||||
@ -136,7 +156,7 @@ export class TransformationsEditor extends React.PureComponent<Props> {
|
||||
);
|
||||
})}
|
||||
</VerticalGroup>
|
||||
</>
|
||||
</VerticalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@ -170,6 +190,11 @@ const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
border: none;
|
||||
padding: ${theme.spacing.sm};
|
||||
|
||||
// hack because these cards use classes from a very different card for some reason
|
||||
.add-data-source-item-text {
|
||||
font-size: ${theme.typography.size.md};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.bg3};
|
||||
box-shadow: none;
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { GrafanaTheme, PanelPluginMeta, PluginState } from '@grafana/data';
|
||||
import { styleMixins, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { Badge, BadgeProps, styleMixins, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { css, cx } from 'emotion';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { PanelPluginBadge } from '../../plugins/PluginSignatureBadge';
|
||||
|
||||
interface Props {
|
||||
isCurrent: boolean;
|
||||
@ -126,3 +125,36 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
});
|
||||
|
||||
export default VizTypePickerPlugin;
|
||||
|
||||
interface PanelPluginBadgeProps {
|
||||
plugin: PanelPluginMeta;
|
||||
}
|
||||
const PanelPluginBadge: React.FC<PanelPluginBadgeProps> = ({ plugin }) => {
|
||||
const display = getPanelStateBadgeDisplayModel(plugin);
|
||||
|
||||
if (plugin.state !== PluginState.deprecated && plugin.state !== PluginState.alpha) {
|
||||
return null;
|
||||
}
|
||||
return <Badge color={display.color} text={display.text} icon={display.icon} tooltip={display.tooltip} />;
|
||||
};
|
||||
|
||||
function getPanelStateBadgeDisplayModel(panel: PanelPluginMeta): BadgeProps {
|
||||
switch (panel.state) {
|
||||
case PluginState.deprecated:
|
||||
return {
|
||||
text: 'Deprecated',
|
||||
icon: 'exclamation-triangle',
|
||||
color: 'red',
|
||||
tooltip: `${panel.name} panel is deprecated`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: 'Alpha',
|
||||
icon: 'rocket',
|
||||
color: 'blue',
|
||||
tooltip: `${panel.name} panel is experimental`,
|
||||
};
|
||||
}
|
||||
|
||||
PanelPluginBadge.displayName = 'PanelPluginBadge';
|
||||
|
@ -1,61 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Icon, IconName, stylesFactory, Tooltip, useTheme } from '@grafana/ui';
|
||||
import {
|
||||
getColorFromHexRgbOrName,
|
||||
GrafanaTheme,
|
||||
PanelPluginMeta,
|
||||
PluginSignatureStatus,
|
||||
PluginState,
|
||||
} from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { Badge, BadgeProps } from '@grafana/ui';
|
||||
import { PluginSignatureStatus } from '@grafana/data';
|
||||
|
||||
interface Props {
|
||||
status: PluginSignatureStatus;
|
||||
}
|
||||
|
||||
export const PluginSignatureBadge: React.FC<Props> = ({ status }) => {
|
||||
const theme = useTheme();
|
||||
const display = getSignatureDisplayModel(status);
|
||||
const styles = getStyles(theme, display);
|
||||
|
||||
return (
|
||||
<Tooltip content={display.tooltip} placement="left">
|
||||
<div className={styles.wrapper}>
|
||||
<Icon name={display.icon} size="sm" />
|
||||
<span>{display.text}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
return <Badge text={display.text} color={display.color} icon={display.icon} tooltip={display.tooltip} />;
|
||||
};
|
||||
|
||||
interface PanelPluginBadgeProps {
|
||||
plugin: PanelPluginMeta;
|
||||
}
|
||||
export const PanelPluginBadge: React.FC<PanelPluginBadgeProps> = ({ plugin }) => {
|
||||
const theme = useTheme();
|
||||
const display = getPanelStateBadgeDisplayModel(plugin);
|
||||
const styles = getStyles(theme, display);
|
||||
|
||||
if (plugin.state !== PluginState.deprecated && plugin.state !== PluginState.alpha) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Icon name={display.icon} size="sm" />
|
||||
<span>{display.text}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface DisplayModel {
|
||||
text: string;
|
||||
icon: IconName;
|
||||
color: string;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
function getSignatureDisplayModel(signature: PluginSignatureStatus): DisplayModel {
|
||||
function getSignatureDisplayModel(signature: PluginSignatureStatus): BadgeProps {
|
||||
switch (signature) {
|
||||
case PluginSignatureStatus.internal:
|
||||
return { text: 'Core', icon: 'cube', color: 'blue', tooltip: 'Core plugin that is bundled with Grafana' };
|
||||
@ -80,71 +36,4 @@ function getSignatureDisplayModel(signature: PluginSignatureStatus): DisplayMode
|
||||
return { text: 'Unsigned', icon: 'exclamation-triangle', color: 'red', tooltip: 'Unsigned external plugin' };
|
||||
}
|
||||
|
||||
function getPanelStateBadgeDisplayModel(panel: PanelPluginMeta): DisplayModel {
|
||||
switch (panel.state) {
|
||||
case PluginState.deprecated:
|
||||
return {
|
||||
text: 'Deprecated',
|
||||
icon: 'exclamation-triangle',
|
||||
color: 'red',
|
||||
tooltip: `${panel.name} panel is deprecated`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: 'Alpha',
|
||||
icon: 'rocket',
|
||||
color: 'blue',
|
||||
tooltip: `${panel.name} panel is experimental`,
|
||||
};
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, model: DisplayModel) => {
|
||||
let sourceColor = getColorFromHexRgbOrName(model.color);
|
||||
let borderColor = '';
|
||||
let bgColor = '';
|
||||
let textColor = '';
|
||||
|
||||
if (theme.isDark) {
|
||||
bgColor = tinycolor(sourceColor)
|
||||
.darken(38)
|
||||
.toString();
|
||||
borderColor = tinycolor(sourceColor)
|
||||
.darken(25)
|
||||
.toString();
|
||||
textColor = tinycolor(sourceColor)
|
||||
.lighten(45)
|
||||
.toString();
|
||||
} else {
|
||||
bgColor = tinycolor(sourceColor)
|
||||
.lighten(30)
|
||||
.toString();
|
||||
borderColor = tinycolor(sourceColor)
|
||||
.lighten(15)
|
||||
.toString();
|
||||
textColor = tinycolor(sourceColor)
|
||||
.darken(40)
|
||||
.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
display: inline-flex;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
margin-top: 6px;
|
||||
background: ${bgColor};
|
||||
border: 1px solid ${borderColor};
|
||||
color: ${textColor};
|
||||
|
||||
> span {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
PluginSignatureBadge.displayName = 'PluginSignatureBadge';
|
||||
|
4
public/app/types/svg.d.ts
vendored
Normal file
4
public/app/types/svg.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
1
public/test/mocks/svg.ts
Normal file
1
public/test/mocks/svg.ts
Normal file
@ -0,0 +1 @@
|
||||
export const svg = 'svg';
|
@ -108,6 +108,11 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/,
|
||||
loader: 'file-loader',
|
||||
options: { name: 'static/img/[name].[hash:8].[ext]' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-3
|
||||
|
@ -86,10 +86,6 @@ module.exports = (env = {}) =>
|
||||
sourceMap: false,
|
||||
preserveUrl: false,
|
||||
}),
|
||||
{
|
||||
test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
|
||||
loader: 'file-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user