Canvas Panel: icon/image select modal (#38844)
* feature: add icon modal * fix: make apply send on change * move to resource folder * allow empty * type * icon modal * fix: edit styles * fix: edit styles Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
@ -12,19 +12,24 @@ export const StringValueEditor: React.FC<StandardEditorProps<string, StringField
|
||||
|
||||
const onValueChange = useCallback(
|
||||
(e: React.SyntheticEvent) => {
|
||||
let nextValue = value ?? '';
|
||||
if (e.hasOwnProperty('key')) {
|
||||
// handling keyboard event
|
||||
const evt = e as React.KeyboardEvent<HTMLInputElement>;
|
||||
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<HTMLInputElement>;
|
||||
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 (
|
||||
|
@ -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<IconConfig, IconData> = {
|
||||
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<IconConfig, IconData> = {
|
||||
|
||||
// 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 = {
|
||||
|
110
public/app/features/dimensions/editors/ResourceCards.tsx
Normal file
@ -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 (
|
||||
<div style={style}>
|
||||
{card && (
|
||||
<div key={card.value} className={styles.card} onClick={() => onChange(`${folder.value}/${card.value}`)}>
|
||||
{folder.value.includes('icons') ? (
|
||||
<SVG src={card.imgUrl} className={styles.img} />
|
||||
) : (
|
||||
<img src={card.imgUrl} className={styles.img} />
|
||||
)}
|
||||
<h6 className={styles.text}>{card.label.substr(0, card.label.length - 4)}</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<string> | undefined;
|
||||
}
|
||||
|
||||
export const ResourceCards = (props: CardProps) => {
|
||||
const { onChange, cards, currentFolder: folder } = props;
|
||||
|
||||
return (
|
||||
<AutoSizer defaultWidth={1920} defaultHeight={1080}>
|
||||
{({ width, height }) => {
|
||||
const cardWidth = 80;
|
||||
const cardHeight = 80;
|
||||
const columnCount = Math.floor(width / cardWidth);
|
||||
const rowCount = Math.ceil(cards.length / columnCount);
|
||||
return (
|
||||
<Grid
|
||||
width={width}
|
||||
height={height}
|
||||
columnCount={columnCount}
|
||||
columnWidth={cardWidth}
|
||||
rowCount={rowCount}
|
||||
rowHeight={cardHeight}
|
||||
itemData={{ cards, columnCount, onChange, folder }}
|
||||
>
|
||||
{memo(Cell, areEqual)}
|
||||
</Grid>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
);
|
||||
};
|
@ -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<string, FieldNamePickerCon
|
||||
settings: {},
|
||||
} as any;
|
||||
|
||||
const dummyImageStringSettings: StandardEditorsRegistryItem<string, StringFieldConfigSettings> = {
|
||||
settings: {
|
||||
placeholder: 'Enter image URL',
|
||||
},
|
||||
} as any;
|
||||
|
||||
export const ResourceDimensionEditor: FC<
|
||||
StandardEditorProps<ResourceDimensionConfig, ResourceDimensionOptions, any>
|
||||
> = (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 && (
|
||||
<Modal isOpen={isOpen} title={`Select ${mediaType}`} onDismiss={() => setOpen(false)} closeOnEscape>
|
||||
<ResourcePicker onChange={onFixedChange} value={value?.fixed} mediaType={mediaType} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Source" labelWidth={labelWidth} grow={true}>
|
||||
<RadioButtonGroup value={mode} options={resourceOptions} onChange={onModeChange} fullWidth />
|
||||
@ -86,21 +87,10 @@ export const ResourceDimensionEditor: FC<
|
||||
)}
|
||||
{mode === ResourceDimensionMode.Fixed && (
|
||||
<InlineFieldRow>
|
||||
{resourceType === 'icon' && (
|
||||
<InlineField label="Icon" labelWidth={labelWidth} grow={true}>
|
||||
<IconSelector value={value?.fixed} onChange={onFixedChange} />
|
||||
</InlineField>
|
||||
)}
|
||||
{resourceType === 'image' && (
|
||||
<InlineField label="Image" labelWidth={labelWidth} grow={true}>
|
||||
<StringValueEditor
|
||||
context={context}
|
||||
value={value?.fixed}
|
||||
onChange={onFixedChange}
|
||||
item={dummyImageStringSettings}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
<InlineField label={null} grow>
|
||||
<Input value={value?.fixed} placeholder="Resource URL" readOnly={true} onClick={openModal} />
|
||||
</InlineField>
|
||||
<Button icon="folder-open" variant="secondary" onClick={openModal} />
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
{mode === ResourceDimensionMode.Mapping && (
|
||||
|
154
public/app/features/dimensions/editors/ResourcePicker.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import React, { useEffect, useState, ChangeEvent } from 'react';
|
||||
import {
|
||||
TabContent,
|
||||
Button,
|
||||
Select,
|
||||
Input,
|
||||
Spinner,
|
||||
TabsBar,
|
||||
Tab,
|
||||
StringValueEditor,
|
||||
useTheme2,
|
||||
stylesFactory,
|
||||
} from '@grafana/ui';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { ResourceCards } from './ResourceCards';
|
||||
import SVG from 'react-inlinesvg';
|
||||
import { css } from '@emotion/css';
|
||||
import { getPublicOrAbsoluteUrl } from '../resource';
|
||||
|
||||
interface Props {
|
||||
value?: string; //img/icons/unicons/0-plus.svg
|
||||
onChange: (value?: string) => void;
|
||||
mediaType: 'icon' | 'image';
|
||||
}
|
||||
|
||||
export function ResourcePicker(props: Props) {
|
||||
const { value, onChange, mediaType } = props;
|
||||
const folders = (mediaType === 'icon' ? ['img/icons/unicons', 'img/icons/iot'] : ['img/bg']).map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
}));
|
||||
const folderOfCurrentValue = value ? folders.filter((folder) => value.indexOf(folder.value) > -1)[0] : folders[0];
|
||||
const [currentFolder, setCurrentFolder] = useState<SelectableValue<string>>(folderOfCurrentValue);
|
||||
const [tabs, setTabs] = useState([
|
||||
{ label: 'Select', active: true },
|
||||
// { label: 'Upload', active: false },
|
||||
]);
|
||||
const [directoryIndex, setDirectoryIndex] = useState<SelectableValue[]>([]);
|
||||
const [defaultList, setDefaultList] = useState<SelectableValue[]>([]);
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
useEffect(() => {
|
||||
// we don't want to load everything before picking a folder
|
||||
if (currentFolder) {
|
||||
getBackendSrv()
|
||||
.get(`public/${currentFolder?.value}/index.json`)
|
||||
.then((data) => {
|
||||
const cards = data.files.map((v: string) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
imgUrl: `public/${currentFolder?.value}/${v}`,
|
||||
}));
|
||||
setDirectoryIndex(cards);
|
||||
setDefaultList(cards);
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}, [currentFolder]);
|
||||
|
||||
const onChangeSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value) {
|
||||
const filtered = directoryIndex.filter((card) =>
|
||||
card.value
|
||||
// exclude file type (.svg) in the search
|
||||
.substr(0, card.value.length - 4)
|
||||
.toLocaleLowerCase()
|
||||
.includes(e.target.value.toLocaleLowerCase())
|
||||
);
|
||||
setDirectoryIndex(filtered);
|
||||
} else {
|
||||
setDirectoryIndex(defaultList);
|
||||
}
|
||||
};
|
||||
const imgSrc = getPublicOrAbsoluteUrl(value!);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.currentItem}>
|
||||
{value && (
|
||||
<>
|
||||
{mediaType === 'icon' && <SVG src={imgSrc} className={styles.img} />}
|
||||
{mediaType === 'image' && <img src={imgSrc} className={styles.img} />}
|
||||
</>
|
||||
)}
|
||||
<StringValueEditor value={value ?? ''} onChange={onChange} item={{} as any} context={{} as any} />
|
||||
<Button variant="secondary" onClick={() => onChange(value)}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
<TabsBar>
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
label={tab.label}
|
||||
key={index}
|
||||
active={tab.active}
|
||||
onChangeTab={() => setTabs(tabs.map((tab, idx) => ({ ...tab, active: idx === index })))}
|
||||
/>
|
||||
))}
|
||||
</TabsBar>
|
||||
<TabContent>
|
||||
{tabs[0].active && (
|
||||
<div className={styles.tabContent}>
|
||||
<Select options={folders} onChange={setCurrentFolder} value={currentFolder} />
|
||||
<Input placeholder="Search" onChange={onChangeSearch} />
|
||||
{directoryIndex ? (
|
||||
<div className={styles.cardsWrapper}>
|
||||
<ResourceCards cards={directoryIndex} onChange={onChange} currentFolder={currentFolder} />
|
||||
</div>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* TODO: add file upload
|
||||
{tabs[1].active && (
|
||||
<FileUpload
|
||||
onFileUpload={({ currentTarget }) => console.log('file', currentTarget?.files && currentTarget.files[0])}
|
||||
className={styles.tabContent}
|
||||
/>
|
||||
)} */}
|
||||
</TabContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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};
|
||||
`,
|
||||
};
|
||||
});
|
@ -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<string> {
|
||||
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)),
|
||||
|
4
public/img/bg/index.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "bg",
|
||||
"files": ["p0.png", "p1.png", "p2.png", "p3.png", "p4.png", "p5.png", "p6.png"]
|
||||
}
|
BIN
public/img/bg/p0.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
public/img/bg/p1.png
Normal file
After Width: | Height: | Size: 670 KiB |
BIN
public/img/bg/p2.png
Normal file
After Width: | Height: | Size: 313 KiB |
BIN
public/img/bg/p3.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
public/img/bg/p4.png
Normal file
After Width: | Height: | Size: 205 KiB |
BIN
public/img/bg/p5.png
Normal file
After Width: | Height: | Size: 226 KiB |
BIN
public/img/bg/p6.png
Normal file
After Width: | Height: | Size: 360 KiB |
31
public/img/icons/iot/faucet.svg
Normal file
@ -0,0 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 512 512" enableBackground="new 0 0 512 512">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,168.751v15.673c38.891,0,70.531,31.64,70.531,70.531h15.673C342.204,207.421,303.532,168.751,256,168.751z" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M261.881,302.02L256,295.344l-5.881,6.676c-6.567,7.455-64.164,73.887-64.164,110.156
|
||||
c0,38.623,31.422,70.045,70.045,70.045s70.045-31.422,70.045-70.045C326.045,375.908,268.448,309.475,261.881,302.02z
|
||||
M256,466.546c-29.98,0-54.371-24.391-54.371-54.371c0-23.103,35.055-69.819,54.35-92.901c5.88,7.064,15.39,18.868,24.808,32.169
|
||||
c19.077,26.943,29.584,48.511,29.584,60.732C310.371,442.155,285.98,466.546,256,466.546z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M232.732,412.734h-15.674c0,21.472,17.469,38.941,38.941,38.941v-15.673C243.171,436.002,232.732,425.563,232.732,412.734 z" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M376.97,187.558v0.001c-19.753-35.44-53.43-59.909-92.235-68.065V76.799h62.694V58.514
|
||||
c0-15.845-12.891-28.735-28.735-28.735H193.306c-15.845,0-28.735,12.89-28.735,28.735v18.286h62.694v42.695
|
||||
c-38.804,8.155-72.481,32.626-92.234,68.065H0v130.612h132.803c11.6,22.604,29.109,41.501,50.889,54.86l8.195-13.36
|
||||
c-20.593-12.631-36.881-30.841-47.104-52.661l-2.114-4.512H47.02v-99.265h97.489l2.186-4.257
|
||||
c21.157-41.202,63.04-66.797,109.305-66.797c46.264,0,88.148,25.595,109.305,66.796l2.186,4.257h97.489v99.265h-95.648
|
||||
l-2.114,4.512c-10.222,21.82-26.51,40.03-47.104,52.661l8.196,13.36c21.779-13.359,39.288-32.257,50.888-54.86H512V187.558H376.97
|
||||
z M31.347,302.497H15.673v-99.265h15.673V302.497z M180.245,61.126v-2.612c0-7.202,5.859-13.061,13.061-13.061h125.388
|
||||
c7.202,0,13.061,5.859,13.061,13.061v2.612h-47.02h-57.469H180.245z M269.061,117.126c-4.312-0.402-8.667-0.621-13.061-0.621
|
||||
c-4.394,0-8.749,0.218-13.061,0.621V76.799h26.122V117.126z M496.327,302.497h-15.673v-99.265h15.673V302.497z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
4
public/img/icons/iot/index.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "line",
|
||||
"files": ["faucet.svg", "pump.svg"]
|
||||
}
|
32
public/img/icons/iot/pump.svg
Normal file
@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 512 512" enableBackground="new 0 0 512 512">
|
||||
<g>
|
||||
<path
|
||||
d="M493.137,59.284c10.401,0,18.863-8.463,18.863-18.863V18.863C512,8.463,503.537,0,493.137,0H374.568
|
||||
c-10.401,0-18.863,8.463-18.863,18.863v21.558c0,10.401,8.463,18.863,18.863,18.863h2.695v48.505h-7.211
|
||||
c-3.032-6.368-9.532-10.779-17.042-10.779h-21.558c-10.401,0-18.863,8.463-18.863,18.863v2.695h-13.474
|
||||
c-10.401,0-18.863,8.463-18.863,18.863v10.779c0,10.401,8.463,18.863,18.863,18.863h13.474v16.168h-48.505v-13.474
|
||||
c0-16.345-13.297-29.642-29.642-29.642h-204.8C13.297,140.126,0,153.423,0,169.768v172.463c0,16.345,13.297,29.642,29.642,29.642
|
||||
h204.8c16.345,0,29.642-13.297,29.642-29.642v-13.474h48.505v16.168h-13.474c-10.401,0-18.863,8.463-18.863,18.863v10.779
|
||||
c0,10.401,8.463,18.863,18.863,18.863h13.474v2.695c0,10.401,8.463,18.863,18.863,18.863h21.558
|
||||
c7.51,0,14.009-4.411,17.042-10.779h7.211v48.505h-2.695c-10.401,0-18.863,8.463-18.863,18.863v21.558
|
||||
c0,10.401,8.463,18.863,18.863,18.863h118.568c10.401,0,18.863-8.463,18.863-18.863v-21.558c0-10.401-8.463-18.863-18.863-18.863
|
||||
h-2.695v-48.505h2.695c10.401,0,18.863-8.463,18.863-18.863V126.653c0-10.401-8.463-18.863-18.863-18.863h-2.695V59.284H493.137z
|
||||
M247.916,342.232c0,7.43-6.044,13.474-13.474,13.474H59.284v-26.947h188.632V342.232z M247.916,312.589H59.284v-26.947h188.632
|
||||
V312.589z M247.916,269.474H59.284v-26.947h188.632V269.474z M247.916,226.358H59.284v-2.695c0-4.465-3.62-8.084-8.084-8.084
|
||||
s-8.084,3.62-8.084,8.084v132.042H29.642c-7.43,0-13.474-6.044-13.474-13.474V169.768c0-7.43,6.044-13.474,13.474-13.474h13.474
|
||||
v35.032c0,4.465,3.62,8.084,8.084,8.084h196.716V226.358z M247.916,183.242H59.284v-26.947h175.158
|
||||
c7.43,0,13.474,6.044,13.474,13.474V183.242z M312.589,377.263h-13.474c-1.485,0-2.695-1.209-2.695-2.695v-10.779
|
||||
c0-1.485,1.209-2.695,2.695-2.695h13.474V377.263z M312.589,312.589h-48.505V199.411h48.505V312.589z M312.589,150.905h-13.474
|
||||
c-1.485,0-2.695-1.209-2.695-2.695v-10.779c0-1.485,1.209-2.695,2.695-2.695h13.474V150.905z M493.137,468.884
|
||||
c1.485,0,2.695,1.209,2.695,2.695v21.558c0,1.485-1.209,2.695-2.695,2.695H374.568c-1.485,0-2.695-1.209-2.695-2.695v-21.558
|
||||
c0-1.485,1.209-2.695,2.695-2.695h32.337c4.465,0,8.084-3.62,8.084-8.084s-3.62-8.084-8.084-8.084h-13.474v-48.505h80.842v48.505
|
||||
h-35.032c-4.465,0-8.084,3.62-8.084,8.084s3.62,8.084,8.084,8.084H493.137z M493.137,123.958c1.485,0,2.695,1.209,2.695,2.695
|
||||
v258.695c0,1.485-1.209,2.695-2.695,2.695H371.874V202.105c0-4.465-3.62-8.084-8.084-8.084c-4.465,0-8.084,3.62-8.084,8.084
|
||||
v194.021c0,1.485-1.209,2.695-2.695,2.695h-21.558c-1.485,0-2.695-1.209-2.695-2.695V115.874c0-1.485,1.209-2.695,2.695-2.695
|
||||
h21.558c1.485,0,2.695,1.209,2.695,2.695v53.895c0,4.465,3.62,8.084,8.084,8.084c4.465,0,8.084-3.62,8.084-8.084v-45.811H493.137z
|
||||
M439.242,59.284h35.032v48.505h-80.842V59.284h13.474c4.465,0,8.084-3.62,8.084-8.084s-3.62-8.084-8.084-8.084h-32.337
|
||||
c-1.485,0-2.695-1.209-2.695-2.695V18.863c0-1.485,1.209-2.695,2.695-2.695h118.568c1.485,0,2.695,1.209,2.695,2.695v21.558
|
||||
c0,1.485-1.209,2.695-2.695,2.695h-53.895c-4.465,0-8.084,3.62-8.084,8.084S434.777,59.284,439.242,59.284z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |