diff --git a/packages/grafana-ui/src/components/OptionsUI/string.tsx b/packages/grafana-ui/src/components/OptionsUI/string.tsx index e2b85e7c4b3..c8f9db7412c 100644 --- a/packages/grafana-ui/src/components/OptionsUI/string.tsx +++ b/packages/grafana-ui/src/components/OptionsUI/string.tsx @@ -12,19 +12,24 @@ export const StringValueEditor: React.FC { + let nextValue = value ?? ''; if (e.hasOwnProperty('key')) { // handling keyboard event const evt = e as React.KeyboardEvent; if (evt.key === 'Enter' && !item.settings?.useTextarea) { - onChange(evt.currentTarget.value.trim() === '' ? undefined : evt.currentTarget.value); + nextValue = evt.currentTarget.value.trim(); } } else { // handling form event const evt = e as React.FormEvent; - onChange(evt.currentTarget.value.trim() === '' ? undefined : evt.currentTarget.value); + nextValue = evt.currentTarget.value.trim(); } + if (nextValue === value) { + return; // no change + } + onChange(nextValue === '' ? undefined : nextValue); }, - [item.settings?.useTextarea, onChange] + [value, item.settings?.useTextarea, onChange] ); return ( diff --git a/public/app/features/canvas/elements/icon.tsx b/public/app/features/canvas/elements/icon.tsx index e5a1c279359..a885ea950e7 100644 --- a/public/app/features/canvas/elements/icon.tsx +++ b/public/app/features/canvas/elements/icon.tsx @@ -1,7 +1,12 @@ import React, { CSSProperties } from 'react'; import { CanvasElementItem, CanvasElementProps } from '../element'; -import { ColorDimensionConfig, ResourceDimensionConfig, ResourceDimensionMode } from 'app/features/dimensions'; +import { + ColorDimensionConfig, + ResourceDimensionConfig, + ResourceDimensionMode, + getPublicOrAbsoluteUrl, +} from 'app/features/dimensions'; import { ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors'; import SVG from 'react-inlinesvg'; import { css } from '@emotion/css'; @@ -62,7 +67,7 @@ export const iconItem: CanvasElementItem = { defaultConfig: { path: { mode: ResourceDimensionMode.Fixed, - fixed: 'question-circle.svg', + fixed: 'img/icons/unicons/question-circle.svg', }, fill: { fixed: '#FFF899' }, }, @@ -74,17 +79,12 @@ export const iconItem: CanvasElementItem = { // Called when data changes prepareData: (ctx: DimensionContext, cfg: IconConfig) => { - const iconRoot = (window as any).__grafana_public_path__ + 'img/icons/unicons/'; let path: string | undefined = undefined; if (cfg.path) { path = ctx.getResource(cfg.path).value(); } if (!path || !isString(path)) { - // must be something? - path = 'question-circle.svg'; - } - if (path.indexOf(':/') < 0) { - path = iconRoot + path; + path = getPublicOrAbsoluteUrl('img/icons/unicons/question-circle.svg'); } const data: IconData = { diff --git a/public/app/features/dimensions/editors/ResourceCards.tsx b/public/app/features/dimensions/editors/ResourceCards.tsx new file mode 100644 index 00000000000..22bbeca39d3 --- /dev/null +++ b/public/app/features/dimensions/editors/ResourceCards.tsx @@ -0,0 +1,110 @@ +import React, { memo, CSSProperties } from 'react'; +import { areEqual, FixedSizeGrid as Grid } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { useTheme2, stylesFactory } from '@grafana/ui'; +import SVG from 'react-inlinesvg'; +import { css } from '@emotion/css'; + +interface CellProps { + columnIndex: number; + rowIndex: number; + style: CSSProperties; + data: any; +} + +function Cell(props: CellProps) { + const { columnIndex, rowIndex, style, data } = props; + const { cards, columnCount, onChange, folder } = data; + const singleColumnIndex = columnIndex + rowIndex * columnCount; + const card = cards[singleColumnIndex]; + const theme = useTheme2(); + const styles = getStyles(theme); + + return ( +
+ {card && ( +
onChange(`${folder.value}/${card.value}`)}> + {folder.value.includes('icons') ? ( + + ) : ( + + )} +
{card.label.substr(0, card.label.length - 4)}
+
+ )} +
+ ); +} + +const getStyles = stylesFactory((theme: GrafanaTheme2) => { + return { + card: css` + display: inline-block; + width: 80px; + height: 80px; + margin: 0.75rem; + text-align: center; + cursor: pointer; + position: relative; + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding-top: 6px; + + :hover { + border-color: ${theme.colors.action.hover}; + box-shadow: ${theme.shadows.z2}; + } + `, + img: css` + width: 50px; + height: 50px; + object-fit: cover; + vertical-align: middle; + fill: ${theme.colors.text.primary}; + `, + text: css` + color: ${theme.colors.text.primary}; + white-space: nowrap; + font-size: 12px; + text-overflow: ellipsis; + display: block; + overflow: hidden; + `, + }; +}); + +interface CardProps { + onChange: (value: string) => void; + cards: SelectableValue[]; + currentFolder: SelectableValue | undefined; +} + +export const ResourceCards = (props: CardProps) => { + const { onChange, cards, currentFolder: folder } = props; + + return ( + + {({ width, height }) => { + const cardWidth = 80; + const cardHeight = 80; + const columnCount = Math.floor(width / cardWidth); + const rowCount = Math.ceil(cards.length / columnCount); + return ( + + {memo(Cell, areEqual)} + + ); + }} + + ); +}; diff --git a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx index 379f15b5fbc..801c15ca24f 100644 --- a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx @@ -1,14 +1,9 @@ -import React, { FC, useCallback } from 'react'; -import { - FieldNamePickerConfigSettings, - StandardEditorProps, - StandardEditorsRegistryItem, - StringFieldConfigSettings, -} from '@grafana/data'; +import React, { FC, useCallback, useState } from 'react'; +import { FieldNamePickerConfigSettings, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data'; import { ResourceDimensionConfig, ResourceDimensionMode, ResourceDimensionOptions } from '../types'; -import { InlineField, InlineFieldRow, RadioButtonGroup, StringValueEditor } from '@grafana/ui'; +import { InlineField, InlineFieldRow, RadioButtonGroup, Button, Modal, Input } from '@grafana/ui'; import { FieldNamePicker } from '../../../../../packages/grafana-ui/src/components/MatchersUI/FieldNamePicker'; -import IconSelector from './IconSelector'; +import { ResourcePicker } from './ResourcePicker'; const resourceOptions = [ { label: 'Fixed', value: ResourceDimensionMode.Fixed, description: 'Fixed value' }, @@ -20,18 +15,12 @@ const dummyFieldSettings: StandardEditorsRegistryItem = { - settings: { - placeholder: 'Enter image URL', - }, -} as any; - export const ResourceDimensionEditor: FC< StandardEditorProps > = (props) => { const { value, context, onChange, item } = props; - const resourceType = item.settings?.resourceType ?? 'icon'; const labelWidth = 9; + const [isOpen, setOpen] = useState(false); const onModeChange = useCallback( (mode) => { @@ -54,19 +43,31 @@ export const ResourceDimensionEditor: FC< ); const onFixedChange = useCallback( - (fixed) => { + (fixed?: string) => { onChange({ ...value, - fixed, + fixed: fixed ?? '', }); + setOpen(false); }, [onChange, value] ); + const openModal = useCallback(() => { + setOpen(true); + }, []); + const mode = value?.mode ?? ResourceDimensionMode.Fixed; + const mediaType = item.settings?.resourceType ?? 'icon'; return ( <> + {isOpen && ( + setOpen(false)} closeOnEscape> + + + )} + @@ -86,21 +87,10 @@ export const ResourceDimensionEditor: FC< )} {mode === ResourceDimensionMode.Fixed && ( - {resourceType === 'icon' && ( - - - - )} - {resourceType === 'image' && ( - - - - )} + + + + + + + {tabs.map((tab, index) => ( + setTabs(tabs.map((tab, idx) => ({ ...tab, active: idx === index })))} + /> + ))} + + + {tabs[0].active && ( +
+ + {directoryIndex ? ( +
+ +
+ ) : ( + + )} +
+ )} + {/* TODO: add file upload + {tabs[1].active && ( + console.log('file', currentTarget?.files && currentTarget.files[0])} + className={styles.tabContent} + /> + )} */} +
+ + ); +} + +const getStyles = stylesFactory((theme: GrafanaTheme2) => { + return { + cardsWrapper: css` + height: calc(100vh - 480px); + `, + tabContent: css` + margin-top: 20px; + & > :nth-child(2) { + margin-top: 10px; + }, + `, + currentItem: css` + display: flex; + justify-content: space-between; + align-items: center; + column-gap: 2px; + margin: -18px 0px 18px 0px; + `, + img: css` + width: 40px; + height: 40px; + fill: ${theme.colors.text.primary}; + `, + }; +}); diff --git a/public/app/features/dimensions/resource.ts b/public/app/features/dimensions/resource.ts index 43d631e16ab..7890ec678b7 100644 --- a/public/app/features/dimensions/resource.ts +++ b/public/app/features/dimensions/resource.ts @@ -5,6 +5,12 @@ import { findField, getLastNotNullFieldValue } from './utils'; //--------------------------------------------------------- // Resource dimension //--------------------------------------------------------- +export function getPublicOrAbsoluteUrl(v: string): string { + if (!v) { + return ''; + } + return v.indexOf(':/') > 0 ? v : (window as any).__grafana_public_path__ + v; +} export function getResourceDimension( frame: DataFrame | undefined, @@ -12,7 +18,7 @@ export function getResourceDimension( ): DimensionSupplier { const mode = config.mode ?? ResourceDimensionMode.Fixed; if (mode === ResourceDimensionMode.Fixed) { - const v = config.fixed!; + const v = getPublicOrAbsoluteUrl(config.fixed!); return { isAssumed: !Boolean(v), fixed: v, @@ -33,7 +39,7 @@ export function getResourceDimension( } if (mode === ResourceDimensionMode.Mapping) { - const mapper = (v: any) => `${v}`; + const mapper = (v: any) => getPublicOrAbsoluteUrl(`${v}`); return { field, get: (i) => mapper(field.values.get(i)), diff --git a/public/img/bg/index.json b/public/img/bg/index.json new file mode 100644 index 00000000000..cb799fec2e9 --- /dev/null +++ b/public/img/bg/index.json @@ -0,0 +1,4 @@ +{ + "name": "bg", + "files": ["p0.png", "p1.png", "p2.png", "p3.png", "p4.png", "p5.png", "p6.png"] +} diff --git a/public/img/bg/p0.png b/public/img/bg/p0.png new file mode 100644 index 00000000000..0dd51f348fe Binary files /dev/null and b/public/img/bg/p0.png differ diff --git a/public/img/bg/p1.png b/public/img/bg/p1.png new file mode 100644 index 00000000000..6169c457452 Binary files /dev/null and b/public/img/bg/p1.png differ diff --git a/public/img/bg/p2.png b/public/img/bg/p2.png new file mode 100644 index 00000000000..50957ef9fcc Binary files /dev/null and b/public/img/bg/p2.png differ diff --git a/public/img/bg/p3.png b/public/img/bg/p3.png new file mode 100644 index 00000000000..3acc7647d56 Binary files /dev/null and b/public/img/bg/p3.png differ diff --git a/public/img/bg/p4.png b/public/img/bg/p4.png new file mode 100644 index 00000000000..6d2e044744b Binary files /dev/null and b/public/img/bg/p4.png differ diff --git a/public/img/bg/p5.png b/public/img/bg/p5.png new file mode 100644 index 00000000000..be44f19c6dd Binary files /dev/null and b/public/img/bg/p5.png differ diff --git a/public/img/bg/p6.png b/public/img/bg/p6.png new file mode 100644 index 00000000000..4d75ed54459 Binary files /dev/null and b/public/img/bg/p6.png differ diff --git a/public/img/icons/iot/faucet.svg b/public/img/icons/iot/faucet.svg new file mode 100644 index 00000000000..76f19684244 --- /dev/null +++ b/public/img/icons/iot/faucet.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/img/icons/iot/index.json b/public/img/icons/iot/index.json new file mode 100644 index 00000000000..4edb38c86db --- /dev/null +++ b/public/img/icons/iot/index.json @@ -0,0 +1,4 @@ +{ + "name": "line", + "files": ["faucet.svg", "pump.svg"] +} diff --git a/public/img/icons/iot/pump.svg b/public/img/icons/iot/pump.svg new file mode 100644 index 00000000000..43cc0be93e8 --- /dev/null +++ b/public/img/icons/iot/pump.svg @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file