Merge pull request #685 from smartxworks/feat/event-history

feat(Editor): add events history
This commit is contained in:
tanbowensg 2023-03-20 16:19:32 +08:00 committed by GitHub
commit e5c452b46a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 425 additions and 139 deletions

View File

@ -386,6 +386,7 @@ export const Table = implementRuntimeComponent({
componentId: handler.componentId,
name: handler.method.name,
parameters: handler.method.parameters || {},
triggerId: component.id,
});
});
};

View File

@ -88,11 +88,13 @@ export default implementRuntimeComponent({
services.apiService.send('uiMethod', {
componentId: inputId,
name: 'resetInputValue',
triggerId: component.id,
});
});
},
});
}, [
component.id,
formControlIds,
services.apiService,
services.stateManager.store,

View File

@ -90,6 +90,7 @@ export const TableTd: React.FC<{
componentId: evaledHandler.componentId,
name: evaledHandler.method.name,
parameters: evaledHandler.method.parameters,
triggerId: component.id,
});
});
};

View File

@ -74,6 +74,7 @@ export const ApiForm: React.FC<Props> = props => {
componentId: component.id,
name: 'triggerFetch',
parameters: {},
triggerId: component.id,
});
}, [services.apiService, component]);
const onMethodChange = useCallback(

View File

@ -1,129 +0,0 @@
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
import {
Badge,
Button,
HStack,
IconButton,
Table,
Tbody,
Td,
Text,
Th,
Thead,
Tr,
VStack,
} from '@chakra-ui/react';
import { observer } from 'mobx-react-lite';
import React, { useMemo } from 'react';
import { EditorServices } from '../types';
import { Pagination } from './Pagination';
type Props = {
services: EditorServices;
};
export const WarningArea: React.FC<Props> = observer(({ services }) => {
const { editorStore } = services;
const [isCollapsed, setIsCollapsed] = React.useState(true);
const [currPage, setCurrPage] = React.useState(0);
const PageSize = 5;
const { validateResult, setSelectedComponentId } = editorStore;
const errorItems = useMemo(() => {
if (isCollapsed) {
return null;
}
return validateResult
.slice(currPage * PageSize, currPage * PageSize + PageSize)
.map((result, i) => {
return (
<Tr key={i}>
<Td
cursor="pointer"
fontWeight="bold"
onClick={() => setSelectedComponentId(result.componentId)}
>
{result.componentId}
</Td>
<Td>{result.traitType || '-'}</Td>
<Td>{result.property || '-'}</Td>
<Td>{result.message}</Td>
</Tr>
);
});
}, [currPage, isCollapsed, setSelectedComponentId, validateResult]);
const savedBadge = useMemo(() => {
return <Badge colorScheme="green">Saved</Badge>;
}, []);
const unsaveBadge = useMemo(() => {
return (
<HStack>
<Button
colorScheme="red"
variant="ghost"
size="sm"
onClick={() => editorStore.saveCurrentComponents()}
>
Save anyway
</Button>
<Badge colorScheme="red">Unsave</Badge>
</HStack>
);
}, [editorStore]);
return (
<VStack
position="absolute"
bottom="0"
left="0"
right="0"
paddingY="2"
paddingX="4"
boxShadow="0 0 4px rgba(0, 0, 0, 0.1)"
background="white"
zIndex="1"
>
<HStack width="full" justifyContent="space-between">
<Text fontSize="md" fontWeight="bold">
Errors
<Badge ml="1" fontSize="0.8em" colorScheme="red">
{editorStore.validateResult.length}
</Badge>
</Text>
<HStack>
{editorStore.isSaved ? savedBadge : unsaveBadge}
<IconButton
aria-label="show errors"
size="sm"
variant="ghost"
icon={isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
onClick={() => setIsCollapsed(prev => !prev)}
/>
</HStack>
</HStack>
<VStack
display={isCollapsed ? 'none' : 'block'}
width="full"
justifyContent="start"
>
<Table size="sm" width="full" maxHeight="200px" overflow="auto">
<Thead>
<Tr>
<Th>Component Id</Th>
<Th>Trait Type</Th>
<Th>Property</Th>
<Th>Message</Th>
</Tr>
</Thead>
<Tbody>{errorItems}</Tbody>
</Table>
<Pagination
currentPage={currPage}
lastPage={Math.ceil(validateResult.length / 5)}
handlePageClick={page => setCurrPage(page)}
/>
</VStack>
</VStack>
);
});

View File

@ -0,0 +1,46 @@
import { Props } from './type';
import React from 'react';
import { DebugTable } from './Table';
import { Box } from '@chakra-ui/react';
export const ErrorLogs: React.FC<Props> = ({ services }) => {
const { validateResult, setSelectedComponentId } = services.editorStore;
const errorColumns = [
{
title: 'Component Id',
dataIndex: 'componentId',
render: (_col: any, item: any) => {
return (
<Box
cursor="pointer"
fontWeight="bold"
onClick={() => setSelectedComponentId(item.componentId)}
>
{item.componentId}
</Box>
);
},
},
{
title: 'Trait Type',
dataIndex: 'traitType',
},
{
title: 'Property',
dataIndex: 'property',
},
{
title: 'Message',
dataIndex: 'message',
},
];
return (
<DebugTable
data={validateResult}
pagination={{ hideOnSinglePage: true }}
columns={errorColumns}
emptyMessage="No Errors"
/>
);
};

View File

@ -0,0 +1,99 @@
import { Props, EventLog } from './type';
import React from 'react';
import { DebugTable } from './Table';
import { Box, Button, Tooltip } from '@chakra-ui/react';
import { css } from '@emotion/css';
type EventLogsProps = Props & {
events: EventLog[];
setEventLogs: React.Dispatch<React.SetStateAction<EventLog[]>>;
};
export const EventLogs: React.FC<EventLogsProps> = ({
services,
events,
setEventLogs,
}) => {
const { setSelectedComponentId } = services.editorStore;
const eventColumns = [
{
title: 'Time',
dataIndex: 'time',
},
{
title: 'Event Type',
dataIndex: 'type',
},
{
title: 'Target',
dataIndex: 'target',
render: (_col: any, item: any) => {
return (
<Box
cursor="pointer"
fontWeight="bold"
onClick={() => setSelectedComponentId(item.target)}
>
{item.target}
</Box>
);
},
},
{
title: 'Method',
dataIndex: 'methodName',
},
{
title: 'Parameters',
dataIndex: 'parameters',
render: (_col: any, item: any) => {
const parameters = JSON.stringify(item.parameters || '');
return (
<Tooltip label={parameters}>
<span>{parameters.length > 16 ? '...' : parameters}</span>
</Tooltip>
);
},
},
{
title: 'TriggerId',
dataIndex: 'triggerId',
render: (_col: any, item: any) => {
return (
<Box
cursor="pointer"
fontWeight="bold"
onClick={() => setSelectedComponentId(item.triggerId)}
>
{item.triggerId}
</Box>
);
},
},
];
return (
<DebugTable
className={css`
table-layout: fixed;
`}
columns={eventColumns}
data={events}
pagination={{ hideOnSinglePage: true }}
emptyMessage="No Event Logs"
footer={
!!events.length && (
<Button
onClick={() => {
setEventLogs([]);
}}
size="sm"
>
clear
</Button>
)
}
/>
);
};

View File

@ -5,6 +5,7 @@ import React from 'react';
type Props = {
currentPage: number;
lastPage: number;
hideOnSinglePage?: boolean;
handlePageClick: (page: number) => void;
};
@ -12,7 +13,12 @@ export const Pagination: React.FC<Props> = ({
currentPage,
lastPage,
handlePageClick,
hideOnSinglePage = false,
}) => {
if (lastPage === 1 && hideOnSinglePage) {
return null;
}
const pages = [];
if (currentPage - 1 >= 0) {
const prevPage = currentPage - 1;
@ -37,7 +43,7 @@ export const Pagination: React.FC<Props> = ({
startIdx = 0;
endIdx = ShowPagesNumber;
}
if (currentPage + 3 >= lastPage) {
if (currentPage + 4 >= lastPage) {
startIdx = Math.max(0, lastPage - ShowPagesNumber);
endIdx = lastPage;
}

View File

@ -0,0 +1,93 @@
import React, { ReactNode, useEffect } from 'react';
import { Box, HStack, Table, Tbody, Td, Th, Thead, Tr, VStack } from '@chakra-ui/react';
import { Pagination } from './Pagination';
import { defaultPageSize } from './const';
type Column = {
title: string;
dataIndex: string;
render?: (col: any, item: any, index: number) => React.ReactElement;
};
type TableProps = {
className?: string;
columns: Column[];
pagination?: {
pageSize?: number;
lastPage?: number;
hideOnSinglePage?: boolean;
};
data: any[];
footer?: ReactNode;
emptyMessage?: string;
};
export const DebugTable: React.FC<TableProps> = ({
columns = [],
pagination = {},
data,
footer,
emptyMessage = 'No Data',
className,
}) => {
const { pageSize = defaultPageSize, hideOnSinglePage = false, lastPage } = pagination;
const [currPage, setCurrPage] = React.useState(0);
const currentData = data.slice(currPage * pageSize, currPage * pageSize + pageSize);
useEffect(() => {
if (data.length <= Number(pageSize) * (currPage - 1)) {
setCurrPage(0);
}
}, [currPage, data.length, pageSize]);
return (
<VStack width="full" justifyContent="start">
<Table
className={className}
size="sm"
width="full"
maxHeight="200px"
overflow="auto"
>
<Thead>
<Tr>
{columns.map(c => {
return <Th key={c.title + c.dataIndex}>{c.title}</Th>;
})}
</Tr>
</Thead>
<Tbody>
{!data.length ? (
<Tr>
<Td colSpan={columns.length}>
<Box textAlign="center">{emptyMessage}</Box>
</Td>
</Tr>
) : (
currentData.map((d, i) => {
return (
<Tr key={i}>
{columns.map(c => {
if (c.render) {
return <Td key={c.dataIndex}>{c.render(c, d, i)}</Td>;
}
return <Td key={c.dataIndex}>{d[c.dataIndex]}</Td>;
})}
</Tr>
);
})
)}
</Tbody>
</Table>
<HStack w="full" justify="end">
<Box flex="1">{footer}</Box>
<Pagination
currentPage={currPage}
lastPage={lastPage || Math.ceil(data.length / pageSize)}
handlePageClick={page => setCurrPage(page)}
hideOnSinglePage={hideOnSinglePage}
/>
</HStack>
</VStack>
);
};

View File

@ -0,0 +1,125 @@
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
import {
Badge,
Button,
HStack,
IconButton,
Tabs,
Text,
TabPanel,
TabPanels,
TabList,
VStack,
Tab,
} from '@chakra-ui/react';
import { observer } from 'mobx-react-lite';
import React, { useEffect, useMemo, useState } from 'react';
import { EventLogs } from './EventLogs';
import { ErrorLogs } from './ErrorLogs';
import type { Props, Event, EventLog } from './type';
import produce from 'immer';
export const WarningArea: React.FC<Props> = observer(({ services }) => {
const { editorStore } = services;
const [isCollapsed, setIsCollapsed] = React.useState(true);
const [eventLogs, setEventLogs] = useState<EventLog[]>([]);
useEffect(() => {
const handler = (type: string, event: unknown) => {
setEventLogs(cur => {
return produce(cur, draft => {
const { name, triggerId, componentId, parameters } = event as Event;
draft.unshift({
type,
methodName: name,
triggerId,
time: new Date().toLocaleTimeString(),
target: componentId,
parameters,
});
});
});
};
services.apiService.on('*', handler);
return () => services.apiService.off('*', handler);
}, [services.apiService]);
const savedBadge = useMemo(() => {
return <Badge colorScheme="green">Saved</Badge>;
}, []);
const unsaveBadge = useMemo(() => {
return (
<HStack>
<Button
colorScheme="red"
variant="ghost"
size="sm"
onClick={() => editorStore.saveCurrentComponents()}
>
Save anyway
</Button>
<Badge colorScheme="red">Unsave</Badge>
</HStack>
);
}, [editorStore]);
return (
<VStack
position="absolute"
bottom="0"
left="0"
right="0"
paddingY="2"
paddingX="4"
boxShadow="0 0 4px rgba(0, 0, 0, 0.1)"
background="white"
zIndex="1"
>
<HStack width="full" justifyContent="space-between">
<Tabs
minH={isCollapsed ? '' : '300px'}
w="full"
variant="soft-rounded"
colorScheme="gray"
>
<TabList>
<Tab alignItems="baseline">
<Text fontSize="md" fontWeight="bold">
Errors
</Text>
<Badge ml="1" fontSize="0.8em" colorScheme="red">
{editorStore.validateResult.length}
</Badge>
</Tab>
<Tab>Logs</Tab>
<HStack w="full" justify="end">
{editorStore.isSaved ? savedBadge : unsaveBadge}
<IconButton
aria-label="show errors"
size="sm"
variant="ghost"
icon={isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
onClick={() => setIsCollapsed(prev => !prev)}
/>
</HStack>
</TabList>
{!isCollapsed && (
<TabPanels>
<TabPanel>
<ErrorLogs services={services} />
</TabPanel>
<TabPanel>
<EventLogs
setEventLogs={setEventLogs}
services={services}
events={eventLogs}
/>
</TabPanel>
</TabPanels>
)}
</Tabs>
</HStack>
</VStack>
);
});

View File

@ -0,0 +1 @@
export const defaultPageSize = 5;

View File

@ -0,0 +1 @@
export * from './WarningArea';

View File

@ -0,0 +1,20 @@
import { EditorServices } from '../../types';
export type Props = {
services: EditorServices;
};
export type Event = {
componentId: string;
name: string;
parameters: any;
triggerId: string;
};
export type EventLog = {
time: string;
type: string;
target: string;
methodName: string;
triggerId: string;
parameters: any;
};

View File

@ -134,6 +134,7 @@ const ModuleRendererContent = React.forwardRef<
componentId: evaledHandler.componentId,
name: evaledHandler.method.name,
parameters: evaledHandler.method.parameters,
triggerId: moduleId,
});
}
};

View File

@ -10,6 +10,7 @@ export function initApiService() {
uiMethod: {
componentId: string;
name: string;
triggerId: string;
parameters?: any;
};
moduleEvent: {

View File

@ -26,7 +26,7 @@ export default implementRuntimeTrait({
state: {},
},
})(() => {
return ({ trait, handlers, services, slotKey }) => {
return ({ trait, handlers, services, slotKey, componentId }) => {
const callbackQueueMap: Record<string, Array<() => void>> = {};
const rawHandlers = trait.properties.handlers;
// setup current handlers
@ -37,7 +37,7 @@ export default implementRuntimeTrait({
callbackQueueMap[handler.type] = [];
}
callbackQueueMap[handler.type].push(
runEventHandler(handler, rawHandlers, Number(i), services, slotKey)
runEventHandler(handler, rawHandlers, Number(i), services, slotKey, componentId)
);
}
@ -60,7 +60,7 @@ export default implementRuntimeTrait({
() => {
handlers.forEach((h, i) => {
if (h.type === MountEvent.mount) {
runEventHandler(h, rawHandlers, i, services, slotKey)();
runEventHandler(h, rawHandlers, i, services, slotKey, componentId)();
}
});
},
@ -69,7 +69,7 @@ export default implementRuntimeTrait({
() => {
handlers.forEach((h, i) => {
if (h.type === MountEvent.update) {
runEventHandler(h, rawHandlers, i, services, slotKey)();
runEventHandler(h, rawHandlers, i, services, slotKey, componentId)();
}
});
},
@ -78,7 +78,7 @@ export default implementRuntimeTrait({
() => {
handlers.forEach((h, i) => {
if (h.type === MountEvent.unmount) {
runEventHandler(h, rawHandlers, i, services, slotKey)();
runEventHandler(h, rawHandlers, i, services, slotKey, componentId)();
}
});
},

View File

@ -171,7 +171,8 @@ export default implementRuntimeTrait({
rawOnComplete,
index,
services,
slotKey
slotKey,
componentId
)();
});
} else {
@ -189,7 +190,14 @@ export default implementRuntimeTrait({
const rawOnError = trait.properties.onError;
onError?.forEach((_, index) => {
runEventHandler(onError[index], rawOnError, index, services, slotKey)();
runEventHandler(
onError[index],
rawOnError,
index,
services,
slotKey,
componentId
)();
});
}
},
@ -208,7 +216,14 @@ export default implementRuntimeTrait({
const rawOnError = trait.properties.onError;
onError?.forEach((_, index) => {
runEventHandler(onError[index], rawOnError, index, services, slotKey)();
runEventHandler(
onError[index],
rawOnError,
index,
services,
slotKey,
componentId
)();
});
}
);

View File

@ -11,7 +11,8 @@ export const runEventHandler = (
rawHandlers: string | PropsBeforeEvaled<Static<typeof CallbackSpec>>,
index: number,
services: UIServices,
slotKey: string
slotKey: string,
triggerId = ''
) => {
const { stateManager } = services;
const send = () => {
@ -36,6 +37,7 @@ export const runEventHandler = (
componentId: evaledHandler.componentId,
name: evaledHandler.method.name,
parameters: evaledHandler.method.parameters,
triggerId,
});
};
const { wait } = handler;