Geomap: Custom markers using ResourcePicker with standard marker fallbacks (#39919)
* add custom icons * use resourcePicker for marker icon * use regular shapes for specific icons * update type * update svgs and remove marker shape selection * update types and resourcePicker * add migration and update markers Co-authored-by: Ryan McKinley <ryantxu@users.noreply.github.com> * quick cleanup * update marker path and remove any * update migration test * use inline snapshot * remove unused * Docs: Add documentation for library elements API (#39829) * LibraryElements: Adds api documentation * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/http_api/library_element.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Refactor: changes after PR comments * Apply suggestions from code review Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Chore: updates after review Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * CodeEditor: making sure we trigger the latest onSave callback provided to the component (#39835) * Fix: prevent queryDisplyText in QueryRowHeader from overflowing (#40094) * Revert "Fix Query Editor Row horizontal overflow (#39419)" This reverts commit 42b1fa0f62d592e0749e90d4a63c4a56066a1915. * fix: prevent queryDisplyText in QueryRowHeader from overflowing * Search: Fix local storage key (#40127) * Default to 'General' if no folder title is present * Add bottom padding * Live: remote write sampling (#40079) * Stat: recompute shared y range during streaming updates (#39485) * NavBar: Order App plugins alphabetically (#40078) * NavBar: Order App plugins alphabetically * Update pkg/api/index.go Co-authored-by: Will Browne <wbrowne@users.noreply.github.com> Co-authored-by: Will Browne <wbrowne@users.noreply.github.com> * Schema: use the generated graph.gen.ts (#40090) * actually generate graph.gen.ts * getting closer * keep file where it is * manual fixes * Update packages/grafana-schema/src/schema/graph.gen.ts Co-authored-by: sam boyer <sam.boyer@grafana.com> * Docs: Whats new in 8.2 (#39945) * Added time range controls updates * Added plugins catalog update * Added enterprise images * Added community contributions highlights for 8.2 * accessibility statement * Update docs/sources/whatsnew/whats-new-in-v8-2.md Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v8-2.md Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v8-2.md Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v8-2.md Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v8-2.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v8-2.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v8-2.md Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v8-2.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v8-2.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Live: array for Processor, Outputter and Subscriber in channel rule top level (#39677) * ReleaseNotes: Updated changelog and release notes for 8.1.7 (#40081) * more * more Co-authored-by: sam boyer <sam.boyer@grafana.com> Co-authored-by: Petros Kolyvas <code@petros.io> Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Alexander Emelin <frvzmb@gmail.com> Co-authored-by: Grot (@grafanabot) <43478413+grafanabot@users.noreply.github.com> * Access Control: Add scope type prefix (#40076) * prefix runtime scopes with key type * Bump mocha from 7.0.1 to 9.1.2 (#39979) * Bump mocha from 7.0.1 to 9.1.2 Bumps [mocha](https://github.com/mochajs/mocha) from 7.0.1 to 9.1.2. - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v7.0.1...v9.1.2) --- updated-dependencies: - dependency-name: mocha dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * kick drone * Remove mocha as it's not used by anything * kick drone Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> * ReleaseNotes: Updated changelog and release notes for 8.2.0 (#40141) * ReleaseNotes: Updated changelog and release notes for 8.2.0 * Add link & remove empty line in CHANGELOG * remove empty line Co-authored-by: Elfo404 <me@giordanoricci.com> * Create search filters by interface (#39843) * Extract search users to a new service * Fix wire provider * Fix common_test and remove RouteRegister * Remove old endpoints * Fix test * Create search filters using interfaces * Move Enterprise filter, rename filter for filters and allow use filters with params * Each filter has unique key * Back activeLast30Days filter to OSS * Fix tests * Delete unusued param * Move filters to searchusers service and small refactor * Fix tests * Encryption: Refactor securejsondata.SecureJsonData to stop relying on global functions (#38865) * Encryption: Add support to encrypt/decrypt sjd * Add datasources.Service as a proxy to datasources db operations * Encrypt ds.SecureJsonData before calling SQLStore * Move ds cache code into ds service * Fix tlsmanager tests * Fix pluginproxy tests * Remove some securejsondata.GetEncryptedJsonData usages * Add pluginsettings.Service as a proxy for plugin settings db operations * Add AlertNotificationService as a proxy for alert notification db operations * Remove some securejsondata.GetEncryptedJsonData usages * Remove more securejsondata.GetEncryptedJsonData usages * Fix lint errors * Minor fixes * Remove encryption global functions usages from ngalert * Fix lint errors * Minor fixes * Minor fixes * Remove securejsondata.DecryptedValue usage * Refactor the refactor * Remove securejsondata.DecryptedValue usage * Move securejsondata to migrations package * Move securejsondata to migrations package * Minor fix * Fix integration test * Fix integration tests * Undo undesired changes * Fix tests * Add context.Context into encryption methods * Fix tests * Fix tests * Fix tests * Trigger CI * Fix test * Add names to params of encryption service interface * Remove bus from CacheServiceImpl * Add logging * Add keys to logger Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> * Add missing key to logger Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> * Undo changes in markdown files * Fix formatting * Add context to secrets service * Rename decryptSecureJsonData to decryptSecureJsonDataFn * Name args in GetDecryptedValueFn * Add template back to NewAlertmanagerNotifier * Copy GetDecryptedValueFn to ngalert * Add logging to pluginsettings * Fix pluginsettings test Co-authored-by: Tania B <yalyna.ts@gmail.com> Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> * Admin: Enable extending filters (#39825) * Setup extensible filters * Fix test * Handle filter as array * Add className * Abstract getFilters * Make docs link external * Use underline for links in tooltips instead of link color Co-authored-by: Selene <selenepinillos@gmail.com> * Chore: update latest.json to 8.2 (#40153) * Doc: Fixed issue 40017 (#40152) * Added content as suggested by Will * removed a few extra words. * Update docs/sources/administration/configuration.md Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * Update docs/sources/administration/configuration.md Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * Update docs/sources/administration/configuration.md Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> * docs: Add keepCokkies cofiguration option in datasources (#39890) Signed-off-by: Vinayak Kadam <kadamvinayak03@gmail.com> * Grammar issues (#40168) * Packaging: document systemd net bind capability rpm and deb installations (#40165) * add systemd net bind capability docs for rpm and deb Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Alerting: declare constants for __dashboardUid__ and __panelId__ literals (#39976) * Babel: Remove unused plugin (#40172) * removed unused babel plugin * Update lock file * Chore(dependencies): Remove puppeteer since we don't use it anywhere (#40137) * Folders: Prevents deletion of General folder (#40192) * Datasources: Fix deletion of datasource if plugin cannot be found (#40095) * fix(pluginsettings): reject with error so datasource plugin loading failures still render ui * feat(pluginpage): handle plugin loading error * refactor(datasources): separate out datasource and meta loading so store has info for deletion * fix(datasourcesettings): introduce loading flag to wait for datasource and meta loading * test(datasourcesettings): fix failing test * test(datasources): assert loading status of datasource settings * test(datasources): update action tests for latest changes * Replace SAML library with fork (#40149) * Update saml library to latest * Use fork of crewjam/saml with fix for certificate chain bug * CloudMonitoring: Migrate to use backend plugin SDK contracts (#38650) * Use SDK contracts for cloudmonitoring * Get build running, tests passing and do some refactoring (#38754) * fix build+tests and refactor * remove alerting stuff * remove unused field * fix plugin fetch * end to end * resp rename * tidy annotations * reformatting * update refID * reformat imports * fix styling * clean up unmarshalling * uncomment + fix tests * appease linter * remove spaces * remove old cruft * add check for empty queries * update tests * remove pm as dep * adjust proxy route contract * fix service loading * use UNIX val * fix endpoint + resp * h@ckz for frontend * fix resp * fix interval * always set custom meta * remove unused param * fix labels fetch * fix linter * fix test + remove unused field * apply pr feedback * fix grafana-auto intervals * fix tests * resolve conflicts * fix bad merge * fix conflicts * remove bad logger import Co-authored-by: Will Browne <wbrowne@users.noreply.github.com> Co-authored-by: Will Browne <will.browne@grafana.com> * Do codegen and check no-diff of all (non-blacklisted) CUE->TS codegen during CI (#39922) * Add file blacklist to `grafana-cli cue gen-ts` cmd * Add CI step checking all cuetsification is done * Add dummy command to make the next one fail * Generate drone bits * Check diff output failure * Echo list of untracked files, for failure locality * Move git cleanness checking into script * Blacklist of cue files is complete and correct * Remove news panel plugin from cuetsify blacklist * Dummy commit, check that untracked gen still fail * Tie off remaining errors * Re-add barchart to blacklist * Remove file left around by earlier pipeline * Commit generated news models.gen.ts * Include eslint as part of cuetsified output gen * Update pkg/cmd/grafana-cli/commands/cuetsify_command.go Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> * Update scripts/drone/steps/lib.star Co-authored-by: Maria Alexandra <239999+axelavargas@users.noreply.github.com> * Update drone.yml * Last fix on .drone.yml Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> Co-authored-by: Maria Alexandra <239999+axelavargas@users.noreply.github.com> * Alerting: add organziation ID to the ngAlert webhook payload (#40189) * Alerting: add organziation ID to the ngAlert webhook payload * remove systemcallfilters sections from systemd unit files (#40176) * Add Headers to http client Options (#40214) * Docs: Add required library for the image renderer (#40201) * update permissions scopes and description for role scopes (#40206) * Chore: Migrate yarn from v1 to v2 (#39082) * Chore: Migrate yarn from v1 to v2 Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com> * ReleaseNotes: Updated changelog and release notes for 8.2.0 (#40233) Co-authored-by: Ryan McKinley <ryantxu@users.noreply.github.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> Co-authored-by: Giordano Ricci <me@giordanoricci.com> Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> Co-authored-by: Alexander Emelin <frvzmb@gmail.com> Co-authored-by: Leon Sorokin <leeoniya@gmail.com> Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> Co-authored-by: Will Browne <wbrowne@users.noreply.github.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: sam boyer <sam.boyer@grafana.com> Co-authored-by: Petros Kolyvas <code@petros.io> Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com> Co-authored-by: Grot (@grafanabot) <43478413+grafanabot@users.noreply.github.com> Co-authored-by: Karl Persson <kalle.persson@grafana.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Selene <selenepinillos@gmail.com> Co-authored-by: Joan López de la Franca Beltran <5459617+joanlopez@users.noreply.github.com> Co-authored-by: Tania B <yalyna.ts@gmail.com> Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> Co-authored-by: Vinayak <vinayak03@users.noreply.github.com> Co-authored-by: Anne E. Ulrich <aeulrich1997@gmail.com> Co-authored-by: Kevin Minehart <kmineh0151@gmail.com> Co-authored-by: Yuriy Tseretyan <yuriy.tseretyan@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.org> Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com> Co-authored-by: idafurjes <36131195+idafurjes@users.noreply.github.com> Co-authored-by: Will Browne <will.browne@grafana.com> Co-authored-by: Maria Alexandra <239999+axelavargas@users.noreply.github.com> Co-authored-by: Jean-Philippe Quéméner <JohnnyQQQQ@users.noreply.github.com> Co-authored-by: Dimitris Sotirakis <dimitrios.sotirakis@grafana.com> Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com> Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
@ -4,6 +4,7 @@ import { ResourceDimensionConfig, ResourceDimensionMode, ResourceDimensionOption
|
||||
import { InlineField, InlineFieldRow, RadioButtonGroup, Button, Modal, Input } from '@grafana/ui';
|
||||
import { FieldNamePicker } from '../../../../../packages/grafana-ui/src/components/MatchersUI/FieldNamePicker';
|
||||
import { ResourcePicker } from './ResourcePicker';
|
||||
import { ResourceFolderName } from '..';
|
||||
|
||||
const resourceOptions = [
|
||||
{ label: 'Fixed', value: ResourceDimensionMode.Fixed, description: 'Fixed value' },
|
||||
@ -58,21 +59,24 @@ export const ResourceDimensionEditor: FC<
|
||||
}, []);
|
||||
|
||||
const mode = value?.mode ?? ResourceDimensionMode.Fixed;
|
||||
const showSourceRadio = item.settings?.showSourceRadio ?? true;
|
||||
const mediaType = item.settings?.resourceType ?? 'icon';
|
||||
const folderName = item.settings?.folderName ?? ResourceFolderName.Icon;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<Modal isOpen={isOpen} title={`Select ${mediaType}`} onDismiss={() => setOpen(false)} closeOnEscape>
|
||||
<ResourcePicker onChange={onFixedChange} value={value?.fixed} mediaType={mediaType} />
|
||||
<ResourcePicker onChange={onFixedChange} value={value?.fixed} mediaType={mediaType} folderName={folderName} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Source" labelWidth={labelWidth} grow={true}>
|
||||
<RadioButtonGroup value={mode} options={resourceOptions} onChange={onModeChange} fullWidth />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
{showSourceRadio && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Source" labelWidth={labelWidth} grow={true}>
|
||||
<RadioButtonGroup value={mode} options={resourceOptions} onChange={onModeChange} fullWidth />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
{mode !== ResourceDimensionMode.Fixed && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Field" labelWidth={labelWidth} grow={true}>
|
||||
|
@ -18,11 +18,13 @@ import { css } from '@emotion/css';
|
||||
import { getPublicOrAbsoluteUrl } from '../resource';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { FileElement, GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
|
||||
import { ResourceFolderName } from '..';
|
||||
|
||||
interface Props {
|
||||
value?: string; //img/icons/unicons/0-plus.svg
|
||||
onChange: (value?: string) => void;
|
||||
mediaType: 'icon' | 'image';
|
||||
folderName: ResourceFolderName;
|
||||
}
|
||||
|
||||
interface ResourceItem {
|
||||
@ -33,12 +35,13 @@ interface ResourceItem {
|
||||
}
|
||||
|
||||
export function ResourcePicker(props: Props) {
|
||||
const { value, onChange, mediaType } = props;
|
||||
const folders = (mediaType === 'icon' ? ['img/icons/unicons', 'img/icons/iot'] : ['img/bg']).map((v) => ({
|
||||
const { value, onChange, mediaType, folderName } = props;
|
||||
const folders = getFolders(mediaType).map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
}));
|
||||
const folderOfCurrentValue = value ? folders.filter((folder) => value.indexOf(folder.value) > -1)[0] : folders[0];
|
||||
|
||||
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 },
|
||||
@ -169,3 +172,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const getFolders = (mediaType: 'icon' | 'image') => {
|
||||
if (mediaType === 'icon') {
|
||||
return [ResourceFolderName.Icon, ResourceFolderName.IOT, ResourceFolderName.Marker];
|
||||
} else {
|
||||
return [ResourceFolderName.BG];
|
||||
}
|
||||
};
|
||||
|
||||
const folderIfExists = (folders: Array<{ label: string; value: string }>, path: string) => {
|
||||
return folders.filter((folder) => path.indexOf(folder.value) > -1)[0] ?? folders[0];
|
||||
};
|
||||
|
@ -71,6 +71,8 @@ export interface ColorDimensionConfig extends BaseDimensionConfig<string> {}
|
||||
/** Places that use the value */
|
||||
export interface ResourceDimensionOptions {
|
||||
resourceType: 'icon' | 'image';
|
||||
folderName?: ResourceFolderName;
|
||||
showSourceRadio?: boolean;
|
||||
}
|
||||
|
||||
export enum ResourceDimensionMode {
|
||||
@ -84,3 +86,10 @@ export enum ResourceDimensionMode {
|
||||
export interface ResourceDimensionConfig extends BaseDimensionConfig<string> {
|
||||
mode: ResourceDimensionMode;
|
||||
}
|
||||
|
||||
export enum ResourceFolderName {
|
||||
Icon = 'img/icons/unicons',
|
||||
IOT = 'img/icons/iot',
|
||||
Marker = 'img/icons/marker',
|
||||
BG = 'img/bg',
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import Feature from 'ol/Feature';
|
||||
import { Point } from 'ol/geom';
|
||||
import * as layer from 'ol/layer';
|
||||
import * as source from 'ol/source';
|
||||
import * as style from 'ol/style';
|
||||
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
@ -19,11 +20,15 @@ import {
|
||||
ScaleDimensionConfig,
|
||||
getScaledDimension,
|
||||
getColorDimension,
|
||||
ResourceDimensionConfig,
|
||||
ResourceDimensionMode,
|
||||
ResourceFolderName,
|
||||
getPublicOrAbsoluteUrl,
|
||||
} from 'app/features/dimensions';
|
||||
import { ScaleDimensionEditor, ColorDimensionEditor } from 'app/features/dimensions/editors';
|
||||
import { ScaleDimensionEditor, ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors';
|
||||
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
||||
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
||||
import { circleMarker, markerMakers } from '../../utils/regularShapes';
|
||||
import { StyleMaker, getMarkerFromPath, MarkerShapePath } from '../../utils/regularShapes';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
|
||||
// Configuration options for Circle overlays
|
||||
@ -31,13 +36,15 @@ export interface MarkersConfig {
|
||||
size: ScaleDimensionConfig;
|
||||
color: ColorDimensionConfig;
|
||||
fillOpacity: number;
|
||||
shape?: string;
|
||||
showLegend?: boolean;
|
||||
markerSymbol: ResourceDimensionConfig;
|
||||
}
|
||||
|
||||
const DEFAULT_SIZE = 5;
|
||||
|
||||
const defaultOptions: MarkersConfig = {
|
||||
size: {
|
||||
fixed: 5,
|
||||
fixed: DEFAULT_SIZE,
|
||||
min: 2,
|
||||
max: 15,
|
||||
},
|
||||
@ -45,8 +52,11 @@ const defaultOptions: MarkersConfig = {
|
||||
fixed: 'dark-green', // picked from theme
|
||||
},
|
||||
fillOpacity: 0.4,
|
||||
shape: 'circle',
|
||||
showLegend: true,
|
||||
markerSymbol: {
|
||||
mode: ResourceDimensionMode.Fixed,
|
||||
fixed: MarkerShapePath.Circle,
|
||||
},
|
||||
};
|
||||
|
||||
export const MARKERS_LAYER_ID = 'markers';
|
||||
@ -88,7 +98,6 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
if (config.showLegend) {
|
||||
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
|
||||
}
|
||||
const shape = markerMakers.getIfExists(config.shape) ?? circleMarker;
|
||||
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
@ -98,6 +107,24 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
return; // ignore empty
|
||||
}
|
||||
|
||||
const markerPath =
|
||||
getPublicOrAbsoluteUrl(config.markerSymbol?.fixed) ?? getPublicOrAbsoluteUrl(MarkerShapePath.Circle);
|
||||
|
||||
const marker = getMarkerFromPath(config.markerSymbol?.fixed);
|
||||
|
||||
const makeIconStyle = (color: string, fillColor: string, radius: number) => {
|
||||
return new style.Style({
|
||||
image: new style.Icon({
|
||||
src: markerPath,
|
||||
color,
|
||||
// opacity,
|
||||
scale: (DEFAULT_SIZE + radius) / 100,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const shape: StyleMaker = marker?.make ?? makeIconStyle;
|
||||
|
||||
const features: Feature<Point>[] = [];
|
||||
|
||||
for (const frame of data.series) {
|
||||
@ -126,8 +153,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
frame,
|
||||
rowIndex: i,
|
||||
});
|
||||
|
||||
dot.setStyle(shape!.make(color, fillColor, radius));
|
||||
dot.setStyle(shape(color, fillColor, radius));
|
||||
features.push(dot);
|
||||
}
|
||||
|
||||
@ -150,17 +176,6 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
// Marker overlay options
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addCustomEditor({
|
||||
id: 'config.color',
|
||||
path: 'config.color',
|
||||
name: 'Marker Color',
|
||||
editor: ColorDimensionEditor,
|
||||
settings: {},
|
||||
defaultValue: {
|
||||
// Configured values
|
||||
fixed: 'grey',
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.size',
|
||||
path: 'config.size',
|
||||
@ -172,18 +187,33 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
},
|
||||
defaultValue: {
|
||||
// Configured values
|
||||
fixed: 5,
|
||||
fixed: DEFAULT_SIZE,
|
||||
min: 1,
|
||||
max: 20,
|
||||
},
|
||||
})
|
||||
.addSelect({
|
||||
path: 'config.shape',
|
||||
name: 'Marker Shape',
|
||||
.addCustomEditor({
|
||||
id: 'config.markerSymbol',
|
||||
path: 'config.markerSymbol',
|
||||
name: 'Marker Symbol',
|
||||
editor: ResourceDimensionEditor,
|
||||
defaultValue: defaultOptions.markerSymbol,
|
||||
settings: {
|
||||
options: markerMakers.selectOptions().options,
|
||||
resourceType: 'icon',
|
||||
showSourceRadio: false,
|
||||
folderName: ResourceFolderName.Marker,
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.color',
|
||||
path: 'config.color',
|
||||
name: 'Marker Color',
|
||||
editor: ColorDimensionEditor,
|
||||
settings: {},
|
||||
defaultValue: {
|
||||
// Configured values
|
||||
fixed: 'grey',
|
||||
},
|
||||
defaultValue: 'circle',
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'config.fillOpacity',
|
||||
@ -194,7 +224,6 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
showIf: (cfg) => markerMakers.getIfExists((cfg as any).config?.shape)?.hasFill,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'config.showLegend',
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { PanelModel, FieldConfigSource } from '@grafana/data';
|
||||
import { mapPanelChangedHandler } from './migrations';
|
||||
|
||||
import { mapMigrationHandler, mapPanelChangedHandler } from './migrations';
|
||||
describe('Worldmap Migrations', () => {
|
||||
let prevFieldConfig: FieldConfigSource;
|
||||
|
||||
@ -106,3 +105,169 @@ const simpleWorldmapConfig = {
|
||||
valueName: 'total',
|
||||
datasource: null,
|
||||
};
|
||||
|
||||
describe('geomap migrations', () => {
|
||||
it('updates marker', () => {
|
||||
const panel = {
|
||||
id: 2,
|
||||
gridPos: {
|
||||
h: 9,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
type: 'geomap',
|
||||
title: 'Panel Title',
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
thresholds: {
|
||||
mode: 'absolute',
|
||||
steps: [
|
||||
{
|
||||
color: 'green',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
value: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
mappings: [],
|
||||
color: {
|
||||
mode: 'thresholds',
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
options: {
|
||||
view: {
|
||||
id: 'zero',
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
zoom: 1,
|
||||
},
|
||||
basemap: {
|
||||
type: 'default',
|
||||
config: {},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
config: {
|
||||
color: {
|
||||
fixed: 'dark-green',
|
||||
},
|
||||
fillOpacity: 0.4,
|
||||
markerSymbol: {
|
||||
fixed: '',
|
||||
mode: 'fixed',
|
||||
},
|
||||
shape: 'circle',
|
||||
showLegend: true,
|
||||
size: {
|
||||
fixed: 5,
|
||||
max: 15,
|
||||
min: 2,
|
||||
},
|
||||
},
|
||||
location: {
|
||||
mode: 'auto',
|
||||
},
|
||||
type: 'markers',
|
||||
},
|
||||
],
|
||||
controls: {
|
||||
showZoom: true,
|
||||
mouseWheelZoom: true,
|
||||
showAttribution: true,
|
||||
showScale: false,
|
||||
showDebug: false,
|
||||
},
|
||||
},
|
||||
pluginVersion: '8.3.0-pre',
|
||||
datasource: null,
|
||||
} as PanelModel;
|
||||
panel.options = mapMigrationHandler(panel);
|
||||
|
||||
expect(panel).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"datasource": null,
|
||||
"fieldConfig": Object {
|
||||
"defaults": Object {
|
||||
"color": Object {
|
||||
"mode": "thresholds",
|
||||
},
|
||||
"mappings": Array [],
|
||||
"thresholds": Object {
|
||||
"mode": "absolute",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"color": "green",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"overrides": Array [],
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 2,
|
||||
"options": Object {
|
||||
"basemap": Object {
|
||||
"config": Object {},
|
||||
"type": "default",
|
||||
},
|
||||
"controls": Object {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showScale": false,
|
||||
"showZoom": true,
|
||||
},
|
||||
"layers": Array [
|
||||
Object {
|
||||
"config": Object {
|
||||
"color": Object {
|
||||
"fixed": "dark-green",
|
||||
},
|
||||
"fillOpacity": 0.4,
|
||||
"markerSymbol": Object {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed",
|
||||
},
|
||||
"showLegend": true,
|
||||
"size": Object {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2,
|
||||
},
|
||||
},
|
||||
"location": Object {
|
||||
"mode": "auto",
|
||||
},
|
||||
"type": "markers",
|
||||
},
|
||||
],
|
||||
"view": Object {
|
||||
"id": "zero",
|
||||
"lat": 0,
|
||||
"lon": 0,
|
||||
"zoom": 1,
|
||||
},
|
||||
},
|
||||
"pluginVersion": "8.3.0-pre",
|
||||
"title": "Panel Title",
|
||||
"type": "geomap",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FieldConfigSource, PanelTypeChangedHandler, Threshold, ThresholdsMode } from '@grafana/data';
|
||||
import { FieldConfigSource, PanelModel, PanelTypeChangedHandler, Threshold, ThresholdsMode } from '@grafana/data';
|
||||
import { GeomapPanelOptions } from './types';
|
||||
import { markerMakers } from './utils/regularShapes';
|
||||
import { MapCenterID } from './view';
|
||||
|
||||
/**
|
||||
@ -97,3 +98,27 @@ function asNumber(v: any): number | undefined {
|
||||
const num = +v;
|
||||
return isNaN(num) ? undefined : num;
|
||||
}
|
||||
|
||||
export const mapMigrationHandler = (panel: PanelModel): Partial<GeomapPanelOptions> => {
|
||||
const pluginVersion = panel?.pluginVersion;
|
||||
if (pluginVersion?.startsWith('8.1') || pluginVersion?.startsWith('8.2') || pluginVersion?.startsWith('8.3')) {
|
||||
if (panel.options?.layers?.length > 0) {
|
||||
const layer = panel.options.layers[0];
|
||||
if (layer?.type === 'markers') {
|
||||
const shape = layer?.config?.shape;
|
||||
if (shape) {
|
||||
const marker = markerMakers.getIfExists(shape);
|
||||
if (marker?.aliasIds && marker.aliasIds?.length > 0) {
|
||||
layer.config.markerSymbol = {
|
||||
fixed: marker.aliasIds[0],
|
||||
mode: 'fixed',
|
||||
};
|
||||
delete layer.config.shape;
|
||||
}
|
||||
return { ...panel.options, layers: Object.assign([], ...panel.options.layers, { 0: layer }) };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return panel.options;
|
||||
};
|
||||
|
@ -3,13 +3,14 @@ import { PanelPlugin } from '@grafana/data';
|
||||
import { GeomapPanel } from './GeomapPanel';
|
||||
import { MapViewEditor } from './editor/MapViewEditor';
|
||||
import { defaultView, GeomapPanelOptions } from './types';
|
||||
import { mapPanelChangedHandler } from './migrations';
|
||||
import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
|
||||
import { getLayerEditor } from './editor/layerEditor';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
.setNoPadding()
|
||||
.setPanelChangeHandler(mapPanelChangedHandler)
|
||||
.setMigrationHandler(mapMigrationHandler)
|
||||
.useFieldConfig()
|
||||
.setPanelOptions((builder, context) => {
|
||||
let category = ['Map view'];
|
||||
|
@ -1,15 +1,38 @@
|
||||
import { Fill, RegularShape, Stroke, Style, Circle } from 'ol/style';
|
||||
import { Registry, RegistryItem } from '@grafana/data';
|
||||
|
||||
interface MarkerMaker extends RegistryItem {
|
||||
make: (color: string, fillColor: string, radius: number) => Style;
|
||||
export type StyleMaker = (color: string, fillColor: string, radius: number, markerPath?: string) => Style;
|
||||
|
||||
export interface MarkerMaker extends RegistryItem {
|
||||
// path to icon that will be shown (but then replaced)
|
||||
aliasIds: string[];
|
||||
make: StyleMaker;
|
||||
hasFill: boolean;
|
||||
}
|
||||
|
||||
export enum RegularShapeId {
|
||||
Circle = 'circle',
|
||||
Square = 'square',
|
||||
Triangle = 'triangle',
|
||||
Star = 'star',
|
||||
Cross = 'cross',
|
||||
X = 'x',
|
||||
}
|
||||
|
||||
export enum MarkerShapePath {
|
||||
Circle = 'img/icons/marker/circle.svg',
|
||||
Square = 'img/icons/marker/square.svg',
|
||||
Triangle = 'img/icons/marker/triangle.svg',
|
||||
Star = 'img/icons/marker/star.svg',
|
||||
Cross = 'img/icons/marker/cross.svg',
|
||||
X = 'img/icons/marker/x-mark.svg',
|
||||
}
|
||||
|
||||
export const circleMarker: MarkerMaker = {
|
||||
id: 'circle',
|
||||
id: RegularShapeId.Circle,
|
||||
name: 'Circle',
|
||||
hasFill: true,
|
||||
aliasIds: [MarkerShapePath.Circle],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
return new Style({
|
||||
image: new Circle({
|
||||
@ -21,12 +44,13 @@ export const circleMarker: MarkerMaker = {
|
||||
},
|
||||
};
|
||||
|
||||
export const markerMakers = new Registry<MarkerMaker>(() => [
|
||||
const makers: MarkerMaker[] = [
|
||||
circleMarker,
|
||||
{
|
||||
id: 'square',
|
||||
id: RegularShapeId.Square,
|
||||
name: 'Square',
|
||||
hasFill: true,
|
||||
aliasIds: [MarkerShapePath.Square],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
@ -40,9 +64,10 @@ export const markerMakers = new Registry<MarkerMaker>(() => [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triangle',
|
||||
id: RegularShapeId.Triangle,
|
||||
name: 'Triangle',
|
||||
hasFill: true,
|
||||
aliasIds: [MarkerShapePath.Triangle],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
@ -57,9 +82,10 @@ export const markerMakers = new Registry<MarkerMaker>(() => [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'star',
|
||||
id: RegularShapeId.Star,
|
||||
name: 'Star',
|
||||
hasFill: true,
|
||||
aliasIds: [MarkerShapePath.Star],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
@ -74,9 +100,10 @@ export const markerMakers = new Registry<MarkerMaker>(() => [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cross',
|
||||
id: RegularShapeId.Cross,
|
||||
name: 'Cross',
|
||||
hasFill: false,
|
||||
aliasIds: [MarkerShapePath.Cross],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
@ -91,9 +118,10 @@ export const markerMakers = new Registry<MarkerMaker>(() => [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'x',
|
||||
id: RegularShapeId.X,
|
||||
name: 'X',
|
||||
hasFill: false,
|
||||
aliasIds: [MarkerShapePath.X],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
@ -107,4 +135,15 @@ export const markerMakers = new Registry<MarkerMaker>(() => [
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
export const markerMakers = new Registry<MarkerMaker>(() => makers);
|
||||
|
||||
export const getMarkerFromPath = (svgPath: string): MarkerMaker | undefined => {
|
||||
for (const [key, val] of Object.entries(MarkerShapePath)) {
|
||||
if (val === svgPath) {
|
||||
return markerMakers.getIfExists(key);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
1
public/img/icons/marker/circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>
|
After Width: | Height: | Size: 98 B |
1
public/img/icons/marker/cross.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,11H13V5a1,1,0,0,0-2,0v6H5a1,1,0,0,0,0,2h6v6a1,1,0,0,0,2,0V13h6a1,1,0,0,0,0-2Z"/></svg>
|
After Width: | Height: | Size: 159 B |
1
public/img/icons/marker/plane.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M21.75,12a1,1,0,0,0-.55-.89L15.08,8.05v-4a3.08,3.08,0,1,0-6.16,0v4L2.8,11.11a1,1,0,0,0-.55.89v3.33a1,1,0,0,0,.43.83,1,1,0,0,0,.92.11l5.32-2V18l-1.82.6a1,1,0,0,0-.68.95V22a1,1,0,0,0,.3.71,1,1,0,0,0,.7.29h9.17a1,1,0,0,0,1-1V19.5a1,1,0,0,0-.68-.95L15.08,18V14.28l5.32,2a1,1,0,0,0,.92-.11,1,1,0,0,0,.43-.83Zm-7.31-.1a1,1,0,0,0-.93.11,1,1,0,0,0-.43.82v5.84a1,1,0,0,0,.69.95l1.81.6V21H8.41v-.78l1.81-.6a1,1,0,0,0,.69-.95V12.83a1,1,0,0,0-.43-.82,1,1,0,0,0-.93-.11l-5.31,2V12.62l6.11-3.06a1,1,0,0,0,.56-.89V4.08a1.08,1.08,0,1,1,2.16,0V8.67a1,1,0,0,0,.56.89l6.11,3.06v1.27Z"/></svg>
|
After Width: | Height: | Size: 654 B |
1
public/img/icons/marker/square.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="85%" height="85%" x="2" y="2" rx="5"/></svg>
|
After Width: | Height: | Size: 117 B |
1
public/img/icons/marker/star.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22,9.67A1,1,0,0,0,21.14,9l-5.69-.83L12.9,3a1,1,0,0,0-1.8,0L8.55,8.16,2.86,9a1,1,0,0,0-.81.68,1,1,0,0,0,.25,1l4.13,4-1,5.68a1,1,0,0,0,.4,1,1,1,0,0,0,1.05.07L12,18.76l5.1,2.68a.93.93,0,0,0,.46.12,1,1,0,0,0,.59-.19,1,1,0,0,0,.4-1l-1-5.68,4.13-4A1,1,0,0,0,22,9.67Zm-6.15,4a1,1,0,0,0-.29.89l.72,4.19-3.76-2a1,1,0,0,0-.94,0l-3.76,2,.72-4.19a1,1,0,0,0-.29-.89l-3-3,4.21-.61a1,1,0,0,0,.76-.55L12,5.7l1.88,3.82a1,1,0,0,0,.76.55l4.21.61Z"/></svg>
|
After Width: | Height: | Size: 506 B |
1
public/img/icons/marker/triangle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.87,19.29l-9-15.58a1,1,0,0,0-1.74,0l-9,15.58a1,1,0,0,0,0,1,1,1,0,0,0,.87.5H21a1,1,0,0,0,.87-.5A1,1,0,0,0,21.87,19.29Zm-17.14-.5L12,6.21l7.27,12.58Z"/></svg>
|
After Width: | Height: | Size: 228 B |
1
public/img/icons/marker/x-mark.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.41,12l6.3-6.29a1,1,0,1,0-1.42-1.42L12,10.59,5.71,4.29A1,1,0,0,0,4.29,5.71L10.59,12l-6.3,6.29a1,1,0,0,0,0,1.42,1,1,0,0,0,1.42,0L12,13.41l6.29,6.3a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42Z"/></svg>
|
After Width: | Height: | Size: 261 B |