mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Resource Picker: update design and layout (#41046)
This commit is contained in:
parent
9c160413f6
commit
fb75411ddc
@ -65,6 +65,7 @@ export const getAvailableIcons = () =>
|
||||
'external-link-alt',
|
||||
'eye',
|
||||
'eye-slash',
|
||||
'ellipsis-h',
|
||||
'fa fa-spinner',
|
||||
'favorite',
|
||||
'file-alt',
|
||||
|
@ -1,21 +1,27 @@
|
||||
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 { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useTheme2, stylesFactory } from '@grafana/ui';
|
||||
import SVG from 'react-inlinesvg';
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { ResourceItem } from './ResourcePicker';
|
||||
|
||||
interface CellProps {
|
||||
columnIndex: number;
|
||||
rowIndex: number;
|
||||
style: CSSProperties;
|
||||
data: any;
|
||||
data: {
|
||||
cards: ResourceItem[];
|
||||
columnCount: number;
|
||||
onChange: (value: string) => void;
|
||||
selected?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function Cell(props: CellProps) {
|
||||
const { columnIndex, rowIndex, style, data } = props;
|
||||
const { cards, columnCount, onChange, folder } = data;
|
||||
const { cards, columnCount, onChange, selected } = data;
|
||||
const singleColumnIndex = columnIndex + rowIndex * columnCount;
|
||||
const card = cards[singleColumnIndex];
|
||||
const theme = useTheme2();
|
||||
@ -24,8 +30,12 @@ function Cell(props: CellProps) {
|
||||
return (
|
||||
<div style={style}>
|
||||
{card && (
|
||||
<div key={card.value} className={styles.card} onClick={() => onChange(`${folder.value}/${card.value}`)}>
|
||||
{folder.value.includes('icons') ? (
|
||||
<div
|
||||
key={card.value}
|
||||
className={selected === card.value ? cx(styles.card, styles.selected) : styles.card}
|
||||
onClick={() => onChange(card.value)}
|
||||
>
|
||||
{card.imgUrl.endsWith('.svg') ? (
|
||||
<SVG src={card.imgUrl} className={styles.img} />
|
||||
) : (
|
||||
<img src={card.imgUrl} className={styles.img} />
|
||||
@ -41,9 +51,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
card: css`
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin: 0.75rem;
|
||||
margin-left: 15px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@ -51,15 +62,20 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding-top: 6px;
|
||||
|
||||
:hover {
|
||||
border-color: ${theme.colors.action.hover};
|
||||
box-shadow: ${theme.shadows.z2};
|
||||
}
|
||||
`,
|
||||
selected: css`
|
||||
border: 2px solid ${theme.colors.primary.main};
|
||||
:hover {
|
||||
border-color: ${theme.colors.primary.main};
|
||||
}
|
||||
`,
|
||||
img: css`
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
vertical-align: middle;
|
||||
fill: ${theme.colors.text.primary};
|
||||
@ -72,23 +88,28 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
`,
|
||||
grid: css`
|
||||
border: 1px solid ${theme.colors.border.medium};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface CardProps {
|
||||
onChange: (value: string) => void;
|
||||
cards: SelectableValue[];
|
||||
currentFolder: SelectableValue<string> | undefined;
|
||||
cards: ResourceItem[];
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const ResourceCards = (props: CardProps) => {
|
||||
const { onChange, cards, currentFolder: folder } = props;
|
||||
const { onChange, cards, value } = props;
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<AutoSizer defaultWidth={1920} defaultHeight={1080}>
|
||||
<AutoSizer defaultWidth={680}>
|
||||
{({ width, height }) => {
|
||||
const cardWidth = 80;
|
||||
const cardHeight = 80;
|
||||
const cardWidth = 90;
|
||||
const cardHeight = 90;
|
||||
const columnCount = Math.floor(width / cardWidth);
|
||||
const rowCount = Math.ceil(cards.length / columnCount);
|
||||
return (
|
||||
@ -99,7 +120,8 @@ export const ResourceCards = (props: CardProps) => {
|
||||
columnWidth={cardWidth}
|
||||
rowCount={rowCount}
|
||||
rowHeight={cardHeight}
|
||||
itemData={{ cards, columnCount, onChange, folder }}
|
||||
itemData={{ cards, columnCount, onChange, selected: value }}
|
||||
className={styles.grid}
|
||||
>
|
||||
{memo(Cell, areEqual)}
|
||||
</Grid>
|
||||
|
@ -75,7 +75,13 @@ export const ResourceDimensionEditor: FC<
|
||||
<>
|
||||
{isOpen && (
|
||||
<Modal isOpen={isOpen} title={`Select ${mediaType}`} onDismiss={() => setOpen(false)} closeOnEscape>
|
||||
<ResourcePicker onChange={onFixedChange} value={value?.fixed} mediaType={mediaType} folderName={folderName} />
|
||||
<ResourcePicker
|
||||
onChange={onFixedChange}
|
||||
value={value?.fixed}
|
||||
mediaType={mediaType}
|
||||
folderName={folderName}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{showSourceRadio && (
|
||||
@ -106,9 +112,9 @@ export const ResourceDimensionEditor: FC<
|
||||
readOnly={true}
|
||||
onClick={openModal}
|
||||
prefix={srcPath && <SVG src={srcPath} className={styles.icon} />}
|
||||
suffix={<Button icon="ellipsis-h" variant="secondary" fill="text" size="sm" onClick={openModal} />}
|
||||
/>
|
||||
</InlineField>
|
||||
<Button icon="folder-open" variant="secondary" onClick={openModal} />
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
{mode === ResourceDimensionMode.Mapping && (
|
||||
@ -138,5 +144,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
fill: currentColor;
|
||||
max-width: 25px;
|
||||
`,
|
||||
});
|
||||
|
@ -1,16 +1,5 @@
|
||||
import React, { useEffect, useState, ChangeEvent } from 'react';
|
||||
import {
|
||||
TabContent,
|
||||
Button,
|
||||
Select,
|
||||
Input,
|
||||
Spinner,
|
||||
TabsBar,
|
||||
Tab,
|
||||
StringValueEditor,
|
||||
useTheme2,
|
||||
stylesFactory,
|
||||
} from '@grafana/ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Select, FilterInput, useTheme2, stylesFactory, Field, Modal, Label, Input } from '@grafana/ui';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { ResourceCards } from './ResourceCards';
|
||||
import SVG from 'react-inlinesvg';
|
||||
@ -25,17 +14,24 @@ interface Props {
|
||||
onChange: (value?: string) => void;
|
||||
mediaType: 'icon' | 'image';
|
||||
folderName: ResourceFolderName;
|
||||
setOpen: (value: boolean) => void;
|
||||
}
|
||||
|
||||
interface ResourceItem {
|
||||
export interface ResourceItem {
|
||||
label: string;
|
||||
value: string;
|
||||
value: string; // includes folder
|
||||
search: string;
|
||||
imgUrl: string;
|
||||
}
|
||||
|
||||
const sourceOptions = [
|
||||
{ label: `Folder`, value: 'folder' },
|
||||
{ label: 'URL', value: 'url' },
|
||||
{ label: 'Upload', value: 'upload' },
|
||||
];
|
||||
|
||||
export function ResourcePicker(props: Props) {
|
||||
const { value, onChange, mediaType, folderName } = props;
|
||||
const { value, onChange, mediaType, folderName, setOpen } = props;
|
||||
const folders = getFolders(mediaType).map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
@ -43,12 +39,13 @@ export function ResourcePicker(props: Props) {
|
||||
|
||||
const folderOfCurrentValue = value || folderName ? folderIfExists(folders, value ?? folderName) : 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<ResourceItem[]>([]);
|
||||
const [filteredIndex, setFilteredIndex] = useState<ResourceItem[]>([]);
|
||||
// select between existing icon folder, url, or upload
|
||||
const [source, setSource] = useState<SelectableValue<string>>(sourceOptions[0]);
|
||||
// pass on new value to confirm button and to show in preview
|
||||
const [newValue, setNewValue] = useState<string>(value ?? '');
|
||||
const [searchQuery, setSearchQuery] = useState<string>();
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
@ -71,7 +68,7 @@ export function ResourcePicker(props: Props) {
|
||||
if (filter(item)) {
|
||||
const idx = item.name.lastIndexOf('.');
|
||||
cards.push({
|
||||
value: item.name,
|
||||
value: `${folder}/${item.name}`,
|
||||
label: item.name,
|
||||
search: (idx ? item.name.substr(0, idx) : item.name).toLowerCase(),
|
||||
imgUrl: `public/${folder}/${item.name}`,
|
||||
@ -86,8 +83,7 @@ export function ResourcePicker(props: Props) {
|
||||
}
|
||||
}, [mediaType, currentFolder]);
|
||||
|
||||
const onChangeSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
let query = e.currentTarget.value;
|
||||
const onChangeSearch = (query: string) => {
|
||||
if (query) {
|
||||
query = query.toLowerCase();
|
||||
setFilteredIndex(directoryIndex.filter((card) => card.search.includes(query)));
|
||||
@ -95,54 +91,72 @@ export function ResourcePicker(props: Props) {
|
||||
setFilteredIndex(directoryIndex);
|
||||
}
|
||||
};
|
||||
const imgSrc = getPublicOrAbsoluteUrl(value!);
|
||||
|
||||
const imgSrc = getPublicOrAbsoluteUrl(newValue!);
|
||||
let shortName = newValue?.substring(newValue.lastIndexOf('/') + 1, newValue.lastIndexOf('.'));
|
||||
if (shortName.length > 20) {
|
||||
shortName = shortName.substring(0, 20) + '...';
|
||||
}
|
||||
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 className={styles.upper}>
|
||||
<div className={styles.child}>
|
||||
<Field label="Source">
|
||||
<Select menuShouldPortal={true} options={sourceOptions} onChange={setSource} value={source} />
|
||||
</Field>
|
||||
{source?.value === 'folder' && (
|
||||
<>
|
||||
<Field label="Folder">
|
||||
<Select menuShouldPortal={true} options={folders} onChange={setCurrentFolder} value={currentFolder} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FilterInput
|
||||
value={searchQuery ?? ''}
|
||||
placeholder="Search"
|
||||
onChange={(v) => {
|
||||
onChangeSearch(v);
|
||||
setSearchQuery(v);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
{source?.value === 'url' && (
|
||||
<Field label="URL">
|
||||
<Input onChange={(e) => setNewValue(e.currentTarget.value)} value={newValue} />
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.iconContainer}>
|
||||
<Field label="Preview">
|
||||
<div className={styles.iconPreview}>
|
||||
{mediaType === 'icon' && <SVG src={imgSrc} className={styles.img} />}
|
||||
{mediaType === 'image' && newValue && <img src={imgSrc} className={styles.img} />}
|
||||
</div>
|
||||
</Field>
|
||||
<Label>{shortName}</Label>
|
||||
</div>
|
||||
</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 menuShouldPortal={true} options={folders} onChange={setCurrentFolder} value={currentFolder} />
|
||||
<Input placeholder="Search" onChange={onChangeSearch} />
|
||||
{filteredIndex ? (
|
||||
<div className={styles.cardsWrapper}>
|
||||
<ResourceCards cards={filteredIndex} onChange={onChange} currentFolder={currentFolder} />
|
||||
</div>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* TODO: add file upload
|
||||
{source?.value === 'folder' && filteredIndex && (
|
||||
<div className={styles.cardsWrapper}>
|
||||
<ResourceCards cards={filteredIndex} onChange={(v) => setNewValue(v)} value={newValue} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant={newValue && newValue !== value ? 'primary' : 'secondary'} onClick={() => onChange(newValue)}>
|
||||
Select
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
{/* TODO: add file upload
|
||||
{tabs[1].active && (
|
||||
<FileUpload
|
||||
onFileUpload={({ currentTarget }) => console.log('file', currentTarget?.files && currentTarget.files[0])}
|
||||
className={styles.tabContent}
|
||||
/>
|
||||
)} */}
|
||||
</TabContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -150,7 +164,10 @@ export function ResourcePicker(props: Props) {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
cardsWrapper: css`
|
||||
height: calc(100vh - 480px);
|
||||
height: calc(100vh - 550px);
|
||||
min-height: 50px;
|
||||
margin-top: 5px;
|
||||
max-width: 680px;
|
||||
`,
|
||||
tabContent: css`
|
||||
margin-top: 20px;
|
||||
@ -158,18 +175,34 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
margin-top: 10px;
|
||||
},
|
||||
`,
|
||||
currentItem: css`
|
||||
iconPreview: css`
|
||||
width: 95px;
|
||||
height: 79px;
|
||||
border: 1px solid ${theme.colors.border.medium};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
column-gap: 2px;
|
||||
margin: -18px 0px 18px 0px;
|
||||
justify-content: center;
|
||||
`,
|
||||
iconContainer: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 40%;
|
||||
align-items: center;
|
||||
`,
|
||||
img: css`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 49px;
|
||||
height: 49px;
|
||||
fill: ${theme.colors.text.primary};
|
||||
`,
|
||||
child: css`
|
||||
width: 60%;
|
||||
`,
|
||||
upper: css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user