@kanaries/graphic-walker 0.3.13 → 0.3.14

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.
@@ -5,16 +5,11 @@ export interface IGlobalStore {
5
5
  commonStore: CommonStore;
6
6
  vizStore: VizSpecStore;
7
7
  }
8
- export declare function destroyGWStore(): void;
9
- export declare function rebootGWStore(): void;
10
8
  interface StoreWrapperProps {
11
- keepAlive?: boolean;
9
+ keepAlive?: boolean | string;
12
10
  storeRef?: React.MutableRefObject<IGlobalStore | null>;
11
+ children?: React.ReactNode;
13
12
  }
14
- export declare class StoreWrapper extends React.Component<StoreWrapperProps> {
15
- constructor(props: StoreWrapperProps);
16
- componentWillUnmount(): void;
17
- render(): JSX.Element;
18
- }
13
+ export declare const StoreWrapper: (props: StoreWrapperProps) => JSX.Element;
19
14
  export declare function useGlobalStore(): IGlobalStore;
20
15
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanaries/graphic-walker",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "scripts": {
5
5
  "dev:front_end": "vite --host",
6
6
  "dev": "npm run dev:front_end",
Binary file
@@ -0,0 +1,29 @@
1
+ import React, { useRef, useState, useEffect } from 'react';
2
+
3
+ export const ImageWithFallback = (
4
+ props: React.ImgHTMLAttributes<HTMLImageElement> & { fallbackSrc: string; timeout: number }
5
+ ) => {
6
+ const { src, fallbackSrc, timeout, ...rest } = props;
7
+ const [failed, setFailed] = useState(false);
8
+ const imgLoadedOnInitSrc = useRef(false);
9
+
10
+ useEffect(() => {
11
+ const timer = setTimeout(() => {
12
+ if (!imgLoadedOnInitSrc.current) setFailed(true);
13
+ }, timeout);
14
+ return () => clearTimeout(timer);
15
+ }, []);
16
+
17
+ return (
18
+ <img
19
+ {...rest}
20
+ src={failed ? src : fallbackSrc}
21
+ onError={() => {
22
+ setFailed(true);
23
+ }}
24
+ onLoad={() => {
25
+ imgLoadedOnInitSrc.current = true;
26
+ }}
27
+ />
28
+ );
29
+ };
@@ -74,6 +74,7 @@ export const ToolbarItemContainerElement = styled.div<{ split: boolean; dark: bo
74
74
  outline: none;
75
75
  width: ${({ split }) => split ? 'calc(var(--height) + 10px)' : 'var(--height)'};
76
76
  height: var(--height);
77
+ align-items: center;
77
78
  overflow: hidden;
78
79
  color: ${({ dark }) => dark ? 'var(--dark-mode-color)' : 'var(--color)'};
79
80
  position: relative;
@@ -100,7 +100,7 @@ const ValueInput: React.FC<ValueInputProps> = props => {
100
100
  type="number"
101
101
  min={min}
102
102
  max={max}
103
- className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
103
+ className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 dark:bg-zinc-900 dark:border-gray-700 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
104
104
  value={value}
105
105
  onChange={(e) => handleSubmitValue(Number(e.target.value))}
106
106
  />
@@ -1,11 +1,15 @@
1
1
  import { observer } from 'mobx-react-lite';
2
- import React, { useMemo, useRef } from 'react';
2
+ import React, { useMemo, useRef, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import styled from 'styled-components';
5
5
 
6
6
  import type { IFilterField, IFilterRule } from '../../interfaces';
7
7
  import { useGlobalStore } from '../../store';
8
8
  import Slider from './slider';
9
+ import {
10
+ ChevronDownIcon,
11
+ ChevronUpIcon,
12
+ } from '@heroicons/react/24/outline';
9
13
 
10
14
  export type RuleFormProps = {
11
15
  field: IFilterField;
@@ -95,7 +99,7 @@ const TabPanel = styled.div``;
95
99
 
96
100
  const TabItem = styled.div``;
97
101
 
98
- const StatusCheckbox: React.FC<{currentNum: number; totalNum: number; onChange: () => void}> = props => {
102
+ const StatusCheckbox: React.FC<{ currentNum: number; totalNum: number; onChange: () => void }> = props => {
99
103
  const { currentNum, totalNum, onChange } = props;
100
104
  const checkboxRef = useRef(null);
101
105
 
@@ -131,16 +135,42 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
131
135
  const { commonStore } = useGlobalStore();
132
136
  const { currentDataset: { dataSource } } = commonStore;
133
137
 
138
+ interface SortConfig {
139
+ key: 'value' | 'count';
140
+ ascending: boolean;
141
+ }
142
+ const [sortConfig, setSortConfig] = useState<SortConfig>({
143
+ key: "count",
144
+ ascending: true
145
+ });
146
+
134
147
  const count = React.useMemo(() => {
135
148
  return dataSource.reduce<Map<string | number, number>>((tmp, d) => {
136
149
  const val = d[field.fid];
137
150
 
138
151
  tmp.set(val, (tmp.get(val) ?? 0) + 1);
139
-
152
+
140
153
  return tmp;
141
154
  }, new Map<string | number, number>());
142
155
  }, [dataSource, field]);
143
156
 
157
+ const sortedList = useMemo(() => {
158
+ const entries = Array.from(count.entries());
159
+ const compare = (a: [string | number, number], b: [string | number, number]) => {
160
+ if (sortConfig.key === 'count') {
161
+ return a[1] - b[1];
162
+ } else {
163
+ if (typeof a[0] === 'number' && typeof b[0] === 'number') {
164
+ return a[0] - b[0];
165
+ } else {
166
+ return String(a[0]).localeCompare(String(b[0]))
167
+ }
168
+ }
169
+ }
170
+ entries.sort(sortConfig.ascending ? compare : (a, b) => -compare(a, b));
171
+ return entries;
172
+ }, [count, sortConfig]);
173
+
144
174
  const { t } = useTranslation('translation');
145
175
 
146
176
  React.useEffect(() => {
@@ -172,7 +202,7 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
172
202
  value: new Set<number | string>(
173
203
  [...count.keys()].filter(key => !curSet.has(key))
174
204
  ),
175
- });
205
+ });
176
206
  }
177
207
  const handleSelectValue = (value, checked) => {
178
208
  if (!field.rule || field.rule?.type !== 'one of') return;
@@ -187,21 +217,37 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
187
217
  }
188
218
  onChange(rule);
189
219
  }
190
-
220
+
191
221
  const selectedValueSum = useMemo(() => {
192
222
  if (!field.rule) return 0;
193
223
  return [...field.rule.value].reduce<number>((sum, key) => {
194
224
  const s = dataSource.filter(which => which[field.fid] === key).length;
195
225
  return sum + s;
196
226
  }, 0)
197
- }, [field.rule?.value])
227
+ }, [field.rule?.value]);
228
+
229
+ const SortButton: React.FC<{ currentKey: SortConfig["key"] }> = ({ currentKey }) => {
230
+ const isCurrentKey = sortConfig.key === currentKey;
231
+ return (
232
+ <span
233
+ className={`ml-2 flex-none rounded bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer ${isCurrentKey ? "text-indigo-600" : "text-gray-500"}`}
234
+ onClick={() => setSortConfig({ key: currentKey, ascending: (isCurrentKey ? !sortConfig.ascending : true) })}
235
+ >
236
+ {isCurrentKey && !sortConfig.ascending
237
+ ? <ChevronDownIcon className="h-4 w-4" />
238
+ : <ChevronUpIcon className="h-4 w-4" />
239
+ }
240
+ </span>
241
+ );
242
+ }
198
243
 
