@centreon/ui 23.10.1-MON-20802-fe-implement-new-visualization.2 → 23.10.1-MON-21543-implement-top-bottom-widget.0

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/Dashboard/Dashboard.styles.ts +6 -1
  3. package/src/Dashboard/Item.tsx +4 -1
  4. package/src/Dashboard/Layout.tsx +1 -1
  5. package/src/Graph/Gauge/Gauge.stories.tsx +10 -0
  6. package/src/Graph/Gauge/Gauge.tsx +10 -1
  7. package/src/Graph/Gauge/PieData.tsx +8 -8
  8. package/src/Graph/Gauge/ResponsiveGauge.tsx +32 -23
  9. package/src/Graph/Gauge/Thresholds.tsx +2 -25
  10. package/src/Graph/Gauge/models.ts +1 -0
  11. package/src/Graph/SingleBar/ResponsiveSingleBar.tsx +41 -24
  12. package/src/Graph/SingleBar/SingleBar.stories.tsx +46 -0
  13. package/src/Graph/SingleBar/ThresholdLine.tsx +31 -12
  14. package/src/Graph/SingleBar/Thresholds.tsx +5 -1
  15. package/src/Graph/SingleBar/models.ts +4 -0
  16. package/src/Graph/Text/Text.stories.tsx +13 -0
  17. package/src/Graph/Text/Text.tsx +19 -12
  18. package/src/Graph/common/timeSeries/index.test.ts +106 -3
  19. package/src/Graph/common/timeSeries/index.ts +67 -4
  20. package/src/Graph/common/timeSeries/models.ts +6 -3
  21. package/src/Graph/common/utils.ts +4 -2
  22. package/src/InputField/Select/Autocomplete/Multi/index.tsx +4 -2
  23. package/src/InputField/Select/Autocomplete/index.tsx +3 -1
  24. package/src/InputField/Text/index.tsx +10 -7
  25. package/src/RichTextEditor/ContentEditable.tsx +0 -1
  26. package/src/RichTextEditor/RichTextEditor.cypress.spec.tsx +5 -13
  27. package/src/RichTextEditor/RichTextEditor.tsx +5 -2
  28. package/src/RichTextEditor/plugins/AutoLinkPlugin/index.tsx +40 -1
  29. package/src/RichTextEditor/plugins/FloatingLinkEditorPlugin.tsx +33 -8
  30. package/src/RichTextEditor/plugins/ToolbarPlugin/index.tsx +10 -4
  31. package/src/ThemeProvider/index.tsx +6 -4
  32. package/src/TopCounterElements/index.tsx +0 -1
  33. package/src/api/useFetchQuery/index.ts +10 -8
  34. package/src/components/Inputs/Switch/Switch.stories.tsx +22 -0
  35. package/src/components/Inputs/Switch/Switch.styles.ts +13 -0
  36. package/src/components/Inputs/Switch/Switch.tsx +18 -0
  37. package/src/components/Inputs/index.ts +1 -0
  38. package/src/components/Modal/Modal.styles.ts +3 -1
  39. package/src/components/index.ts +1 -0
@@ -4,7 +4,7 @@ import { Typography, useTheme } from '@mui/material';
4
4
 
5
5
  import { LineChartData, Thresholds } from '../common/models';
6
6
  import {
7
- formatMetricValue,
7
+ formatMetricValueWithUnit,
8
8
  getMetricWithLatestData
9
9
  } from '../common/timeSeries';
10
10
  import { getColorFromDataAndTresholds } from '../common/utils';
@@ -12,7 +12,9 @@ import { getColorFromDataAndTresholds } from '../common/utils';
12
12
  import { useTextStyles } from './Text.styles';
13
13
 
