This is an automated email from the ASF dual-hosted git repository. chenguangsheng pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/eventmesh-dashboard.git
commit 16993bc8e6dee21c001cb77a82acb82b45c38de5 Author: qqeasonchen <[email protected]> AuthorDate: Tue Mar 28 17:22:24 2023 +0800 transfer dashboard from parent --- README.md | 34 +++ components/client/GrpcClientTable.tsx | 217 +++++++++++++++++ components/client/HTTPClientTable.tsx | 217 +++++++++++++++++ components/client/TCPClientTable.tsx | 223 ++++++++++++++++++ components/event/EventTable.tsx | 371 +++++++++++++++++++++++++++++ components/eventCatalogs/Create.tsx | 137 +++++++++++ components/eventCatalogs/Details.tsx | 121 ++++++++++ components/eventCatalogs/constant.ts | 27 +++ components/eventCatalogs/types.ts | 40 ++++ components/index/Configuration.tsx | 249 ++++++++++++++++++++ components/index/Endpoint.tsx | 105 +++++++++ components/metrics/MetricsTable.tsx | 313 +++++++++++++++++++++++++ components/navigation/MenuItem.tsx | 115 +++++++++ components/navigation/Menus.tsx | 179 ++++++++++++++ components/navigation/MenusMobile.tsx | 58 +++++ components/navigation/Sidebar.tsx | 63 +++++ components/registry/RegistryTable.tsx | 170 ++++++++++++++ components/topic/TopicTable.tsx | 275 ++++++++++++++++++++++ components/workflow/Create.tsx | 136 +++++++++++ components/workflow/Details/Details.tsx | 285 +++++++++++++++++++++++ components/workflow/Details/Instances.tsx | 164 +++++++++++++ components/workflow/Details/index.ts | 22 ++ components/workflow/constant.ts | 41 ++++ components/workflow/types.ts | 54 +++++ context/context.tsx | 87 +++++++ context/reducer.ts | 40 ++++ context/type.ts | 40 ++++ next.config.js | 25 ++ package.json | 45 ++++ pages/_app.tsx | 46 ++++ pages/_document.tsx | 38 +++ pages/event.tsx | 33 +++ pages/eventCatalogs.tsx | 223 ++++++++++++++++++ pages/grpc.tsx | 33 +++ pages/http.tsx | 33 +++ pages/index.tsx | 35 +++ pages/metrics.tsx | 33 +++ pages/registry.tsx | 33 +++ pages/tcp.tsx | 33 +++ pages/topic.tsx | 33 +++ pages/workflows.tsx | 375 ++++++++++++++++++++++++++++++ public/favicon.ico | Bin 0 -> 25931 bytes static/images/logo.png | Bin 0 -> 58522 bytes tsconfig.json | 20 ++ 44 files changed, 4821 insertions(+) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c87e042 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/components/client/GrpcClientTable.tsx b/components/client/GrpcClientTable.tsx new file mode 100644 index 0000000..1ffdfea --- /dev/null +++ b/components/client/GrpcClientTable.tsx @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + HStack, + Select, + Input, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + useToast, + Box, + Button, +} from '@chakra-ui/react'; +import axios from 'axios'; +import { useContext, useEffect, useState } from 'react'; +import { AppContext } from '../../context/context'; + +interface GrpcClient { + env: string, + subsystem: string, + url: string, + pid: number, + host: string, + port: number, + version: string, + idc: string, + group: string, + purpose: string, + protocol: string, +} + +interface GrpcClientProps { + url: string, + group: string, +} + +interface RemoveGrpcClientRequest { + url: string, +} + +const GrpcClientRow = ({ + url, group, +}: GrpcClientProps) => { + const { state } = useContext(AppContext); + + const toast = useToast(); + const [loading, setLoading] = useState(false); + const onRemoveClick = async () => { + try { + setLoading(true); + await axios.delete<RemoveGrpcClientRequest>(`${state.endpoint}/client/grpc`, { + data: { + url, + }, + }); + setLoading(false); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to remove the gRPC Client', + description: error.message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } + } + }; + + return ( + <Tr> + <Td>{url}</Td> + <Td>{group}</Td> + <Td> + <HStack> + <Button + colorScheme="red" + isLoading={loading} + onClick={onRemoveClick} + > + Remove + </Button> + </HStack> + </Td> + </Tr> + ); +}; + +const GrpcClientTable = () => { + const { state } = useContext(AppContext); + + const [searchInput, setSearchInput] = useState<string>(''); + const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => { + setSearchInput(event.currentTarget.value); + }; + + const [groupSet, setGroupSet] = useState<Set<string>>(new Set()); + const [groupFilter, setGroupFilter] = useState<string>(''); + const handleGroupSelectChange = (event: React.FormEvent<HTMLSelectElement>) => { + setGroupFilter(event.currentTarget.value); + }; + + const [GrpcClientList, setGrpcClientList] = useState<GrpcClient[]>([]); + const toast = useToast(); + useEffect(() => { + const fetch = async () => { + try { + const { data } = await axios.get<GrpcClient[]>(`${state.endpoint}/client/grpc`); + setGrpcClientList(data); + + const nextGroupSet = new Set<string>(); + data.forEach(({ group }) => { + nextGroupSet.add(group); + }); + setGroupSet(nextGroupSet); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to fetch the list of gRPC Clients', + description: 'Unable to connect to the EventMesh daemon', + status: 'error', + duration: 3000, + isClosable: true, + }); + setGrpcClientList([]); + } + } + }; + + fetch(); + }, []); + + return ( + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + > + <HStack + spacing="2" + > + <Input + w="200%" + placeholder="Search" + value={searchInput} + onChange={handleSearchInputChange} + /> + <Select + placeholder="Select Group" + onChange={handleGroupSelectChange} + > + {Array.from(groupSet).map((group) => ( + <option value={group} key={group}>{group}</option> + ))} + </Select> + </HStack> + + <TableContainer> + <Table variant="simple"> + <Thead> + <Tr> + <Th>URL</Th> + <Th>Group</Th> + <Th>Action</Th> + </Tr> + </Thead> + <Tbody> + {GrpcClientList && GrpcClientList.filter(({ + url, group, + }) => { + if (searchInput && !url.includes(searchInput)) { + return false; + } + if (groupFilter && groupFilter !== group) { + return false; + } + return true; + }).map(({ + url, group, + }) => ( + <GrpcClientRow + url={url} + group={group} + /> + ))} + </Tbody> + </Table> + </TableContainer> + </Box> + ); +}; + +export default GrpcClientTable; diff --git a/components/client/HTTPClientTable.tsx b/components/client/HTTPClientTable.tsx new file mode 100644 index 0000000..832ea91 --- /dev/null +++ b/components/client/HTTPClientTable.tsx @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + HStack, + Select, + Input, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + useToast, + Box, + Button, +} from '@chakra-ui/react'; +import axios from 'axios'; +import { useContext, useEffect, useState } from 'react'; +import { AppContext } from '../../context/context'; + +interface HTTPClient { + env: string, + subsystem: string, + url: string, + pid: number, + host: string, + port: number, + version: string, + idc: string, + group: string, + purpose: string, + protocol: string, +} + +interface HTTPClientProps { + url: string, + group: string, +} + +interface RemoveHTTPClientRequest { + url: string, +} + +const HTTPClientRow = ({ + url, group, +}: HTTPClientProps) => { + const { state } = useContext(AppContext); + + const toast = useToast(); + const [loading, setLoading] = useState(false); + const onRemoveClick = async () => { + try { + setLoading(true); + await axios.delete<RemoveHTTPClientRequest>(`${state.endpoint}/client/http`, { + data: { + url, + }, + }); + setLoading(false); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to remove the HTTP Client', + description: error.message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } + } + }; + + return ( + <Tr> + <Td>{url}</Td> + <Td>{group}</Td> + <Td> + <HStack> + <Button + colorScheme="red" + isLoading={loading} + onClick={onRemoveClick} + > + Remove + </Button> + </HStack> + </Td> + </Tr> + ); +}; + +const HTTPClientTable = () => { + const { state } = useContext(AppContext); + + const [searchInput, setSearchInput] = useState<string>(''); + const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => { + setSearchInput(event.currentTarget.value); + }; + + const [groupSet, setGroupSet] = useState<Set<string>>(new Set()); + const [groupFilter, setGroupFilter] = useState<string>(''); + const handleGroupSelectChange = (event: React.FormEvent<HTMLSelectElement>) => { + setGroupFilter(event.currentTarget.value); + }; + + const [HTTPClientList, setHTTPClientList] = useState<HTTPClient[]>([]); + const toast = useToast(); + useEffect(() => { + const fetch = async () => { + try { + const { data } = await axios.get<HTTPClient[]>(`${state.endpoint}/client/http`); + setHTTPClientList(data); + + const nextGroupSet = new Set<string>(); + data.forEach(({ group }) => { + nextGroupSet.add(group); + }); + setGroupSet(nextGroupSet); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to fetch the list of HTTP Clients', + description: 'Unable to connect to the EventMesh daemon', + status: 'error', + duration: 3000, + isClosable: true, + }); + setHTTPClientList([]); + } + } + }; + + fetch(); + }, []); + + return ( + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + > + <HStack + spacing="2" + > + <Input + w="200%" + placeholder="Search" + value={searchInput} + onChange={handleSearchInputChange} + /> + <Select + placeholder="Select Group" + onChange={handleGroupSelectChange} + > + {Array.from(groupSet).map((group) => ( + <option value={group} key={group}>{group}</option> + ))} + </Select> + </HStack> + + <TableContainer> + <Table variant="simple"> + <Thead> + <Tr> + <Th>URL</Th> + <Th>Group</Th> + <Th>Action</Th> + </Tr> + </Thead> + <Tbody> + {HTTPClientList && HTTPClientList.filter(({ + url, group, + }) => { + if (searchInput && !url.includes(searchInput)) { + return false; + } + if (groupFilter && groupFilter !== group) { + return false; + } + return true; + }).map(({ + url, group, + }) => ( + <HTTPClientRow + url={url} + group={group} + /> + ))} + </Tbody> + </Table> + </TableContainer> + </Box> + ); +}; + +export default HTTPClientTable; diff --git a/components/client/TCPClientTable.tsx b/components/client/TCPClientTable.tsx new file mode 100644 index 0000000..dc24589 --- /dev/null +++ b/components/client/TCPClientTable.tsx @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + HStack, + Select, + Input, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + useToast, + Box, + Button, +} from '@chakra-ui/react'; +import axios from 'axios'; +import { useContext, useEffect, useState } from 'react'; +import { AppContext } from '../../context/context'; + +interface TCPClient { + env: string, + subsystem: string, + url: string, + pid: number, + host: string, + port: number, + version: string, + idc: string, + group: string, + purpose: string, + protocol: string, +} + +interface TCPClientProps { + host: string, + port: number, + group: string, +} + +interface RemoveTCPClientRequest { + host: string, + port: number, +} + +const TCPClientRow = ({ + host, port, group, +}: TCPClientProps) => { + const { state } = useContext(AppContext); + + const toast = useToast(); + const [loading, setLoading] = useState(false); + const onRemoveClick = async () => { + try { + setLoading(true); + await axios.delete<RemoveTCPClientRequest>(`${state.endpoint}/client/tcp`, { + data: { + host, + port, + }, + }); + setLoading(false); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to remove the TCP Client', + description: error.message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } + } + }; + + return ( + <Tr> + <Td>{`${host}:${port}`}</Td> + <Td>{group}</Td> + <Td> + <HStack> + <Button + colorScheme="red" + isLoading={loading} + onClick={onRemoveClick} + > + Remove + </Button> + </HStack> + </Td> + </Tr> + ); +}; + +const TCPClientTable = () => { + const { state } = useContext(AppContext); + + const [searchInput, setSearchInput] = useState<string>(''); + const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => { + setSearchInput(event.currentTarget.value); + }; + + const [groupSet, setGroupSet] = useState<Set<string>>(new Set()); + const [groupFilter, setGroupFilter] = useState<string>(''); + const handleGroupSelectChange = (event: React.FormEvent<HTMLSelectElement>) => { + setGroupFilter(event.currentTarget.value); + }; + + const [TCPClientList, setTCPClientList] = useState<TCPClient[]>([]); + const toast = useToast(); + useEffect(() => { + const fetch = async () => { + try { + const { data } = await axios.get<TCPClient[]>(`${state.endpoint}/client/tcp`); + setTCPClientList(data); + + const nextGroupSet = new Set<string>(); + data.forEach(({ group }) => { + nextGroupSet.add(group); + }); + setGroupSet(nextGroupSet); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to fetch the list of TCP Clients', + description: 'Unable to connect to the EventMesh daemon', + status: 'error', + duration: 3000, + isClosable: true, + }); + setTCPClientList([]); + } + } + }; + + fetch(); + }, []); + + return ( + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + > + <HStack + spacing="2" + > + <Input + w="200%" + placeholder="Search" + value={searchInput} + onChange={handleSearchInputChange} + /> + <Select + placeholder="Select Group" + onChange={handleGroupSelectChange} + > + {Array.from(groupSet).map((group) => ( + <option value={group} key={group}>{group}</option> + ))} + </Select> + </HStack> + + <TableContainer> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Host</Th> + <Th>Host</Th> + <Th>Group</Th> + <Th>Action</Th> + </Tr> + </Thead> + <Tbody> + {TCPClientList && TCPClientList.filter(({ + host, port, group, + }) => { + const address = `${host}:${port}`; + if (searchInput && !address.includes(searchInput)) { + return false; + } + if (groupFilter && groupFilter !== group) { + return false; + } + return true; + }).map(({ + host, port, group, + }) => ( + <TCPClientRow + host={host} + port={port} + group={group} + /> + ))} + </Tbody> + </Table> + </TableContainer> + </Box> + ); +}; + +export default TCPClientTable; diff --git a/components/event/EventTable.tsx b/components/event/EventTable.tsx new file mode 100644 index 0000000..b0d0a13 --- /dev/null +++ b/components/event/EventTable.tsx @@ -0,0 +1,371 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + HStack, + Input, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + useToast, + Box, + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useDisclosure, + Select, + VStack, + Textarea, +} from '@chakra-ui/react'; +import axios from 'axios'; +import { useContext, useEffect, useState } from 'react'; +import { CloudEvent } from 'cloudevents'; +import { AppContext } from '../../context/context'; + +interface Topic { + name: string, + messageCount: number, +} + +interface EventProps { + event: CloudEvent<string>, +} + +interface CreateEventRequest { + event: CloudEvent<string>, +} + +const CreateEventModal = () => { + const { state } = useContext(AppContext); + + const { isOpen, onOpen, onClose } = useDisclosure(); + + const [id, setId] = useState(''); + const handleIdChange = (event: React.FormEvent<HTMLInputElement>) => { + setId(event.currentTarget.value); + }; + + const [source, setSource] = useState(''); + const handleSourceChange = (event: React.FormEvent<HTMLInputElement>) => { + setSource(event.currentTarget.value); + }; + + const [subject, setSubject] = useState(''); + const handleSubjectChange = (event: React.FormEvent<HTMLInputElement>) => { + setSubject(event.currentTarget.value); + }; + + const [type, setType] = useState(''); + const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => { + setType(event.currentTarget.value); + }; + + const [data, setData] = useState(''); + const handleDataChange = (event: React.FormEvent<HTMLInputElement>) => { + setData(event.currentTarget.value); + }; + + const toast = useToast(); + const [loading, setLoading] = useState(false); + const onCreateClick = async () => { + try { + setLoading(true); + await axios.post<CreateEventRequest>(`${state.endpoint}/event`, new CloudEvent({ + source, + subject, + type, + data, + specversion: '1.0', + })); + onClose(); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to publish the event', + description: error.message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } + } finally { + setLoading(false); + } + }; + + return ( + <> + <Button + w="25%" + colorScheme="blue" + onClick={onOpen} + > + Create Event + </Button> + <Modal isOpen={isOpen} onClose={onClose}> + <ModalOverlay /> + <ModalContent> + <ModalHeader>Create Event</ModalHeader> + <ModalCloseButton /> + <ModalBody> + <VStack> + <Input + placeholder="Event ID" + value={id} + onChange={handleIdChange} + /> + <Input + placeholder="Event Source" + value={source} + onChange={handleSourceChange} + /> + <Input + placeholder="Event Subject" + value={subject} + onChange={handleSubjectChange} + /> + <Input + placeholder="Event Type" + value={type} + onChange={handleTypeChange} + /> + <Input + placeholder="Event Data" + value={data} + onChange={handleDataChange} + /> + </VStack> + </ModalBody> + + <ModalFooter> + <Button + mr={2} + onClick={onClose} + > + Close + </Button> + <Button + colorScheme="blue" + onClick={onCreateClick} + isLoading={loading} + isDisabled={ + id.length === 0 || subject.length === 0 || source.length === 0 || type.length === 0 + } + > + Create + </Button> + </ModalFooter> + </ModalContent> + </Modal> + </> + ); +}; + +const EventRow = ({ + event, +}: EventProps) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const eventDataBase64 = event.data_base64 || ''; + const eventData = Buffer.from(eventDataBase64, 'base64').toString('utf-8'); + + return ( + <> + <Modal isOpen={isOpen} onClose={onClose}> + <ModalOverlay /> + <ModalContent> + <ModalHeader>Event Data</ModalHeader> + <ModalCloseButton /> + <ModalBody> + <Box> + <Textarea isDisabled value={eventData} /> + </Box> + </ModalBody> + + <ModalFooter> + <Button + mr={2} + onClick={onClose} + > + Close + </Button> + </ModalFooter> + </ModalContent> + </Modal> + + <Tr> + <Td>{event.id}</Td> + <Td>{event.subject}</Td> + <Td>{new Date(Number(event.reqc2eventmeshtimestamp)).toLocaleString()}</Td> + <Td> + <HStack> + <Button + colorScheme="blue" + onClick={onOpen} + > + View Data + </Button> + </HStack> + + </Td> + </Tr> + </> + ); +}; + +const EventTable = () => { + const { state } = useContext(AppContext); + + const [searchInput, setSearchInput] = useState<string>(''); + const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => { + setSearchInput(event.currentTarget.value); + }; + + const [eventList, setEventList] = useState<CloudEvent<string>[]>([]); + const [topicList, setTopicList] = useState<Topic[]>([]); + const [topic, setTopic] = useState<Topic>({ + name: '', + messageCount: 0, + }); + const handleTopicChange = (event: React.FormEvent<HTMLSelectElement>) => { + setTopic({ + name: event.currentTarget.value, + messageCount: 0, + }); + }; + + const toast = useToast(); + + useEffect(() => { + const fetch = async () => { + try { + const { data } = await axios.get<Topic[]>(`${state.endpoint}/topic`); + setTopicList(data); + if (data.length !== 0) { + setTopic(data[0]); + } + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to fetch the list of events', + description: 'unable to connect to the EventMesh daemon', + status: 'error', + duration: 3000, + isClosable: true, + }); + setEventList([]); + } + } + }; + + fetch(); + }, []); + + useEffect(() => { + const fetch = async () => { + try { + if (topic.name !== '') { + const eventResponse = await axios.get<string[]>(`${state.endpoint}/event`, { + params: { + topicName: topic.name, + offset: 0, + length: 15, + }, + }); + setEventList(eventResponse.data.map((rawEvent) => JSON.parse(rawEvent))); + } + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to fetch the list of events', + description: 'Unable to connect to the EventMesh daemon', + status: 'error', + duration: 3000, + isClosable: true, + }); + setEventList([]); + } + } + }; + + fetch(); + }, [topic]); + + return ( + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + > + <HStack + spacing="2" + > + <Input + w="100%" + placeholder="Search" + value={searchInput} + onChange={handleSearchInputChange} + /> + <Select + w="100%" + onChange={handleTopicChange} + > + {topicList.map(({ name }) => ( + <option value={name} key={name} selected={topic.name === name}>{name}</option> + ))} + </Select> + <CreateEventModal /> + </HStack> + + <TableContainer> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Event Id</Th> + <Th>Event Subject</Th> + <Th>Event Time</Th> + <Th>Action</Th> + </Tr> + </Thead> + <Tbody> + {eventList.filter(() => true).map((event) => ( + <EventRow + key={event.id} + event={event} + /> + ))} + </Tbody> + </Table> + </TableContainer> + </Box> + ); +}; + +export default EventTable; diff --git a/components/eventCatalogs/Create.tsx b/components/eventCatalogs/Create.tsx new file mode 100644 index 0000000..7d75f4b --- /dev/null +++ b/components/eventCatalogs/Create.tsx @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, useRef, useState } from 'react'; + +import { + Drawer, + DrawerContent, + DrawerCloseButton, + DrawerOverlay, + DrawerHeader, + DrawerBody, + DrawerFooter, + Button, + Box, + useToast, + Spinner, +} from '@chakra-ui/react'; +import Editor, { Monaco } from '@monaco-editor/react'; +import axios from 'axios'; + +const ApiRoot = process.env.NEXT_PUBLIC_EVENTCATALOG_API_ROOT; + +const Create: FC<{ visible: boolean; onClose: () => void; onSucceed:()=>void +}> = ({ visible = false, onClose, onSucceed }) => { + const toast = useToast(); + const [isSubmitting, setIsSubmitting] = useState(false); + const editorRef = useRef<Monaco | null>(null); + const defaultEditorValue = '# Your code goes here'; + + const handleEditorDidMount = (editor: any) => { + // here is the editor instance + // you can store it in `useRef` for further usage + editorRef.current = editor; + }; + + const onSubmit = () => { + setIsSubmitting(true); + + try { + const value = editorRef.current.getValue(); + if (value === defaultEditorValue) { + toast({ + title: 'Invalid definition', + description: 'Please input your workflow definition properly', + status: 'warning', + position: 'top-right', + }); + setIsSubmitting(false); + return; + } + axios + .post( + `${ApiRoot}/catalog`, + { event: { definition: value } }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + .then(() => { + toast({ + title: 'Succeeded', + status: 'success', + position: 'top-right', + }); + onSucceed(); + setIsSubmitting(false); + }) + .catch((error) => { + toast({ + title: 'Failed', + description: error.response.data, + status: 'error', + position: 'top-right', + }); + setIsSubmitting(false); + }); + } catch (error) { + setIsSubmitting(false); + } + }; + + return ( + <Drawer + isOpen={visible} + size="xl" + placement="right" + onClose={() => onClose()} + > + <DrawerOverlay /> + <DrawerContent> + <DrawerCloseButton /> + <DrawerHeader>Create Catalog</DrawerHeader> + <DrawerBody> + <Box height="full"> + <Editor + height="100%" + defaultLanguage="yaml" + defaultValue="# Your code goes here" + onMount={handleEditorDidMount} + theme="vs-dark" + /> + </Box> + </DrawerBody> + <DrawerFooter justifyContent="flex-start"> + <Button colorScheme="blue" mr={3} onClick={onSubmit}> + {isSubmitting ? <Spinner colorScheme="white" size="sm" /> : 'Submit'} + </Button> + <Button variant="ghost" colorScheme="blue" onClick={onClose}> + Cancel + </Button> + </DrawerFooter> + </DrawerContent> + + </Drawer> + ); +}; + +export default Create; diff --git a/components/eventCatalogs/Details.tsx b/components/eventCatalogs/Details.tsx new file mode 100644 index 0000000..b4d33ee --- /dev/null +++ b/components/eventCatalogs/Details.tsx @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, useRef } from 'react'; + +import { + Drawer, + DrawerContent, + DrawerCloseButton, + DrawerOverlay, + DrawerHeader, + DrawerBody, + FormLabel, + Box, + Flex, + Text, + Stack, + Badge, +} from '@chakra-ui/react'; +import moment from 'moment'; +import Editor, { Monaco } from '@monaco-editor/react'; +import { EventCatalogType } from './types'; +// import { WorkflowStatusMap } from './constant'; + +const Details: FC<{ + visible: boolean; + data?: EventCatalogType | undefined; + onClose: () => void; +}> = ({ visible = false, data, onClose = () => {} }) => { + const editorRef = useRef(null); + const handleEditorDidMount = (editor: Monaco) => { + editorRef.current = editor; + editor.setValue(data?.definition ?? ''); + }; + + return ( + <Drawer + isOpen={visible} + size="xl" + placement="right" + onClose={() => onClose()} + > + <DrawerOverlay /> + <DrawerContent> + <DrawerCloseButton /> + <DrawerHeader> + {data?.title} + <Badge ml={2}> + Version + {' '} + {data?.version} + </Badge> + </DrawerHeader> + <DrawerBody> + <Flex flexDirection="column" h="full"> + <Stack direction="row"> + <Flex width="240px" flexDirection="column"> + <Box mb="1"> + <FormLabel opacity={0.5}>Catalog ID</FormLabel> + <Text>{data?.id}</Text> + </Box> + <Box mt="1" mb="3"> + <FormLabel opacity={0.5}>Title</FormLabel> + <Text>{data?.title}</Text> + </Box> + <Box mt="1" mb="3"> + <FormLabel opacity={0.5}>File Name</FormLabel> + <Text>{data?.file_name}</Text> + </Box> + </Flex> + <Flex flexDirection="column"> + <Box mb="1"> + <FormLabel opacity={0.5}>Created At</FormLabel> + <Text> + {moment(data?.create_time).format('YYYY-MM-DD HH:mm:ss')} + </Text> + </Box> + <Box mt="1" mb="3"> + <FormLabel opacity={0.5}>Updated At</FormLabel> + <Text> + {moment(data?.update_time).format('YYYY-MM-DD HH:mm:ss')} + </Text> + </Box> + </Flex> + </Stack> + <Box flex={1}> + <Editor + height="100%" + defaultLanguage="yaml" + defaultValue="# Your code goes here" + onMount={handleEditorDidMount} + theme="vs-dark" + options={{ readOnly: true }} + /> + </Box> + </Flex> + </DrawerBody> + </DrawerContent> + </Drawer> + ); +}; +Details.defaultProps = { + data: undefined, +}; +export default Details; diff --git a/components/eventCatalogs/constant.ts b/components/eventCatalogs/constant.ts new file mode 100644 index 0000000..df9cfab --- /dev/null +++ b/components/eventCatalogs/constant.ts @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CatalogStatusEnum } from './types'; + +export const WorkflowStatusMap = new Map([ + [CatalogStatusEnum.Normal, 'Normal'], + [CatalogStatusEnum.Deleted, 'Deleted'], +]); + +export default WorkflowStatusMap; diff --git a/components/eventCatalogs/types.ts b/components/eventCatalogs/types.ts new file mode 100644 index 0000000..8ab7004 --- /dev/null +++ b/components/eventCatalogs/types.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type SchemaTypes = { + schemaId: string; + lastVersion: string; + description: string; +}; + +export type EventCatalogType = { + create_time: string, + definition: string, + file_name: string, + id: 0, + status: 0, + title: string, + update_time: string, + version: string +}; + +export enum CatalogStatusEnum { + 'Normal' = 1, + 'Deleted' = -1, +} diff --git a/components/index/Configuration.tsx b/components/index/Configuration.tsx new file mode 100755 index 0000000..8258db2 --- /dev/null +++ b/components/index/Configuration.tsx @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + Text, + Table, + TableContainer, + Tbody, + Th, + Thead, + Tr, + Td, + Box, +} from '@chakra-ui/react'; +import axios from 'axios'; +import React, { useContext, useEffect, useState } from 'react'; +import { AppContext } from '../../context/context'; + +interface EventMeshConfiguration { + sysID: string; + namesrvAddr: string; + eventMeshEnv: string; + eventMeshIDC: string; + eventMeshCluster: string; + eventMeshServerIp: string; + eventMeshName: string; + eventMeshWebhookOrigin: string; + eventMeshServerSecurityEnable: boolean; + eventMeshServerRegistryEnable: boolean; + + eventMeshTcpServerPort: number; + eventMeshTcpServerEnabled: boolean; + + eventMeshHttpServerPort: number; + eventMeshHttpServerUseTls: boolean; + + eventMeshGrpcServerPort: number; + eventMeshGrpcServerUseTls: boolean; +} + +const Configuration = () => { + const { state } = useContext(AppContext); + const [configuration, setConfiguration] = useState< + Partial<EventMeshConfiguration> + >({}); + + useEffect(() => { + const controller = new AbortController(); + const fetch = async () => { + try { + const { data } = await axios.get<EventMeshConfiguration>( + `${state.endpoint}/configuration`, + { + signal: controller.signal, + }, + ); + setConfiguration(data); + } catch (error) { + setConfiguration({}); + } + }; + + fetch(); + + return () => { + controller.abort(); + }; + }, [state.endpoint]); + + type ConfigurationRecord = Record< + string, + string | number | boolean | undefined + >; + const commonConfiguration: ConfigurationRecord = { + 'System ID': configuration.sysID, + 'NameServer Address': configuration.namesrvAddr, + 'EventMesh Environment': configuration.eventMeshEnv, + 'EventMesh IDC': configuration.eventMeshIDC, + 'EventMesh Cluster': configuration.eventMeshCluster, + 'EventMesh Server IP': configuration.eventMeshServerIp, + 'EventMEsh Name': configuration.eventMeshName, + 'EventMesh Webhook Origin': configuration.eventMeshWebhookOrigin, + 'EventMesh Server Security Enable': + configuration.eventMeshServerSecurityEnable, + 'EventMesh Server Registry Enable': + configuration.eventMeshServerRegistryEnable, + }; + + const tcpConfiguration: ConfigurationRecord = { + 'TCP Server Port': configuration.eventMeshTcpServerPort, + 'TCP Server Enabled': configuration.eventMeshTcpServerEnabled, + }; + + const httpConfiguration: ConfigurationRecord = { + 'HTTP Server Port': configuration.eventMeshHttpServerPort, + 'HTTP Server TLS Enabled': configuration.eventMeshHttpServerUseTls, + }; + + const grpcConfiguration: ConfigurationRecord = { + 'gRPC Server Port': configuration.eventMeshGrpcServerPort, + 'gRPC Server TLS Enabled': configuration.eventMeshGrpcServerUseTls, + }; + + const convertConfigurationToTable = ( + configurationRecord: Record<string, string | number | boolean | undefined>, + ) => Object.entries(configurationRecord).map(([key, value]) => { + if (value === undefined) { + return ( + <Tr> + <Td>{key}</Td> + <Td>Undefined</Td> + </Tr> + ); + } + + return ( + <Tr> + <Td>{key}</Td> + <Td>{value.toString()}</Td> + </Tr> + ); + }); + + if (Object.keys(configuration).length === 0) { + return ( + <Box + maxW="full" + bg="white" + borderWidth="2px" + borderRadius="md" + borderColor="rgb(211,85,25)" + overflow="hidden" + p="4" + mt="4" + opacity="0.8" + > + <Text + fontSize="l" + fontWeight="semibold" + color="rgb(211,85,25)" + textAlign={['left', 'center']} + > + EventMesh Daemon Not Connected + </Text> + </Box> + ); + } + + return ( + <> + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + mt="4" + > + <Text w="full">EventMesh Configuration</Text> + + <TableContainer mt="4"> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Configuration Field</Th> + <Th>Value</Th> + </Tr> + </Thead> + <Tbody>{convertConfigurationToTable(commonConfiguration)}</Tbody> + </Table> + </TableContainer> + </Box> + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + mt="4" + > + <Text w="full">TCP Configuration</Text> + + <TableContainer mt="4"> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Configuration Field</Th> + <Th>Value</Th> + </Tr> + </Thead> + <Tbody>{convertConfigurationToTable(tcpConfiguration)}</Tbody> + </Table> + </TableContainer> + + <Text w="full" mt="4"> + HTTP Configuration + </Text> + + <TableContainer mt="4"> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Configuration Field</Th> + <Th>Value</Th> + </Tr> + </Thead> + <Tbody>{convertConfigurationToTable(httpConfiguration)}</Tbody> + </Table> + </TableContainer> + + <Text w="full" mt="4"> + gRPC Configuration + </Text> + + <TableContainer mt="4"> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Configuration Field</Th> + <Th>Value</Th> + </Tr> + </Thead> + <Tbody>{convertConfigurationToTable(grpcConfiguration)}</Tbody> + </Table> + </TableContainer> + </Box> + </> + ); +}; + +export default Configuration; diff --git a/components/index/Endpoint.tsx b/components/index/Endpoint.tsx new file mode 100755 index 0000000..9e550fa --- /dev/null +++ b/components/index/Endpoint.tsx @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + HStack, + Input, + VStack, + Button, + Text, + useToast, +} from '@chakra-ui/react'; +import axios from 'axios'; +import React, { useContext, useEffect, useState } from 'react'; +import { AppContext } from '../../context/context'; + +const Endpoint = () => { + const { state, dispatch } = useContext(AppContext); + const toast = useToast(); + const [endpointInput, setEndpointInput] = useState('http://localhost:10106'); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setEndpointInput(state.endpoint); + }, [state.endpoint]); + + const handleEndpointInputChange = (event: React.FormEvent<HTMLInputElement>) => { + setEndpointInput(event.currentTarget.value); + }; + + const handleSaveButtonClick = async () => { + try { + setLoading(true); + await axios.get(`${endpointInput}/client`); + dispatch({ + type: 'SetEndPointAction', + payload: { + endpoint: endpointInput, + }, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: `Failed to connect to ${endpointInput}`, + description: error.message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } + } finally { + setLoading(false); + } + }; + + return ( + <VStack + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + > + <Text + w="full" + > + EventMesh Admin Endpoint + </Text> + <HStack + w="full" + > + <Input + placeholder="Apache EventMesh Backend Endpoint" + value={endpointInput} + onChange={handleEndpointInputChange} + /> + <Button + colorScheme="blue" + isLoading={loading} + onClick={handleSaveButtonClick} + > + Save + </Button> + </HStack> + </VStack> + ); +}; + +export default Endpoint; diff --git a/components/metrics/MetricsTable.tsx b/components/metrics/MetricsTable.tsx new file mode 100755 index 0000000..9b80d49 --- /dev/null +++ b/components/metrics/MetricsTable.tsx @@ -0,0 +1,313 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + Text, + Table, + TableContainer, + Tbody, + Th, + Thead, + Tr, + Td, + Box, +} from '@chakra-ui/react'; +import axios from 'axios'; +import React, { useContext, useEffect, useState } from 'react'; +import { AppContext } from '../../context/context'; + +interface EventMeshMetrics { + maxHTTPTPS: number, + avgHTTPTPS: number, + maxHTTPCost: number, + avgHTTPCost: number, + avgHTTPBodyDecodeCost: number, + httpDiscard: number, + maxBatchSendMsgTPS: number, + avgBatchSendMsgTPS: number, + sendBatchMsgNumSum: number, + sendBatchMsgFailNumSum: number, + sendBatchMsgFailRate: number, + sendBatchMsgDiscardNumSum: number, + maxSendMsgTPS: number, + avgSendMsgTPS: number, + sendMsgNumSum: number, + sendMsgFailNumSum: number, + sendMsgFailRate: number, + replyMsgNumSum: number, + replyMsgFailNumSum: number, + maxPushMsgTPS: number, + avgPushMsgTPS: number, + pushHTTPMsgNumSum: number, + pushHTTPMsgFailNumSum: number, + pushHTTPMsgFailRate: number, + maxHTTPPushLatency : number, + avgHTTPPushLatency : number, + batchMsgQueueSize : number, + sendMsgQueueSize : number, + pushMsgQueueSize : number, + retryHTTPQueueSize : number, + avgBatchSendMsgCost : number, + avgSendMsgCost : number, + avgReplyMsgCost : number, + + // TCP Metrics + retryTCPQueueSize: number, + client2eventMeshTCPTPS : number, + eventMesh2mqTCPTPS : number, + mq2eventMeshTCPTPS : number, + eventMesh2clientTCPTPS : number, + allTCPTPS : number, + allTCPConnections : number, + subTopicTCPNum : number +} + +const MetricsTable = () => { + const { state } = useContext(AppContext); + const [metrics, setMetrics] = useState<Partial<EventMeshMetrics>>({}); + + useEffect(() => { + const fetch = async () => { + try { + const { data } = await axios.get<EventMeshMetrics>(`${state.endpoint}/metrics`); + setMetrics(data); + } catch (error) { + setMetrics({}); + } + }; + + fetch(); + }, []); + + type MetricRecord = Record<string, number | undefined>; + const httpMetrics: MetricRecord = { + 'Max HTTP TPS': metrics.maxHTTPTPS, + 'Avg HTTP TPS': metrics.avgHTTPTPS, + 'Max HTTP Cost': metrics.maxHTTPCost, + 'Avg HTTP Cost': metrics.avgHTTPCost, + 'Avg HTTP Body Decode Cost': metrics.avgHTTPBodyDecodeCost, + 'HTTP Discard': metrics.httpDiscard, + }; + const batchMetrics: MetricRecord = { + 'Max Batch Send Msg TPS': metrics.maxBatchSendMsgTPS, + 'Avg Batch Send Msg TPS': metrics.avgBatchSendMsgTPS, + 'Send Batch Msg Num Sum': metrics.sendBatchMsgNumSum, + 'Send Batch Msg Fail Num Sum': metrics.sendBatchMsgFailNumSum, + 'Send Batch Msg Fail Rate': metrics.sendBatchMsgFailRate, + 'Send Batch Msg Discard Num Sum': metrics.sendBatchMsgDiscardNumSum, + }; + const sendMetrics: MetricRecord = { + 'Max Send Msg TPS': metrics.maxSendMsgTPS, + 'Avg Send Msg TPS': metrics.avgSendMsgTPS, + 'Send Msg Num Sum': metrics.sendMsgNumSum, + 'Send Msg Fail Num Sum': metrics.sendMsgFailNumSum, + 'Send Msg Fail Rate': metrics.sendMsgFailRate, + 'Reply Msg Num Sum': metrics.replyMsgNumSum, + 'Reply Msg Fail Num Sum': metrics.replyMsgFailNumSum, + }; + const pushMetrics: MetricRecord = { + 'Max Push Msg TPS': metrics.maxPushMsgTPS, + 'Avg Push Msg TPS': metrics.avgPushMsgTPS, + 'Push HTTP Msg Num Sum': metrics.pushHTTPMsgNumSum, + 'Push HTTP Msg Fail Num Sum': metrics.pushHTTPMsgFailNumSum, + 'Push HTTP Msg Fail Rate': metrics.pushHTTPMsgFailRate, + 'Max HTTP Push Latency': metrics.maxHTTPPushLatency, + 'Avg HTTP Push Latency': metrics.avgHTTPPushLatency, + }; + const tcpMetrics: MetricRecord = { + 'Retry TCP Queue Size': metrics.retryTCPQueueSize, + 'Client2eventMesh TCP TPS': metrics.client2eventMeshTCPTPS, + 'EventMesh2mq TCP TPS': metrics.eventMesh2mqTCPTPS, + 'MQ2eventMesh TCP TPS': metrics.mq2eventMeshTCPTPS, + 'EventMesh2client TCP TPS': metrics.eventMesh2clientTCPTPS, + 'All TCP TPS': metrics.allTCPTPS, + 'All TCP Connections': metrics.allTCPConnections, + 'Sub Topic TCP Num': metrics.subTopicTCPNum, + }; + + const convertConfigurationToTable = ( + metricRecord: Record<string, string | number | boolean | undefined>, + ) => Object.entries(metricRecord).map(([key, value]) => { + if (value === undefined) { + return ( + <Tr> + <Td>{key}</Td> + <Td>Undefined</Td> + </Tr> + ); + } + + return ( + <Tr> + <Td>{key}</Td> + <Td>{value.toString()}</Td> + </Tr> + ); + }); + + if (Object.keys(metrics).length === 0) { + return ( + <Box + maxW="full" + bg="white" + borderWidth="2px" + borderRadius="md" + borderColor="rgb(211,85,25)" + overflow="hidden" + p="4" + mt="4" + opacity="0.8" + > + <Text fontSize="l" fontWeight="semibold" color="rgb(211,85,25)" textAlign={['left', 'center']}> + EventMesh Daemon Not Connected + </Text> + </Box> + ); + } + return ( + <> + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + mt="4" + > + <Text + w="full" + > + EventMesh HTTP Metrics + </Text> + + <TableContainer mt="4"> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Metric</Th> + <Th>Value</Th> + </Tr> + </Thead> + <Tbody> + {convertConfigurationToTable(httpMetrics)} + </Tbody> + </Table> + </TableContainer> + + <Text + w="full" + mt="4" + > + HTTP Batch Metrics + </Text> + + <TableContainer mt="4"> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Metric</Th> + <Th>Value</Th> + </Tr> + </Thead> + <Tbody> + {convertConfigurationToTable(batchMetrics)} + </Tbody> + </Table> + </TableContainer> + + <Text + w="full" + mt="4" + > + HTTP Send Metrics + </Text> + + <TableContainer mt="4"> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Metric</Th> + <Th>Value</Th> + </Tr> + </Thead> + <Tbody> + {convertConfigurationToTable(sendMetrics)} + </Tbody> + </Table> + </TableContainer> + + <Text + w="full" + mt="4" + > + HTTP Push Metrics + </Text> + + <TableContainer mt="4"> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Metric</Th> + <Th>Value</Th> + </Tr> + </Thead> + <Tbody> + {convertConfigurationToTable(pushMetrics)} + </Tbody> + </Table> + </TableContainer> + </Box> + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + mt="4" + > + + <Text + w="full" + mt="4" + > + TCP Metrics + </Text> + + <TableContainer mt="4"> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Metric</Th> + <Th>Value</Th> + </Tr> + </Thead> + <Tbody> + {convertConfigurationToTable(tcpMetrics)} + </Tbody> + </Table> + </TableContainer> + + </Box> + </> + ); +}; + +export default MetricsTable; diff --git a/components/navigation/MenuItem.tsx b/components/navigation/MenuItem.tsx new file mode 100644 index 0000000..31f119b --- /dev/null +++ b/components/navigation/MenuItem.tsx @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { FC, ReactNode } from 'react'; +import { + Flex, FlexProps, Link, Text, +} from '@chakra-ui/react'; + +interface MenuItemProps extends FlexProps { + selected: boolean; + active: boolean; + href: string; + children: string | number; + setActiveName: (name: string) => void; +} + +export const MenuItem = ({ + selected, + active, + href, + children, + setActiveName, +}: MenuItemProps) => ( + <Link + position="relative" + href={href} + h="8" + mt={1.5} + mb={1.5} + ml={3} + mr={3} + boxSizing="border-box" + style={{ textDecoration: 'none' }} + _focus={{ boxShadow: 'none' }} + onMouseOver={() => setActiveName(children.toString())} + onMouseOut={() => setActiveName('')} + > + <Flex + position="absolute" + zIndex={2} + w="full" + h="full" + p="6" + borderRadius="lg" + align="center" + role="group" + cursor="pointer" + fontSize="md" + transition="color 0.15s, background-color 0.15s" + color={selected || active ? '#2a62ad' : 'current'} + fontWeight={selected ? 'bolder' : 'none'} + bgColor={selected || active ? '#dce5fe' : 'none'} + wordBreak="break-word" + overflowWrap="normal" + > + {children} + </Flex> + {/* <Box + position="absolute" + zIndex={1} + p={selected || active ? '4' : 0} + borderRadius="lg" + role="group" + cursor="pointer" + transition="width 0.3s, opacity 0.4s" + bgGradient="linear(28deg,#2a4cad, #2a6bad, #28c9ff)" + h="100%" + w={selected || active ? '100%' : '0'} + opacity={selected || active ? 1 : 0} + /> */} + </Link> +); + +export const MenuGroupItem: FC<{ name: string; children: ReactNode }> = ( + props, +) => { + const { name, children } = props; + return ( + <Flex + flexDirection="column" + w="full" + justifyContent="center" + alignContent="center" + > + {name && ( + <Text + mt={5} + mb="2" + pl="6" + fontSize="xs" + color="#a2b5d8" + fontWeight="bold" + > + {name.toUpperCase()} + </Text> + )} + {children} + </Flex> + ); +}; diff --git a/components/navigation/Menus.tsx b/components/navigation/Menus.tsx new file mode 100644 index 0000000..72be935 --- /dev/null +++ b/components/navigation/Menus.tsx @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, ReactNode, useState } from 'react'; +import { + Box, + BoxProps, + Button, + Flex, + useColorModeValue, + Image, +} from '@chakra-ui/react'; +import { IconType } from 'react-icons'; +import { ArrowBackIcon } from '@chakra-ui/icons'; + +import { useRouter } from 'next/router'; + +import { + FiList, FiGrid, FiServer, FiDatabase, FiMenu, +} from 'react-icons/fi'; +import LogoImg from '../../static/images/logo.png'; +import { MenuItem, MenuGroupItem } from './MenuItem'; + +const Menus: Array<{ + group?: string; + name: string; + icon: IconType; + href: string; + subPath?:string[] +}> = [ + { name: 'Overview', icon: FiList, href: '/' }, + { name: 'Metrics', icon: FiMenu, href: '/metrics' }, + { name: 'Registry', icon: FiDatabase, href: '/registry' }, + { name: 'Topic', icon: FiGrid, href: '/topic' }, + { name: 'Event', icon: FiDatabase, href: '/event' }, + { + group: 'Workflow', + name: 'Workflows', + icon: FiServer, + href: '/workflows', + subPath: ['/workflows/create'], + }, + { + group: 'Workflow', + name: 'Event Catalogs', + icon: FiServer, + href: '/eventCatalogs', + }, + { + group: 'Clients', + name: 'TCP', + icon: FiServer, + href: '/tcp', + }, + { + group: 'Clients', + name: 'HTTP', + icon: FiServer, + href: '/http', + }, + { + group: 'Clients', + name: 'gRPC', + icon: FiServer, + href: '/grpc', + }, +]; + +interface MenuProps extends BoxProps { + onClose: () => void; +} +interface IGroupItem { + name?: string; + children: ReactNode[]; +} + +const NavMenu: FC<MenuProps> = ({ display = {}, onClose }) => { + const router = useRouter(); + const [curMenu, setCurMenu] = useState(''); + const curRoute = router.pathname; + + const MenuByGroup = Menus.reduce<{ + [groupName: string]: IGroupItem; + }>( + (groupItems, item) => { + const { + group, name, href, subPath, + } = item; + const menuItem = ( + <MenuItem + key={`menu_item_${name}`} + selected={curRoute === href || (subPath?.includes(curRoute) ?? false)} + active={curMenu === group} + href={href} + setActiveName={(selectedName:string) => setCurMenu(selectedName)} + > + {name} + </MenuItem> + ); + + if (!group) { + groupItems.topMenu.children.push(menuItem); + return groupItems; + } + + if (!groupItems[group]) { + groupItems[group] = { name: group, children: [] }; + } + groupItems[group].children.push(menuItem); + + return groupItems; + }, + { topMenu: { children: [] } }, + ); + + return ( + <Box + display={display} + pos="fixed" + w={{ base: 'full', md: 60 }} + borderRight="1px" + borderRightColor={useColorModeValue('gray.200', 'gray.700')} + h="full" + bg={useColorModeValue('white', 'gray.900')} + boxShadow="base" + > + <Flex + mt={{ base: 5, md: 10 }} + mb={{ base: 5, md: 10 }} + alignItems={{ base: 'space-between', md: 'center' }} + justifyContent={{ base: 'space-between', md: 'center' }} + w={{ base: 'full' }} + > + <Image + display={{ base: 'none', md: 'block' }} + w={100} + src={LogoImg.src} + alt="Dan Abramov" + /> + <Button + display={{ base: 'block', md: 'none' }} + w={{ base: 'full' }} + size="lg" + textAlign="left" + onClick={onClose} + > + <ArrowBackIcon mr={2} /> + Back + </Button> + </Flex> + + <Flex flexDirection="column" alignItems="center"> + {Object.entries(MenuByGroup).map((groupItem) => ( + <MenuGroupItem key={`group_item_${groupItem[1].name}`} name={groupItem[1].name ?? ''}> + {groupItem[1].children} + </MenuGroupItem> + ))} + </Flex> + </Box> + ); +}; + +export default NavMenu; diff --git a/components/navigation/MenusMobile.tsx b/components/navigation/MenusMobile.tsx new file mode 100644 index 0000000..8885f14 --- /dev/null +++ b/components/navigation/MenusMobile.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + IconButton, + Flex, + useColorModeValue, + Text, + FlexProps, +} from '@chakra-ui/react'; +import { FiMenu } from 'react-icons/fi'; + +interface MobileProps extends FlexProps { + onOpen: () => void; +} + +const MobileNav = ({ onOpen }: MobileProps) => ( + <Flex + ml={{ base: 0, md: 60 }} + px={{ base: 4, md: 24 }} + height="20" + alignItems="center" + bg={useColorModeValue('white', 'gray.900')} + borderBottomWidth="1px" + borderBottomColor={useColorModeValue('gray.200', 'gray.700')} + justifyContent="flex-start" + display={{ base: 'flex', md: 'none' }} + > + <IconButton + variant="outline" + onClick={onOpen} + aria-label="open menu" + icon={<FiMenu />} + /> + + <Text fontSize="2xl" ml="8" fontWeight="bold"> + EventMesh + </Text> + </Flex> +); + +export default MobileNav; diff --git a/components/navigation/Sidebar.tsx b/components/navigation/Sidebar.tsx new file mode 100644 index 0000000..e8e349b --- /dev/null +++ b/components/navigation/Sidebar.tsx @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable react/jsx-props-no-spreading */ +import React, { FC, ReactNode } from 'react'; +import { + Box, + useColorModeValue, + Drawer, + DrawerContent, + useDisclosure, +} from '@chakra-ui/react'; +import Menus from './Menus'; +import MenusMobile from './MenusMobile'; + +const Sidebar: FC<{ children: ReactNode }> = ({ children }) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + return ( + <> + <Box minH="100vh" bg={useColorModeValue('gray.100', 'gray.900')}> + <Menus + display={{ base: 'none', md: 'block' }} + onClose={() => onClose} + /> + <MenusMobile onOpen={onOpen} /> + <Box ml={{ base: 0, md: 60 }} p="4"> + {children} + </Box> + </Box> + <Drawer + autoFocus={false} + isOpen={isOpen} + placement="left" + onClose={onClose} + returnFocusOnClose={false} + onOverlayClick={onClose} + size="full" + > + <DrawerContent> + <Menus onClose={onClose} /> + </DrawerContent> + </Drawer> + </> + ); +}; + +export default Sidebar; diff --git a/components/registry/RegistryTable.tsx b/components/registry/RegistryTable.tsx new file mode 100644 index 0000000..719fde6 --- /dev/null +++ b/components/registry/RegistryTable.tsx @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + HStack, + Select, + Input, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + useToast, + Box, +} from '@chakra-ui/react'; +import axios from 'axios'; +import { useContext, useEffect, useState } from 'react'; +import { AppContext } from '../../context/context'; + +interface EventMeshInstance { + eventMeshClusterName: string, + eventMeshName: string, + endpoint: string, + lastUpdateTimestamp: number, + metadata: string +} + +const EventMeshInstanceRow = ({ + eventMeshClusterName, eventMeshName, endpoint, lastUpdateTimestamp, metadata, +}: EventMeshInstance) => ( + <Tr> + <Td>{eventMeshClusterName}</Td> + <Td>{eventMeshName}</Td> + <Td>{endpoint}</Td> + <Td>{lastUpdateTimestamp}</Td> + <Td>{metadata}</Td> + </Tr> +); + +const RegistryTable = () => { + const { state } = useContext(AppContext); + + const [searchInput, setSearchInput] = useState<string>(''); + const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => { + setSearchInput(event.currentTarget.value); + }; + + const [groupSet, setGroupSet] = useState<Set<string>>(new Set()); + const [groupFilter, setGroupFilter] = useState<string>(''); + const handleGroupSelectChange = (event: React.FormEvent<HTMLSelectElement>) => { + setGroupFilter(event.currentTarget.value); + }; + + const [EventMeshInstanceList, setEventMeshInstanceList] = useState<EventMeshInstance[]>([]); + const toast = useToast(); + useEffect(() => { + const fetch = async () => { + try { + const { data } = await axios.get<EventMeshInstance[]>(`${state.endpoint}/registry`); + setEventMeshInstanceList(data); + + const nextGroupSet = new Set<string>(); + data.forEach(({ eventMeshClusterName }) => { + nextGroupSet.add(eventMeshClusterName); + }); + setGroupSet(nextGroupSet); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to fetch registry list', + description: 'unable to connect to the EventMesh daemon', + status: 'error', + duration: 3000, + isClosable: true, + }); + setEventMeshInstanceList([]); + } + } + }; + + fetch(); + }, []); + + return ( + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + > + <HStack + spacing="2" + > + <Input + w="200%" + placeholder="Search" + value={searchInput} + onChange={handleSearchInputChange} + /> + <Select + placeholder="Select Group" + onChange={handleGroupSelectChange} + > + {Array.from(groupSet).map((group) => ( + <option value={group} key={group}>{group}</option> + ))} + </Select> + </HStack> + + <TableContainer> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Event Mesh Cluster Name</Th> + <Th>Event Mesh Name</Th> + <Th>Endpoint</Th> + <Th>Last Update Timestamp</Th> + <Th>Metadata</Th> + </Tr> + </Thead> + <Tbody> + {EventMeshInstanceList && EventMeshInstanceList.filter(({ + eventMeshClusterName, eventMeshName, + }) => { + if (searchInput && !eventMeshName.includes(searchInput)) { + return false; + } + if (groupFilter && groupFilter !== eventMeshClusterName) { + return false; + } + return true; + }).map(({ + eventMeshClusterName, eventMeshName, endpoint, lastUpdateTimestamp, metadata, + }) => ( + <EventMeshInstanceRow + eventMeshClusterName={eventMeshClusterName} + eventMeshName={eventMeshName} + endpoint={endpoint} + lastUpdateTimestamp={lastUpdateTimestamp} + metadata={metadata} + /> + ))} + </Tbody> + </Table> + </TableContainer> + </Box> + ); +}; + +export default RegistryTable; diff --git a/components/topic/TopicTable.tsx b/components/topic/TopicTable.tsx new file mode 100644 index 0000000..f3a1369 --- /dev/null +++ b/components/topic/TopicTable.tsx @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + HStack, + Input, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + useToast, + Box, + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useDisclosure, +} from '@chakra-ui/react'; +import axios from 'axios'; +import { useContext, useEffect, useState } from 'react'; +import { AppContext } from '../../context/context'; + +interface Topic { + name: string, + messageCount: number, +} + +interface TopicProps { + name: string, + messageCount: number, +} + +interface CreateTopicRequest { + name: string, +} + +interface RemoveTopicRequest { + name: string, +} + +const CreateTopicModal = () => { + const { state } = useContext(AppContext); + + const { isOpen, onOpen, onClose } = useDisclosure(); + const [topicName, setTopicName] = useState(''); + const handleTopicNameChange = (event: React.FormEvent<HTMLInputElement>) => { + setTopicName(event.currentTarget.value); + }; + + const toast = useToast(); + const [loading, setLoading] = useState(false); + const onCreateClick = async () => { + try { + setLoading(true); + await axios.post<CreateTopicRequest>(`${state.endpoint}/topic`, { + name: topicName, + }); + onClose(); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to create the topic', + description: error.message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } + } finally { + setLoading(false); + } + }; + + return ( + <> + <Button + colorScheme="blue" + onClick={onOpen} + > + Create Topic + </Button> + <Modal isOpen={isOpen} onClose={onClose}> + <ModalOverlay /> + <ModalContent> + <ModalHeader>Create Topic</ModalHeader> + <ModalCloseButton /> + <ModalBody> + <Input + placeholder="Topic Name" + value={topicName} + onChange={handleTopicNameChange} + /> + </ModalBody> + + <ModalFooter> + <Button + mr={2} + onClick={onClose} + > + Close + </Button> + <Button + colorScheme="blue" + onClick={onCreateClick} + isLoading={loading} + isDisabled={topicName.length === 0} + > + Create + </Button> + </ModalFooter> + </ModalContent> + </Modal> + </> + ); +}; + +const TopicRow = ({ + name, + messageCount, +}: TopicProps) => { + const { state } = useContext(AppContext); + + const toast = useToast(); + const [loading, setLoading] = useState(false); + const onRemoveClick = async () => { + try { + setLoading(true); + await axios.delete<RemoveTopicRequest>(`${state.endpoint}/topic`, { + data: { + name, + }, + }); + setLoading(false); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to remove the topic', + description: error.message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } + } + }; + + return ( + <Tr> + <Td>{name}</Td> + <Td>{messageCount}</Td> + <Td> + <HStack> + <Button + colorScheme="red" + isLoading={loading} + onClick={onRemoveClick} + > + Remove + </Button> + </HStack> + </Td> + </Tr> + ); +}; + +const TopicTable = () => { + const { state } = useContext(AppContext); + + const [searchInput, setSearchInput] = useState<string>(''); + const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => { + setSearchInput(event.currentTarget.value); + }; + + const [topicList, setTopicList] = useState<Topic[]>([]); + const toast = useToast(); + useEffect(() => { + const fetch = async () => { + try { + const { data } = await axios.get<Topic[]>(`${state.endpoint}/topic`); + setTopicList(data); + } catch (error) { + if (axios.isAxiosError(error)) { + toast({ + title: 'Failed to fetch the list of topics', + description: 'Unable to connect to the EventMesh daemon', + status: 'error', + duration: 3000, + isClosable: true, + }); + setTopicList([]); + } + } + }; + + fetch(); + }, []); + + return ( + <Box + maxW="full" + bg="white" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="4" + > + <HStack + spacing="2" + > + <Input + w="100%" + placeholder="Search" + value={searchInput} + onChange={handleSearchInputChange} + /> + <CreateTopicModal /> + </HStack> + + <TableContainer> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Topic Name</Th> + <Th>Message Count</Th> + <Th>Action</Th> + </Tr> + </Thead> + <Tbody> + {topicList.filter(({ + name, + }) => { + if (searchInput && !name.includes(searchInput)) { + return false; + } + return true; + }).map(({ + name, + messageCount, + }) => ( + <TopicRow + name={name} + messageCount={messageCount} + /> + ))} + </Tbody> + </Table> + </TableContainer> + </Box> + ); +}; + +export default TopicTable; diff --git a/components/workflow/Create.tsx b/components/workflow/Create.tsx new file mode 100644 index 0000000..d15d5a4 --- /dev/null +++ b/components/workflow/Create.tsx @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, useRef, useState } from 'react'; + +import { + Drawer, + DrawerContent, + DrawerOverlay, + DrawerHeader, + DrawerBody, + DrawerFooter, + DrawerCloseButton, + Button, + useToast, + Spinner, +} from '@chakra-ui/react'; +import axios from 'axios'; + +import Editor, { Monaco } from '@monaco-editor/react'; + +const ApiRoot = process.env.NEXT_PUBLIC_WORKFLOW_API_ROOT; + +const Create: FC<{ + visible: boolean; + onClose: () => void; + onSucceed: () => void; +}> = ({ visible = false, onClose = () => {}, onSucceed = () => {} }) => { + const toast = useToast(); + const [isSubmitting, setIsSubmitting] = useState(false); + const editorRef = useRef<Monaco | null>(null); + const defaultEditorValue = '# Your code goes here'; + + const onSubmit = () => { + setIsSubmitting(true); + + try { + const value = editorRef.current.getValue(); + if (value === defaultEditorValue) { + toast({ + title: 'Invalid definition', + description: 'Please input your workflow definition properly', + status: 'warning', + position: 'top-right', + }); + setIsSubmitting(false); + return; + } + axios + .post( + `${ApiRoot}/workflow`, + { workflow: { definition: value } }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + .then(() => { + toast({ + title: 'Succeeded', + status: 'success', + position: 'top-right', + }); + onSucceed(); + setIsSubmitting(false); + }) + .catch((error) => { + toast({ + title: 'Failed', + description: error.response.data, + status: 'error', + position: 'top-right', + }); + setIsSubmitting(false); + }); + } catch (error) { + setIsSubmitting(false); + } + }; + + const handleEditorDidMount = (editor: Monaco) => { + editorRef.current = editor; + }; + + return ( + <Drawer + isOpen={visible} + size="xl" + placement="right" + closeOnEsc={false} + onClose={() => onClose()} + > + <DrawerOverlay /> + <DrawerContent> + <DrawerCloseButton /> + <DrawerHeader>Create New Workflow</DrawerHeader> + <DrawerBody> + <Editor + height="1000px" + defaultLanguage="yaml" + defaultValue={defaultEditorValue} + onMount={handleEditorDidMount} + theme="vs-dark" + /> + </DrawerBody> + <DrawerFooter justifyContent="flex-start"> + <Button colorScheme="blue" mr={3} onClick={onSubmit}> + {isSubmitting ? <Spinner colorScheme="white" size="sm" /> : 'Submit'} + </Button> + <Button variant="ghost" colorScheme="blue" onClick={onClose}> + Cancel + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + ); +}; + +export default React.memo(Create); diff --git a/components/workflow/Details/Details.tsx b/components/workflow/Details/Details.tsx new file mode 100644 index 0000000..601cee9 --- /dev/null +++ b/components/workflow/Details/Details.tsx @@ -0,0 +1,285 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { + FC, useEffect, useRef, useState, +} from 'react'; + +import { + Drawer, + DrawerContent, + DrawerOverlay, + DrawerHeader, + DrawerBody, + DrawerFooter, + DrawerCloseButton, + Box, + FormLabel, + Flex, + Text, + Button, + Stack, + Tabs, + TabList, + TabPanels, + Tab, + TabPanel, + useToast, + AlertDialog, + AlertDialogOverlay, + AlertDialogContent, + AlertDialogHeader, + AlertDialogBody, + AlertDialogFooter, + Alert, + Badge, +} from '@chakra-ui/react'; + +import { WarningIcon, InfoIcon } from '@chakra-ui/icons'; +import moment from 'moment'; + +import Editor, { Monaco } from '@monaco-editor/react'; +import axios from 'axios'; +import { WorkflowType, WorkflowStatusEnum } from '../types'; +// import { WorkflowStatusMap } from '../constant'; +import Intances from './Instances'; + +const ApiRoot = process.env.NEXT_PUBLIC_WORKFLOW_API_ROOT; + +const Details: FC<{ + visible: boolean; + data?: WorkflowType | null; + onClose: () => void; + onSaved: () => void; +}> = ({ + visible = false, data, onClose = () => {}, onSaved = () => {}, +}) => { + const toast = useToast(); + const editorRef = useRef<Monaco | null>(null); + const [isShowConfirm, setIsShowComfirm] = useState(false); + const cancelRef = useRef(null); + const handleEditorDidMount = (editor: Monaco) => { + editorRef.current = editor; + editor.setValue(data?.definition ?? ''); + }; + + const onSubmit = () => { + const value = editorRef.current.getValue(); + + axios + .post( + `${ApiRoot}/workflow`, + { + workflow: { + workflow_id: data?.workflow_id, + definition: value, + }, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + .then(() => { + toast({ + title: 'Workflow saved', + status: 'success', + position: 'top-right', + }); + setIsShowComfirm(false); + onSaved(); + onClose(); + }) + .catch((error) => { + setIsShowComfirm(false); + toast({ + title: 'Failed to save', + description: error.response.data, + status: 'error', + position: 'top-right', + }); + }); + }; + + const onConfirm = () => { + const value = editorRef.current.getValue(); + if (data?.definition === value) { + onClose(); + return; + } + setIsShowComfirm(true); + }; + + useEffect(() => { + if (editorRef.current) { + editorRef.current.setValue(data?.definition ?? ''); + } + }, [data, editorRef]); + + return ( + <> + <AlertDialog + leastDestructiveRef={cancelRef} + isOpen={isShowConfirm} + onClose={onClose} + > + <AlertDialogOverlay> + <AlertDialogContent> + <AlertDialogHeader + fontSize="lg" + fontWeight="bold" + alignItems="center" + > + <WarningIcon boxSize="6" mr={2} color="orange" /> + <Text fontSize="xl" as="b"> + Confirm + </Text> + </AlertDialogHeader> + <AlertDialogBody> + Are you sure to save the changes to + {' '} + <Text as="b">{data?.workflow_id}</Text> + ? + </AlertDialogBody> + <AlertDialogFooter> + <Button ref={cancelRef} onClick={() => setIsShowComfirm(false)}> + No + </Button> + <Button colorScheme="blue" onClick={onSubmit} ml={3}> + Save + </Button> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialogOverlay> + </AlertDialog> + + <Drawer + isOpen={visible} + size="xl" + placement="right" + onClose={() => onClose()} + > + <DrawerOverlay /> + <DrawerContent> + <DrawerCloseButton /> + <DrawerHeader> + {data?.workflow_id} + {/* <Badge + ml={2} + colorScheme={ + data?.status === WorkflowStatusEnum.Normal ? 'blue' : 'red' + } + > + {WorkflowStatusMap.get(data?.status ?? 0) ?? '-'} + </Badge> */} + <Badge ml={2}> + Version + {' '} + {data?.version} + </Badge> + </DrawerHeader> + <DrawerBody> + <Stack mb="15px" direction="row" spacing={10}> + <Flex flexDirection="column" h="full"> + <Box mb={2}> + <FormLabel opacity={0.5}>Workflow Name</FormLabel> + <Text>{data?.workflow_name}</Text> + </Box> + <Box mb={2}> + <FormLabel opacity={0.5}>Created At</FormLabel> + <Text> + {moment(data?.update_time).format('YYYY-MM-DD HH:mm:ss')} + </Text> + </Box> + <Box> + <FormLabel opacity={0.5}>Updated At</FormLabel> + <Text> + {moment(data?.update_time).format('YYYY-MM-DD HH:mm:ss')} + </Text> + </Box> + </Flex> + + <Flex flexDirection="column" h="full"> + <Box mb={2}> + <FormLabel opacity={0.5}>Total Instance</FormLabel> + <Text>{data?.total_instances}</Text> + </Box> + <Box mb={2}> + <FormLabel opacity={0.5}>Running</FormLabel> + <Text>{data?.total_running_instances}</Text> + </Box> + <Box> + <FormLabel opacity={0.5}>Failed</FormLabel> + <Text>{data?.total_failed_instances}</Text> + </Box> + </Flex> + </Stack> + + <Tabs> + <TabList> + <Tab>Definition</Tab> + <Tab>Instances</Tab> + </TabList> + <TabPanels> + <TabPanel> + <Alert status="info" mb={2}> + <InfoIcon color="#3182ce" mr={2} /> + <Text fontSize="sm" color="#3182ce"> + You can edit the workflow directly and click "OK" to save + it + </Text> + </Alert> + <Editor + height="1000px" + defaultLanguage="yaml" + defaultValue="# Your code goes here" + onMount={handleEditorDidMount} + theme="vs-dark" + /> + </TabPanel> + <TabPanel> + <Intances workflowId={data?.workflow_id ?? ''} /> + </TabPanel> + </TabPanels> + </Tabs> + </DrawerBody> + <DrawerFooter justifyContent="flex-start"> + <Button colorScheme="blue" mr={3} onClick={onConfirm}> + OK + </Button> + <Button + colorScheme="blue" + mr={3} + variant="ghost" + onClick={onClose} + > + Cancel + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + </> + ); +}; + +Details.defaultProps = { + data: null, +}; +export default Details; diff --git a/components/workflow/Details/Instances.tsx b/components/workflow/Details/Instances.tsx new file mode 100644 index 0000000..3c46ebd --- /dev/null +++ b/components/workflow/Details/Instances.tsx @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { + FC, useCallback, useEffect, useState, +} from 'react'; +import axios from 'axios'; +import { + Flex, + Text, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + TableCaption, + Tag, + Spinner, +} from '@chakra-ui/react'; +import moment from 'moment'; +import { WorkflowInstanceType } from '../types'; +import { + WorkflowIntanceStatusMap, + WorkflowIntanceStatusColorMap, +} from '../constant'; + +const ApiRoot = process.env.NEXT_PUBLIC_WORKFLOW_API_ROOT; + +const Instances: FC<{ workflowId: string }> = ({ workflowId }) => { + const [isLoading, setIsLoading] = useState(true); + const [instances, setInstances] = useState<WorkflowInstanceType[]>([]); + const [total, setTotal] = useState(0); + const [pageIndex, setPageIndex] = useState(1); + const pageSize = 10; + + const getWorkflows = useCallback(async () => { + setIsLoading(true); + try { + const reqParams: { + page: number; + size: number; + workflow_id?: string; + } = { + page: pageIndex, + size: pageSize, + workflow_id: workflowId, + }; + + const { data } = await axios.get<{ + total: number; + workflow_instances: WorkflowInstanceType[]; + }>(`${ApiRoot}/workflow/instances`, { + params: reqParams, + }); + setInstances([...instances, ...(data?.workflow_instances ?? [])]); + setTotal(data.total); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + } + }, [workflowId, pageIndex, pageSize]); + + useEffect(() => { + const controller = new AbortController(); + getWorkflows(); + return () => { + controller.abort(); + }; + }, [workflowId, pageIndex, pageSize]); + + return ( + <TableContainer> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Instance ID</Th> + <Th>Status</Th> + <Th>Updated at</Th> + <Th>Created At</Th> + </Tr> + </Thead> + <Tbody> + {instances.map((workflow) => ( + <Tr key={workflow.workflow_instance_id}> + <Td>{workflow.workflow_instance_id}</Td> + <Td> + <Tag + size="sm" + colorScheme={WorkflowIntanceStatusColorMap.get( + workflow.workflow_status, + )} + variant="outline" + > + {WorkflowIntanceStatusMap.get(workflow.workflow_status)} + </Tag> + </Td> + <Td> + {moment(workflow.update_time).format('YYYY-MM-DD HH:mm:ss')} + </Td> + <Td> + {moment(workflow.create_time).format('YYYY-MM-DD HH:mm:ss')} + </Td> + </Tr> + ))} + </Tbody> + + {instances.length === 0 && ( + <TableCaption> + <Text variant="xs" color="#909090"> + empty + </Text> + </TableCaption> + )} + + {instances.length > 0 && ( + <TableCaption> + <Flex alignItems="center"> + <Text fontSize="sx"> + {`${instances.length} of ${total} intance${ + total > 1 ? 's' : '' + } in list `} + </Text> + {isLoading ? ( + <Spinner ml={2} size="sm" colorScheme="blue" /> + ) : ( + instances.length < total && ( + <Text + color={instances.length < total ? '#3182ce' : ''} + ml="2" + cursor="pointer" + as="u" + onClick={() => setPageIndex(pageIndex + 1)} + > + Load More + </Text> + ) + )} + </Flex> + </TableCaption> + )} + </Table> + </TableContainer> + ); +}; + +export default Instances; diff --git a/components/workflow/Details/index.ts b/components/workflow/Details/index.ts new file mode 100644 index 0000000..11a5b7d --- /dev/null +++ b/components/workflow/Details/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Details from './Details'; + +export default Details; diff --git a/components/workflow/constant.ts b/components/workflow/constant.ts new file mode 100644 index 0000000..f39a0c0 --- /dev/null +++ b/components/workflow/constant.ts @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { WorkflowStatusEnum, WorkflowInstanceStatusEnum } from './types'; + +export const WorkflowStatusMap = new Map([ + [WorkflowStatusEnum.Normal, 'Normal'], + [WorkflowStatusEnum.Deleted, 'Deleted'], +]); + +export const WorkflowIntanceStatusMap = new Map([ + [WorkflowInstanceStatusEnum.Sleep, 'Sleeping'], + [WorkflowInstanceStatusEnum.Wait, 'Waiting'], + [WorkflowInstanceStatusEnum.Process, 'Processing'], + [WorkflowInstanceStatusEnum.Succeed, 'Succeeded'], + [WorkflowInstanceStatusEnum.Fail, 'Failed'], +]); + +export const WorkflowIntanceStatusColorMap = new Map([ + [WorkflowInstanceStatusEnum.Sleep, 'gray'], + [WorkflowInstanceStatusEnum.Wait, 'orange'], + [WorkflowInstanceStatusEnum.Process, 'blue'], + [WorkflowInstanceStatusEnum.Succeed, 'green'], + [WorkflowInstanceStatusEnum.Fail, 'red'], +]); diff --git a/components/workflow/types.ts b/components/workflow/types.ts new file mode 100644 index 0000000..45fe58a --- /dev/null +++ b/components/workflow/types.ts @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type WorkflowType = { + create_time: string; + definition: string; + id: number; + status: number; + total_failed_instances: number; + total_instances: number; + total_running_instances: number; + update_time: string; + version: string; + workflow_id: string; + workflow_name: string; +}; + +export type WorkflowInstanceType = { + create_time: string, + id: number, + update_time: string, + workflow_id : string, + workflow_instance_id : string, + workflow_status : number +}; + +export enum WorkflowStatusEnum { + 'Normal' = 1, + 'Deleted' = -1, +} + +export enum WorkflowInstanceStatusEnum { + Sleep = 1, + Wait = 2, + Process = 3, + Succeed = 4, + Fail = 5, +} diff --git a/context/context.tsx b/context/context.tsx new file mode 100644 index 0000000..19b074f --- /dev/null +++ b/context/context.tsx @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + useMemo, + useEffect, + useState, + createContext, + Dispatch, +} from 'react'; +import { useImmerReducer } from 'use-immer'; +import { State, Action } from './type'; +import reducer from './reducer'; + +const initialState: State = { + endpoint: 'http://localhost:10106', +}; + +const AppContext = createContext<{ + state: State; + dispatch: Dispatch<Action>; +}>({ + state: initialState, + dispatch: () => null, +}); + +interface AppProviderProps { + children: React.ReactNode; +} + +const AppProvider = ({ children }: AppProviderProps) => { + const [state, dispatch] = useImmerReducer( + reducer, + initialState, + ); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const localState = localStorage.getItem('state'); + if (localState === null) { + return; + } + const parsedState: State = JSON.parse(localState); + dispatch({ + type: 'SetState', + payload: { + endpoint: parsedState.endpoint, + }, + }); + }, []); + + useEffect(() => { + if (!isLoading) localStorage.setItem('state', JSON.stringify(state)); + setIsLoading(false); + }, [state]); + + const context = useMemo(() => ({ + state, + dispatch, + }), [state]); + + return ( + <AppContext.Provider + value={context} + > + {children} + </AppContext.Provider> + ); +}; + +export { AppContext, AppProvider }; diff --git a/context/reducer.ts b/context/reducer.ts new file mode 100644 index 0000000..6e4eed7 --- /dev/null +++ b/context/reducer.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Action, State } from './type'; + +const reducer = ( + state: State, + action: Action, +) => { + switch (action.type) { + case 'SetState': + state.endpoint = action.payload.endpoint; + break; + + case 'SetEndPointAction': + state.endpoint = action.payload.endpoint; + break; + + default: + break; + } +}; + +export default reducer; diff --git a/context/type.ts b/context/type.ts new file mode 100644 index 0000000..5deb2ba --- /dev/null +++ b/context/type.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface State { + endpoint: string; +} + +interface SetState { + type: 'SetState'; + payload: { + endpoint: string; + }; +} + +interface SetEndPointAction { + type: 'SetEndPointAction'; + payload: { + endpoint: string; + }; +} + +export type Action = + | SetState + | SetEndPointAction; diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..b0f480c --- /dev/null +++ b/next.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +module.exports = nextConfig diff --git a/package.json b/package.json new file mode 100644 index 0000000..22d8dc8 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "eventmesh-dashboard", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint . --cache --fix --ext .ts,.tsx" + }, + "dependencies": { + "@chakra-ui/icons": "^2.0.15", + "@chakra-ui/react": "^2.1.2", + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@fontsource/inter": "^4.5.10", + "@monaco-editor/react": "^4.4.6", + "axios": "^0.27.2", + "cloudevents": "^6.0.2", + "framer-motion": "^6.3.6", + "immer": "^9.0.15", + "moment": "^2.29.4", + "next": "12.1.6", + "react": "18.1.0", + "react-dom": "18.1.0", + "react-icons": "^4.4.0", + "swr": "^1.3.0", + "use-immer": "^0.7.0" + }, + "devDependencies": { + "@types/node": "17.0.38", + "@types/react": "18.0.10", + "@types/react-dom": "18.0.5", + "@typescript-eslint/eslint-plugin": "^5.4.0", + "@typescript-eslint/parser": "^5.4.0", + "eslint": "8.16.0", + "eslint-config-airbnb": "^19.0.1", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "typescript": "4.7.2" + } +} diff --git a/pages/_app.tsx b/pages/_app.tsx new file mode 100644 index 0000000..6095173 --- /dev/null +++ b/pages/_app.tsx @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable react/jsx-props-no-spreading */ +import '@fontsource/inter'; +import { ChakraProvider, extendTheme } from '@chakra-ui/react'; +import type { AppProps } from 'next/app'; +import Sidebar from '../components/navigation/Sidebar'; +import { AppProvider } from '../context/context'; + +const theme = extendTheme({ + initialColorMode: 'light', + useSystemColorMode: true, + fonts: { + heading: 'Inter, sans-serif', + body: 'Inter, sans-serif', + }, +}); + +const Application = ({ Component, pageProps }: AppProps) => ( + <ChakraProvider theme={theme}> + <AppProvider> + <Sidebar> + <Component {...pageProps} /> + </Sidebar> + </AppProvider> + </ChakraProvider> +); + +export default Application; diff --git a/pages/_document.tsx b/pages/_document.tsx new file mode 100644 index 0000000..dd32fc0 --- /dev/null +++ b/pages/_document.tsx @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ColorModeScript } from '@chakra-ui/react'; +import NextDocument, { + Html, Head, Main, NextScript, +} from 'next/document'; + +export default class Document extends NextDocument { + render() { + return ( + <Html lang="en"> + <Head /> + <body> + <ColorModeScript /> + <Main /> + <NextScript /> + </body> + </Html> + ); + } +} diff --git a/pages/event.tsx b/pages/event.tsx new file mode 100644 index 0000000..8214d74 --- /dev/null +++ b/pages/event.tsx @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Head from 'next/head'; +import type { NextPage } from 'next'; +import EventTable from '../components/event/EventTable'; + +const Event: NextPage = () => ( + <> + <Head> + <title>Event | Apache EventMesh Dashboard</title> + </Head> + <EventTable /> + </> +); + +export default Event; diff --git a/pages/eventCatalogs.tsx b/pages/eventCatalogs.tsx new file mode 100644 index 0000000..f33d57e --- /dev/null +++ b/pages/eventCatalogs.tsx @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import Head from 'next/head'; +import type { NextPage } from 'next'; + +import { + Divider, + Button, + Flex, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Box, + Spinner, + Text, +} from '@chakra-ui/react'; +import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; +import axios from 'axios'; +import moment from 'moment'; +import Details from '../components/eventCatalogs/Details'; +import CreateCatalog from '../components/eventCatalogs/Create'; +import { EventCatalogType } from '../components/eventCatalogs/types'; +import { WorkflowStatusMap } from '../components/eventCatalogs/constant'; + +const ApiRoot = process.env.NEXT_PUBLIC_EVENTCATALOG_API_ROOT; + +const EventCatalogs: NextPage = () => { + const [isShowCreate, setIsShowCreate] = useState(false); + const [curCatalog, setCurCatalog] = useState<EventCatalogType>(); + + const [catalogs, setCatalogs] = useState<EventCatalogType[]>([]); + const [total, setTotal] = useState(0); + + const pageSize = 10; + const [isLoading, setIsLoading] = useState(true); + const [pageIndex, setPageIndex] = useState(1); + + const [refreshFlag, setRefreshFlag] = useState<number>(+new Date()); + + const getEventCatalogs = useCallback(async () => { + setIsLoading(true); + try { + const { data } = await axios.get<{ + total: number; + events: EventCatalogType[]; + }>(`${ApiRoot}/catalog`, { + params: { page: pageIndex, size: pageSize }, + }); + setCatalogs(data.events); + setTotal(data.total); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + } + }, []); + + useEffect(() => { + const controller = new AbortController(); + getEventCatalogs(); + return () => { + controller.abort(); + }; + }, [pageIndex, pageSize, refreshFlag]); + + return ( + <> + <Head> + <title>Event Catalogs | Apache EventMesh Dashboard</title> + </Head> + <Box + w="full" + h="full" + bg="white" + flexDirection="column" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="6" + > + <Flex w="full" justifyContent="space-between" mt="2" mb="2"> + <Button + size="md" + backgroundColor="#2a62ad" + color="white" + _hover={{ bg: '#dce5fe', color: '#2a62ad' }} + onClick={() => setIsShowCreate(true)} + > + Create Catalog + </Button> + <Button + size="md" + colorScheme="blue" + variant="ghost" + onClick={() => setRefreshFlag(+new Date())} + > + Refresh + </Button> + </Flex> + <Divider mt="15" mb="15" orientation="horizontal" /> + <TableContainer> + <Table variant="simple"> + <Thead> + <Tr> + {/* <Th>Catalog ID</Th> */} + <Th>Title</Th> + <Th>File Name</Th> + <Th>Version</Th> + <Th>Status</Th> + <Th>Created At</Th> + <Th>Updated At</Th> + </Tr> + </Thead> + <Tbody> + {catalogs.map((catalog) => ( + <Tr key={catalog.id}> + {/* <Td> + <Button + colorScheme="blue" + variant="ghost" + onClick={() => setCurCatalog(catalog)} + > + {catalog.id} + </Button> + </Td> */} + <Td> + <Button + colorScheme="blue" + variant="ghost" + onClick={() => setCurCatalog(catalog)} + > + {catalog.title} + </Button> + </Td> + <Td>{catalog.file_name}</Td> + <Td>{catalog.version}</Td> + <Td>{WorkflowStatusMap.get(catalog.status)}</Td> + <Td> + {moment(catalog.create_time).format('YYYY-MM-DD HH:mm:ss')} + </Td> + <Td> + {moment(catalog.update_time).format('YYYY-MM-DD HH:mm:ss')} + </Td> + </Tr> + ))} + </Tbody> + </Table> + </TableContainer> + <Flex mt={4} alignItems="center"> + {isLoading ? ( + <Spinner colorScheme="blue" size="sm" /> + ) : ( + <Text fontSize="sm" color="#909090"> + {total} + {` catalog${total > 1 ? 's' : ''} in total, `} + {`page ${pageIndex} of ${Math.ceil(total / pageSize)}`} + </Text> + )} + <Flex flex={1} justifyContent="flex-end" align="center"> + <Button + mr={2} + size="sm" + leftIcon={<ChevronLeftIcon />} + colorScheme="blue" + variant="outline" + disabled={pageIndex < 2} + onClick={() => setPageIndex(pageIndex - 1)} + > + Prev + </Button> + <Button + size="sm" + rightIcon={<ChevronRightIcon />} + colorScheme="blue" + variant="outline" + disabled={pageIndex >= Math.ceil(total / pageSize)} + onClick={() => setPageIndex(pageIndex + 1)} + > + Next + </Button> + </Flex> + </Flex> + </Box> + <Details + visible={Boolean(curCatalog)} + data={curCatalog} + onClose={() => setCurCatalog(undefined)} + /> + <CreateCatalog + visible={isShowCreate} + onSucceed={() => { + setIsShowCreate(false); + setPageIndex(1); + setRefreshFlag(+new Date()); + }} + onClose={() => setIsShowCreate(false)} + /> + </> + ); +}; + +export default EventCatalogs; diff --git a/pages/grpc.tsx b/pages/grpc.tsx new file mode 100644 index 0000000..8d68014 --- /dev/null +++ b/pages/grpc.tsx @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Head from 'next/head'; +import type { NextPage } from 'next'; +import GrpcClientTable from '../components/client/GrpcClientTable'; + +const GrpcClient: NextPage = () => ( + <> + <Head> + <title>Grpc Client | Apache EventMesh Dashboard</title> + </Head> + <GrpcClientTable /> + </> +); + +export default GrpcClient; diff --git a/pages/http.tsx b/pages/http.tsx new file mode 100644 index 0000000..2d692d9 --- /dev/null +++ b/pages/http.tsx @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Head from 'next/head'; +import type { NextPage } from 'next'; +import HTTPClientTable from '../components/client/HTTPClientTable'; + +const HTTPClient: NextPage = () => ( + <> + <Head> + <title>HTTP Client | Apache EventMesh Dashboard</title> + </Head> + <HTTPClientTable /> + </> +); + +export default HTTPClient; diff --git a/pages/index.tsx b/pages/index.tsx new file mode 100755 index 0000000..7077600 --- /dev/null +++ b/pages/index.tsx @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Head from 'next/head'; +import type { NextPage } from 'next'; +import Endpoint from '../components/index/Endpoint'; +import Configuration from '../components/index/Configuration'; + +const Index: NextPage = () => ( + <> + <Head> + <title>Apache EventMesh Dashboard</title> + </Head> + <Endpoint /> + <Configuration /> + </> +); + +export default Index; diff --git a/pages/metrics.tsx b/pages/metrics.tsx new file mode 100644 index 0000000..3793b64 --- /dev/null +++ b/pages/metrics.tsx @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Head from 'next/head'; +import type { NextPage } from 'next'; +import MetricsTable from '../components/metrics/MetricsTable'; + +const Metrics: NextPage = () => ( + <> + <Head> + <title>Metrics | Apache EventMesh Dashboard</title> + </Head> + <MetricsTable /> + </> +); + +export default Metrics; diff --git a/pages/registry.tsx b/pages/registry.tsx new file mode 100644 index 0000000..b2dd367 --- /dev/null +++ b/pages/registry.tsx @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Head from 'next/head'; +import type { NextPage } from 'next'; +import RegistryTable from '../components/registry/RegistryTable'; + +const Registry: NextPage = () => ( + <> + <Head> + <title>Registry | Apache EventMesh Dashboard</title> + </Head> + <RegistryTable /> + </> +); + +export default Registry; diff --git a/pages/tcp.tsx b/pages/tcp.tsx new file mode 100644 index 0000000..93a4028 --- /dev/null +++ b/pages/tcp.tsx @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Head from 'next/head'; +import type { NextPage } from 'next'; +import TCPClientTable from '../components/client/TCPClientTable'; + +const TCPClient: NextPage = () => ( + <> + <Head> + <title>TCP Client | Apache EventMesh Dashboard</title> + </Head> + <TCPClientTable /> + </> +); + +export default TCPClient; diff --git a/pages/topic.tsx b/pages/topic.tsx new file mode 100644 index 0000000..9023151 --- /dev/null +++ b/pages/topic.tsx @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Head from 'next/head'; +import type { NextPage } from 'next'; +import TopicTable from '../components/topic/TopicTable'; + +const Topic: NextPage = () => ( + <> + <Head> + <title>Topic | Apache EventMesh Dashboard</title> + </Head> + <TopicTable /> + </> +); + +export default Topic; diff --git a/pages/workflows.tsx b/pages/workflows.tsx new file mode 100644 index 0000000..29fd982 --- /dev/null +++ b/pages/workflows.tsx @@ -0,0 +1,375 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { + useCallback, useEffect, useState, useRef, +} from 'react'; +import Head from 'next/head'; +import type { NextPage } from 'next'; +import moment from 'moment'; +import { + ChevronLeftIcon, + ChevronRightIcon, + WarningTwoIcon, +} from '@chakra-ui/icons'; + +import { + Divider, + Button, + Flex, + Input, + Stack, + // Select, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Text, + AlertDialog, + AlertDialogOverlay, + AlertDialogContent, + AlertDialogHeader, + AlertDialogBody, + AlertDialogFooter, + useToast, + Box, + Spinner, +} from '@chakra-ui/react'; +import axios from 'axios'; +import Details from '../components/workflow/Details'; +import Create from '../components/workflow/Create'; +import { WorkflowType } from '../components/workflow/types'; +// import { WorkflowStatusMap } from '../components/workflow/constant'; + +const ApiRoot = process.env.NEXT_PUBLIC_WORKFLOW_API_ROOT; + +const Workflows: NextPage = () => { + const toast = useToast(); + + const [isLoading, setIsLoading] = useState(true); + const [workflows, setWorkflows] = useState<WorkflowType[]>([]); + const [total, setTotal] = useState(0); + const [keywordFilter, setKeywordFilter] = useState(''); + const [statusFilter, setStatusFilter] = useState('any'); + const [pageIndex, setPageIndex] = useState(1); + const pageSize = 10; + const [refreshFlag, setRefreshFlag] = useState<number>(+new Date()); + const [isShowCreate, setIsShowCreate] = useState(false); + const [isShowDetails, setIsShowDetails] = useState(false); + const [isShowCancelConfirm, setIsShowCancelComfirm] = useState(false); + const cancelRef = useRef(null); + + const [selectedWorkflow, setSelectedWorkflow] = useState<WorkflowType | null>( + null, + ); + + const onDelete = () => { + axios + .delete(`${ApiRoot}/workflow/${selectedWorkflow?.workflow_id}`) + .then(() => { + toast({ + title: 'Workflow has been deleted', + description: ( + <Box> + <Text>{`Workflow ID: ${selectedWorkflow?.workflow_id}`}</Text> + <Text>{`Workflow Name: ${selectedWorkflow?.workflow_name}`}</Text> + </Box> + ), + status: 'success', + position: 'top-right', + }); + setIsShowCancelComfirm(false); + setSelectedWorkflow(null); + setRefreshFlag(+new Date()); + }) + .catch((error) => { + setIsShowCancelComfirm(false); + toast({ + title: 'Failed to delete', + description: error.response.data, + status: 'error', + position: 'top-right', + }); + }); + }; + + const getWorkflows = useCallback(async () => { + setIsLoading(true); + try { + const reqParams: { + page: number; + size: number; + workflow_id?: string; + status?: string; + } = { + page: pageIndex, + size: pageSize, + }; + // if (statusFilter) { + // reqParams.status = statusFilter; + // } + if (keywordFilter) { + reqParams.workflow_id = keywordFilter; + } + const { data } = await axios.get<{ + total: number; + workflows: WorkflowType[]; + }>(`${ApiRoot}/workflow`, { + params: reqParams, + }); + setWorkflows(data.workflows); + setTotal(data.total); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + } + }, [pageIndex, pageSize, keywordFilter, statusFilter, refreshFlag]); + + useEffect(() => { + const controller = new AbortController(); + getWorkflows(); + return () => { + controller.abort(); + }; + }, [pageIndex, pageSize, keywordFilter, statusFilter, refreshFlag]); + + return ( + <> + <Head> + <title>Workflows | Apache EventMesh Dashboard</title> + </Head> + <Flex + w="full" + h="full" + bg="white" + flexDirection="column" + borderWidth="1px" + borderRadius="md" + overflow="hidden" + p="6" + > + <Flex w="full" justifyContent="space-between" mt="2" mb="2"> + <Button + size="md" + backgroundColor="#2a62ad" + color="white" + _hover={{ bg: '#dce5fe', color: '#2a62ad' }} + onClick={() => setIsShowCreate(true)} + > + Create Workflow + </Button> + <Stack direction="row" spacing="2"> + <Input + size="md" + placeholder="Workflow ID" + value={keywordFilter} + onChange={(evt) => setKeywordFilter(evt.target.value)} + /> + {/* <Select + size="md" + placeholder="Status" + value={statusFilter} + onChange={(event) => setStatusFilter(event.target.value)} + > + <option value="1">Running</option> + <option value="-1">Deleted</option> + </Select> */} + <Box> + <Button + colorScheme="blue" + variant="ghost" + onClick={() => setRefreshFlag(+new Date())} + > + Refresh + </Button> + </Box> + </Stack> + </Flex> + <Divider mt="15" mb="15" orientation="horizontal" /> + <TableContainer> + <Table variant="simple"> + <Thead> + <Tr> + <Th>Workflow ID</Th> + <Th>Workflow Name</Th> + {/* <Th>Status</Th> */} + <Th isNumeric>Total Instance</Th> + <Th isNumeric>Running</Th> + <Th isNumeric>Failed</Th> + <Th>Updated at</Th> + <Th>Created At</Th> + <Th>Actions</Th> + </Tr> + </Thead> + <Tbody> + {workflows.map((workflow) => ( + <Tr key={workflow.workflow_id}> + <Td> + <Button + size="sm" + colorScheme="blue" + variant="ghost" + onClick={() => { + setIsShowDetails(true); + setSelectedWorkflow(workflow); + }} + > + {workflow.workflow_id} + </Button> + </Td> + <Td>{workflow.workflow_name}</Td> + + {/* <Td>{WorkflowStatusMap.get(workflow.status)}</Td> */} + <Td isNumeric>{workflow.total_instances}</Td> + <Td isNumeric>{workflow.total_running_instances}</Td> + <Td isNumeric>{workflow.total_failed_instances}</Td> + <Td> + {moment(workflow.update_time).format('YYYY-MM-DD HH:mm:ss')} + </Td> + <Td> + {moment(workflow.create_time).format('YYYY-MM-DD HH:mm:ss')} + </Td> + <Td> + <Button + size="sm" + colorScheme="blue" + variant="ghost" + onClick={() => { + setSelectedWorkflow(workflow); + setIsShowCancelComfirm(true); + }} + > + Delete + </Button> + </Td> + </Tr> + ))} + </Tbody> + </Table> + </TableContainer> + <Flex mt={4} alignItems="center"> + {isLoading ? ( + <Spinner colorScheme="blue" size="sm" /> + ) : ( + <Text fontSize="sm" color="#909090"> + {total} + {` workflow${total > 1 ? 's' : ''} in total, `} + {`page ${pageIndex} of ${Math.ceil(total / pageSize)}`} + </Text> + )} + <Flex flex={1} justifyContent="flex-end" align="center"> + <Button + mr={2} + size="sm" + leftIcon={<ChevronLeftIcon />} + colorScheme="blue" + variant="outline" + disabled={pageIndex < 2} + onClick={() => setPageIndex(pageIndex - 1)} + > + Prev + </Button> + <Button + size="sm" + rightIcon={<ChevronRightIcon />} + colorScheme="blue" + variant="outline" + disabled={pageIndex >= Math.ceil(total / pageSize)} + onClick={() => setPageIndex(pageIndex + 1)} + > + Next + </Button> + </Flex> + </Flex> + </Flex> + + <AlertDialog + leastDestructiveRef={cancelRef} + isOpen={isShowCancelConfirm} + onClose={() => setIsShowCancelComfirm(false)} + > + <AlertDialogOverlay> + <AlertDialogContent> + <AlertDialogHeader fontSize="lg" fontWeight="bold"> + <Flex alignItems="center"> + <WarningTwoIcon mr={2} boxSize={6} color="orange" /> + <Text fontSize="xl" as="b"> + Confirm + </Text> + </Flex> + </AlertDialogHeader> + + <AlertDialogBody> + Are you sure to delete + {' '} + <Text fontSize="sm" as="b"> + {selectedWorkflow?.workflow_name} + </Text> + ? + <Box /> + </AlertDialogBody> + + <AlertDialogFooter> + <Button + ref={cancelRef} + onClick={() => { + setIsShowCancelComfirm(false); + setSelectedWorkflow(null); + }} + > + No + </Button> + <Button colorScheme="blue" onClick={() => onDelete()} ml={3}> + Delete + </Button> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialogOverlay> + </AlertDialog> + + <Details + visible={isShowDetails} + data={selectedWorkflow} + onSaved={() => { + setIsShowDetails(false); + setRefreshFlag(+new Date()); + }} + onClose={() => { + setIsShowDetails(false); + setSelectedWorkflow(null); + }} + /> + + <Create + visible={isShowCreate} + onSucceed={() => { + setIsShowCreate(false); + setPageIndex(1); + setRefreshFlag(+new Date()); + }} + onClose={() => setIsShowCreate(false)} + /> + </> + ); +}; + +export default Workflows; diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/public/favicon.ico differ diff --git a/static/images/logo.png b/static/images/logo.png new file mode 100644 index 0000000..e854551 Binary files /dev/null and b/static/images/logo.png differ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..99710e8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
