1import React, { useMemo } from 'react';
2import { LineStyle } from '@grafana/schema';
3import { FieldOverrideEditorProps, SelectableValue } from '@grafana/data';
4import { HorizontalGroup, IconButton, RadioButtonGroup, Select } from '@grafana/ui';
5
6type LineFill = 'solid' | 'dash' | 'dot';
7
8const lineFillOptions: Array<SelectableValue<LineFill>> = [
9  {
10    label: 'Solid',
11    value: 'solid',
12  },
13  {
14    label: 'Dash',
15    value: 'dash',
16  },
17  {
18    label: 'Dots',
19    value: 'dot',
20  },
21];
22
23const dashOptions: Array<SelectableValue<string>> = [
24  '10, 10', // default
25  '10, 15',
26  '10, 20',
27  '10, 25',
28  '10, 30',
29  '10, 40',
30  '15, 10',
31  '20, 10',
32  '25, 10',
33  '30, 10',
34  '40, 10',
35  '50, 10',
36  '5, 10',
37  '30, 3, 3',
38].map((txt) => ({
39  label: txt,
40  value: txt,
41}));
42
43const dotOptions: Array<SelectableValue<string>> = [
44  '0, 10', // default
45  '0, 20',
46  '0, 30',
47  '0, 40',
48  '0, 3, 3',
49].map((txt) => ({
50  label: txt,
51  value: txt,
52}));
53
54export const LineStyleEditor: React.FC<FieldOverrideEditorProps<LineStyle, any>> = ({ value, onChange }) => {
55  const options = useMemo(() => (value?.fill === 'dash' ? dashOptions : dotOptions), [value]);
56  const current = useMemo(() => {
57    if (!value?.dash?.length) {
58      return options[0];
59    }
60    const str = value.dash?.join(', ');
61    const val = options.find((o) => o.value === str);
62    if (!val) {
63      return {
64        label: str,
65        value: str,
66      };
67    }
68    return val;
69  }, [value, options]);
70
71  return (
72    <HorizontalGroup>
73      <RadioButtonGroup
74        value={value?.fill || 'solid'}
75        options={lineFillOptions}
76        onChange={(v) => {
77          let dash: number[] | undefined = undefined;
78          if (v === 'dot') {
79            dash = parseText(dotOptions[0].value!);
80          } else if (v === 'dash') {
81            dash = parseText(dashOptions[0].value!);
82          }
83          onChange({
84            ...value,
85            fill: v!,
86            dash,
87          });
88        }}
89      />
90      {value?.fill && value?.fill !== 'solid' && (
91        <>
92          <Select
93            menuShouldPortal
94            allowCustomValue={true}
95            options={options}
96            value={current}
97            width={20}
98            onChange={(v) => {
99              onChange({
100                ...value,
101                dash: parseText(v.value ?? ''),
102              });
103            }}
104            formatCreateLabel={(t) => `Segments: ${parseText(t).join(', ')}`}
105          />
106          <div>
107            &nbsp;
108            <a
109              title="The input expects a segment list"
110              href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash#Parameters"
111              target="_blank"
112              rel="noreferrer"
113            >
114              <IconButton name="question-circle" />
115            </a>
116          </div>
117        </>
118      )}
119    </HorizontalGroup>
120  );
121};
122
123function parseText(txt: string): number[] {
124  const segments: number[] = [];
125  for (const s of txt.split(/(?:,| )+/)) {
126    const num = Number.parseInt(s, 10);
127    if (!isNaN(num)) {
128      segments.push(num);
129    }
130  }
131  return segments;
132}
133