14
14
  interface Props {
15
+ baseColor?: string;
15
16
  data?: LineChartData;
17
+ displayAsRaw?: boolean;
16
18
  labels: {
17
19
  critical: string;
18
20
  warning: string;
@@ -23,7 +25,9 @@ interface Props {
23
25
  export const Text = ({
24
26
  thresholds,
25
27
  data,
26
- labels
28
+ displayAsRaw,
29
+ labels,
30
+ baseColor
27
31
  }: Props): JSX.Element | null => {
28
32
  const theme = useTheme();
29
33
  const { classes } = useTextStyles();
@@ -37,20 +41,21 @@ export const Text = ({
37
41
  const metricUnit = metric?.unit ?? '';
38
42
 
39
43
  const color = getColorFromDataAndTresholds({
44
+ baseColor,
40
45
  data: metricValue,
41
46
  theme,
42
47
  thresholds
43
48
  });
44
49
 
45
50
  const warningThresholdLabels = thresholds.warning.map(({ value }) =>
46
- formatMetricValue({
51
+ formatMetricValueWithUnit({
47
52
  unit: metricUnit,
48
53
  value
49
54
  })
50
55
  );
51
56
 
52
57
  const criticalThresholdLabels = thresholds.critical.map(({ value }) =>
53
- formatMetricValue({
58
+ formatMetricValueWithUnit({
54
59
  unit: metricUnit,
55
60
  value
56
61
  })
@@ -58,24 +63,26 @@ export const Text = ({
58
63
 
59
64
  return (
60
65
  <div className={classes.graphText}>
61
- <Typography sx={{ color }} variant="h3">
62
- {formatMetricValue({
63
- unit: metricUnit,
64
- value: metricValue
65
- })}{' '}
66
- {metricUnit}
66
+ <Typography sx={{ color }} variant="h2">
67
+ <strong>
68
+ {formatMetricValueWithUnit({
69
+ isRaw: displayAsRaw,
70
+ unit: metricUnit,
71
+ value: metricValue
72
+ })}
73
+ </strong>
67
74
  </Typography>
68
75
  {thresholds.enabled && (
69
76
  <div className={classes.thresholds}>
70
77
  <Typography sx={{ color: 'warning.main' }} variant="h5">
71
78
  {labels.warning}
72
79
  {': '}
73
- {warningThresholdLabels.join(' - ')} {metricUnit}
80
+ {warningThresholdLabels.join(' - ')}
74
81
  </Typography>
75
82
  <Typography sx={{ color: 'error.main' }} variant="h5">
76
83
  {labels.critical}
77
84
  {': '}
78
- {criticalThresholdLabels.join(' - ')} {metricUnit}
85
+ {criticalThresholdLabels.join(' - ')}
79
86
  </Typography>
80
87
  </div>
81
88
  )}
@@ -335,10 +335,10 @@ describe('timeSeries', () => {
335
335
  describe(timeSeries.formatMetricValue, () => {
336
336
  const cases: Array<TestCase> = [
337
337
  [218857269, '', 1000, '218.86m'],
338
- [218857269, '', 1024, '208.72M'],
338
+ [218857269, '', 1024, '208.72 M'],
339
339
  [0.12232323445, '', 1000, '0.12'],
340
- [1024, 'B', 1000, '1K'],
341
- [1024, 'B', 1024, '1K'],
340
+ [1024, 'B', 1000, '1 KB'],
341
+ [1024, 'B', 1024, '1 KB'],
342
342
  [null, 'B', 1024, null]
343
343
  ];
344
344
 
@@ -504,3 +504,106 @@ describe('timeSeries', () => {
504
504
  });
505
505
  });
506
506
  });
507
+
508
+ describe('Format value with unit', () => {
509
+ const units = [
510
+ 'B',
511
+ 'bytes',
512
+ 'bytespersecond',
513
+ 'B/s',
514
+ 'B/sec',
515
+ 'o',
516
+ 'octets',
517
+ 'b/s',
518
+ 'b',
519
+ 'ms',
520
+ '%',
521
+ ''
522
+ ];
523
+
524
+ const getExpectedResult = (unit): string => {
525
+ if (unit === '') {
526
+ return '324.23m';
527
+ }
528
+
529
+ return `309.21 M${unit}`;
530
+ };
531
+
532
+ const humanReadableTestCases = units.map((unit) => {
533
+ if (unit === '%') {
534
+ return {
535
+ expectedResult: '45.56%',
536
+ unit,
537
+ value: 45.55678
538
+ };
539
+ }
540
+
541
+ if (unit === 'ms') {
542
+ return {
543
+ expectedResult: '34.23 seconds',
544
+ unit,
545
+ value: 34232
546
+ };
547
+ }
548
+
549
+ return {
550
+ expectedResult: getExpectedResult(unit),
551
+ unit,
552
+ value: 324234232.34233
553
+ };
554
+ });
555
+
556
+ const rawTestCases = units.map((unit) => {
557
+ if (unit === '%') {
558
+ return {
559
+ expectedResult: '45.55678%',
560
+ unit,
561
+ value: 45.55678
562
+ };
563
+ }
564
+
565
+ if (unit === 'ms') {
566
+ return {
567
+ expectedResult: '34232 ms',
568
+ unit,
569
+ value: 34232
570
+ };
571
+ }
572
+
573
+ return {
574
+ expectedResult:
575
+ unit === '' ? '324234232.34233 ' : `324234232.34233 ${unit}`,
576
+ unit,
577
+ value: 324234232.34233
578
+ };
579
+ });
580
+
581
+ describe('Format the value as human readable', () => {
582
+ it.each(humanReadableTestCases)(
583
+ 'formats the value with $unit',
584
+ ({ value, unit, expectedResult }) => {
585
+ expect(
586
+ timeSeries.formatMetricValueWithUnit({
587
+ unit,
588
+ value
589
+ })
590
+ ).toEqual(expectedResult);
591
+ }
592
+ );
593
+ });
594
+
595
+ describe('Format the value as raw', () => {
596
+ it.each(rawTestCases)(
597
+ 'formats the value with $unit',
598
+ ({ value, unit, expectedResult }) => {
599
+ expect(
600
+ timeSeries.formatMetricValueWithUnit({
601
+ isRaw: true,
602
+ unit,
603
+ value
604
+ })
605
+ ).toEqual(expectedResult);
606
+ }
607
+ );
608
+ });
609
+ });
@@ -28,7 +28,10 @@ import {
28
28
  lt,
29
29
  identity,
30
30
  head,
31
- last
31
+ last,
32
+ cond,
33
+ always,
34
+ T
32
35
  } from 'ramda';
33
36
 
34
37
  import { margin } from '../../LineChart/common';
@@ -440,6 +443,37 @@ const getRightScale = ({
440
443
  });
441
444
  };
442
445
 
446
+ const formatTime = (value: number): string => {
447
+ if (value < 1000) {
448
+ return `${numeral(value).format('0.[00]a')} ms`;
449
+ }
450
+
451
+ const t = numeral(value / 1000).format('0.[00]a');
452
+
453
+ return `${t} seconds`;
454
+ };
455
+
456
+ const registerMsUnitToNumeral = (): null => {
457
+ try {
458
+ numeral.register('format', 'milliseconds', {
459
+ format: (value) => {
460
+ return formatTime(value);
461
+ },
462
+ regexps: {
463
+ format: /(ms)/,
464
+ unformat: /(ms)/
465
+ },
466
+ unformat: () => ''
467
+ });
468
+
469
+ return null;
470
+ } catch (_) {
471
+ return null;
472
+ }
473
+ };
474
+
475
+ registerMsUnitToNumeral();
476
+
443
477
  const formatMetricValue = ({
444
478
  value,
445
479
  unit,
@@ -463,11 +497,14 @@ const formatMetricValue = ({
463
497
 
464
498
  const base1024 = base2Units.includes(unit) || Number(base) === 1024;
465
499
 
466
- const formatSuffix = base1024 ? ' ib' : 'a';
500
+ const formatSuffix = cond([
501
+ [equals('ms'), always(' ms')],
502
+ [T, always(base1024 ? ' ib' : 'a')]
503
+ ])(unit);
467
504
 
468
505
  const formattedMetricValue = numeral(Math.abs(value))
469
506
  .format(`0.[00]${formatSuffix}`)
470
- .replace(/\s|i|B/g, '');
507
+ .replace(/iB/g, unit);
471
508
 
472
509
  if (lt(value, 0)) {
473
510
  return `-${formattedMetricValue}`;
@@ -476,6 +513,31 @@ const formatMetricValue = ({
476
513
  return formattedMetricValue;
477
514
  };
478
515
 
516
+ const formatMetricValueWithUnit = ({
517
+ value,
518
+ unit,
519
+ base = 1000,
520
+ isRaw = false
521
+ }: FormatMetricValueProps & { isRaw?: boolean }): string | null => {
522
+ if (isNil(value)) {
523
+ return null;
524
+ }
525
+
526
+ if (isRaw) {
527
+ const unitText = equals('%', unit) ? unit : ` ${unit}`;
528
+
529
+ return `${value}${unitText}`;
530
+ }
531
+
532
+ if (equals('%', unit)) {
533
+ return `${numeral(Math.abs(value)).format('0.[00]')}%`;
534
+ }
535
+
536
+ const formattedMetricValue = formatMetricValue({ base, unit, value });
537
+
538
+ return formattedMetricValue;
539
+ };
540
+
479
541
  const getStackedYScale = ({
480
542
  leftScale,
481
543
  rightScale
@@ -557,5 +619,6 @@ export {
557
619
  getStackedYScale,
558
620
  getTimeValue,
559
621
  bisectDate,
560
- getMetricWithLatestData
622
+ getMetricWithLatestData,
623
+ formatMetricValueWithUnit
561
624
  };
@@ -14,14 +14,17 @@ interface DsData {
14
14
  export interface Metric {
15
15
  average_value: number | null;
16
16
  crit: number | null;
17
- data: Array<number>;
18
- ds_data: DsData;
17
+ critical_high_threshold: number | null;
18
+ critical_low_threshold: number | null;
19
+ data: Array<number | null>;
20
+ ds_data?: DsData;
19
21
  legend: string;
20
22
  maximum_value: number | null;
21
23
  metric: string;
22
24
  minimum_value: number | null;
23
25
  unit: string;
24
- warn: number | null;
26
+ warning_high_threshold: number | null;
27
+ warning_low_threshold: number | null;
25
28
  }
26
29
 
27
30
  type TimeSeries = { timeTick: string };
@@ -17,6 +17,7 @@ import { Theme } from '@mui/material';
17
17
  import { Thresholds } from './models';
18
18
 
19
19
  interface GetColorFromDataAndThresholdsProps {
20
+ baseColor?: string;
20
21
  data: number;
21
22
  theme: Theme;
22
23
  thresholds: Thresholds;
@@ -25,10 +26,11 @@ interface GetColorFromDataAndThresholdsProps {
25
26
  export const getColorFromDataAndTresholds = ({
26
27
  data,
27
28
  thresholds,
28
- theme
29
+ theme,
30
+ baseColor
29
31
  }: GetColorFromDataAndThresholdsProps): string => {
30
32
  if (!thresholds.enabled) {
31
- return theme.palette.success.main;
33
+ return baseColor || theme.palette.primary.main;
32
34
  }
33
35
 
34
36
  const criticalValues = pluck('value', thresholds.critical).sort();
@@ -1,7 +1,7 @@
1
1
  import { includes, map, prop, reject } from 'ramda';
2
2
  import { makeStyles } from 'tss-react/mui';
3
3
 
4
- import { Chip } from '@mui/material';
4
+ import { Chip, ChipProps } from '@mui/material';
5
5
  import { UseAutocompleteProps } from '@mui/material/useAutocomplete';
6
6
 
7
7
  import Autocomplete, { Props as AutocompleteProps } from '..';
@@ -18,7 +18,6 @@ const useStyles = makeStyles()((theme) => ({
18
18
  width: theme.spacing(1.5)
19
19
  },
20
20
  tag: {
21
- backgroundColor: theme.palette.divider,
22
21
  fontSize: theme.typography.caption.fontSize
23
22
  }
24
23
  }));
@@ -33,6 +32,7 @@ export interface Props
33
32
  UseAutocompleteProps<SelectEntry, Multiple, DisableClearable, FreeSolo>,
34
33
  'multiple'
35
34
  > {
35
+ chipProps?: ChipProps;
36
36
  disableSortedOptions?: boolean;
37
37
  getTagLabel?: (option) => string;
38
38
  optionProperty?: string;
@@ -45,6 +45,7 @@ const MultiAutocompleteField = ({
45
45
  optionProperty = 'name',
46
46
  getOptionLabel = (option): string => option.name,
47
47
  getTagLabel = (option): string => option[optionProperty],
48
+ chipProps,
48
49
  ...props
49
50
  }: Props): JSX.Element => {
50
51
  const { classes } = useStyles();
@@ -60,6 +61,7 @@ const MultiAutocompleteField = ({
60
61
  label={getTagLabel(option)}
61
62
  size="medium"
62
63
  {...getTagProps({ index })}
64
+ {...chipProps}
63
65
  />
64
66
  ));
65
67
 
@@ -32,6 +32,7 @@ export type Props = {
32
32
  displayPopupIcon?: boolean;
33
33
  endAdornment?: React.ReactElement;
34
34
  error?: string;
35
+ getOptionItemLabel?: (option) => string;
35
36
  hideInput?: boolean;
36
37
  label: string;
37
38
  loading?: boolean;
@@ -158,6 +159,7 @@ const AutocompleteField = ({
158
159
  autoSize = false,
159
160
  autoSizeDefaultWidth = 0,
160
161
  autoSizeCustomPadding,
162
+ getOptionItemLabel = (option) => option.name,
161
163
  ...autocompleteProps
162
164
  }: Props): JSX.Element => {
163
165
  const { classes, cx } = useStyles({ hideInput });
@@ -249,7 +251,7 @@ const AutocompleteField = ({
249
251
  <Option
250
252
  thumbnailUrl={displayOptionThumbnail ? option.url : undefined}
251
253
  >
252
- {option.name}
254
+ {getOptionItemLabel(option)}
253
255
  </Option>
254
256
  </li>
255
257
  );
@@ -1,6 +1,6 @@
1
1
  import { forwardRef, useCallback } from 'react';
2
2
 
3
- import { isNil } from 'ramda';
3
+ import { equals, isNil } from 'ramda';
4
4
  import { makeStyles } from 'tss-react/mui';
5
5
 
6
6
  import {
@@ -9,7 +9,8 @@ import {
9
9
  TextFieldProps,
10
10
  Theme,
11
11
  Tooltip,
12
- Typography
12
+ Typography,
13
+ Box
13
14
  } from '@mui/material';
14
15
 
15
16
  import { getNormalizedId } from '../../utils';
@@ -18,8 +19,9 @@ import useAutoSize from './useAutoSize';
18
19
 
19
20
  const useStyles = makeStyles<{ displayAsBlock: boolean }>()(
20
21
  (theme: Theme, { displayAsBlock }) => ({
21
- compact: {
22
- fontSize: 'x-small'
22
+ autoSizeCompact: {
23
+ paddingRight: theme.spacing(1),
24
+ paddingTop: theme.spacing(0.6)
23
25
  },
24
26
  hiddenText: {
25
27
  display: 'table',
@@ -140,7 +142,7 @@ const TextField = forwardRef(
140
142
  }, [innerValue, debounced, defaultValue]);
141
143
 
142
144
  return (
143
- <>
145
+ <Box sx={{ width: autoSize ? 'auto' : '100%' }}>
144
146
  <Tooltip placement="top" title={tooltipTitle}>
145
147
  <MuiTextField
146
148
  data-testid={dataTestId}
@@ -162,7 +164,8 @@ const TextField = forwardRef(
162
164
  className: cx(
163
165
  classes.inputBase,
164
166
  {
165
- [classes.transparent]: transparent
167
+ [classes.transparent]: transparent,
168
+ [classes.autoSizeCompact]: autoSize && equals(size, 'compact')
166
169
  },
167
170
  className
168
171
  ),
@@ -195,7 +198,7 @@ const TextField = forwardRef(
195
198
  {rest.value || externalValueForAutoSize || innerValue}
196
199
  </Typography>
197
200
  )}
198
- </>
201
+ </Box>
199
202
  );
200
203
  }
201
204
  );
@@ -160,7 +160,6 @@ const ContentEditable = ({
160
160
  className,
161
161
  isFocused && classes.inputFocused
162
162
  )}
163
- id={namespace}
164
163
  >
165
164
  {editable && isTextEmpty && (
166
165
  <Typography className={classes.placeholder}>
@@ -56,7 +56,7 @@ describe('Rich Text Editor', () => {
56
56
  .should('be.visible')
57
57
  .and('have.value', '');
58
58
 
59
- cy.get('#RichTextEditor').contains('Type here...');
59
+ cy.findByLabelText('RichTextEditor').parent().contains('Type here...');
60
60
  });
61
61
 
62
62
  it('displays changes with undo redo buttons', () => {
@@ -187,11 +187,6 @@ describe('Rich Text Editor', () => {
187
187
  });
188
188
 
189
189
  it('displays editor when editable props is false and an initialState exist', () => {
190
- cy.get('[data-testid="RichTextEditor"]').should(
191
- 'have.attr',
192
- 'contenteditable',
193
- 'false'
194
- );
195
190
  cy.findByLabelText('Undo').should('not.exist');
196
191
  cy.findByLabelText('Redo').should('not.exist');
197
192
  cy.findByLabelText('bold').should('not.exist');
@@ -216,11 +211,6 @@ describe('Rich Text Editor', () => {
216
211
  });
217
212
 
218
213
  it('displays editor when editable props is false and an initialState exist', () => {
219
- cy.get('[data-testid="RichTextEditor"]').should(
220
- 'have.attr',
221
- 'contenteditable',
222
- 'false'
223
- );
224
214
  cy.findByLabelText('Undo').should('be.disabled');
225
215
  cy.findByLabelText('Redo').should('be.disabled');
226
216
  cy.findByLabelText('bold').should('be.disabled');
@@ -245,12 +235,14 @@ describe('Rich Text Editor', () => {
245
235
  });
246
236
  });
247
237
  it('displays editor with custom namespace', () => {
248
- cy.get('#CypressComponentTest > p').should('be.exist');
238
+ cy.get('[data-testid="CypressComponentTest"] > p').should('be.exist');
249
239
  cy.get('[data-testid="CypressComponentTest"]').should('be.exist');
250
240
  });
251
241
 
252
242
  it('displays editor with custom placeholder', () => {
253
- cy.get('#CypressComponentTest > p').contains('Type Cypress test...');
243
+ cy.get('[data-testid="CypressComponentTest"]')
244
+ .parent()
245
+ .contains('Type Cypress test...');
254
246
  });
255
247
 
256
248
  it('displays editor with custom minimum input height', () => {
@@ -23,6 +23,7 @@ import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin';
23
23
  export interface RichTextEditorProps {
24
24
  contentClassName?: string;
25
25
  disabled?: boolean;
26
+ displayBlockButtons?: boolean;
26
27
  displayMacrosButton?: boolean;
27
28
  editable: boolean;
28
29
  editorState?: string;
@@ -132,9 +133,10 @@ const RichTextEditor = ({
132
133
  onBlur,
133
134
  contentClassName,
134
135
  displayMacrosButton = false,
135
- disabled,
136
+ disabled = false,
136
137
  openLinkInNewTab = true,
137
- initialize
138
+ initialize,
139
+ displayBlockButtons = true
138
140
  }: RichTextEditorProps): JSX.Element => {
139
141
  const { classes } = useStyles({ toolbarPositions });
140
142
 
@@ -174,6 +176,7 @@ const RichTextEditor = ({
174
176
  <div className={classes.toolbar}>
175
177
  <ToolbarPlugin
176
178
  disabled={disabled}
179
+ displayBlockButtons={displayBlockButtons}
177
180
  displayMacrosButton={displayMacrosButton}
178
181
  editable={editable}
179
182
  />
@@ -1,7 +1,11 @@
1
+ import { useEffect, useState } from 'react';
2
+
1
3
  import {
2
4
  AutoLinkPlugin,
3
5
  LinkMatcher
4
6
  } from '@lexical/react/LexicalAutoLinkPlugin';
7
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
8
+ import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, FOCUS_COMMAND } from 'lexical';
5
9
 
6
10
  interface LinkAttributes {
7
11
  rel?: null | string;
@@ -56,7 +60,42 @@ const AutoCompleteLinkPlugin = ({
56
60
  openLinkInNewTab
57
61
  }: {
58
62
  openLinkInNewTab: boolean;
59
- }): JSX.Element => {
63
+ }): JSX.Element | null => {
64
+ const [editor] = useLexicalComposerContext();
65
+ const [hasFocus, setFocus] = useState(false);
66
+
67
+ useEffect(
68
+ () =>
69
+ editor.registerCommand(
70
+ BLUR_COMMAND,
71
+ () => {
72
+ setFocus(false);
73
+
74
+ return false;
75
+ },
76
+ COMMAND_PRIORITY_LOW
77
+ ),
78
+ []
79
+ );
80
+
81
+ useEffect(
82
+ () =>
83
+ editor.registerCommand(
84
+ FOCUS_COMMAND,
85
+ () => {
86
+ setFocus(true);
87
+
88
+ return false;
89
+ },
90
+ COMMAND_PRIORITY_LOW
91
+ ),
92
+ []
93
+ );
94
+
95
+ if (!hasFocus) {
96
+ return null;
97
+ }
98
+
60
99
  return <AutoLinkPlugin matchers={getMatchers(openLinkInNewTab)} />;
61
100
  };
62
101