@evergis/react 3.1.67 → 3.1.69

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.
package/dist/react.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
- import { IconButton, Flex, transition, Chip, Icon, Description, FlexSpan, IconToggle, Popup, Menu, DraggableTree, shadows, Divider, LegendToggler, Tooltip as Tooltip$1, DropdownField, MultiSelectContainer, IconButtonButton, FlatButton, DraggableTreeContainer, Dialog, DialogTitle, DialogContent, WaitingButton, LinearProgress, H2, ThemeProvider, defaultTheme, Preview, Blank, Popover, Expander, darkTheme, UploaderItemArea, UploaderTitleWrapper, Uploader, NumberRangeSlider, useAsyncAutocomplete, AutoComplete, Dropdown, Checkbox, CircularProgress, RangeNumberInput, dateFormat } from '@evergis/uilib-gl';
2
+ import { IconButton, Flex, transition, Chip, Icon, Description, FlexSpan, IconToggle, Popup, Menu, DraggableTree, shadows, Divider, LegendToggler, Tooltip as Tooltip$1, DropdownField, MultiSelectContainer, IconButtonButton, FlatButton, DraggableTreeContainer, Dialog, DialogTitle, DialogContent, CircularProgress, darkTheme, LinearProgress, H2, ThemeProvider, defaultTheme, Preview, Blank, Popover, Expander, UploaderItemArea, UploaderTitleWrapper, Uploader, NumberRangeSlider, useAsyncAutocomplete, AutoComplete, Dropdown, Checkbox, RangeNumberInput, dateFormat } from '@evergis/uilib-gl';
3
3
  import { createContext, memo, useRef, useState, useEffect, useCallback, useContext, useMemo, Fragment } from 'react';
4
4
  import styled, { createGlobalStyle, css, useTheme } from 'styled-components';
5
5
  import { lineChartClassNames, BarChart as BarChart$1, barChartClassNames, LineChart, PieChart } from '@evergis/charts';
@@ -22,6 +22,9 @@ import 'mapbox-gl/dist/mapbox-gl.css';
22
22
  import { Swiper, SwiperSlide } from 'swiper/react';
23
23
  import ReactMarkdown from 'react-markdown';
24
24
  import remarkGfm from 'remark-gfm';
25
+ import { Terminal } from '@xterm/xterm';
26
+ import { FitAddon } from '@xterm/addon-fit';
27
+ import '@xterm/xterm/css/xterm.css';
25
28
 
