@kanaries/graphic-walker 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/package.json +3 -2
  2. package/src/App.tsx +140 -0
  3. package/src/assets/kanaries.ico +0 -0
  4. package/src/components/clickMenu.tsx +29 -0
  5. package/src/components/container.tsx +16 -0
  6. package/src/components/dataTypeIcon.tsx +20 -0
  7. package/src/components/liteForm.tsx +16 -0
  8. package/src/components/modal.tsx +85 -0
  9. package/src/components/sizeSetting.tsx +95 -0
  10. package/src/components/tabs/pureTab.tsx +70 -0
  11. package/src/config.ts +57 -0
  12. package/src/constants.ts +1 -0
  13. package/src/dataSource/config.ts +62 -0
  14. package/src/dataSource/dataSelection/csvData.tsx +77 -0
  15. package/src/dataSource/dataSelection/gwFile.tsx +38 -0
  16. package/src/dataSource/dataSelection/index.tsx +57 -0
  17. package/src/dataSource/dataSelection/publicData.tsx +57 -0
  18. package/src/dataSource/index.tsx +78 -0
  19. package/src/dataSource/pannel.tsx +71 -0
  20. package/src/dataSource/table.tsx +125 -0
  21. package/src/dataSource/utils.ts +47 -0
  22. package/src/fields/aestheticFields.tsx +23 -0
  23. package/src/fields/components.tsx +159 -0
  24. package/src/fields/datasetFields/dimFields.tsx +45 -0
  25. package/src/fields/datasetFields/fieldPill.tsx +10 -0
  26. package/src/fields/datasetFields/index.tsx +28 -0
  27. package/src/fields/datasetFields/meaFields.tsx +58 -0
  28. package/src/fields/fieldsContext.tsx +59 -0
  29. package/src/fields/filterField/filterEditDialog.tsx +143 -0
  30. package/src/fields/filterField/filterPill.tsx +113 -0
  31. package/src/fields/filterField/index.tsx +61 -0
  32. package/src/fields/filterField/slider.tsx +236 -0
  33. package/src/fields/filterField/tabs.tsx +421 -0
  34. package/src/fields/obComponents/obFContainer.tsx +40 -0
  35. package/src/fields/obComponents/obPill.tsx +48 -0
  36. package/src/fields/posFields/index.tsx +33 -0
  37. package/src/fields/select.tsx +31 -0
  38. package/src/fields/utils.ts +31 -0
  39. package/src/index.css +13 -0
  40. package/src/index.tsx +12 -0
  41. package/src/insightBoard/index.tsx +30 -0
  42. package/src/insightBoard/mainBoard.tsx +203 -0
  43. package/src/insightBoard/radioGroupButtons.tsx +50 -0
  44. package/src/insightBoard/selectionSpec.ts +113 -0
  45. package/src/insightBoard/std2vegaSpec.ts +184 -0
  46. package/src/insightBoard/utils.ts +32 -0
  47. package/src/insights.ts +408 -0
  48. package/src/interfaces.ts +154 -0
  49. package/src/locales/en-US.json +140 -0
  50. package/src/locales/i18n.ts +50 -0
  51. package/src/locales/zh-CN.json +140 -0
  52. package/src/main.tsx +10 -0
  53. package/src/models/visSpecHistory.ts +129 -0
  54. package/src/renderer/index.tsx +104 -0
  55. package/src/segments/visNav.tsx +48 -0
  56. package/src/services.ts +139 -0
  57. package/src/store/commonStore.ts +158 -0
  58. package/src/store/index.tsx +53 -0
  59. package/src/store/visualSpecStore.ts +586 -0
  60. package/src/utils/autoMark.ts +34 -0
  61. package/src/utils/index.ts +251 -0
  62. package/src/utils/normalization.ts +158 -0
  63. package/src/utils/save.ts +46 -0
  64. package/src/vis/future-react-vega.tsx +193 -0
  65. package/src/vis/gen-vega.tsx +52 -0
  66. package/src/vis/react-vega.tsx +398 -0
  67. package/src/visualSettings/index.tsx +252 -0
  68. package/src/visualSettings/menubar.tsx +109 -0
  69. package/src/vite-env.d.ts +1 -0
  70. package/src/workers/explainer.worker.js +78 -0
  71. package/src/workers/filter.worker.js +70 -0
