Canvas: use grafana datasource to list resource files (#39204)
This commit is contained in:
@@ -27,7 +27,7 @@ export abstract class FunctionalVector<T = any> implements Vector<T>, Iterable<T
|
|||||||
return vectorator(this).map(transform);
|
return vectorator(this).map(transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
filter<V>(predicate: (item: T) => V) {
|
filter(predicate: (item: T) => boolean): T[] {
|
||||||
return vectorator(this).filter(predicate);
|
return vectorator(this).filter(predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,8 @@ export function vectorator<T>(vector: Vector<T>) {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
filter<V>(predicate: (item: T) => V) {
|
/** Add a predicate where you return true if it should *keep* the value */
|
||||||
|
filter(predicate: (item: T) => boolean): T[] {
|
||||||
const result: T[] = [];
|
const result: T[] = [];
|
||||||
for (const val of this) {
|
for (const val of this) {
|
||||||
if (predicate(val)) {
|
if (predicate(val)) {
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import {
|
|||||||
stylesFactory,
|
stylesFactory,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
|
||||||
import { ResourceCards } from './ResourceCards';
|
import { ResourceCards } from './ResourceCards';
|
||||||
import SVG from 'react-inlinesvg';
|
import SVG from 'react-inlinesvg';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { getPublicOrAbsoluteUrl } from '../resource';
|
import { getPublicOrAbsoluteUrl } from '../resource';
|
||||||
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
|
import { FileElement, GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value?: string; //img/icons/unicons/0-plus.svg
|
value?: string; //img/icons/unicons/0-plus.svg
|
||||||
@@ -24,6 +25,13 @@ interface Props {
|
|||||||
mediaType: 'icon' | 'image';
|
mediaType: 'icon' | 'image';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ResourceItem {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
search: string;
|
||||||
|
imgUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function ResourcePicker(props: Props) {
|
export function ResourcePicker(props: Props) {
|
||||||
const { value, onChange, mediaType } = props;
|
const { value, onChange, mediaType } = props;
|
||||||
const folders = (mediaType === 'icon' ? ['img/icons/unicons', 'img/icons/iot'] : ['img/bg']).map((v) => ({
|
const folders = (mediaType === 'icon' ? ['img/icons/unicons', 'img/icons/iot'] : ['img/bg']).map((v) => ({
|
||||||
@@ -36,43 +44,52 @@ export function ResourcePicker(props: Props) {
|
|||||||
{ label: 'Select', active: true },
|
{ label: 'Select', active: true },
|
||||||
// { label: 'Upload', active: false },
|
// { label: 'Upload', active: false },
|
||||||
]);
|
]);
|
||||||
const [directoryIndex, setDirectoryIndex] = useState<SelectableValue[]>([]);
|
const [directoryIndex, setDirectoryIndex] = useState<ResourceItem[]>([]);
|
||||||
const [defaultList, setDefaultList] = useState<SelectableValue[]>([]);
|
const [filteredIndex, setFilteredIndex] = useState<ResourceItem[]>([]);
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// we don't want to load everything before picking a folder
|
// we don't want to load everything before picking a folder
|
||||||
if (currentFolder) {
|
const folder = currentFolder?.value;
|
||||||
getBackendSrv()
|
if (folder) {
|
||||||
.get(`public/${currentFolder?.value}/index.json`)
|
const filter =
|
||||||
.then((data) => {
|
mediaType === 'icon'
|
||||||
const cards = data.files.map((v: string) => ({
|
? (item: FileElement) => item.name.endsWith('.svg')
|
||||||
value: v,
|
: (item: FileElement) => item.name.endsWith('.png') || item.name.endsWith('.gif');
|
||||||
label: v,
|
|
||||||
imgUrl: `public/${currentFolder?.value}/${v}`,
|
getDatasourceSrv()
|
||||||
}));
|
.get('-- Grafana --')
|
||||||
setDirectoryIndex(cards);
|
.then((ds) => {
|
||||||
setDefaultList(cards);
|
(ds as GrafanaDatasource).listFiles(folder).subscribe({
|
||||||
})
|
next: (frame) => {
|
||||||
.catch((e) => console.error(e));
|
const cards: ResourceItem[] = [];
|
||||||
} else {
|
frame.forEach((item) => {
|
||||||
return;
|
if (filter(item)) {
|
||||||
|
const idx = item.name.lastIndexOf('.');
|
||||||
|
cards.push({
|
||||||
|
value: item.name,
|
||||||
|
label: item.name,
|
||||||
|
search: (idx ? item.name.substr(0, idx) : item.name).toLowerCase(),
|
||||||
|
imgUrl: `public/${folder}/${item.name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setDirectoryIndex(cards);
|
||||||
|
setFilteredIndex(cards);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [currentFolder]);
|
}, [mediaType, currentFolder]);
|
||||||
|
|
||||||
const onChangeSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
const onChangeSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.value) {
|
let query = e.currentTarget.value;
|
||||||
const filtered = directoryIndex.filter((card) =>
|
if (query) {
|
||||||
card.value
|
query = query.toLowerCase();
|
||||||
// exclude file type (.svg) in the search
|
setFilteredIndex(directoryIndex.filter((card) => card.search.includes(query)));
|
||||||
.substr(0, card.value.length - 4)
|
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes(e.target.value.toLocaleLowerCase())
|
|
||||||
);
|
|
||||||
setDirectoryIndex(filtered);
|
|
||||||
} else {
|
} else {
|
||||||
setDirectoryIndex(defaultList);
|
setFilteredIndex(directoryIndex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const imgSrc = getPublicOrAbsoluteUrl(value!);
|
const imgSrc = getPublicOrAbsoluteUrl(value!);
|
||||||
@@ -106,9 +123,9 @@ export function ResourcePicker(props: Props) {
|
|||||||
<div className={styles.tabContent}>
|
<div className={styles.tabContent}>
|
||||||
<Select options={folders} onChange={setCurrentFolder} value={currentFolder} />
|
<Select options={folders} onChange={setCurrentFolder} value={currentFolder} />
|
||||||
<Input placeholder="Search" onChange={onChangeSearch} />
|
<Input placeholder="Search" onChange={onChangeSearch} />
|
||||||
{directoryIndex ? (
|
{filteredIndex ? (
|
||||||
<div className={styles.cardsWrapper}>
|
<div className={styles.cardsWrapper}>
|
||||||
<ResourceCards cards={directoryIndex} onChange={onChange} currentFolder={currentFolder} />
|
<ResourceCards cards={filteredIndex} onChange={onChange} currentFolder={currentFolder} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DataSourceWithBackend, getBackendSrv, getGrafanaLiveSrv, getTemplateSrv
|
|||||||
import {
|
import {
|
||||||
AnnotationQuery,
|
AnnotationQuery,
|
||||||
AnnotationQueryRequest,
|
AnnotationQueryRequest,
|
||||||
|
DataFrameView,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
@@ -17,6 +18,7 @@ import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQue
|
|||||||
import AnnotationQueryEditor from './components/AnnotationQueryEditor';
|
import AnnotationQueryEditor from './components/AnnotationQueryEditor';
|
||||||
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
let counter = 100;
|
let counter = 100;
|
||||||
|
|
||||||
@@ -124,6 +126,23 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
|
|||||||
return of(); // nothing
|
return of(); // nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listFiles(path: string): Observable<DataFrameView<FileElement>> {
|
||||||
|
return this.query({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
queryType: GrafanaQueryType.List,
|
||||||
|
path,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any).pipe(
|
||||||
|
map((v) => {
|
||||||
|
const frame = v.data[0] ?? toDataFrame({});
|
||||||
|
return new DataFrameView<FileElement>(frame);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
metricFindQuery(options: any) {
|
metricFindQuery(options: any) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
@@ -183,3 +202,8 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileElement {
|
||||||
|
name: string;
|
||||||
|
['media-type']: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "bg",
|
|
||||||
"files": ["p0.png", "p1.png", "p2.png", "p3.png", "p4.png", "p5.png", "p6.png"]
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "line",
|
|
||||||
"files": ["faucet.svg", "pump.svg"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user