26
29
  const AddFeatureButton = ({ title, icon = "feature_add" /* , layerName, geometryType*/ }) => {
27
30
  // const [, handleAddFeature] = useFeatureCreator(layerName, geometryType);
@@ -4826,14 +4829,14 @@ const usePythonTask = () => {
4826
4829
  const { api, t } = useGlobalContext();
4827
4830
  const { addSubscription, connection, unsubscribeById } = useServerNotificationsContext();
4828
4831
  const [status, setStatus] = useState(RemoteTaskStatus.Unknown);
4829
- const [result, setResult] = useState(null);
4832
+ const [log, setLog] = useState(null);
4830
4833
  const [error, setError] = useState(null);
4831
4834
  const [loading, setLoading] = useState(false);
4832
4835
  const [executionTime, setExecutionTime] = useState(null);
4833
4836
  const [taskId, setTaskId] = useState(null);
4834
4837
  const [subscriptionId, setSubscriptionId] = useState(null);
4835
4838
  const [isLogDialogOpen, setIsLogDialogOpen] = useState(false);
4836
- const resultRef = useRef(null);
4839
+ const logRef = useRef(null);
4837
4840
  const reset = useCallback(() => {
4838
4841
  setStatus(RemoteTaskStatus.Unknown);
4839
4842
  setError(null);
@@ -4845,7 +4848,7 @@ const usePythonTask = () => {
4845
4848
  const runTask = useCallback(async ({ resourceId, parameters, script, fileName, methodName, }) => {
4846
4849
  reset();
4847
4850
  setStatus(RemoteTaskStatus.Process);
4848
- setResult(null);
4851
+ setLog(null);
4849
4852
  setLoading(true);
4850
4853
  const start = Date.now();
4851
4854
  let prototypeId = await api.remoteTaskManager.createTaskPrototype({
@@ -4870,7 +4873,7 @@ const usePythonTask = () => {
4870
4873
  const { id: newTaskId, success } = await api.remoteTaskManager.startTask1(prototypeId);
4871
4874
  if (!success) {
4872
4875
  setStatus(RemoteTaskStatus.Error);
4873
- setResult(t("taskRunFail", { ns: "devMode", defaultValue: "Не удалось запустить задачу" }));
4876
+ setLog(t("taskRunFail", { ns: "devMode", defaultValue: "Не удалось запустить задачу" }));
4874
4877
  setExecutionTime(Date.now() - start);
4875
4878
  setLoading(false);
4876
4879
  }
@@ -4883,15 +4886,14 @@ const usePythonTask = () => {
4883
4886
  const onNotification = ({ data }) => {
4884
4887
  if (data?.taskId === newTaskId) {
4885
4888
  setStatus(data.status);
4886
- setResult([data.log, resultRef.current].filter(Boolean).join("\n\n"));
4887
- setError(null);
4889
+ setLog([logRef.current, data.log].filter(Boolean).join("\n\n"));
4888
4890
  setExecutionTime(Date.now() - start);
4889
4891
  setLoading(false);
4890
4892
  setTaskId([RemoteTaskStatus.Completed, RemoteTaskStatus.Error].includes(data.status)
4891
- ? undefined
4893
+ ? null
4892
4894
  : newTaskId);
4893
4895
  setSubscriptionId([RemoteTaskStatus.Completed, RemoteTaskStatus.Error].includes(data.status)
4894
- ? undefined
4896
+ ? null
4895
4897
  : newSubscriptionId);
4896
4898
  if ([RemoteTaskStatus.Completed, RemoteTaskStatus.Error].includes(data.status)) {
4897
4899
  unsubscribeById(newSubscriptionId);
@@ -4913,14 +4915,14 @@ const usePythonTask = () => {
4913
4915
  setIsLogDialogOpen(false);
4914
4916
  }, []);
4915
4917
  useEffect(() => {
4916
- resultRef.current = result;
4917
- }, [result]);
4918
+ logRef.current = log;
4919
+ }, [log]);
4918
4920
  return {
4919
4921
  taskId,
4920
4922
  runTask,
4921
4923
  stopTask,
4922
4924
  openLog,
4923
- result,
4925
+ log,
4924
4926
  error,
4925
4927
  status,
4926
4928
  loading,
@@ -6759,36 +6761,138 @@ const UploadContainer = memo(({ type, elementConfig, renderElement }) => {
6759
6761
  return (jsxs(Fragment$1, { children: [jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), isVisible && renderElement({ id: "uploader", wrap: false })] }));
6760
6762
  });
6761
6763
 
6762
- const StatusBadge = styled(Flex) `
6763
- margin-bottom: 1rem;
6764
- padding: 0.5rem 1rem;
6765
- background-color: ${({ $statusColor }) => $statusColor};
6764
+ const StatusBadge = styled(Chip) `
6765
+ background-color: ${({ bgColor }) => bgColor};
6766
6766
  color: ${({ theme }) => theme.palette.iconContrast};
6767
- border-radius: 0.25rem;
6768
6767
  `;
6769
- const LogContainer = styled.div `
6768
+
6769
+ const TerminalWrapper = styled.div `
6770
6770
  flex: 1;
6771
- padding: 1rem;
6772
- background-color: ${({ theme }) => theme.palette.element};
6773
- color: ${({ theme }) => theme.palette.textPrimary};
6774
- font-family: monospace;
6775
- font-size: 0.875rem;
6776
- overflow: auto;
6777
- white-space: pre-wrap;
6778
- word-break: break-word;
6779
- border-radius: 0.25rem;
6780
- max-height: 31.25rem;
6771
+ overflow: hidden;
6772
+ padding: 0;
6773
+ margin: 0;
6774
+ box-sizing: border-box;
6775
+ min-height: 0;
6776
+
6777
+ .xterm-viewport {
6778
+ overflow-y: auto;
6779
+ }
6780
+
6781
+ .xterm-screen .xterm-rows span {
6782
+ letter-spacing: 0 !important;
6783
+ }
6781
6784
  `;
6782
6785
 
6786
+ const LogTerminal = ({ log }) => {
6787
+ const terminalRef = useRef(null);
6788
+ const xtermRef = useRef(null);
6789
+ const fitAddonRef = useRef(null);
6790
+ const previousLogRef = useRef("");
6791
+ const theme = useTheme();
6792
+ // Initialize terminal
6793
+ useEffect(() => {
6794
+ if (!terminalRef.current)
6795
+ return;
6796
+ // Create terminal instance
6797
+ const terminal = new Terminal({
6798
+ cursorBlink: false,
6799
+ fontSize: 12,
6800
+ fontFamily: '"Nunito Sans", sans-serif',
6801
+ scrollback: 10000,
6802
+ convertEol: true,
6803
+ lineHeight: 1.5,
6804
+ letterSpacing: 0,
6805
+ theme: {
6806
+ background: theme.palette.background,
6807
+ foreground: theme.palette.textPrimary,
6808
+ cursor: theme.palette.primary,
6809
+ },
6810
+ });
6811
+ // Create fit addon
6812
+ const fitAddon = new FitAddon();
6813
+ terminal.loadAddon(fitAddon);
6814
+ // Open terminal
6815
+ terminal.open(terminalRef.current);
6816
+ fitAddon.fit();
6817
+ // Store refs
6818
+ xtermRef.current = terminal;
6819
+ fitAddonRef.current = fitAddon;
6820
+ // Handle window resize
6821
+ const handleResize = () => {
6822
+ fitAddon.fit();
6823
+ };
6824
+ window.addEventListener("resize", handleResize);
6825
+ // Cleanup
6826
+ return () => {
6827
+ window.removeEventListener("resize", handleResize);
6828
+ terminal.dispose();
6829
+ xtermRef.current = null;
6830
+ fitAddonRef.current = null;
6831
+ };
6832
+ }, [theme]);
6833
+ // Update log content
6834
+ useEffect(() => {
6835
+ if (!xtermRef.current)
6836
+ return;
6837
+ // Handle different log types
6838
+ if (typeof log === "string") {
6839
+ // For string logs, only write the new content (append mode)
6840
+ const previousLog = previousLogRef.current;
6841
+ if (log !== previousLog) {
6842
+ if (log.startsWith(previousLog)) {
6843
+ // Log is accumulated - write only the new part
6844
+ const newContent = log.substring(previousLog.length);
6845
+ xtermRef.current.write(`${newContent.replace(/\n/g, "\r\n")}\r\n`);
6846
+ }
6847
+ else {
6848
+ // Log was replaced completely - clear and write all
6849
+ xtermRef.current.clear();
6850
+ xtermRef.current.write(`${log.replace(/\n/g, "\r\n")}\r\n`);
6851
+ }
6852
+ previousLogRef.current = log;
6853
+ }
6854
+ }
6855
+ else if (typeof log === "object") {
6856
+ // JSON object (results) - always replace
6857
+ xtermRef.current.clear();
6858
+ const formatted = JSON.stringify(log, null, 2);
6859
+ xtermRef.current.write(formatted.replace(/\n/g, "\r\n"));
6860
+ previousLogRef.current = "";
6861
+ }
6862
+ else if (!log) {
6863
+ // No log - clear terminal
6864
+ xtermRef.current.clear();
6865
+ previousLogRef.current = "";
6866
+ }
6867
+ // Scroll to bottom
6868
+ xtermRef.current.scrollToBottom();
6869
+ }, [log]);
6870
+ // Fit terminal on container resize
6871
+ useEffect(() => {
6872
+ if (!fitAddonRef.current)
6873
+ return;
6874
+ const resizeObserver = new ResizeObserver(() => {
6875
+ fitAddonRef.current?.fit();
6876
+ });
6877
+ if (terminalRef.current) {
6878
+ resizeObserver.observe(terminalRef.current);
6879
+ }
6880
+ return () => {
6881
+ resizeObserver.disconnect();
6882
+ };
6883
+ }, []);
6884
+ return jsx(TerminalWrapper, { ref: terminalRef });
6885
+ };
6886
+
6783
6887
  const STATUS_TRANSLATION_KEYS = {
6784
- [RemoteTaskStatus.Process]: "task.status.process",
6785
- [RemoteTaskStatus.Completed]: "task.status.completed",
6786
- [RemoteTaskStatus.Error]: "task.status.error",
6787
- [RemoteTaskStatus.Unknown]: "task.status.unknown",
6788
- };
6789
- const STATUS_DEFAULT_VALUES = {
6790
- [RemoteTaskStatus.Process]: "Выполняется...",
6791
- [RemoteTaskStatus.Completed]: "Завершено",
6888
+ [RemoteTaskStatus.Process]: "taskProcess",
6889
+ [RemoteTaskStatus.Completed]: "taskCompleted",
6890
+ [RemoteTaskStatus.Error]: "taskError",
6891
+ [RemoteTaskStatus.Unknown]: "taskUnknown",
6892
+ };
6893
+ const STATUS_TITLES = {
6894
+ [RemoteTaskStatus.Process]: "В процессе",
6895
+ [RemoteTaskStatus.Completed]: "Выполнено",
6792
6896
  [RemoteTaskStatus.Error]: "Ошибка",
6793
6897
  [RemoteTaskStatus.Unknown]: "Неизвестно",
6794
6898
  };
@@ -6798,27 +6902,30 @@ const STATUS_COLORS = {
6798
6902
  [RemoteTaskStatus.Error]: "#f44336",
6799
6903
  [RemoteTaskStatus.Unknown]: "#757575",
6800
6904
  };
6905
+ const STATUS_ICONS = {
6906
+ [RemoteTaskStatus.Process]: "loading_circle",
6907
+ [RemoteTaskStatus.Completed]: "success",
6908
+ [RemoteTaskStatus.Error]: "error",
6909
+ [RemoteTaskStatus.Unknown]: "play",
6910
+ };
6801
6911
 
6802
6912
  const LogDialog = ({ isOpen, onClose, logs, status, statusColors }) => {
6803
6913
  const { t } = useGlobalContext();
6804
- const contentRef = useRef(null);
6805
- useEffect(() => {
6806
- if (contentRef.current) {
6807
- contentRef.current.scrollTop = 0;
6808
- }
6809
- }, [logs]);
6810
- const getStatusText = useCallback((status) => {
6914
+ const getStatusTitle = useCallback((status) => {
6811
6915
  const translationKey = STATUS_TRANSLATION_KEYS[status] || STATUS_TRANSLATION_KEYS[RemoteTaskStatus.Unknown];
6812
- const defaultValue = STATUS_DEFAULT_VALUES[status] || STATUS_DEFAULT_VALUES[RemoteTaskStatus.Unknown];
6916
+ const defaultValue = STATUS_TITLES[status] || STATUS_TITLES[RemoteTaskStatus.Unknown];
6813
6917
  return t(translationKey, { ns: "dashboard", defaultValue });
6814
6918
  }, [t]);
6815
6919
  const getStatusColor = useCallback((status) => {
6816
6920
  return statusColors?.[status] || STATUS_COLORS[status] || STATUS_COLORS[RemoteTaskStatus.Unknown];
6817
6921
  }, [statusColors]);
6818
- return (jsxs(Dialog, { isOpen: isOpen, onCloseRequest: onClose, modal: true, maxWidth: "800px", minWidth: "600px", minHeight: "400px", children: [jsx(DialogTitle, { children: jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [jsx("span", { children: t("task.logs.title", { ns: "dashboard", defaultValue: "Логи выполнения задачи" }) }), jsx(IconButton, { kind: "close", onClick: onClose })] }) }), jsx(DialogContent, { children: jsxs(Flex, { flexDirection: "column", height: "100%", marginBottom: "2rem", children: [jsxs(StatusBadge, { "$statusColor": getStatusColor(status), children: [t("task.status.label", { ns: "dashboard", defaultValue: "Статус" }), ": ", getStatusText(status)] }), jsx(LogContainer, { ref: contentRef, children: logs || t("task.logs.empty", { ns: "dashboard", defaultValue: "Логи отсутствуют" }) })] }) })] }));
6922
+ return (jsxs(Dialog, { isOpen: isOpen, onCloseRequest: onClose, modal: true, maxWidth: "800px", minWidth: "600px", minHeight: "600px", children: [jsx(DialogTitle, { children: jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [jsxs(Flex, { alignItems: "center", children: [jsx(FlexSpan, { marginRight: "1rem", children: t("taskLogs", { ns: "dashboard", defaultValue: "Логи выполнения задачи" }) }), jsx(StatusBadge, { text: getStatusTitle(status), bgColor: getStatusColor(status) })] }), jsx(IconButton, { kind: "close", onClick: onClose })] }) }), jsx(DialogContent, { children: jsx(Flex, { flexDirection: "column", height: "100%", marginBottom: "2rem", children: jsx(LogTerminal, { log: logs || t("taskLogsEmpty", { ns: "dashboard", defaultValue: "Логи отсутствуют" }) }) }) })] }));
6819
6923
  };
6820
6924
 
6821
- const StatusWaitingButton = styled(WaitingButton) `
6925
+ const StyledButton = styled(FlatButton) `
6926
+ display: flex;
6927
+ align-items: center;
6928
+
6822
6929
  ${({ status = RemoteTaskStatus.Unknown, statusColors }) => !!statusColors?.[status] && css `
6823
6930
  transition: background-color ${transition.toggle};
6824
6931
  background-color: ${statusColors[status]};
@@ -6829,14 +6936,33 @@ const StatusWaitingButton = styled(WaitingButton) `
6829
6936
  `}
6830
6937
  `;
6831
6938
 
6832
- const TaskContainer = memo(({ type, elementConfig }) => {
6939
+ const StatusWaitingButton = ({ title, icon = "play", status, statusColors, isWaiting, isDisabled, onClick }) => {
6940
+ const { t } = useGlobalContext();
6941
+ const renderTitle = useMemo(() => status === RemoteTaskStatus.Process
6942
+ ? t("", { ns: "dashboard", defaultValue: "Остановить" })
6943
+ : status === RemoteTaskStatus.Unknown ? title : STATUS_TITLES[status], [status, t, title]);
6944
+ const renderIcon = useMemo(() => {
6945
+ const iconComponent = isWaiting ? (jsx(CircularProgress, { diameter: 1, theme: darkTheme }))
6946
+ : status !== RemoteTaskStatus.Unknown
6947
+ ? jsx(Icon, { kind: STATUS_ICONS[status] })
6948
+ : jsx(Icon, { kind: icon });
6949
+ return (jsx(FlexSpan, { marginRight: renderTitle ? "0.5rem" : 0, children: iconComponent }));
6950
+ }, [icon, isWaiting, renderTitle, status]);
6951
+ return (jsxs(StyledButton, { status: status, statusColors: statusColors, disabled: isDisabled, onClick: onClick, children: [renderIcon, renderTitle] }));
6952
+ };
6953
+
6954
+ const TaskContainer = memo(({ type, elementConfig, renderElement }) => {
6833
6955
  const { t, ewktGeometry } = useGlobalContext();
6834
6956
  const { dataSources, filters: selectedFilters } = useWidgetContext(type);
6835
6957
  const { currentPage } = useWidgetPage(type);
6836
- const { taskId, runTask, stopTask, status, openLog, loading, isLogDialogOpen, closeLog, result } = usePythonTask();
6958
+ const { taskId, runTask, stopTask, status, openLog, loading, isLogDialogOpen, closeLog, log } = usePythonTask();
6837
6959
  const { options } = elementConfig || {};
6838
6960
  const { title, relatedResources, center, icon, statusColors } = options || {};
6839
6961
  const onClick = useCallback(async () => {
6962
+ if (taskId) {
6963
+ await stopTask();
6964
+ return;
6965
+ }
6840
6966
  await Promise.all(relatedResources.map(({ resourceId, parameters, script, fileName, methodName }) => {
6841
6967
  const newParams = applyQueryFilters({
6842
6968
  parameters,
@@ -6847,8 +6973,8 @@ const TaskContainer = memo(({ type, elementConfig }) => {
6847
6973
  });
6848
6974
  return runTask({ resourceId, parameters: newParams, script, fileName, methodName });
6849
6975
  }));
6850
- }, [currentPage.filters, dataSources, ewktGeometry, relatedResources, runTask, selectedFilters]);
6851
- return (jsx(Fragment$1, { children: jsxs(Flex, { justifyContent: center ? "center" : "flex-start", children: [jsxs(StatusWaitingButton, { primary: true, status: status, statusColors: statusColors, isWaiting: loading, disabled: !relatedResources?.length, onClick: onClick, children: [icon && jsx(FlexSpan, { marginRight: "0.5rem", children: jsx(Icon, { kind: icon }) }), title || t("run", { ns: "dashboard", defaultValue: "Запуск" })] }), !!taskId && (jsx(IconButton, { kind: "stop", onClick: stopTask })), !!result && (jsxs(Fragment$1, { children: [jsx(IconButton, { kind: "info", onClick: openLog }), jsx(LogDialog, { isOpen: isLogDialogOpen, onClose: closeLog, logs: result, status: status, statusColors: statusColors })] }))] }) }));
6976
+ }, [currentPage.filters, dataSources, ewktGeometry, relatedResources, runTask, selectedFilters, stopTask, taskId]);
6977
+ return (jsxs(Fragment$1, { children: [jsx(ExpandableTitle, { elementConfig: elementConfig, type: type, renderElement: renderElement }), jsxs(Flex, { justifyContent: center ? "center" : "flex-start", children: [jsx(StatusWaitingButton, { title: title || t("run", { ns: "dashboard", defaultValue: "Запуск" }), icon: icon, status: status, statusColors: statusColors, isWaiting: loading || !!taskId, isDisabled: !relatedResources?.length, onClick: onClick }), !!(log || taskId) && (jsxs(Fragment$1, { children: [jsx(IconButton, { kind: "info", onClick: openLog }), jsx(LogDialog, { logs: log, status: status, statusColors: statusColors, isOpen: isLogDialogOpen, onClose: closeLog })] }))] })] }));
6852
6978
  });
6853
6979
 
6854
6980
  const containerComponents = {
@@ -7678,27 +7804,30 @@ const StyledIcon = styled(Icon) `
7678
7804
 
7679
7805
  const ElementIcon = memo(({ type, elementConfig }) => {
7680
7806
  const { attributes } = useWidgetContext(type);
7681
- const { value, attributeName, options } = elementConfig || {};
7807
+ const { value, attributeName, options, style } = elementConfig || {};
7682
7808
  const { fontSize, fontColor } = options || {};
7683
7809
  const iconValue = useMemo(() => (attributeName ? attributes?.find(item => item.name === attributeName)?.value : value), [attributeName, attributes, value]);
7684
- return jsx(StyledIcon, { kind: iconValue, fontSize: fontSize, fontColor: fontColor });
7810
+ return jsx(StyledIcon, { kind: iconValue, fontSize: fontSize, fontColor: fontColor, style: style });
7685
7811
  });
7686
7812
 
7687
7813
  const ElementImage = memo(({ type, elementConfig }) => {
7688
7814
  const { attributes } = useWidgetContext(type);
7689
- const { value, attributeName, options } = elementConfig || {};
7815
+ const { value, attributeName, options, style } = elementConfig || {};
7690
7816
  const { width } = options || {};
7691
7817
  const firstImage = useMemo(() => {
7692
7818
  if (value) {
7693
- return value.toString();
7819
+ return getResourceUrl(value.toString());
7694
7820
  }
7695
7821
  if (!attributeName || Array.isArray(attributeName)) {
7696
7822
  return null;
7697
7823
  }
7698
7824
  const attribute = attributes?.find(item => item.name === attributeName);
7699
- return attribute?.value?.split(";")?.[0];
7825
+ const imageUrl = attribute?.value?.split(";")?.[0];
7826
+ if (!imageUrl)
7827
+ return null;
7828
+ return getResourceUrl(imageUrl);
7700
7829
  }, [attributeName, attributes, value]);
7701
- return firstImage ? jsx("img", { src: firstImage, alt: firstImage, width: width }) : null;
7830
+ return firstImage ? jsx("img", { src: firstImage, alt: firstImage, width: width, style: style }) : null;
7702
7831
  });
7703
7832
 
7704
7833
  const ElementLegend = memo(({ type, element, elementConfig }) => {