@@ -0,0 +1,236 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { filter, fromEvent, map, throttleTime } from 'rxjs';
4
+
5
+ const SliderContainer = styled.div({
6
+ display: 'flex',
7
+ flexDirection: 'column',
8
+ alignItems: 'stretch',
9
+ justifyContent: 'stretch',
10
+ overflow: 'hidden',
11
+ paddingBlock: '1em',
12
+
13
+ '> .output': {
14
+ display: 'flex',
15
+ flexDirection: 'row',
16
+ flexWrap: 'wrap',
17
+ alignItems: 'stretch',
18
+ justifyContent: 'space-between',
19
+
20
+ '> output': {
21
+ userSelect: 'none',
22
+ minWidth: '4em',
23
+ paddingInline: '0.5em',
24
+ textAlign: 'center',
25
+ },
26
+ },
27
+ });
28
+
29
+ const SliderElement = styled.div({
30
+ marginInline: '1em',
31
+ paddingBlock: '10px',
32
+ flexGrow: 1,
33
+ flexShrink: 1,
34
+ display: 'flex',
35
+ flexDirection: 'row',
36
+ alignItems: 'center',
37
+ justifyContent: 'stretch',
38
+ });
39
+
40
+ const SliderTrack = styled.div({
41
+ flexGrow: 1,
42
+ flexShrink: 1,
43
+ backgroundColor: '#ccc',
44
+ border: '1px solid #aaa',
45
+ height: '10px',
46
+ borderRadius: '5px',
47
+ position: 'relative',
48
+ });
49
+
50
+ const SliderThumb = styled.div({
51
+ position: 'absolute',
52
+ top: '50%',
53
+ cursor: 'ew-resize',
54
+ backgroundColor: '#e2e2e2',
55
+ backgroundImage: `
56
+ linear-gradient(#666, #666 4%, transparent 4%, transparent 96%, #666 95%),
57
+ linear-gradient(90deg, #666, #666 10%, transparent 10%, transparent 90%, #666 90%)
58
+ `,
59
+ width: '10px',
60
+ height: '20px',
61
+ borderRadius: '2px',
62
+ outline: 'none',
63
+
64
+ ':hover': {
65
+ backgroundColor: '#fff',
66
+ },
67
+ });
68
+
69
+ const SliderSlice = styled.div({
70
+ backgroundColor: '#fff',
71
+ position: 'absolute',
72
+ height: '100%',
73
+ borderRadius: '5px',
74
+
75
+ ':hover': {
76
+ backgroundColor: '#fff',
77
+ },
78
+ });
79
+
80
+
81
+ const nicer = (range: readonly [number, number], value: number): string => {
82
+ const precision = /(\.\d*)$/.exec(((range[1] - range[0]) / 1000).toString())![0].length;
83
+
84
+ return value.toFixed(precision).replace(/\.?0+$/, '');
85
+ };
86
+
87
+ interface SliderProps {
88
+ min: number;
89
+ max: number;
90
+ value: readonly [number, number];
91
+ onChange: (value: readonly [number, number]) => void;
92
+ isDateTime?: boolean;
93
+ }
94
+
95
+ const Slider: React.FC<SliderProps> = React.memo(function Slider ({
96
+ min,
97
+ max,
98
+ value,
99
+ onChange,
100
+ isDateTime = false,
101
+ }) {
102
+ const [dragging, setDragging] = React.useState<'left' | 'right' | null>(null);
103
+ const trackRef = React.useRef<HTMLDivElement | null>(null);
104
+ const sliceRef = React.useRef<HTMLDivElement | null>(null);
105
+
106
+ const range: typeof value = [
107
+ (value[0] - min) / ((max - min) || 1),
108
+ (value[1] - min) / ((max - min) || 1)
109
+ ];
110
+
111
+ const mouseOffsetRef = React.useRef(0);
112
+
113
+ React.useEffect(() => {
114
+ if (dragging) {
115
+ const stop = (ev?: MouseEvent) => {
116
+ setDragging(null);
117
+ ev?.stopPropagation();
118
+ };
119
+
120
+ const dragHandler = fromEvent(document.body, 'mousemove').pipe(
121
+ throttleTime(100),
122
+ map(ev => {
123
+ if (!trackRef.current || !dragging) {
124
+ return null;
125
+ }
126
+
127
+ if ((ev as MouseEvent).buttons !== 1) {
128
+ stop();
129
+
130
+ return null;
131
+ }
132
+
133
+ const { x, width } = trackRef.current.getBoundingClientRect();
134
+
135
+ const pos = Math.min(
136
+ dragging === 'left' ? range[1] : 1,
137
+ Math.max(
138
+ dragging === 'right' ? range[0] : 0,
139
+ ((ev as MouseEvent).clientX - mouseOffsetRef.current - x) / width
140
+ )
141
+ );
142
+
143
+ return pos;
144
+ }),
145
+ filter(pos => {
146
+ return pos !== null && pos !== range[dragging === 'left' ? 0 : 1];
147
+ }),
148
+ ).subscribe(pos => {
149
+ const next: [number, number] = [...range];
150
+ next[dragging === 'left' ? 0 : 1] = pos as number;
151
+
152
+ next[0] = next[0] * ((max - min) || 1) + min;
153
+ next[1] = next[1] * ((max - min) || 1) + min;
154
+
155
+ onChange(next);
156
+ });
157
+
158
+ document.body.addEventListener('mouseup', stop);
159
+
160
+ return () => {
161
+ document.body.removeEventListener('mouseup', stop);
162
+ dragHandler.unsubscribe();
163
+ };
164
+ }
165
+ }, [dragging, range, onChange, min, max]);
166
+
167
+ return (
168
+ <SliderContainer>
169
+ <div className="output">
170
+ <output htmlFor="slider:min">
171
+ {isDateTime ? `${new Date(value[0])}` : nicer([min, max], value[0])}
172
+ </output>
173
+ <output htmlFor="slider:max">
174
+ {isDateTime ? `${new Date(value[1])}` : nicer([min, max], value[1])}
175
+ </output>
176
+ </div>
177
+ <SliderElement>
178
+ <SliderTrack
179
+ ref={e => trackRef.current = e}
180
+ >
181
+ <SliderSlice
182
+ role="presentation"
183
+ ref={e => sliceRef.current = e}
184
+ style={{
185
+ left: `${range[0] * 100}%`,
186
+ width: `${(range[1] - range[0]) * 100}%`,
187
+ }}
188
+ />
189
+ <SliderThumb
190
+ role="slider"
191
+ aria-label="minimum"
192
+ id="slider:min"
193
+ aria-valuemin={min}
194
+ aria-valuemax={max}
195
+ aria-valuenow={value[0]}
196
+ aria-valuetext={isDateTime ? `${new Date(value[0])}` : nicer([min, max], value[0])}
197
+ tabIndex={-1}
198
+ onMouseDown={ev => {
199
+ if (ev.buttons === 1) {
200
+ mouseOffsetRef.current = ev.nativeEvent.offsetX;
201
+ setDragging('left');
202
+ }
203
+ }}
204
+ style={{
205
+ left: `${range[0] * 100}%`,
206
+ transform: 'translate(-100%, -50%)',
207
+ }}
208
+ />
209
+ <SliderThumb
210
+ role="slider"
211
+ aria-label="maximum"
212
+ id="slider:max"
213
+ aria-valuemin={min}
214
+ aria-valuemax={max}
215
+ aria-valuenow={value[1]}
216
+ aria-valuetext={isDateTime ? `${new Date(value[1])}` : nicer([min, max], value[1])}
217
+ tabIndex={-1}
218
+ onMouseDown={ev => {
219
+ if (ev.buttons === 1) {
220
+ mouseOffsetRef.current = ev.nativeEvent.offsetX;
221
+ setDragging('right');
222
+ }
223
+ }}
224
+ style={{
225
+ left: `${range[1] * 100}%`,
226
+ transform: 'translate(0, -50%)',
227
+ }}
228
+ />
229
+ </SliderTrack>
230
+ </SliderElement>
231
+ </SliderContainer>
232
+ );
233
+ });
234
+
235
+
236
+ export default Slider;
@@ -0,0 +1,421 @@
1
+ import { observer } from 'mobx-react-lite';
2
+ import React from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import styled from 'styled-components';
5
+
6
+ import type { IFilterField, IFilterRule } from '../../interfaces';
7
+ import { useGlobalStore } from '../../store';
8
+ import Slider from './slider';
9
+
10
+
11
+ export type RuleFormProps = {
12
+ field: IFilterField;
13
+ onChange: (rule: IFilterRule) => void;
14
+ };
15
+
16
+ const Container = styled.div({
17
+ marginBlock: '1em',
18
+ marginInline: '2em',
19
+
20
+ '> .btn-grp': {
21
+ display: 'flex',
22
+ flexDirection: 'row',
23
+ marginBlock: '0.4em 0.6em',
24
+
25
+ '> *': {
26
+ marginInlineStart: '0.6em',
27
+
28
+ '&:first-child': {
29
+ marginInlineStart: 0,
30
+ },
31
+ },
32
+ },
33
+ });
34
+
35
+ export const Button = styled.button({
36
+ '&:hover': {
37
+ backgroundColor: 'rgba(243, 244, 246, 0.5)',
38
+ },
39
+ color: 'rgb(55, 65, 81)',
40
+ boxShadow: '1px 1px 2px #0002, inset 2px 2px 4px #0001',
41
+ paddingBlock: '0.2em',
42
+ paddingInline: '0.5em',
43
+ userSelect: 'none',
44
+ cursor: 'pointer',
45
+ });
46
+
47
+ const Table = styled.div({
48
+ display: 'grid',
49
+ gridTemplateColumns: '4em auto max-content',
50
+ maxHeight: '30vh',
51
+ overflowY: 'scroll',
52
+ '& > *': {
53
+ marginBlock: '2px',
54
+ paddingInline: '4px',
55
+ whiteSpace: 'nowrap',
56
+ overflow: 'hidden',
57
+ textOverflow: 'ellipsis',
58
+ userSelect: 'none',
59
+ },
60
+ '& > input, & > *[for]': {
61
+ cursor: 'pointer',
62
+ },
63
+ });
64
+
65
+ const TabsContainer = styled.div({
66
+ display: 'flex',
67
+ flexDirection: 'column',
68
+ alignItems: 'stretch',
69
+ justifyContent: 'stretch',
70
+ });
71
+
72
+ const TabList = styled.div({
73
+ display: 'flex',
74
+ flexDirection: 'row',
75
+ alignItems: 'stretch',
76
+ justifyContent: 'flex-start',
77
+ overflow: 'hidden',
78
+ });
79
+
80
+ const TabHeader = styled.label({
81
+ outline: 'none',
82
+ userSelect: 'none',
83
+ paddingBlock: '0.4em',
84
+ paddingInline: '1em 2em',
85
+ borderWidth: '1px',
86
+ borderRadius: '4px 4px 0 0',
87
+ position: 'relative',
88
+
89
+ '&[aria-selected]': {
90
+ borderBottomColor: '#0000',
91
+ zIndex: 15,
92
+ },
93
+ '&[aria-selected=false]': {
94
+ backgroundColor: '#f8f8f8',
95
+ borderBottomColor: '#e2e2e2',
96
+ cursor: 'pointer',
97
+ zIndex: 14,
98
+ },
99
+ });
100
+
101
+ const TabPanel = styled.div({});
102
+
103
+ const TabItem = styled.div({});
104
+
105
+ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = observer(({
106
+ active,
107
+ field,
108
+ onChange,
109
+ }) => {
110
+ const { commonStore } = useGlobalStore();
111
+ const { currentDataset: { dataSource } } = commonStore;
112
+
113
+ const count = React.useMemo(() => {
114
+ return dataSource.reduce<Map<string | number, number>>((tmp, d) => {
115
+ const val = d[field.fid];
116
+
117
+ tmp.set(val, (tmp.get(val) ?? 0) + 1);
118
+
119
+ return tmp;
120
+ }, new Map<string | number, number>());
121
+ }, [dataSource, field]);
122
+
123
+ const { t } = useTranslation('translation', { keyPrefix: 'filters' });
124
+
125
+ React.useEffect(() => {
126
+ if (active && field.rule?.type !== 'one of') {
127
+ onChange({
128
+ type: 'one of',
129
+ value: new Set<string | number>(count.keys()),
130
+ });
131
+ }
132
+ }, [active, onChange, field, count]);
133
+
134
+ return field.rule?.type === 'one of' ? (
135
+ <Container>
136
+ <Table>
137
+ <label className="header">
138
+ {t('header.visibility')}
139
+ </label>
140
+ <label className="header">
141
+ {t('header.value')}
142
+ </label>
143
+ <label className="header">
144
+ {t('header.count')}
145
+ </label>
146
+ </Table>
147
+ <Table>
148
+ {
149
+ [...count.entries()].map(([value, count], idx) => {
150
+ const id = `rule_checkbox_${idx}`;
151
+
152
+ return (
153
+ <React.Fragment key={idx}>
154
+ <input
155
+ type="checkbox"
156
+ checked={field.rule?.type === 'one of' && field.rule.value.has(value)}
157
+ id={id}
158
+ aria-describedby={`${id}_label`}
159
+ title={String(value)}
160
+ onChange={({ target: { checked } }) => {
161
+ if (field.rule?.type !== 'one of') {
162
+ return;
163
+ }
164
+
165
+ const rule: IFilterRule = {
166
+ type: 'one of',
167
+ value: new Set(field.rule.value)
168
+ };
169
+
170
+ if (checked) {
171
+ rule.value.add(value);
172
+ } else {
173
+ rule.value.delete(value);
174
+ }
175
+
176
+ onChange(rule);
177
+ }}
178
+ />
179
+ <label
180
+ id={`${id}_label`}
181
+ htmlFor={id}
182
+ title={String(value)}
183
+ >
184
+ {value}
185
+ </label>
186
+ <label
187
+ htmlFor={id}
188
+ >
189
+ {count}
190
+ </label>
191
+ </React.Fragment>
192
+ );
193
+ })
194
+ }
195
+ </Table>
196
+ <Table className="text-gray-600">
197
+ <label></label>
198
+ <label>
199
+ {t('selected_keys', { count: field.rule.value.size })}
200
+ </label>
201
+ <label>
202
+ {[...field.rule.value].reduce<number>((sum, key) => {
203
+ const s = dataSource.filter(which => which[field.fid] === key).length;
204
+
205
+ return sum + s;
206
+ }, 0)}
207
+ </label>
208
+ </Table>
209
+ <div className="btn-grp">
210
+ <Button
211
+ onClick={() => {
212
+ if (field.rule?.type === 'one of') {
213
+ const curSet = field.rule.value;
214
+
215
+ onChange({
216
+ type: 'one of',
217
+ value: new Set<number | string>(
218
+ curSet.size === count.size
219
+ ? []
220
+ : count.keys()
221
+ ),
222
+ });
223
+ }
224
+ }}
225
+ >
226
+ {
227
+ field.rule.value.size === count.size
228
+ ? t('btn.unselect_all')
229
+ : t('btn.select_all')
230
+ }
231
+ </Button>
232
+ <Button
233
+ onClick={() => {
234
+ if (field.rule?.type === 'one of') {
235
+ const curSet = field.rule.value;
236
+
237
+ onChange({
238
+ type: 'one of',
239
+ value: new Set<number | string>(
240
+ [...count.keys()].filter(key => !curSet.has(key))
241
+ ),
242
+ });
243
+ }
244
+ }}
245
+ >
246
+ {t('btn.reverse')}
247
+ </Button>
248
+ </div>
249
+ </Container>
250
+ ) : null;
251
+ });
252
+
253
+ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean }> = observer(({
254
+ active,
255
+ field,
256
+ onChange,
257
+ }) => {
258
+ const { commonStore } = useGlobalStore();
259
+ const { currentDataset: { dataSource } } = commonStore;
260
+
261
+ const sorted = React.useMemo(() => {
262
+ return dataSource.reduce<number[]>((list, d) => {
263
+ try {
264
+ const time = new Date(d[field.fid]).getTime();
265
+
266
+ list.push(time);
267
+ } catch (error) {
268
+
269
+ }
270
+ return list;
271
+ }, []).sort((a, b) => a - b);
272
+ }, [dataSource, field]);
273
+
274
+ const [min, max] = React.useMemo(() => {
275
+ return [sorted[0] ?? 0, Math.max(sorted[sorted.length - 1] ?? 0, sorted[0] ?? 0)];
276
+ }, [sorted]);
277
+
278
+ React.useEffect(() => {
279
+ if (active && field.rule?.type !== 'temporal range') {
280
+ onChange({
281
+ type: 'temporal range',
282
+ value: [sorted[0] ?? 0, Math.max(sorted[sorted.length - 1] ?? 0, sorted[0] ?? 0)],
283
+ });
284
+ }
285
+ }, [onChange, field, sorted, active]);
286
+
287
+ const handleChange = React.useCallback((value: readonly [number, number]) => {
288
+ onChange({
289
+ type: 'temporal range',
290
+ value,
291
+ });
292
+ }, []);
293
+
294
+ return field.rule?.type === 'temporal range' ? (
295
+ <Container>
296
+ <Slider
297
+ min={min}
298
+ max={max}
299
+ value={field.rule.value}
300
+ onChange={handleChange}
301
+ isDateTime
302
+ />
303
+ </Container>
304
+ ) : null;
305
+ });
306
+
307
+ export const FilterRangeRule: React.FC<RuleFormProps & { active: boolean }> = observer(({
308
+ active,
309
+ field,
310
+ onChange,
311
+ }) => {
312
+ const { commonStore } = useGlobalStore();
313
+ const { currentDataset: { dataSource } } = commonStore;
314
+
315
+ const sorted = React.useMemo(() => {
316
+ return dataSource.map(d => d[field.fid]).sort((a, b) => a - b);
317
+ }, [dataSource, field]);
318
+
319
+ const [min, max] = React.useMemo(() => {
320
+ return [sorted[0] ?? 0, Math.max(sorted[sorted.length - 1] ?? 0, sorted[0] ?? 0)];
321
+ }, [sorted]);
322
+
323
+ React.useEffect(() => {
324
+ if (active && field.rule?.type !== 'range') {
325
+ onChange({
326
+ type: 'range',
327
+ value: [min, max],
328
+ });
329
+ }
330
+ }, [onChange, field, min, max, active]);
331
+
332
+ const handleChange = React.useCallback((value: readonly [number, number]) => {
333
+ onChange({
334
+ type: 'range',
335
+ value,
336
+ });
337
+ }, []);
338
+
339
+ return field.rule?.type === 'range' ? (
340
+ <Container>
341
+ <Slider
342
+ min={min}
343
+ max={max}
344
+ value={field.rule.value}
345
+ onChange={handleChange}
346
+ />
347
+ </Container>
348
+ ) : null;
349
+ });
350
+
351
+ const filterTabs: Record<IFilterRule['type'], React.FC<RuleFormProps & { active: boolean }>> = {
352
+ 'one of': FilterOneOfRule,
353
+ 'range': FilterRangeRule,
354
+ 'temporal range': FilterTemporalRangeRule,
355
+ };
356
+
357
+ export interface TabsProps extends RuleFormProps {
358
+ tabs: IFilterRule['type'][];
359
+ }
360
+
361
+ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
362
+ const { vizStore } = useGlobalStore();
363
+ const { draggableFieldState } = vizStore;
364
+
365
+ const { t } = useTranslation('translation', { keyPrefix: 'constant.filter_type' });
366
+
367
+ const [which, setWhich] = React.useState(field.rule?.type ?? tabs[0]!);
368
+
369
+ return (
370
+ <TabsContainer>
371
+ <TabList role="tablist">
372
+ {
373
+ tabs.map((tab, i) => (
374
+ <TabHeader
375
+ key={i}
376
+ role="tab"
377
+ aria-selected={which === tab}
378
+ id={`filter-tab-${tab.replaceAll(/ /g, '_')}`}
379
+ aria-controls={`filter-panel-${tab.replaceAll(/ /g, '_')}`}
380
+ tabIndex={-1}
381
+ onClick={() => {
382
+ if (which !== tab) {
383
+ setWhich(tab);
384
+ }
385
+ }}
386
+ >
387
+ {t(tab.replaceAll(/ /g, '_'))}
388
+ </TabHeader>
389
+ ))
390
+ }
391
+ </TabList>
392
+ <TabPanel>
393
+ {
394
+ tabs.map((tab, i) => {
395
+ const Component = filterTabs[tab];
396
+
397
+ return draggableFieldState === null ? null : (
398
+ <TabItem
399
+ key={i}
400
+ id={`filter-panel-${tab.replaceAll(/ /g, '_')}`}
401
+ aria-labelledby={`filter-tab-${tab.replaceAll(/ /g, '_')}`}
402
+ role="tabpanel"
403
+ hidden={which !== tab}
404
+ tabIndex={0}
405
+ >
406
+ <Component
407
+ field={field}
408
+ onChange={onChange}
409
+ active={which === tab}
410
+ />
411
+ </TabItem>
412
+ );
413
+ })
414
+ }
415
+ </TabPanel>
416
+ </TabsContainer>
417
+ );
418
+ });
419
+
420
+
421
+ export default Tabs;
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite'
3
+ import { FieldsContainer } from '../components';
4
+ import { useGlobalStore } from '../../store';
5
+ import {
6
+ Draggable,
7
+ DroppableProvided
8
+ } from "react-beautiful-dnd";
9
+ import { IDraggableStateKey } from '../../interfaces';
10
+ import OBPill from './obPill';
11
+
12
+ interface FieldContainerProps {
13
+ provided: DroppableProvided
14
+ /**
15
+ * draggable Field Id
16
+ */
17
+ dkey: IDraggableStateKey;
18
+ }
19
+ const OBFieldContainer: React.FC<FieldContainerProps> = props => {
20
+ const { provided, dkey } = props;
21
+ const { vizStore } = useGlobalStore();
22
+ const { draggableFieldState } = vizStore;
23
+ return <FieldsContainer
24
+ {...provided.droppableProps}
25
+ ref={provided.innerRef}
26
+ >
27
+ {/* {provided.placeholder} */}
28
+ {draggableFieldState[dkey.id].map((f, index) => (
29
+ <Draggable key={f.dragId} draggableId={f.dragId} index={index}>
30
+ {(provided, snapshot) => {
31
+ return (
32
+ <OBPill dkey={dkey} fIndex={index} provided={provided} />
33
+ );
34
+ }}
35
+ </Draggable>
36
+ ))}
37
+ </FieldsContainer>
38
+ }
39
+
40
+ export default observer(OBFieldContainer);