199
244
  return field.rule?.type === 'one of' ? (
200
245
  <Container>
201
246
  <div>{t('constant.filter_type.one_of')}</div>
202
- <div className="text-gray-500">{t('constant.filter_type.one_of_desc')}</div>
247
+ <div className="text-gray-500 dark:text-gray-300">{t('constant.filter_type.one_of_desc')}</div>
203
248
  <div className="btn-grp">
204
249
  <Button
250
+ className="dark:bg-zinc-900 dark:text-gray-200 dark:hover:bg-gray-800"
205
251
  onClick={() => handleToggleFullOrEmptySet()}
206
252
  >
207
253
  {
@@ -211,12 +257,13 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
211
257
  }
212
258
  </Button>
213
259
  <Button
260
+ className="dark:bg-zinc-900 dark:text-gray-200 dark:hover:bg-gray-800"
214
261
  onClick={() => handleToggleReverseSet()}
215
262
  >
216
263
  {t('filters.btn.reverse')}
217
264
  </Button>
218
265
  </div>
219
- <Table className="bg-slate-50">
266
+ <Table className="bg-slate-50 dark:bg-gray-800">
220
267
  <div className="flex justify-center items-center">
221
268
  <StatusCheckbox
222
269
  currentNum={field.rule.value.size}
@@ -224,17 +271,19 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
224
271
  onChange={handleToggleFullOrEmptySet}
225
272
  />
226
273
  </div>
227
- <label className="header text-gray-500">
274
+ <label className="header text-gray-500 dark:text-gray-300 flex items-center">
228
275
  {t('filters.header.value')}
276
+ <SortButton currentKey="value" />
229
277
  </label>
230
- <label className="header text-gray-500">
278
+ <label className="header text-gray-500 dark:text-gray-300 flex items-center">
231
279
  {t('filters.header.count')}
280
+ <SortButton currentKey="count" />
232
281
  </label>
233
282
  </Table>
234
283
  {/* <hr /> */}
235
284
  <Table>
236
285
  {
237
- [...count.entries()].map(([value, count], idx) => {
286
+ sortedList.map(([value, count], idx) => {
238
287
  const id = `rule_checkbox_${idx}`;
239
288
 
240
289
  return (
@@ -299,8 +348,8 @@ const CalendarInput: React.FC<CalendarInputProps> = props => {
299
348
  }
300
349
  return (
301
350
  <input
302
- className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
303
- type="datetime-local"
351
+ className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 dark:bg-zinc-900 dark:border-gray-700 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
352
+ type="datetime-local"
304
353
  min={dateStringFormatter(min)}
305
354
  max={dateStringFormatter(max)}
306
355
  defaultValue={dateStringFormatter(value)}
@@ -326,7 +375,7 @@ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean
326
375
 
327
376
  list.push(time);
328
377
  } catch (error) {
329
-
378
+
330
379
  }
331
380
  return list;
332
381
  }, []).sort((a, b) => a - b);
@@ -463,7 +512,7 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
463
512
  React.useEffect(() => {
464
513
  if (!tabs.includes(which)) setWhich(tabs[0]);
465
514
  }, [tabs])
466
-
515
+
467
516
  return (
468
517
  <TabsContainer>
469
518
  <div>
@@ -472,13 +521,13 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
472
521
  return (
473
522
  <div className="flex my-2" key={option}>
474
523
  <div className="align-top">
475
- <input
524
+ <input
476
525
  type="radio"
477
526
  className="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600"
478
527
  id={option}
479
528
  checked={option === which}
480
529
  onChange={e => setWhich((e.target as HTMLInputElement).value as typeof which)}
481
- name="filter_type"
530
+ name="filter_type"
482
531
  value={option}
483
532
  />
484
533
  </div>
@@ -495,7 +544,7 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
495
544
  })
496
545
  }
497
546
  </div>
498
- <hr className="my-0.5"/>
547
+ <hr className="my-0.5" />
499
548
  <TabPanel>
500
549
  {
501
550
  tabs.map((tab, i) => {
@@ -1,62 +1,63 @@
1
- import React, { useContext } from 'react';
2
- import { CommonStore } from './commonStore'
3
- import { VizSpecStore } from './visualSpecStore'
1
+ import React, { useContext, useMemo, useEffect } from 'react';
2
+ import { CommonStore } from './commonStore';
3
+ import { VizSpecStore } from './visualSpecStore';
4
4
 
5
5
  export interface IGlobalStore {
6
6
  commonStore: CommonStore;
7
7
  vizStore: VizSpecStore;
8
8
  }
9
9
 
10
- const commonStore = new CommonStore();
11
- const vizStore = new VizSpecStore(commonStore);
12
-
13
- const initStore: IGlobalStore = {
14
- commonStore,
15
- vizStore
16
- }
10
+ const StoreDict: Record<string, IGlobalStore> = {};
11
+ const createStore = () => {
12
+ const commonStore = new CommonStore();
13
+ const vizStore = new VizSpecStore(commonStore);
14
+ return {
15
+ commonStore,
16
+ vizStore,
17
+ };
18
+ };
19
+ const getStore = (key?: string): IGlobalStore => {
20
+ if (key) {
21
+ if (!StoreDict[key]) StoreDict[key] = createStore();
22
+ return StoreDict[key];
23
+ } else {
24
+ return createStore();
25
+ }
26
+ };
17
27
 
18
28
  const StoreContext = React.createContext<IGlobalStore>(null!);
19
-
20
- export function destroyGWStore() {
21
- initStore.commonStore.destroy();
22
- initStore.vizStore.destroy();
23
- }
24
-
25
- export function rebootGWStore() {
26
- const cs = new CommonStore();
27
- const vs = new VizSpecStore(cs);
28
- initStore.commonStore = cs;
29
- initStore.vizStore = vs;
30
- }
31
-
32
29
  interface StoreWrapperProps {
33
- keepAlive?: boolean;
30
+ keepAlive?: boolean | string;
34
31
  storeRef?: React.MutableRefObject<IGlobalStore | null>;
32
+ children?: React.ReactNode;
35
33
  }
36
- export class StoreWrapper extends React.Component<StoreWrapperProps> {
37
- constructor(props: StoreWrapperProps) {
38
- super(props)
34
+
35
+ const noop = () => {};
36
+
37
+ export const StoreWrapper = (props: StoreWrapperProps) => {
38
+ const storeKey = props.keepAlive ? `${props.keepAlive}` : '';
39
+ const store = useMemo(() => getStore(storeKey), [storeKey]);
40
+ useEffect(() => {
39
41
  if (props.storeRef) {
40
- props.storeRef.current = initStore;
42
+ const ref = props.storeRef;
43
+ ref.current = store;
44
+ return () => {
45
+ ref.current = null;
46
+ };
41
47
  }
42
- if (props.keepAlive) {
43
- rebootGWStore();
44
- }
45
- }
46
- componentWillUnmount() {
47
- if (!this.props.keepAlive) {
48
- if (this.props.storeRef) {
49
- this.props.storeRef.current = null;
50
- }
51
- destroyGWStore();
48
+ return noop;
49
+ }, [props.storeRef]);
50
+ useEffect(() => {
51
+ if (!storeKey) {
52
+ return () => {
53
+ store.commonStore.destroy();
54
+ store.vizStore.destroy();
55
+ };
52
56
  }
53
- }
54
- render() {
55
- return <StoreContext.Provider value={initStore}>
56
- { this.props.children }
57
- </StoreContext.Provider>
58
- }
59
- }
57
+ return noop;
58
+ }, [storeKey]);
59
+ return <StoreContext.Provider value={store}>{props.children}</StoreContext.Provider>;
60
+ };
60
61
 
61
62
  export function useGlobalStore() {
62
63
  return useContext(StoreContext);
@@ -33,7 +33,8 @@ import Toolbar, { ToolbarItemProps } from '../components/toolbar';
33
33
  import { ButtonWithShortcut } from './menubar';
34
34
  import { useCurrentMediaTheme } from '../utils/media';
35
35
  import throttle from '../utils/throttle';
36
- import KanariesLogo from '../assets/kanaries-logo.svg';
36
+ import KanariesLogo from '../assets/kanaries.png';
37
+ import { ImageWithFallback } from '../components/timeoutImg';
37
38
 
38
39
  const Invisible = styled.div`
39
40
  clip: rect(1px, 1px, 1px, 1px);
@@ -101,30 +102,8 @@ const VisualSettings: React.FC<IVisualSettings> = ({
101
102
 
102
103
  const dark = useCurrentMediaTheme(darkModePreference) === 'dark';
103
104
 
104
- console.log('kanaries logo', KanariesLogo);
105
-
106
105
  const items = useMemo<ToolbarItemProps[]>(() => {
107
106
  const builtInItems = [
108
- {
109
- key: 'kanaries',
110
- label: 'kanaries',
111
- icon: () => (
112
- // Kanaries brand info is not allowed to be removed or changed unless you are granted with special permission.
113
- <a href="https://kanaries.net" target="_blank">
114
- <img
115
- id="kanaries-logo"
116
- className="m-1"
117
- src="https://imagedelivery.net/tSvh1MGEu9IgUanmf58srQ/b6bc899f-a129-4c3a-d08f-d406166d0c00/public"
118
- alt="kanaries"
119
- onError={(e) => {
120
- // @ts-ignore
121
- e.target.src = KanariesLogo;
122
- }}
123
- />
124
- </a>
125
- ),
126
- },
127
- '-',
128
107
  {
129
108
  key: 'undo',
130
109
  label: 'undo (Ctrl + Z)',
@@ -512,6 +491,24 @@ const VisualSettings: React.FC<IVisualSettings> = ({
512
491
  commonStore.setShowCodeExportPanel(true);
513
492
  },
514
493
  },
494
+ '-',
495
+ {
496
+ key: 'kanaries',
497
+ label: 'kanaries',
498
+ icon: () => (
499
+ // Kanaries brand info is not allowed to be removed or changed unless you are granted with special permission.
500
+ <a href="https://docs.kanaries.net" target="_blank">
501
+ <ImageWithFallback
502
+ id="kanaries-logo"
503
+ className="p-1.5 opacity-70 hover:opacity-100"
504
+ src="https://imagedelivery.net/tSvh1MGEu9IgUanmf58srQ/b6bc899f-a129-4c3a-d08f-d406166d0c00/public"
505
+ fallbackSrc={KanariesLogo}
506
+ timeout={1000}
507
+ alt="kanaries"
508
+ />
509
+ </a>
510
+ ),
511
+ },
515
512
  ] as ToolbarItemProps[];
516
513
 
517
514
  const items = builtInItems.filter((item) => typeof item === 'string' || !exclude.includes(item.key));