diff --git a/README.md b/README.md index 61d1a468..68d211d0 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,33 @@ We want to achieve this by structuring our UI according to the topology of Apach * Node.js Version **20.0.0** or higher * Docker Desktop -## Backend and Pulsar instance +## Startup First, build the application JAR from the `backend` directory with: `./mvnw package -DskipTests` -Then, start Docker Desktop and create the pulsar setup from the root-directory with: +### Start Frontend, Backend and Pulsar instance without demodata + +Start Docker Desktop and create the pulsar setup from the root-directory with: + +```bash +echo BACKEND_IP=localhost >> .env +docker-compose --profile backend --profile frontend up --build -d +``` + +### Start Frontend, Backend and Pulsar instance with demodata + +Start Docker Desktop and create the pulsar setup from the root-directory with: ```bash echo BACKEND_IP=localhost >> .env -docker-compose --profile demodata --profile backend --profile frontend up --build -d +docker-compose --profile backend --profile frontend --profile demodata -f docker-compose.yml -f docker-compose-setup.yml up --build -d ``` Notes: +* The `docker-compose.yml` includes the pulsar, backend and frontend services. +* The `docker-compose-setup.yml` includes services for the local or AWS demodata setup. `-f` selects the files used for the startup. * `--build` is needed for the first startup, or when the demodata docker images are changed * `-d` runs the container in the background, so u can close the terminal without terminating the app itself. * `--profile demodata` is needed when you want to create the demo topology and start the demo producers & consumers, that will continuously send and receive messages @@ -64,5 +77,5 @@ so you need to pass it to the `docker-compose` via `-e` flag. ```bash echo BACKEND_IP=${EC2_IP_ADDRESS} >> .env -docker-compose --profile demodata-aws --profile backend --profile frontend up --build -d +docker-compose --profile backend --profile frontend --profile demodata-aws -f docker-compose.yml -f docker-compose-setup.yml up --build -d ``` diff --git a/backend/src/main/java/de/amos/apachepulsarui/ApachePulsarUiApplication.java b/backend/src/main/java/de/amos/apachepulsarui/ApachePulsarUiApplication.java index 656ff76b..56c2ddc3 100644 --- a/backend/src/main/java/de/amos/apachepulsarui/ApachePulsarUiApplication.java +++ b/backend/src/main/java/de/amos/apachepulsarui/ApachePulsarUiApplication.java @@ -6,6 +6,7 @@ package de.amos.apachepulsarui; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; @@ -17,6 +18,9 @@ @EnableCaching public class ApachePulsarUiApplication { + @Value("${frontend.url}") + private String allowedOrigin; + public static void main(String[] args) { SpringApplication.run(ApachePulsarUiApplication.class, args); } @@ -26,7 +30,7 @@ public WebMvcConfigurer configurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry corsRegistry) { - corsRegistry.addMapping("/**").allowedOrigins("*"); + corsRegistry.addMapping("/**").allowedOrigins(allowedOrigin); } }; } diff --git a/backend/src/main/java/de/amos/apachepulsarui/controller/MessageController.java b/backend/src/main/java/de/amos/apachepulsarui/controller/MessageController.java index 2af3672d..11f46973 100644 --- a/backend/src/main/java/de/amos/apachepulsarui/controller/MessageController.java +++ b/backend/src/main/java/de/amos/apachepulsarui/controller/MessageController.java @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Set; @RestController @RequestMapping("/messages") @@ -30,8 +31,10 @@ public class MessageController { public ResponseEntity getMessages(@RequestParam String topic, @RequestParam(required = false, defaultValue = "10") Integer numMessages, @RequestParam(required = false, defaultValue = "") List producers, - @RequestParam(required = false, defaultValue = "") List subscriptions) { - List messageDtos = messageService.getLatestMessagesFiltered(topic, numMessages, producers, subscriptions); + @RequestParam(required = false, defaultValue = "") List subscriptions) + { + Set messageDtos = messageService.getLatestMessagesFiltered(topic, numMessages, producers, subscriptions); return new ResponseEntity<>(new MessagesDto(messageDtos), HttpStatus.OK); } + } diff --git a/backend/src/main/java/de/amos/apachepulsarui/dto/MessagesDto.java b/backend/src/main/java/de/amos/apachepulsarui/dto/MessagesDto.java index 23a05e67..318bde2c 100644 --- a/backend/src/main/java/de/amos/apachepulsarui/dto/MessagesDto.java +++ b/backend/src/main/java/de/amos/apachepulsarui/dto/MessagesDto.java @@ -8,13 +8,13 @@ import lombok.AllArgsConstructor; import lombok.Data; -import java.util.List; +import java.util.Set; @Data @AllArgsConstructor public class MessagesDto { - private List messages; + private Set messages; } diff --git a/backend/src/main/java/de/amos/apachepulsarui/service/MessageService.java b/backend/src/main/java/de/amos/apachepulsarui/service/MessageService.java index 40b3cb40..a7d37f96 100644 --- a/backend/src/main/java/de/amos/apachepulsarui/service/MessageService.java +++ b/backend/src/main/java/de/amos/apachepulsarui/service/MessageService.java @@ -19,7 +19,11 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Service @Slf4j @@ -27,8 +31,8 @@ public class MessageService { private final PulsarAdmin pulsarAdmin; - public List getLatestMessagesFiltered(String topic, Integer numMessages, List producers, List subscriptions) { - List messageDtos = getLatestMessagesOfTopic(topic, numMessages); + public Set getLatestMessagesFiltered(String topic, Integer numMessages, List producers, List subscriptions) { + Set messageDtos = getLatestMessagesOfTopic(topic, numMessages); if (!producers.isEmpty()) { messageDtos = filterByProducers(messageDtos, producers); } @@ -39,12 +43,14 @@ public List getLatestMessagesFiltered(String topic, Integer numMessa return messageDtos; } - private List filterBySubscription(List messageDtos, Integer numMessages, String topic, List subscriptions) { + private Set filterBySubscription(Set messageDtos, Integer numMessages, String topic, List subscriptions) { List messageIds = subscriptions.stream() .flatMap(s -> peekMessageIds(topic, s, numMessages).stream()) .toList(); - return messageDtos.stream().filter(m -> messageIds.contains(m.getMessageId())).toList(); + return messageDtos.stream() + .filter(m -> messageIds.contains(m.getMessageId())) + .collect(Collectors.toCollection(LinkedHashSet::new)); } private List peekMessageIds(String topic, String subscription, Integer numMessages) { @@ -60,14 +66,13 @@ private List peekMessageIds(String topic, String subscription, Integer n } - private List filterByProducers(List messageDtos, List producers) { + private Set filterByProducers(Set messageDtos, List producers) { return messageDtos.stream() .filter(m -> producers.contains(m.getProducer())) - .toList(); - + .collect(Collectors.toCollection(LinkedHashSet::new)); } - private List getLatestMessagesOfTopic(String topic, Integer numMessages) { + private Set getLatestMessagesOfTopic(String topic, Integer numMessages) { var schema = getSchemaIfExists(topic); try { var messages = new ArrayList>(); @@ -83,7 +88,10 @@ private List getLatestMessagesOfTopic(String topic, Integer numMessa } return messages.stream() .map(message -> MessageDto.fromExistingMessage(message, schema)) - .toList(); + // latest message first in set + .sorted(Comparator.comparing(MessageDto::getPublishTime, Comparator.reverseOrder())) + // linked to keep the order! + .collect(Collectors.toCollection(LinkedHashSet::new)); } catch (PulsarAdminException e) { throw new PulsarApiException( "Could not examine the amount of '%d' messages for topic '%s'".formatted(numMessages, topic), diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 82632e45..5cb5d666 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -2,3 +2,4 @@ server.port=8081 pulsar.consumer.url = pulsar://localhost:6650 pulsar.admin.url = http://localhost:8080 server.servlet.context-path=/api +frontend.url = http://localhost:8082 diff --git a/backend/src/test/java/de/amos/apachepulsarui/controller/MessageControllerTest.java b/backend/src/test/java/de/amos/apachepulsarui/controller/MessageControllerTest.java index a14dd673..a7a8827f 100644 --- a/backend/src/test/java/de/amos/apachepulsarui/controller/MessageControllerTest.java +++ b/backend/src/test/java/de/amos/apachepulsarui/controller/MessageControllerTest.java @@ -16,8 +16,9 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import static java.util.Collections.emptyList; import static org.hamcrest.Matchers.equalTo; @@ -37,7 +38,7 @@ public class MessageControllerTest { @Test void getMessages_returnsMessages() throws Exception { - List messageDtos = List.of( + Set messageDtos = Set.of( aMessage("persistent://public/default/spaceships", "Nebuchadnezzar"), aMessage("persistent://public/default/spaceships", "Serenity") ); @@ -54,7 +55,7 @@ void getMessages_returnsMessages() throws Exception { @Test void getMessages_withoutNumMessages_returns10Messages() throws Exception { - var messageDtos = new ArrayList(); + HashSet messageDtos = new HashSet<>(); for (int i = 0; i < 10; i++) { messageDtos.add(aMessage("persistent://public/default/test", "Test" + i)); } @@ -70,7 +71,7 @@ void getMessages_withoutNumMessages_returns10Messages() throws Exception { @Test void getMessages_withProducer_returns10Messages() throws Exception { - var messageDtos = new ArrayList(); + HashSet messageDtos = new HashSet<>(); for (int i = 0; i < 10; i++) { messageDtos.add(aMessage("persistent://public/default/test", "Test" + i)); } @@ -86,7 +87,7 @@ void getMessages_withProducer_returns10Messages() throws Exception { @Test void getMessages_withSubscription_returns10Messages() throws Exception { - var messageDtos = new ArrayList(); + HashSet messageDtos = new HashSet<>(); for (int i = 0; i < 10; i++) { messageDtos.add(aMessage("persistent://public/default/test", "Test" + i)); } diff --git a/backend/src/test/java/de/amos/apachepulsarui/service/MessageServiceIntegrationTest.java b/backend/src/test/java/de/amos/apachepulsarui/service/MessageServiceIntegrationTest.java index 0774fec3..8f632307 100644 --- a/backend/src/test/java/de/amos/apachepulsarui/service/MessageServiceIntegrationTest.java +++ b/backend/src/test/java/de/amos/apachepulsarui/service/MessageServiceIntegrationTest.java @@ -65,7 +65,7 @@ void getNumberOfLatestMessagesFromTopic_returnsMessages() throws Exception { } var messages = messageService.getLatestMessagesFiltered(TOPICNAME, 1, emptyList(), emptyList()); - MessageDto messageReceived = messages.get(0); + MessageDto messageReceived = messages.iterator().next(); assertThat(messageReceived.getMessageId()).isNotEmpty(); // generated assertThat(messageReceived.getTopic()).isEqualTo(messageToSend.getTopic()); assertThat(messageReceived.getPayload()).isEqualTo(messageToSend.getPayload()); @@ -91,7 +91,7 @@ void getNumberOfLatestMessagesFromTopicFilteredByProducer_returnsMessages() thro } var messages = messageService.getLatestMessagesFiltered(TOPICNAME, 1, List.of(producerName), emptyList()); - MessageDto messageReceived = messages.get(0); + MessageDto messageReceived = messages.iterator().next(); assertThat(messageReceived.getMessageId()).isNotEmpty(); // generated assertThat(messageReceived.getTopic()).isEqualTo(messageToSend.getTopic()); assertThat(messageReceived.getPayload()).isEqualTo(messageToSend.getPayload()); @@ -155,7 +155,7 @@ void getNumberOfLatestMessagesFromTopicFilteredBySubscription_returnsMessages() } var messages = messageService.getLatestMessagesFiltered(TOPICNAME, 1, emptyList(), List.of(subscriptionName)); - MessageDto messageReceived = messages.get(0); + MessageDto messageReceived = messages.iterator().next(); assertThat(messageReceived.getMessageId()).isNotEmpty(); // generated assertThat(messageReceived.getTopic()).isEqualTo(messageToSend.getTopic()); assertThat(messageReceived.getPayload()).isEqualTo(messageToSend.getPayload()); @@ -179,7 +179,7 @@ void getNumberOfLatestMessagesFromTopic_forMessageWithSchema_returnsSchema() thr } var messages = messageService.getLatestMessagesFiltered(TOPICNAME, 1, emptyList(), emptyList()); - MessageDto messageReceived = messages.get(0); + MessageDto messageReceived = messages.iterator().next(); assertThat(messageReceived.getSchema()).isEqualTo(schema.getSchemaInfo().getSchemaDefinition()); } diff --git a/docker-compose-setup.yml b/docker-compose-setup.yml new file mode 100644 index 00000000..f312b003 --- /dev/null +++ b/docker-compose-setup.yml @@ -0,0 +1,49 @@ +version: '3' +services: + + backend: + depends_on: + setup-topology: + condition: service_completed_successfully + + setup-topology: + image: pulsarui/setuptopology + profiles: + - demodata + build: + context: demodata/setup-topology + dockerfile: Dockerfile + environment: + - 'PULSAR_ADMIN_URL=http://pulsar:8080' + - 'USE_AWS=false' + depends_on: + pulsar: + condition: service_healthy + + setup-topology-aws: + image: pulsarui/setuptopology + profiles: + - demodata-aws + build: + context: demodata/setup-topology + dockerfile: Dockerfile + environment: + - 'PULSAR_ADMIN_URL=http://pulsar:8080' + - 'USE_AWS=true' + depends_on: + pulsar: + condition: service_healthy + + demo-producer-consumer: + image: pulsarui/demoproducerconsumer + profiles: + - demodata + build: + context: demodata/demo-producer-consumer + dockerfile: Dockerfile + environment: + - 'PULSAR_URL=pulsar://pulsar:6650' + - 'PUBLISH_INTERVAL_SECONDS=30' + depends_on: + setup-topology: + condition: service_completed_successfully \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 31707acf..d30df79c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: environment: - "PULSAR_CONSUMER_URL=pulsar://pulsar:6650" - "PULSAR_ADMIN_URL=http://pulsar:8080" + - "FRONTEND_URL=http://${BACKEND_IP}:8082" depends_on: pulsar: condition: service_healthy @@ -48,46 +49,4 @@ services: test: ["CMD", "bin/pulsar-admin", "brokers", "healthcheck"] interval: 5s timeout: 10s - retries: 10 - - setup-topology: - image: pulsarui/setuptopology - profiles: - - demodata - build: - context: demodata/setup-topology - dockerfile: Dockerfile - environment: - - 'PULSAR_ADMIN_URL=http://pulsar:8080' - - 'USE_AWS=false' - depends_on: - pulsar: - condition: service_healthy - - setup-topology-aws: - image: pulsarui/setuptopology - profiles: - - demodata-aws - build: - context: demodata/setup-topology - dockerfile: Dockerfile - environment: - - 'PULSAR_ADMIN_URL=http://pulsar:8080' - - 'USE_AWS=true' - depends_on: - pulsar: - condition: service_healthy - - demo-producer-consumer: - image: pulsarui/demoproducerconsumer - profiles: - - demodata - build: - context: demodata/demo-producer-consumer - dockerfile: Dockerfile - environment: - - 'PULSAR_URL=pulsar://pulsar:6650' - - 'PUBLISH_INTERVAL_SECONDS=30' - depends_on: - setup-topology: - condition: service_completed_successfully + retries: 10 \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 019ce8ea..6116e1ae 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,98 +10,25 @@ import { selectView } from './store/globalSlice' import NavBar from './components/NavBar' import Dashboard from './components/Dashboard' import { Route, BrowserRouter as Router, Routes } from 'react-router-dom' -import ClusterGroup from './components/pages/cluster' -import NamespaceGroup from './components/pages/namespace' -import TenantGroup from './components/pages/tenant' -import TopicGroup from './components/pages/topic' - -const allData: Array = [] -const allMessages: Array = [] - +import ClusterGroup from './routes/cluster' +import NamespaceGroup from './routes/namespace' +import TenantGroup from './routes/tenant' +import TopicGroup from './routes/topic' + +/** + * The main application component. + * It sets up the main routes for the application and also renders the main Dashboard and NavBar components. + * + * @component + * @returns The main application component rendered to the DOM. + */ function App() { - const view = useAppSelector(selectView) - /** Landing Page Logic */ - // const showLP = useAppSelector(selectShowLP) - /** End of Landing Page Logic */ - - /*const allTenants = allData - .map((item) => item.tenants) - .filter((el) => el.length > 0) - .flat() - - const allNamespaces = allTenants - .map((tenant) => tenant.namespaces) - .filter((el) => el.length > 0) - .flat() - - const allTopics = allNamespaces - .map((namespace) => namespace.topics) - .filter((el) => el.length > 0) - .flat()*/ - - /*const allMessages = allData - .flatMap((item) => item.namespaces) - .flatMap((namespace) => namespace.topics) - .map((topic) => topic.messages) - .filter((el) => el.length > 0) - .flat()*/ - - /*let filteredData: - | Array - | Array - | Array = allData - - if (view.selectedNav === 'namespace') { - filteredData = allNamespaces - } else if (view.selectedNav === 'topic') { - filteredData = allTopics - }*/ - - /*const selectNewElement = ( - item: SampleCluster | SampleNamespace | SampleTopic - ) => { - const selEl = getNewElementTag(item.tag, item.id) - console.log(selEl) - //dispatch(setNav(selEl[0])) - }*/ - - //can later on be replaced by the fetchDataThunk - /*const getData = () => { - fetch('dummy/dummyClusters.json', { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }) - .then(function (response) { - return response.json() - }) - .then(function (json) { - allData = json - }) - }*/ - - /*const getMessages = () => { - fetch('dummy/dummyMessages.json', { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }) - .then(function (response) { - return response.json() - }) - .then(function (json) { - allMessages = json - }) - }*/ - return ( <>
- + }> }> diff --git a/frontend/src/Helpers.tsx b/frontend/src/Helpers.tsx index fc4e0ce1..c001c7dd 100644 --- a/frontend/src/Helpers.tsx +++ b/frontend/src/Helpers.tsx @@ -2,94 +2,6 @@ // SPDX-FileCopyrightText: 2010-2021 Dirk Riehle -export function instanceOfSampleCluster( - object: - | SampleCluster - | SampleTenant - | SampleNamespace - | SampleTopic - | SampleMessage -): object is SampleCluster { - if (object) { - return 'tenants' in object - } else return false -} - -export function instanceOfSampleNamespace( - object: - | SampleCluster - | SampleTenant - | SampleNamespace - | SampleTopic - | SampleMessage -): object is SampleNamespace { - if (object) { - return 'topics' in object - } else return false -} - -export function instanceOfSampleTopic( - object: - | SampleCluster - | SampleTenant - | SampleNamespace - | SampleTopic - | SampleMessage -): object is SampleTopic { - if (object) { - return 'topicStatsDto' in object - } else return false -} - -export function instanceOfSampleTenant( - object: - | SampleCluster - | SampleTenant - | SampleNamespace - | SampleTopic - | SampleMessage -): object is SampleTenant { - if (object) { - return 'tenantInfo' in object - } else return false -} - -export function instanceOfSampleMessage( - object: - | SampleCluster - | SampleTenant - | SampleNamespace - | SampleTopic - | SampleMessage -): object is SampleMessage { - if (object) { - return 'payload' in object - } else return false -} - -export const flattenClustersToTenants = (myClusters: Array) => { - return myClusters - .map((cluster) => cluster.tenants) - .filter((el) => el.length > 0) - .flat() -} - -export const flattenTenantsToNamespaces = (myTenants: Array) => { - return myTenants - .map((tenant) => tenant.namespaces) - .filter((el) => el.length > 0) - .flat() -} - -export const flattenNamespacesToTopics = ( - myNamespaces: Array -) => { - return myNamespaces - .map((namespace) => namespace.topics) - .filter((el) => el.length > 0) - .flat() -} - /** * Helper function to add comma separator if not the last element. * @param index the index of current element in array. diff --git a/frontend/src/__tests__/ClusterGroup.test.tsx b/frontend/src/__tests__/ClusterGroup.test.tsx index 336f6cdc..6861eea9 100644 --- a/frontend/src/__tests__/ClusterGroup.test.tsx +++ b/frontend/src/__tests__/ClusterGroup.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { render, screen } from '@testing-library/react' import { Provider } from 'react-redux' import store from '../store' -import ClusterView from '../components/pages/cluster/ClusterView' +import ClusterView from '../routes/cluster/ClusterView' import { Route, BrowserRouter as Router, Routes } from 'react-router-dom' const dataTest: Array = [ diff --git a/frontend/src/__tests__/Form.test.tsx b/frontend/src/__tests__/Form.test.tsx deleted file mode 100644 index 29043288..00000000 --- a/frontend/src/__tests__/Form.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react' -import { render, fireEvent } from '@testing-library/react' -import Form from '../components/form/Form' -import { Provider } from 'react-redux' -import store from '../store' - -const triggerUpdateTest = (msg: string, tpc: string) => { - console.log(msg, tpc) -} - -const dataTest = [ - { - id: '100', - name: 'Topic 1', - messages: [ - { - id: '1001', - value: 'Test value 1', - topic: '100', - }, - { - id: '1002', - value: 'Test value 2', - topic: '100', - }, - { - id: '1003', - value: 'Test value 3', - topic: '100', - }, - ], - }, - { - id: '200', - name: 'Topic 2', - messages: [ - { - id: '2001', - value: 'Test value 1', - topic: '200', - }, - { - id: '2002', - value: 'Test value 2', - topic: '200', - }, - ], - }, -] - -test('should add values to inputs and submit form', async () => { - const formComponent = render( - -
- - ) - const selectinput = formComponent.getByTestId('demo-simple-select') - fireEvent.change(selectinput, { target: { value: '100' } }) - const textinput = formComponent.getByTestId('demo-simple-texfield') - fireEvent.change(textinput as HTMLInputElement, { - target: { value: 'Test with Jest' }, - }) - expect((textinput as HTMLInputElement).value).toBe('Test with Jest') - const form = await formComponent.findByTestId('demo-message-form') - fireEvent.submit(form) -}) diff --git a/frontend/src/__tests__/NamespaceGroup.test.tsx b/frontend/src/__tests__/NamespaceGroup.test.tsx index cac93ec4..cc6fd835 100644 --- a/frontend/src/__tests__/NamespaceGroup.test.tsx +++ b/frontend/src/__tests__/NamespaceGroup.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { render, screen } from '@testing-library/react' import { Provider } from 'react-redux' import store from '../store' -import NamespaceView from '../components/pages/namespace/NamespaceView' +import NamespaceView from '../routes/namespace/NamespaceView' import { Route, BrowserRouter as Router, Routes } from 'react-router-dom' const dataTest: Array = [ diff --git a/frontend/src/__tests__/TenantGroup.test.tsx b/frontend/src/__tests__/TenantGroup.test.tsx index b87e6e54..0b387d46 100644 --- a/frontend/src/__tests__/TenantGroup.test.tsx +++ b/frontend/src/__tests__/TenantGroup.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { render, screen } from '@testing-library/react' import { Provider } from 'react-redux' import store from '../store' -import TenantView from '../components/pages/tenant/TenantView' +import TenantView from '../routes/tenant/TenantView' import { Route, BrowserRouter as Router, Routes } from 'react-router-dom' const dataTest: Array = [ diff --git a/frontend/src/__tests__/TopicGroup.test.tsx b/frontend/src/__tests__/TopicGroup.test.tsx index 69bd5f53..d5a409c2 100644 --- a/frontend/src/__tests__/TopicGroup.test.tsx +++ b/frontend/src/__tests__/TopicGroup.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { render, screen } from '@testing-library/react' import { Provider } from 'react-redux' import store from '../store' -import TopicView from '../components/pages/topic/TopicView' +import TopicView from '../routes/topic/TopicView' import { Route, BrowserRouter as Router, Routes } from 'react-router-dom' const dataTest: Array = [ diff --git a/frontend/src/assets/images/Logo_RBI_relaunch.svg b/frontend/src/assets/images/Logo_RBI_relaunch.svg deleted file mode 100644 index 2dc72ef3..00000000 --- a/frontend/src/assets/images/Logo_RBI_relaunch.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/assets/images/demo-graph1.png b/frontend/src/assets/images/demo-graph1.png deleted file mode 100644 index 3f42c9e6..00000000 Binary files a/frontend/src/assets/images/demo-graph1.png and /dev/null differ diff --git a/frontend/src/assets/images/demo-graph2.png b/frontend/src/assets/images/demo-graph2.png deleted file mode 100644 index e2b82e0c..00000000 Binary files a/frontend/src/assets/images/demo-graph2.png and /dev/null differ diff --git a/frontend/src/assets/images/gradient-bg.jpg b/frontend/src/assets/images/gradient-bg.jpg deleted file mode 100644 index 9d598a27..00000000 Binary files a/frontend/src/assets/images/gradient-bg.jpg and /dev/null differ diff --git a/frontend/src/assets/images/landing-background.jpg b/frontend/src/assets/images/landing-background.jpg deleted file mode 100644 index 01cd56b8..00000000 Binary files a/frontend/src/assets/images/landing-background.jpg and /dev/null differ diff --git a/frontend/src/assets/images/team-logo-no-margins.png b/frontend/src/assets/images/team-logo-no-margins.png new file mode 100644 index 00000000..8027f1cf Binary files /dev/null and b/frontend/src/assets/images/team-logo-no-margins.png differ diff --git a/frontend/src/assets/styles/card.scss b/frontend/src/assets/styles/card.scss index 68e07652..dbe7d4eb 100644 --- a/frontend/src/assets/styles/card.scss +++ b/frontend/src/assets/styles/card.scss @@ -23,29 +23,8 @@ width: 100%; padding: 0px; margin-top: 22px; - .MuiButtonBase-root.MuiButton-root.MuiButton-contained { - display: flex; - justify-content: center; - align-items: center; - width: 35%; min-width: 120px; - height: 44px; - padding: 20px; - background: $gradient-blue; - border-radius: $primary-radius; - font-weight: 500; - font-size: $font-small; - line-height: 150%; - color: $primary-white; - text-transform: unset; - box-shadow: none; - - &.outlined-button { - background: transparent; - border: 1px solid $primary-blue; - color: $primary-blue; - } } } } diff --git a/frontend/src/assets/styles/dashboard.scss b/frontend/src/assets/styles/dashboard.scss index 2cc57897..c7ef754a 100644 --- a/frontend/src/assets/styles/dashboard.scss +++ b/frontend/src/assets/styles/dashboard.scss @@ -34,6 +34,10 @@ display: flex; flex-direction: column; gap: 64px; + position: relative; + .dashboard-header { + justify-content: space-between; + } } .secondary-dashboard { @@ -47,20 +51,6 @@ padding: 0px; padding-bottom: 4px; } - - .reset-all-filter-button { - width: 100%; - cursor: pointer; - background-color: $primary-blue; - border-radius: $primary-radius; - color: $primary-white; - font-size: $font-small; - display: flex; - align-items: center; - justify-content: center; - min-height: 30px; - margin: 0px 0px 38px 0px; - } } .dashboard-title { @@ -82,7 +72,7 @@ display: flex; justify-content: space-between; align-items: center; - margin: 0px 0px 0px 0px; + margin: 34px 0px 0px 0px; &.filters-wrapper-mobile { display: none; } @@ -105,7 +95,7 @@ overflow: auto; } - .MuiPaper-root.MuiPaper-elevation.MuiPaper-rounded { + .MuiPaper-root.MuiPaper-elevation.MuiPaper-rounded:not(.MuiAlert-root) { box-shadow: none; border-top: 1px solid $primary-grey; border-bottom: 1px solid $primary-grey; @@ -124,60 +114,4 @@ } } -.message-box-wrapper.MuiBox-root { - background-color: $primary-white; - width: 80%; - height: 80vh; - top: 100px; - overflow-y: hidden; - padding-top: 0px; - position: relative; - margin: 0 auto; - border-radius: $primary-radius; - box-shadow: $primary-shadow; - .message-box.MuiBox-root { - width: 100%; - height: 100%; - padding: 34px; - background-color: transparent; - overflow-y: scroll; - -ms-overflow-style: none; /* Internet Explorer 10+ */ - scrollbar-width: none; /* Firefox */ - &::-webkit-scrollbar { - display: none; /* Safari and Chrome */ - } - .message-filter-wrapper { - display: flex; - flex-direction: column; - gap: 30px; - margin-bottom: 32px; - .message-title { - color: $primary-black; - font-family: $primary-font; - font-weight: 700; - font-size: $font-medium; - } - .message-filter { - display: flex; - gap: 18px; - align-items: center; - } - } - .MuiButtonBase-root.MuiButton-root.MuiButton-contained { - display: flex; - justify-content: center; - align-items: center; - width: 120px; - height: 44px; - padding: 20px; - background: $gradient-blue; - border-radius: $primary-radius; - font-weight: 500; - font-size: $font-small; - line-height: 150%; - color: $primary-white; - text-transform: unset; - box-shadow: none; - } - } -} + diff --git a/frontend/src/assets/styles/globals.scss b/frontend/src/assets/styles/globals.scss index 1fd3269c..b9da6f19 100644 --- a/frontend/src/assets/styles/globals.scss +++ b/frontend/src/assets/styles/globals.scss @@ -31,18 +31,36 @@ span { width: 100%; } -.dashboard-container { - margin: 64px; -} - -.close-modal-button.MuiButtonBase-root.MuiIconButton-root { - position: absolute; - right: 34px; - top: 34px; +.MuiButtonBase-root.MuiButton-root.MuiButton-contained { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + max-width: 260px; + height: 44px; + padding: 20px; background: $gradient-blue; + border-radius: $primary-radius; + font-weight: 500; + font-size: $font-small; + line-height: 150%; color: $primary-white; - padding: 10px; - z-index: 1; + text-transform: unset; + box-shadow: none; + + &.full-button { + max-width: 100%; + } + + &.outlined-button { + background: transparent; + border: 1px solid $primary-blue; + color: $primary-blue; + } +} + +.dashboard-container { + margin: 64px; } .custom-scrollbar-wrapper { diff --git a/frontend/src/assets/styles/modal.scss b/frontend/src/assets/styles/modal.scss index 90a7cd23..733e255b 100644 --- a/frontend/src/assets/styles/modal.scss +++ b/frontend/src/assets/styles/modal.scss @@ -16,3 +16,104 @@ color: grey; } } + + +.modal-box.MuiBox-root { + background-color: $primary-white; + width: 80%; + height: 100%; + top: 50px; + overflow-y: hidden; + max-height: calc(100vh - 100px); + padding-top: 0px; + position: relative; + margin: 0 auto; + border-radius: $primary-radius; + box-shadow: $primary-shadow; + .modal-box-inner.MuiBox-root { + width: 100%; + height: 100%; + padding: 34px; + overflow-y: scroll; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + background-color: transparent; + justify-content: center; + &::-webkit-scrollbar { + display: none; /* Safari and Chrome */ + } + .info-logo { + width: 20%; + } + .modal-content { + display: flex; + flex-direction: column; + justify-content: center; + gap: 30px; + max-width: 1000px; + &.messages-container { + margin-bottom: 32px; + } + .modal-title { + color: $primary-black; + font-family: $primary-font; + font-weight: 700; + font-size: $font-medium; + } + .message-filter { + display: flex; + gap: 18px; + align-items: center; + } + .project-description { + text-align: center; + } + .triple-icon-text { + display: flex; + justify-content: space-between; + gap: 34px; + .single-icon-text { + display: flex; + align-items: center; + text-align: center; + flex-direction: column; + gap: 18px; + svg { + width: 34px; + height: 34px; + fill: $primary-blue; + } + } + } + } + .MuiButtonBase-root.MuiButton-root.MuiButton-contained { + display: flex; + justify-content: center; + align-items: center; + width: 120px; + height: 44px; + padding: 20px; + background: $gradient-blue; + border-radius: $primary-radius; + font-weight: 500; + font-size: $font-small; + line-height: 150%; + color: $primary-white; + text-transform: unset; + box-shadow: none; + &.more-info-button { + width: 180px; + } + } + } +} + +.close-modal-button.MuiButtonBase-root.MuiIconButton-root { + position: absolute; + right: 34px; + top: 34px; + background: $gradient-blue; + color: $primary-white; + padding: 10px; + z-index: 1; +} diff --git a/frontend/src/assets/styles/responsive.scss b/frontend/src/assets/styles/responsive.scss index 4a853f05..cbaeb169 100644 --- a/frontend/src/assets/styles/responsive.scss +++ b/frontend/src/assets/styles/responsive.scss @@ -89,9 +89,6 @@ margin: auto; } } - .reset-all-filter-button { - margin: 18px 0px; - } .desktop-filters { display: none; } @@ -107,4 +104,48 @@ } } } + .modal-box.MuiBox-root { + .modal-box-inner.MuiBox-root { + .info-logo { + width: 45%; + } + .modal-content { + gap: 18px; + justify-content: unset; + .triple-icon-text { + flex-direction: column; + gap: 18px; + justify-content: center; + .single-icon-text { + gap: 10px; + } + } + margin-bottom: 18px; + } + .MuiButtonBase-root.MuiButton-root.MuiButton-contained { + &.more-info-button { + width: 100%; + } + } + } + } + .close-modal-button.MuiButtonBase-root.MuiIconButton-root { + position: absolute; + right: 18px; + top: 18px; + background: $gradient-blue; + color: $primary-white; + padding: 4px; + z-index: 1; + } +} + +@media only screen and (max-width: $screen-sm-max) { + .modal-box.MuiBox-root { + .modal-box-inner.MuiBox-root { + .info-logo { + width: 75%; + } + } + } } diff --git a/frontend/src/assets/styles/styles.scss b/frontend/src/assets/styles/styles.scss index 35779d40..5446dd52 100644 --- a/frontend/src/assets/styles/styles.scss +++ b/frontend/src/assets/styles/styles.scss @@ -9,5 +9,5 @@ @import './form.scss'; @import './dashboard.scss'; @import './card.scss'; -@import './responsive.scss'; @import './modal.scss'; +@import './responsive.scss'; diff --git a/frontend/src/assets/styles/variables.scss b/frontend/src/assets/styles/variables.scss index 8dc87374..73bdde67 100644 --- a/frontend/src/assets/styles/variables.scss +++ b/frontend/src/assets/styles/variables.scss @@ -22,10 +22,8 @@ $font-large: 26px; $font-xlarge: 35px; // Small tablets and large smartphones (landscape view) -//$screen-sm-max: 576px; +$screen-sm-max: 576px; // Small tablets (portrait view) $screen-md-max: 899px; -// Regular desktops -//$screen-lg-min: 1400px; // Large desktops $screen-xl-min: 1700px; diff --git a/frontend/src/components/Card.tsx b/frontend/src/components/Card.tsx deleted file mode 100644 index 9dad66f6..00000000 --- a/frontend/src/components/Card.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: 2010-2021 Dirk Riehle -/* -import React from 'react' -import ClusterView from './views/ClusterView' -import NamespaceView from './views/NamespaceView' -import TopicView from './views/TopicView' -import MessageView from './views/MessageView' -import TenantView from './views/TenantView' -import { - instanceOfSampleCluster, - instanceOfSampleTenant, - instanceOfSampleNamespace, - instanceOfSampleTopic, - instanceOfSampleMessage, -} from '../Helpers' - -const Card: React.FC = ({ data, handleClick }) => { - return ( -
- {instanceOfSampleCluster(data) ? ( - - ) : instanceOfSampleTenant(data) ? ( - - ) : instanceOfSampleNamespace(data) ? ( - - ) : instanceOfSampleTopic(data) ? ( - - ) : instanceOfSampleMessage(data) ? ( - - ) : ( -
- )} -
- ) -} - -export default Card -*/ diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index e3126eb0..3e43d0ac 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2010-2021 Dirk Riehle -import React, { useEffect, useState } from 'react' +import React, { useEffect } from 'react' import FilterListIcon from '@mui/icons-material/FilterList' import CustomFilter from './custom/CustomFilter' import { useAppDispatch } from '../store/hooks' @@ -16,24 +16,22 @@ import { fetchOptionsThunk, resetAllFilters, updateFilterAccordingToNav, - HierarchyInPulsar, } from '../store/filterSlice' -import { triggerRequest } from './pages/requestTriggerSlice' +import { triggerRequest } from '../routes/requestTriggerSlice' import { Button } from '@mui/material' +import { Topology } from '../enum' -const Dashboard: React.FC = ({ - completeMessages, - view, - children, -}) => { - const [searchQuery, setSearchQuery] = useState('') - /* - const [clusterQuery, setClusterQuery] = useState([]) - const [tenantQuery, setTenantQuery] = useState([]) - const [namespaceQuery, setNamespaceQuery] = useState([]) - const [topicQuery, setTopicQuery] = useState([]) - const [messageQuery, setMessageQuery] = useState([]) - */ +/** + * Dashboard is a React component that provides a dashboard with filter options. + * It's the primary UI for user's interaction where it allows them to apply or reset filters on data. + * The children param is used to display the topology views. + * It also fetches filter options beforehand and updates filter according to navigation. + * + * @component + * @param children - Child components + * @returns The rendered Dashboard component. + */ +const Dashboard: React.FC = ({ children }) => { const dispatch = useAppDispatch() const navigate = useNavigate() @@ -42,298 +40,26 @@ const Dashboard: React.FC = ({ if (location.pathname === '/') navigate('/cluster') // fetch all filter options once beforehand dispatch(fetchOptionsThunk()) - dispatch(updateFilterAccordingToNav(location.pathname as HierarchyInPulsar)) + dispatch(updateFilterAccordingToNav(location.pathname as Topology)) }, []) - /* - const divideData = (sampleData: Array) => { - if (view == 'tenant') { - return allTenants - } else if (view == 'namespace') { - return allNamespaces - } else if (view === 'topic') { - return allTopics - } else if (view === 'message') { - return completeMessages - } else return sampleData - } - - const includeItemsByFilterQuery = ( - ids: string[], - sample: - | any - | Array - | Array - | Array - | Array - | Array - ) => { - if (ids.length > 0) { - let newSample = [] - newSample = sample.filter( - ( - c: - | SampleCluster - | SampleTenant - | SampleNamespace - | SampleTopic - | SampleMessage - ) => - instanceOfSampleTopic(c) - ? ids.includes(c.localName) - : ids.includes(c.id) - ) - return newSample - } else return sample - } - - const includeItemsBySearchQuery = ( - query: string, - sample: - | any - | Array - | Array - | Array - | Array - | Array - ) => { - if (query) { - return sample.filter( - ( - d: - | SampleCluster - | SampleTenant - | SampleNamespace - | SampleTopic - | SampleMessage - ) => { - if (instanceOfSampleMessage(d)) { - return d.payload.includes(query) - } else if (instanceOfSampleTopic(d)) { - return d.localName.includes(query) - } else return d.id.includes(query) - } - ) - } else return sample - } - - const getMessagesByFilters = ( - sampleClusters: Array, - sampleTenants: Array, - sampleNamespaces: Array, - sampleTopics: Array, - sampleMessages: Array - ) => { - const newDataIds = sampleClusters.map((c: SampleCluster) => c.id) - const newTenantsIds = sampleTenants.map((t: SampleTenant) => t.id) - const newNamespacesIds = sampleNamespaces.map((n: SampleNamespace) => n.id) - const newTopicsIds = sampleTopics.map((t: SampleTopic) => t.id) - let newMessages = [] - newMessages = sampleMessages.filter(function (m: SampleMessage) { - return ( - newDataIds.includes(m.cluster) && - newTenantsIds.includes(m.tenant) && - newNamespacesIds.includes(m.namespace) && - newTopicsIds.includes(m.topic) - ) - }) - return newMessages - } - - const applySearchbarQuery = ( - searchQuery: string, - clusterData: Array, - tenantData: Array, - namespaceData: Array, - topicData: Array, - messageData: Array - ) => { - if (view === 'cluster') { - return includeItemsBySearchQuery(searchQuery, clusterData) - } else if (view === 'tenant') { - return includeItemsBySearchQuery(searchQuery, tenantData) - } else if (view === 'namespace') { - return includeItemsBySearchQuery(searchQuery, namespaceData) - } else if (view === 'topic') { - return includeItemsBySearchQuery(searchQuery, topicData) - } else if (view === 'message') { - return includeItemsBySearchQuery(searchQuery, messageData) - } else return clusterData - } - */ - /* - const filterData = ( - query: string, - sampleData: Array, - sampleMessages: Array - ) => { - //Case 1: no filter or search is applied - if ( - clusterQuery.length <= 0 && - tenantQuery.length <= 0 && - namespaceQuery.length <= 0 && - topicQuery.length <= 0 && - messageQuery.length <= 0 && - !query - ) { - return divideData(sampleData) - } //Case 2: a filter or search is applied - else { - //We select only the Clusters included in the cluster query state (clusterQuery) - const newClusters = includeItemsByFilterQuery(clusterQuery, sampleData) - //We dig deeper and get the Tenants of the filtered Clusters, we then flatten the array - let newTenants = flattenClustersToTenants(newClusters) - //We select only the Tenants included in the tenant query state (tenantQuery) - newTenants = includeItemsByFilterQuery(tenantQuery, newTenants) - //We dig deeper and get the Namespaces of the filtered Tenants, we then flatten the array - let newNamespaces = flattenTenantsToNamespaces(newTenants) - //We select only the Namespaces included in the namespace query state (namespaceQuery) - newNamespaces = includeItemsByFilterQuery(namespaceQuery, newNamespaces) - //We dig deeper and get the Topics of the filtered Namespaces, we then flatten the array - let newTopics = flattenNamespacesToTopics(newNamespaces) - //We select only the Topics included in the topic query state (topicQuery) - newTopics = includeItemsByFilterQuery(topicQuery, newTopics) - //In the function below, we use the ids of the filtered Clusters, Tenants, - //Namespaces, and Topics to derive inderctly which messages need to be displayed - let newMessages = getMessagesByFilters( - newClusters, - newTenants, - newNamespaces, - newTopics, - sampleMessages - ) - //Finally, we select only the Messages included in the message query state (messageQuery) - newMessages = includeItemsByFilterQuery(messageQuery, newMessages) - - //Lastly, we use the applySearchbarQuery function to filter our new data based - //on what's inserted in the main searchbar - const newData = applySearchbarQuery( - query, - newClusters, - newTenants, - newNamespaces, - newTopics, - newMessages - ) - - return newData - } - } - */ - /* - let dataFiltered: - | Array - | Array - | Array - | Array - | Array = completeData - - if (view) { - //This is the data that will be displayed in the frontend, it is the output of our "filterData" function - dataFiltered = filterData(searchQuery, completeData, completeMessages) - } - */ - /* - //This function moves from one view to another on click, it takes the id of the clicked element, and - //uses this information to set the clicked id as selected filter in the next view by using the "handleChange" function - const handleClick = ( - e: React.MouseEvent, - currentEl: SampleCluster | SampleTenant | SampleNamespace | SampleTopic - ) => { - e.preventDefault() - //Save ID - let selectedId = currentEl.id - if (instanceOfSampleTopic(currentEl)) { - selectedId = currentEl.localName - } - if (view === 'cluster') { - //Update filters - handleChange(selectedId, view) - //Switch view - dispatch(setNav('tenant')) - } else if (view === 'tenant') { - handleChange(selectedId, view) - dispatch(setNav('namespace')) - } else if (view === 'namespace') { - handleChange(selectedId, view) - dispatch(setNav('topic')) - } else if (view === 'topic') { - handleChange(selectedId, view) - dispatch(setNav('message')) - } - } -*/ /** * Resets all filters and triggers another page request to update the currently displayed cards + * @function + * @returns {void} */ const resetFilters = () => { dispatch(resetAllFilters()) dispatch(triggerRequest()) } - /* - //This function should decide wether to display the "Reset all filters" button on a certain view - //or not, this is necessary because a user could be f.e. on a Topic View, apply some filters for the - //Topics, and then switch back to the Cluster View, in this case the button should disappear - const checkQueryLength = ( - currentView: - | 'cluster' - | 'tenant' - | 'namespace' - | 'topic' - | 'message' - | null - | string - | undefined - ) => { - if (currentView === 'cluster' && clusterQuery.length > 0) { - return true - } else if ( - currentView === 'tenant' && - (tenantQuery.length > 0 || clusterQuery.length > 0) - ) { - return true - } else if ( - currentView === 'namespace' && - (namespaceQuery.length > 0 || - tenantQuery.length > 0 || - clusterQuery.length > 0) - ) { - return true - } else if ( - currentView === 'topic' && - (topicQuery.length > 0 || - namespaceQuery.length > 0 || - tenantQuery.length > 0 || - clusterQuery.length > 0) - ) { - return true - } else if ( - currentView === 'message' && - (messageQuery.length > 0 || - topicQuery.length > 0 || - namespaceQuery.length > 0 || - tenantQuery.length > 0 || - clusterQuery.length > 0) - ) { - return true - } else return false - } -*/ - - //const dashboardTitle = view + 's' - return (
{children}
- {/* */}
- +
diff --git a/frontend/src/components/InfoModal.tsx b/frontend/src/components/InfoModal.tsx index 8a234b31..a094a285 100644 --- a/frontend/src/components/InfoModal.tsx +++ b/frontend/src/components/InfoModal.tsx @@ -3,11 +3,23 @@ // SPDX-FileCopyrightText: 2019 Georg Schwarz import * as React from 'react' -import Button from '@mui/material/Button' -import { Box, Modal, Typography } from '@mui/material' +import { Box, Modal, IconButton, Button } from '@mui/material' +import CloseIcon from '@mui/icons-material/Close' +import ChevronRight from '@mui/icons-material/ChevronRight' import InfoRoundedIcon from '@mui/icons-material/InfoRounded' +import logo from '../assets/images/team-logo-no-margins.png' +import FilterListIcon from '@mui/icons-material/FilterList' +import BarChartIcon from '@mui/icons-material/BarChart' +import SearchIcon from '@mui/icons-material/Search' -export function InfoModal() { +/** + * InfoModal is a React component that provides an informative modal interface. + * It handles opening and closing of the modal and displays information about the project mission. + * + * @component + * @returns The rendered InfoModal component. + */ +export const InfoModal: React.FC = () => { const [open, setOpen] = React.useState(false) const handleClickOpen = () => { @@ -17,20 +29,6 @@ export function InfoModal() { const handleClose = () => { setOpen(false) } - const style = { - position: 'absolute' as const, - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 400, - bgcolor: 'background.paper', - border: '2px solid #000', - boxShadow: 24, - p: 4, - } - const title = 'Project Mission' - const content = - 'Our mission consists of building a Web-UI that can easily be used by users that have some experience with managing and maintaining Apache Pulsar installations to understand and work on their infrastructure...' return (
@@ -45,13 +43,59 @@ export function InfoModal() { aria-labelledby="modal-modal-title" aria-describedby="modal-modal-description" > - - - {title} - - - {content} - + + + + + +
+ logo +
+

Mission

+

+ Our mission consists of building a Web-UI that can easily be + used by users that have some experience with managing and + maintaining Apache Pulsar installations to understand and work + on their infrastructure +

+
+
+
+ +

+ Explore clusters, tenants, namespace, and topics by drilling + at different topology levels. +

+
+
+ +

+ View and analyze both general and detailed real-time + information about clusters of your interest. +

+
+
+ +

+ Use the filters listed in the dashboard to customize your + search and get data of your interest. +

+
+
+ +
+
diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index 1cf3368f..5e1de7c6 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -15,27 +15,36 @@ import Button from '@mui/material/Button' import MenuItem from '@mui/material/MenuItem' import { useAppDispatch } from '../store/hooks' import { setNav } from '../store/globalSlice' -// import { selectEndpoint } from '../store/globalSlice' import logo from '../assets/images/team-logo-light.png' -// import { Input } from '@mui/material' import { InfoModal } from './InfoModal' import { useLocation, useNavigate } from 'react-router-dom' -import { - updateFilterAccordingToNav, - HierarchyInPulsar, -} from '../store/filterSlice' +import { updateFilterAccordingToNav } from '../store/filterSlice' +import { Topology } from '../enum' //Removed 'Message' from this array for now -const pages = ['Cluster', 'Tenant', 'Namespace', 'Topic'] +const pages = [ + Topology.CLUSTER, + Topology.TENANT, + Topology.NAMESPACE, + Topology.TOPIC, +] -function NavBar() { +/** + * NavBar is a React component that provides a navigational bar interface. + * It manages navigation menu state and provides handlers for opening and closing Modals as well as clicking on navigation items. + * The NavBar renders AppBar which contains navigation options for different pages. + * It also contains the InfoModal component. + * + * @component + * @returns The rendered NavBar component. + */ +const NavBar: React.FC = () => { const [anchorElNav, setAnchorElNav] = React.useState(null) const handleOpenNavMenu = (event: React.MouseEvent) => { setAnchorElNav(event.currentTarget) } const dispatch = useAppDispatch() - //const selectedNav = useAppSelector(selectView).selectedNav const navigate = useNavigate() const location = useLocation() @@ -43,19 +52,13 @@ function NavBar() { setAnchorElNav(null) } - const handleClickOnNav = (tag: string) => { - tag = tag.toLowerCase() + // handles click on navigation items + const handleClickOnNav = (tag: Topology) => { + //tag = tag.toLowerCase() dispatch(setNav(tag)) navigate('/' + tag) } - // const handleClickOnDisconnect = () => { - // //TODO add disconnect functionality - // // dispatch(backToLP()) - // } - - // const endpoint = useAppSelector(selectEndpoint) - return ( @@ -70,27 +73,6 @@ function NavBar() { verticalAlign: 'middle', }} /> - {/* */} { handleClickOnNav(page) - updateFilterAccordingToNav( - page.toLowerCase() as HierarchyInPulsar - ) + updateFilterAccordingToNav(page) }} > {page} @@ -149,11 +129,7 @@ function NavBar() { disabled={page.toLowerCase() == location.pathname.slice(1)} onClick={() => { handleClickOnNav(page) - dispatch( - updateFilterAccordingToNav( - page.toLowerCase() as HierarchyInPulsar - ) - ) + dispatch(updateFilterAccordingToNav(page)) }} sx={{ my: 2, color: 'white', display: 'block' }} > diff --git a/frontend/src/components/buttons/FlushCacheButton.tsx b/frontend/src/components/buttons/FlushCacheButton.tsx new file mode 100644 index 00000000..f9faeb04 --- /dev/null +++ b/frontend/src/components/buttons/FlushCacheButton.tsx @@ -0,0 +1,92 @@ +import * as React from 'react' +import { Box, Button, Snackbar } from '@mui/material' +import MuiAlert, { AlertProps } from '@mui/material/Alert' +import config from '../../config' +import axios from 'axios' + +const Alert = React.forwardRef(function Alert( + props, + ref +) { + return +}) + +/** + * Flush backend-cache button component. + * When user clicks on it, it calls the backend api to flush the cache in backend + * to get the latest data in pulsar. + */ +export default function FlushCacheButton() { + const [open, setOpen] = React.useState(false) + const [error, setError] = React.useState(false) + + /** + * Call the backend api endpoint to flush the backend-cache + */ + const flushCache = () => { + const url = config.backendUrl + '/api/cache/flush' + axios + .get(url) + .then((response) => { + if (response.status !== 200) { + setError(true) + } + setError(false) + }) + .catch((error) => { + console.log(error.message) + setError(true) + }) + } + + // when clicked, sends the request and set the open to true to get alert. + const handleClick = () => { + flushCache() + setOpen(true) + } + + // closes alert. + const handleClose = ( + event?: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === 'clickaway') { + return + } + + setOpen(false) + } + + return ( + + + + {error ? ( + + Error when flush the cache + + ) : ( + + Cache flushed! + + )} + + + ) +} diff --git a/frontend/src/components/custom/CustomAccordion.tsx b/frontend/src/components/custom/CustomAccordion.tsx deleted file mode 100644 index 64b5a907..00000000 --- a/frontend/src/components/custom/CustomAccordion.tsx +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: 2010-2021 Dirk Riehle - -import * as React from 'react' -import Accordion from '@mui/material/Accordion' -import AccordionSummary from '@mui/material/AccordionSummary' -import AccordionDetails from '@mui/material/AccordionDetails' -import Typography from '@mui/material/Typography' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' - -const CustomAccordion: React.FC = ({ data }) => { - return ( -
- {data && - data.length > 0 && - data.map((item) => ( - - } - aria-controls="panel1a-content" - id="panel1a-header" - > - {item.name} - - -

Topic Messages:

- {item.messages.length > 0 && - item.messages.map((message: Message) => ( - - {message.value} - - ))} -
-
- ))} -
- ) -} - -export default CustomAccordion diff --git a/frontend/src/components/custom/CustomCheckbox.tsx b/frontend/src/components/custom/CustomCheckbox.tsx index 13ec33a3..f7685d7d 100644 --- a/frontend/src/components/custom/CustomCheckbox.tsx +++ b/frontend/src/components/custom/CustomCheckbox.tsx @@ -5,19 +5,19 @@ import React from 'react' import { useAppDispatch } from '../../store/hooks' import { addFilter, deleteFilter } from '../../store/filterSlice' -import { triggerRequest } from '../pages/requestTriggerSlice' +import { triggerRequest } from '../../routes/requestTriggerSlice' const CustomCheckbox: React.FC = ({ id, text, - typology, + topology, selected, }) => { const dispatch = useAppDispatch() const handleClick = (): void => { dispatch(triggerRequest()) - if (selected) dispatch(deleteFilter({ filterName: typology, id: id })) - else dispatch(addFilter({ filterName: typology, id: id })) + if (selected) dispatch(deleteFilter({ filterName: topology, id: id })) + else dispatch(addFilter({ filterName: topology, id: id })) } return ( diff --git a/frontend/src/components/custom/CustomFilter.tsx b/frontend/src/components/custom/CustomFilter.tsx index 900923cf..01d24dc8 100644 --- a/frontend/src/components/custom/CustomFilter.tsx +++ b/frontend/src/components/custom/CustomFilter.tsx @@ -12,11 +12,9 @@ import CustomRadio from './CustomRadio' import CustomSearchbar from './CustomSearchbar' import { useAppSelector } from '../../store/hooks' import { selectAllFilters, selectOptions } from '../../store/filterSlice' +import { Topology } from '../../enum' -const CustomFilter: React.FC = ({ - messages, - currentView, -}) => { +const CustomFilter: React.FC = ({ currentView }) => { // Options are what we've got from apis so far. const options = useAppSelector(selectOptions) // filters are displayed in filters. @@ -27,7 +25,6 @@ const CustomFilter: React.FC = ({ const [topicSearchQuery, setTopicSearchQuery] = useState('') const [producerSearchQuery, setProducerSearchQuery] = useState('') const [subscriptionSearchQuery, setSubscriptionSearchQuery] = useState('') - const [messageSearchQuery, setMessageSearchQuery] = useState('') const filteredCheckboxes = (searchQuery: string, completeArray: string[]) => { let filteredArr = [] @@ -58,10 +55,6 @@ const CustomFilter: React.FC = ({ subscriptionSearchQuery, options.allSubscriptions ) - const filteredMessages = filteredCheckboxes( - messageSearchQuery, - options.allMessages - ) const viewLevelOne = currentView === 'topic' || currentView === 'message' const viewLevelTwo = @@ -96,7 +89,7 @@ const CustomFilter: React.FC = ({ key={'checkbox-cluster' + Math.floor(Math.random() * 999999)} text={item} id={item} - typology={'cluster'} + topology={Topology.CLUSTER} selected={filters.cluster.includes(item) ? true : false} > ))} @@ -124,7 +117,7 @@ const CustomFilter: React.FC = ({ key={'checkbox-tenant' + Math.floor(Math.random() * 999999)} text={item} id={item} - typology={'tenant'} + topology={Topology.TENANT} selected={filters.tenant.includes(item) ? true : false} > ))} @@ -155,7 +148,7 @@ const CustomFilter: React.FC = ({ } text={item} id={item} - typology={'namespace'} + topology={Topology.NAMESPACE} selected={filters.namespace.includes(item) ? true : false} > ))} @@ -187,7 +180,7 @@ const CustomFilter: React.FC = ({ } text={item} id={item} - typology={'topic'} + topology={Topology.TOPIC} selected={filters.topic.includes(item) ? true : false} > ))} @@ -216,7 +209,7 @@ const CustomFilter: React.FC = ({ } text={item} id={item} - typology={'producer'} + topology={Topology.PRODUCER} selected={filters.producer.includes(item) ? true : false} > ))} @@ -245,7 +238,7 @@ const CustomFilter: React.FC = ({ } text={item} id={item} - typology={'subscription'} + topology={Topology.SUBSCRIPTION} selected={ filters.subscription.includes(item) ? true : false } @@ -256,37 +249,6 @@ const CustomFilter: React.FC = ({ )} - {currentView === 'message' && ( - - } - aria-controls="panel1a-content" - > -

Messages

-
- - -
- {filteredMessages && - filteredMessages.length > 0 && - filteredMessages.map((item: string) => ( - - ))} -
-
-
- )} ) } diff --git a/frontend/src/components/custom/CustomRadio.tsx b/frontend/src/components/custom/CustomRadio.tsx index d6ff1a0a..d6d59bef 100644 --- a/frontend/src/components/custom/CustomRadio.tsx +++ b/frontend/src/components/custom/CustomRadio.tsx @@ -5,19 +5,19 @@ import React from 'react' import { useAppDispatch } from '../../store/hooks' import { addFilterWithRadio, deleteFilter } from '../../store/filterSlice' -import { triggerRequest } from '../pages/requestTriggerSlice' +import { triggerRequest } from '../../routes/requestTriggerSlice' const CustomRadio: React.FC = ({ id, text, - typology, + topology, selected, }) => { const dispatch = useAppDispatch() const handleClick = (): void => { dispatch(triggerRequest()) - if (selected) dispatch(deleteFilter({ filterName: typology, id: id })) - else dispatch(addFilterWithRadio({ filterName: typology, id: id })) + if (selected) dispatch(deleteFilter({ filterName: topology, id: id })) + else dispatch(addFilterWithRadio({ filterName: topology, id: id })) } return ( diff --git a/frontend/src/components/form/Form.tsx b/frontend/src/components/form/Form.tsx deleted file mode 100644 index 8662c46d..00000000 --- a/frontend/src/components/form/Form.tsx +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: 2010-2021 Dirk Riehle - -import React, { useState } from 'react' -import { TextField, Button } from '@mui/material' -import TopicSelect from '../custom/CustomSelect' -import CustomAccordion from '../custom/CustomAccordion' -import { SelectChangeEvent } from '@mui/material/Select' -import { - selectMessage, - selectTopic, - setMessage, - setTopic, -} from './formControllerSlice' -import { useAppDispatch, useAppSelector } from '../../store/hooks' - -const Form = (props: formProps) => { - const { data, triggerUpdate }: formProps = props - - const [topicError, setTopicError] = useState(false) - const [messageError, setMessageError] = useState(false) - - const topic = useAppSelector(selectTopic) - const message = useAppSelector(selectMessage) - - const dispatch = useAppDispatch() - - const handleSubmit = (event: { preventDefault: () => void }) => { - event.preventDefault() - - setMessageError(false) - setTopicError(false) - - if (message == '') { - setMessageError(true) - } - if (topic == '') { - setTopicError(true) - } - - if (message && topic) { - console.log(message, topic) - } - - triggerUpdate(message, topic) - } - - return ( -
- -

- Message Management -

-

- Use the form below to add a message to one of the existing Topics. -

-
- ) => - dispatch(setTopic(e.target.value)) - } - label="Topic" - error={topicError} - /> - ) => - dispatch(setMessage(e.target.value)) - } - variant="outlined" - type="text" - sx={{ mb: 3 }} - fullWidth - value={message} - error={messageError} - /> -
- - -
-

- Topic View -

-

- Click on a Topic to view the associated Messages. -

- -
-
- ) -} - -export default Form diff --git a/frontend/src/components/form/formControllerSlice.tsx b/frontend/src/components/form/formControllerSlice.tsx deleted file mode 100644 index 300cf912..00000000 --- a/frontend/src/components/form/formControllerSlice.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: 2010-2021 Dirk Riehle - -import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { RootState } from '../../store' - -export type FormControllerState = { - topic: string - message: string -} - -const initialState: FormControllerState = { - topic: '', - message: '', -} - -// eslint-disable-next-line -const formControllerSlice = createSlice({ - name: 'formControl', - initialState, - reducers: { - setTopic: (state: FormControllerState, action: PayloadAction) => { - state.topic = action.payload - }, - setMessage: (state: FormControllerState, action: PayloadAction) => { - state.message = action.payload - }, - }, -}) - -const { actions, reducer } = formControllerSlice - -const selectTopic = (state: RootState): string => state.formControl.topic - -const selectMessage = (state: RootState): string => state.formControl.message - -export const { setTopic, setMessage } = actions - -export { selectTopic, selectMessage } - -export default reducer diff --git a/frontend/src/components/landing/LandingPage.tsx b/frontend/src/components/landing/LandingPage.tsx deleted file mode 100644 index f432c400..00000000 --- a/frontend/src/components/landing/LandingPage.tsx +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: 2010-2021 Dirk Riehle -// SPDX-FileCopyrightText: 2019 Georg Schwarz - -// import React from 'react' -// import { TextField, Button } from '@mui/material' -// import '../../assets/styles/landing.scss' -// import team_logo from '../../assets/images/team-logo-light.png' -// import RbiLogo from './RbiLogo' -// import { useAppDispatch } from '../../store/hooks' -// import { moveToApp, setEndpoint } from '../../store/globalSlice' - -// const LandingPage: React.FC = () => { -// // Endpoint string of pulsar -// let endpoint = '' - -// const dispatch = useAppDispatch() - -// /** -// * Validate the endpoint that users type in -// * @param endpoint -// * @returns whether there is error in the endpoint -// */ -// const validateEndpoint = (endpoint: string): boolean => { -// // so far it's hardcoded, need to add the validation logic later on. -// return endpoint ? true : false -// } - -// const handleChange = (e: React.FormEvent): void => { -// const target = e.target as HTMLInputElement -// endpoint = target.value -// } - -// const handleSubmit = (e: React.FormEvent): void => { -// e.preventDefault() - -// // If the endpoint is not valid -// if (!validateEndpoint(endpoint)) { -// return -// } - -// // If the endpoint is valid -// dispatch(setEndpoint(endpoint)) -// dispatch(moveToApp()) -// } - -// return ( -//
-//
-//
-//
-// apache-pulsar-log -//
-//
-//
-//
-//
-//
-//
-//

Pulsar Endpoint

-// -// -// -//
-//
-//
-//
-//

-// Apache Pulsar UI is an open-source project developed in -// collaboration with: -//

-// -//
-//
-//
-//
-// ) -// } - -// export default LandingPage diff --git a/frontend/src/components/landing/RbiLogo.tsx b/frontend/src/components/landing/RbiLogo.tsx deleted file mode 100644 index 9cea5038..00000000 --- a/frontend/src/components/landing/RbiLogo.tsx +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: 2010-2021 Dirk Riehle -// SPDX-FileCopyrightText: 2019 Georg Schwarz - -import React from 'react' - -const RbiLogo = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} - -export default RbiLogo diff --git a/frontend/src/components/modals/ConsumerAccordion.tsx b/frontend/src/components/modals/ConsumerAccordion.tsx index a6c823f5..b17091a8 100644 --- a/frontend/src/components/modals/ConsumerAccordion.tsx +++ b/frontend/src/components/modals/ConsumerAccordion.tsx @@ -11,7 +11,7 @@ import { } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import axios from 'axios' -import ModalInfo from './ModalInfo' +import InformationText from './InformationText' import config from '../../config' interface ConsumerAccordionProps { @@ -20,6 +20,30 @@ interface ConsumerAccordionProps { isActive: boolean } +/** + * ConsumerAccordion is a react accordion component + * for displaying consumer information in pulsar. + * It shows the consumer details. + * + * The following information is shown in the consumer information popup: + * address: Address of this consumer + * availablePermits: Number of available message permits for the consumer + * BytesOutCounter: Total bytes delivered to consumer (bytes) + * ClientVersion: Client library version + * ConnectedSince: Timestamp of connection + * ConsumerName: Name of the consumer + * LastAckedTimestamp: + * LastConsumedTimestamp: + * MessageOutConter: Total messages delivered to consumer (msg). + * UnackedMessages: Number of unacknowledged messages for the consumer, where an * unacknowledged message is one that has been sent to the consumer but not yet acknowledged + * isBlockedConsumerOnUnackedMsgs: Flag to verify if consumer is blocked due to reaching * threshold of unacked messages + * + * @component + * @param consumerName - The name of current consumer. + * @param topicName - The name of topic which this consumer belongs to. + * @param isActive - Whether this consumer is active (in pulsar there is only one active consumer) + * @returns The rendered ConsumerAccordion component. + */ const ConsumerAccordion: React.FC = ({ consumerName, topicName, @@ -69,40 +93,43 @@ const ConsumerAccordion: React.FC = ({ {consumerName} - - + - - - - - - - - diff --git a/frontend/src/components/modals/ConsumerModal.tsx b/frontend/src/components/modals/ConsumerModal.tsx deleted file mode 100644 index 36130a8f..00000000 --- a/frontend/src/components/modals/ConsumerModal.tsx +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: 2010-2021 Dirk Riehle - -import React, { useState } from 'react' -import { Modal, Box, Typography, IconButton } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import axios from 'axios' -import config from '../../config' - -/** -The following information is shown in the consumer information popup: - -address: Address of this consumer -availablePermits: Number of available message permits for the consumer -BytesOutCounter: Total bytes delivered to consumer (bytes) -ClientVersion: Client library version -ConnectedSince: Timestamp of connection -ConsumerName: Name of the consumer -LastAckedTimestamp: -LastConsumedTimestamp: -MessageOutConter: Total messages delivered to consumer (msg). -UnackedMessages: Number of unacknowledged messages for the consumer, where an unacknowledged message is one that has been sent to the consumer but not yet acknowledged -isBlockedConsumerOnUnackedMsgs: Flag to verify if consumer is blocked due to reaching threshold of unacked messages -*/ - -interface ConsumerModalProps { - consumer: { - topicName: string - consumerName: string - } -} - -const ConsumerModal: React.FC = ({ consumer }) => { - const { topicName, consumerName } = consumer - - const [open, setOpen] = useState(false) - const [consumerDetails, setConsumerDetails] = useState() - - const handleOpen = () => { - fetchData() - setOpen(true) - } - - const handleClose = () => { - setOpen(false) - } - - const fetchData = () => { - const url = config.backendUrl + `/api/topic/consumer/${consumerName}` - - // Sending GET request - const params = { - topic: topicName, - } - axios - .get(url, { params }) - .then((response) => { - setConsumerDetails(response.data) - }) - .catch((error) => { - console.log(error) - }) - } - - return ( - <> - - {consumer.consumerName},{' '} - - - - - - - - Consumer Name: {consumer.consumerName} - - - Address:{' '} - {consumerDetails?.address ? consumerDetails.address : 'N/A'} - - - Available permits:{' '} - {consumerDetails?.availablePermits - ? consumerDetails.availablePermits - : 'N/A'} - - - Bytes out counter:{' '} - {consumerDetails?.bytesOutCounter - ? consumerDetails.bytesOutCounter - : 'N/A'} - - - Client version:{' '} - {consumerDetails?.clientVersion - ? consumerDetails.clientVersion - : 'N/A'} - - - Connected since:{' '} - {consumerDetails?.connectedSince - ? consumerDetails.connectedSince - : 'N/A'} - - - Last acked timestamp:{' '} - {consumerDetails?.lastAckedTimestamp - ? consumerDetails.lastAckedTimestamp - : 'N/A'} - - - Last consumed timestamp:{' '} - {consumerDetails?.lastConsumedTimestamp - ? consumerDetails.lastConsumedTimestamp - : 'N/A'} - - - Message out counter:{' '} - {consumerDetails?.messageOutCounter - ? consumerDetails.messageOutCounter - : 'N/A'} - - - Unacked messages:{' '} - {consumerDetails?.unackedMessages - ? consumerDetails.unackedMessages - : 'N/A'} - - - Blocked consumer on unacked msgs:{' '} - {consumerDetails?.blockedConsumerOnUnackedMsgs - ? consumerDetails.blockedConsumerOnUnackedMsgs - ? 'true' - : 'false' - : 'N/A'} - - - - - ) -} - -export default ConsumerModal diff --git a/frontend/src/components/modals/InformationText.tsx b/frontend/src/components/modals/InformationText.tsx new file mode 100644 index 00000000..69967d6d --- /dev/null +++ b/frontend/src/components/modals/InformationText.tsx @@ -0,0 +1,28 @@ +import React from 'react' + +interface ModalInfoProps { + title: string + detailedInfo: boolean | number | string | undefined +} + +/** + * InformationText is a react component for displaying detailed information for other + * modals. + * + * @component + * @param title - The title of information. + * @param detailedInfo - The detail of information. + * @returns Rendered InformationText. + */ +const InformationText: React.FC = ({ title, detailedInfo }) => { + return ( +
+

{title}:

+

+ {detailedInfo?.toString() ? detailedInfo.toString() : 'N/A'} +

+
+ ) +} + +export default InformationText diff --git a/frontend/src/components/modals/MessageModal.tsx b/frontend/src/components/modals/MessageModal.tsx index ecfada74..9065b690 100644 --- a/frontend/src/components/modals/MessageModal.tsx +++ b/frontend/src/components/modals/MessageModal.tsx @@ -8,19 +8,9 @@ import CloseIcon from '@mui/icons-material/Close' import axios from 'axios' import config from '../../config' import { ChevronRight } from '@mui/icons-material' -import MessageView from '../pages/message/MessageView' +import MessageView from '../../routes/message/MessageView' import { Masonry } from 'react-plock' -/** -The following information is shown in the producer information popup: -Address: Address of this publisher. -AverageMsgSize: Average message size published by this publisher. -ClientVersion: Client library version. -ConnectedSince: Timestamp of connection. -ProducerId: Id of this publisher. -ProducerName: Producer name. -*/ - interface MessageModalProps { topic: string } @@ -29,9 +19,27 @@ interface MessageResponse { messages: MessageInfo[] } +/** + * MessageModal is a react component for displaying message information in pulsar. + * + * The following information is shown in MessageModal: + * messageId: the id of the message + * topic: the topic this message belongs to + * payload: payload this message contains + * schema: + * namespace: the name space this message belongs to + * tenant: the tenant this message belongs to + * publishTime: + * producer: the producer this message belongs to + * + * @component + * @param topic - The name of topic + * @returns The rendered MessageModal component. + */ const MessageModal: React.FC = ({ topic }) => { const [open, setOpen] = useState(false) const [amount, setAmount] = useState(10) + const [inputValue, setInputValue] = useState('10') const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -76,7 +84,28 @@ const MessageModal: React.FC = ({ topic }) => { }) } const handleAmountChange = (event: React.ChangeEvent) => { - setAmount(Number(event.target.value)) + const value = event.target.value + setInputValue(value) // set the inputValue state + + if (value === '') { + return // allow empty input but don't update amount + } + + // Convert input value to a number + let newAmount = Number(value) + + // Take the absolute value + newAmount = Math.abs(newAmount) + + // Round it down to nearest integer + newAmount = Math.floor(newAmount) + + // Set the minimum possible value to 1 + newAmount = Math.max(1, newAmount) + + // Update state + setAmount(newAmount) + setInputValue(newAmount.toString()) } return ( @@ -89,25 +118,25 @@ const MessageModal: React.FC = ({ topic }) => { Drill down - - + + -
+
-

Messages ({data.length})

+

Messages ({data.length})

diff --git a/frontend/src/components/modals/ModalInfo.tsx b/frontend/src/components/modals/ModalInfo.tsx deleted file mode 100644 index ac836d5b..00000000 --- a/frontend/src/components/modals/ModalInfo.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' - -interface ModalInfoProps { - title: string - detailedInfo: boolean | number | string | undefined -} - -const ModalInfo: React.FC = ({ title, detailedInfo }) => { - return ( -
-

{title}:

-

- {detailedInfo?.toString() ? detailedInfo.toString() : 'N/A'} -

-
- ) -} - -export default ModalInfo diff --git a/frontend/src/components/modals/ProducerModal.tsx b/frontend/src/components/modals/ProducerModal.tsx index a4a66d76..7a6bb892 100644 --- a/frontend/src/components/modals/ProducerModal.tsx +++ b/frontend/src/components/modals/ProducerModal.tsx @@ -6,20 +6,10 @@ import React, { useState } from 'react' import { Modal, Box, Typography, IconButton, Divider } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import axios from 'axios' -import ModalInfo from './ModalInfo' +import InformationText from './InformationText' import config from '../../config' import { convertTimestampToDateTime } from '../../Helpers' -/** -The following information is shown in the producer information popup: -Address: Address of this publisher. -AverageMsgSize: Average message size published by this publisher. -ClientVersion: Client library version. -ConnectedSince: Timestamp of connection. -ProducerId: Id of this publisher. -ProducerName: Producer name. -*/ - interface ProducerModalProps { producer: { producerName: string @@ -31,6 +21,23 @@ interface MessageResponse { messages: MessageInfo[] } +/** + * ProducerModal is a react component for displaying producer information in pulsar. + * + * The following information is shown in the producer information popup: + * Address: Address of this producer. + * AverageMsgSize: Average message size published by this producer. + * ClientVersion: Client library version. + * ConnectedSince: Timestamp of connection. + * ProducerId: Id of this publisher. + * ProducerName: Producer name. + * + * @component + * @param producer + * @param producer.producerName - The name of producer. + * @param producer.topicName - The name of topic it belongs to. + * @returns The rendered ProducerModal component. + */ const ProducerModal: React.FC = ({ producer }) => { const { producerName, topicName } = producer @@ -138,23 +145,23 @@ const ProducerModal: React.FC = ({ producer }) => { Producer: {producer.producerName} - - - - - @@ -162,7 +169,7 @@ const ProducerModal: React.FC = ({ producer }) => { )} {messagesError || messages.length > 0 ? ( <> - + {messages.map((message, index) => { return ( <> @@ -184,7 +191,7 @@ const ProducerModal: React.FC = ({ producer }) => { })} ) : ( - + )} diff --git a/frontend/src/components/modals/SubscriptionModal.tsx b/frontend/src/components/modals/SubscriptionModal.tsx index 09108b66..724c3b06 100644 --- a/frontend/src/components/modals/SubscriptionModal.tsx +++ b/frontend/src/components/modals/SubscriptionModal.tsx @@ -7,7 +7,7 @@ import { Modal, Box, Typography, IconButton, Divider } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import axios from 'axios' import ConsumerAccordion from './ConsumerAccordion' -import ModalInfo from './ModalInfo' +import InformationText from './InformationText' import config from '../../config' import { convertTimestampToDateTime } from '../../Helpers' @@ -34,6 +34,26 @@ interface MessageResponse { messages: MessageInfo[] } +/** + * SubscriptionModal is a react component for displaying subscription information in pulsar. + * + * The following information is shown in the subscription information popup: + * Consumers: List of connected consumers on this subscription w/ their stats. + * -> When clicked, the corresponding consumer accordion is expanded + * ConsumersCount: Number of total consumers + * BacklogSize: Size of backlog in byte + * MsgBacklog: Number of entries in the subscription backlog + * BytesOutCounter: Total bytes delivered to consumer (bytes) + * MsgOutCounter: Total messages delivered to consumer (msg) + * isReplicated: Mark that the subscription state is kept in sync across different regions + * Type: The subscription type as defined by SubscriptionType + * Messages: 10 messages in this subscription + * + * @component + * @param subscription - The name of subscription. + * @param topic - The name of topic. + * @returns The rendered SubscriptionModal component. + */ const SubscriptionModal: React.FC = ({ subscription, topic, @@ -147,28 +167,31 @@ const SubscriptionModal: React.FC = ({ {subscriptionError || ( <> - - - - - - - + @@ -179,7 +202,7 @@ const SubscriptionModal: React.FC = ({ isActive={true} /> ) : ( - + )} {subscriptionDetail?.inactiveConsumers && subscriptionDetail?.inactiveConsumers.length > 0 ? ( @@ -194,13 +217,13 @@ const SubscriptionModal: React.FC = ({ ) }) ) : ( - + )} )} {messagesError || messages.length > 0 ? ( <> - + {messages.map((message, index) => { return ( <> @@ -222,7 +245,7 @@ const SubscriptionModal: React.FC = ({ })} ) : ( - + )} diff --git a/frontend/src/enum.ts b/frontend/src/enum.ts new file mode 100644 index 00000000..5798eba7 --- /dev/null +++ b/frontend/src/enum.ts @@ -0,0 +1,8 @@ +export enum Topology { + CLUSTER = 'cluster', + TENANT = 'tenant', + NAMESPACE = 'namespace', + TOPIC = 'topic', + PRODUCER = 'producer', + SUBSCRIPTION = 'subscription', +} diff --git a/frontend/src/components/pages/cluster/ClusterView.tsx b/frontend/src/routes/cluster/ClusterView.tsx similarity index 85% rename from frontend/src/components/pages/cluster/ClusterView.tsx rename to frontend/src/routes/cluster/ClusterView.tsx index ffcda413..fa86c364 100644 --- a/frontend/src/components/pages/cluster/ClusterView.tsx +++ b/frontend/src/routes/cluster/ClusterView.tsx @@ -7,13 +7,24 @@ import { Button, CardActions, Collapse } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ExpandLessIcon from '@mui/icons-material/ExpandLess' import ChevronRight from '@mui/icons-material/ChevronRight' -import { useAppDispatch } from '../../../store/hooks' -import { addFilterByDrilling } from '../../../store/filterSlice' +import { useAppDispatch } from '../../store/hooks' +import { addFilterByDrilling } from '../../store/filterSlice' import { useNavigate } from 'react-router-dom' import axios from 'axios' -import config from '../../../config' -import { addCommaSeparator } from '../../../Helpers' +import config from '../../config' +import { addCommaSeparator } from '../../Helpers' +import { Topology } from '../../enum' +/** + * ClusterView is a React component for visualizing cluster details. + * It shows key properties of a cluster such as its name, number of tenants and namespaces, + * and allows for the navigation to the detailed view. + * + * @component + * @param data - The data object containing the cluster information. + * + * @returns The rendered ClusterView component. + */ const ClusterView: React.FC = ({ data }) => { const { name, numberOfNamespaces, numberOfTenants }: ClusterInfo = data const [expanded, setExpanded] = useState(false) @@ -23,12 +34,12 @@ const ClusterView: React.FC = ({ data }) => { const navigate = useNavigate() const handleDrillDown = () => { - dispatch(addFilterByDrilling({ filterName: 'cluster', id: name })) + dispatch(addFilterByDrilling({ filterName: Topology.CLUSTER, id: name })) navigate('/tenant') } const handleDrillDownToTenant = (itemId: string) => { - dispatch(addFilterByDrilling({ filterName: 'tenant', id: itemId })) + dispatch(addFilterByDrilling({ filterName: Topology.TENANT, id: itemId })) navigate('/tenant') } diff --git a/frontend/src/components/pages/cluster/index.tsx b/frontend/src/routes/cluster/index.tsx similarity index 77% rename from frontend/src/components/pages/cluster/index.tsx rename to frontend/src/routes/cluster/index.tsx index 51e8aeb6..e3bfd524 100644 --- a/frontend/src/components/pages/cluster/index.tsx +++ b/frontend/src/routes/cluster/index.tsx @@ -3,13 +3,14 @@ // SPDX-FileCopyrightText: 2019 Georg Schwarz import React, { useEffect, useState } from 'react' -import { useAppDispatch, useAppSelector } from '../../../store/hooks' +import { useAppDispatch, useAppSelector } from '../../store/hooks' import axios from 'axios' import ClusterView from './ClusterView' -import { selectCluster } from '../../../store/filterSlice' +import { selectCluster } from '../../store/filterSlice' import { selectTrigger } from '../requestTriggerSlice' -import config from '../../../config' +import config from '../../config' import { Masonry } from 'react-plock' +import FlushCacheButton from '../../components/buttons/FlushCacheButton' export interface ResponseCluster { clusters: ClusterInfo[] @@ -18,6 +19,8 @@ export interface ResponseCluster { /** * Card group component for the cluster type. * Displays the ClusterView cards, title, loading window and network error. + * + * @component * @returns Rendered cluster view cards for the dashboard component */ const ClusterGroup: React.FC = () => { @@ -44,11 +47,18 @@ const ClusterGroup: React.FC = () => { return (
-

Available Clusters ({data.length})

-

- Tenants: {sumElements(data, 'numberOfTenants')}, Namespaces:{' '} - {sumElements(data, 'numberOfNamespaces')} -

+
+
+

+ Available Clusters ({data.length}) +

+

+ Tenants: {sumElements(data, 'numberOfTenants')}, Namespaces:{' '} + {sumElements(data, 'numberOfNamespaces')} +

+
+ +
{loading ? (
Loading...
) : error ? ( diff --git a/frontend/src/components/pages/message/MessageView.tsx b/frontend/src/routes/message/MessageView.tsx similarity index 85% rename from frontend/src/components/pages/message/MessageView.tsx rename to frontend/src/routes/message/MessageView.tsx index cab56e72..3259d42b 100644 --- a/frontend/src/components/pages/message/MessageView.tsx +++ b/frontend/src/routes/message/MessageView.tsx @@ -4,6 +4,14 @@ import React from 'react' +/** + * MessageView is a React component for visualizing message details. + * It shows key properties of a message such as its id, related topic and namespace. + * + * @component + * @param data - The data object containing the cluster information. + * @returns The rendered ClusterView component. + */ const MessageView: React.FC = ({ data }) => { const { messageId, diff --git a/frontend/src/components/pages/namespace/NamespaceView.tsx b/frontend/src/routes/namespace/NamespaceView.tsx similarity index 85% rename from frontend/src/components/pages/namespace/NamespaceView.tsx rename to frontend/src/routes/namespace/NamespaceView.tsx index b6d9e600..6037ca32 100644 --- a/frontend/src/components/pages/namespace/NamespaceView.tsx +++ b/frontend/src/routes/namespace/NamespaceView.tsx @@ -8,15 +8,22 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess' import ChevronRight from '@mui/icons-material/ChevronRight' import { Collapse, CardActions, Button } from '@mui/material' import { useNavigate } from 'react-router-dom' -import { - addFilterByDrilling, - resetAllFilters, -} from '../../../store/filterSlice' -import { useAppDispatch } from '../../../store/hooks' +import { addFilterByDrilling, resetAllFilters } from '../../store/filterSlice' +import { useAppDispatch } from '../../store/hooks' import axios from 'axios' -import { addCommaSeparator } from '../../../Helpers' -import config from '../../../config' +import { addCommaSeparator } from '../../Helpers' +import config from '../../config' +import { Topology } from '../../enum' +/** + * NamespaceView is a React component for visualizing namespace details. + * It shows key properties of a namespace such as its id, related tenants and numberOfTopics, + * and allows for the navigation to the detailed view. + * + * @component + * @param data - The data object containing the cluster information. + * @returns The rendered ClusterView component. + */ const NamespaceView: React.FC = ({ data }) => { const { id, tenant, numberOfTopics }: NamespaceInfo = data @@ -43,16 +50,16 @@ const NamespaceView: React.FC = ({ data }) => { } const handleDrillDown = () => { - dispatch(addFilterByDrilling({ filterName: 'namespace', id: id })) + dispatch(addFilterByDrilling({ filterName: Topology.NAMESPACE, id: id })) navigate('/topic') } const handleDrillUp = () => { dispatch(resetAllFilters()) - dispatch(addFilterByDrilling({ filterName: 'tenant', id: tenant })) + dispatch(addFilterByDrilling({ filterName: Topology.TENANT, id: tenant })) navigate('/tenant') } const handleDrillDownToTopic = (itemId: string) => { - dispatch(addFilterByDrilling({ filterName: 'topic', id: itemId })) + dispatch(addFilterByDrilling({ filterName: Topology.TOPIC, id: itemId })) navigate('/topic') } const handleExpand = () => { diff --git a/frontend/src/components/pages/namespace/index.tsx b/frontend/src/routes/namespace/index.tsx similarity index 82% rename from frontend/src/components/pages/namespace/index.tsx rename to frontend/src/routes/namespace/index.tsx index 5509d69c..ec95d746 100644 --- a/frontend/src/components/pages/namespace/index.tsx +++ b/frontend/src/routes/namespace/index.tsx @@ -3,17 +3,18 @@ // SPDX-FileCopyrightText: 2019 Georg Schwarz import React, { useEffect, useState } from 'react' -import { useAppDispatch, useAppSelector } from '../../../store/hooks' +import { useAppDispatch, useAppSelector } from '../../store/hooks' import axios from 'axios' import { selectCluster, selectNamespace, selectTenant, -} from '../../../store/filterSlice' +} from '../../store/filterSlice' import NamespaceView from './NamespaceView' import { selectTrigger } from '../requestTriggerSlice' -import config from '../../../config' +import config from '../../config' import { Masonry } from 'react-plock' +import FlushCacheButton from '../../components/buttons/FlushCacheButton' export interface ResponseNamespace { namespaces: NamespaceInfo[] @@ -22,7 +23,9 @@ export interface ResponseNamespace { /** * Card group component for the namespace type. * Displays the NamespaceView cards, title, loading window and network error. - * @returns Rendered namespace view cards for the dashboard component + * + * @component + * @returns Rendered namespace view cards for the dashboard component */ const NamespaceGroup: React.FC = () => { const [data, setData] = useState([]) @@ -67,8 +70,15 @@ const NamespaceGroup: React.FC = () => { return (
-

Available Namespaces ({data.length})

-

Topics: {sumTopics(data)}

+
+
+

+ Available Namespaces ({data.length}) +

+

Topics: {sumTopics(data)}

+
+ +
{loading ? (
Loading...
) : error ? ( diff --git a/frontend/src/components/pages/requestTriggerSlice.tsx b/frontend/src/routes/requestTriggerSlice.tsx similarity index 95% rename from frontend/src/components/pages/requestTriggerSlice.tsx rename to frontend/src/routes/requestTriggerSlice.tsx index f7423827..acc70bf1 100644 --- a/frontend/src/components/pages/requestTriggerSlice.tsx +++ b/frontend/src/routes/requestTriggerSlice.tsx @@ -3,7 +3,7 @@ // SPDX-FileCopyrightText: 2019 Georg Schwarz import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { RootState } from '../../store' +import { RootState } from '../store' export type RequestTriggerState = { trigger: boolean diff --git a/frontend/src/components/pages/tenant/TenantView.tsx b/frontend/src/routes/tenant/TenantView.tsx similarity index 84% rename from frontend/src/components/pages/tenant/TenantView.tsx rename to frontend/src/routes/tenant/TenantView.tsx index 44a8b169..37c2c073 100644 --- a/frontend/src/components/pages/tenant/TenantView.tsx +++ b/frontend/src/routes/tenant/TenantView.tsx @@ -8,15 +8,22 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess' import ChevronRight from '@mui/icons-material/ChevronRight' import { Collapse, CardActions, Button } from '@mui/material' import { useNavigate } from 'react-router-dom' -import { - addFilterByDrilling, - resetAllFilters, -} from '../../../store/filterSlice' -import { useAppDispatch } from '../../../store/hooks' +import { addFilterByDrilling, resetAllFilters } from '../../store/filterSlice' +import { useAppDispatch } from '../../store/hooks' import axios from 'axios' -import { addCommaSeparator } from '../../../Helpers' -import config from '../../../config' +import { addCommaSeparator } from '../../Helpers' +import config from '../../config' +import { Topology } from '../../enum' +/** + * TenantView is a React component for visualizing tenant details. + * It shows key properties of a tenant such as its name, info and numberOfNamespaces, + * and allows for the navigation to the detailed view. + * + * @component + * @param data - The data object containing the cluster information. + * @returns The rendered ClusterView component. + */ const TenantView: React.FC = ({ data }) => { const { name, tenantInfo, numberOfNamespaces, numberOfTopics }: TenantInfo = data @@ -44,16 +51,18 @@ const TenantView: React.FC = ({ data }) => { } const handleDrillDown = () => { - dispatch(addFilterByDrilling({ filterName: 'tenant', id: name })) + dispatch(addFilterByDrilling({ filterName: Topology.TENANT, id: name })) navigate('/namespace') } const handleDrillUp = (itemId: string) => { dispatch(resetAllFilters()) - dispatch(addFilterByDrilling({ filterName: 'cluster', id: itemId })) + dispatch(addFilterByDrilling({ filterName: Topology.CLUSTER, id: itemId })) navigate('/cluster') } const handleDrillDownToNamespace = (itemId: string) => { - dispatch(addFilterByDrilling({ filterName: 'namespace', id: itemId })) + dispatch( + addFilterByDrilling({ filterName: Topology.NAMESPACE, id: itemId }) + ) navigate('/namespace') } const handleExpand = () => { diff --git a/frontend/src/components/pages/tenant/index.tsx b/frontend/src/routes/tenant/index.tsx similarity index 80% rename from frontend/src/components/pages/tenant/index.tsx rename to frontend/src/routes/tenant/index.tsx index 6db8dd53..ff6be8bd 100644 --- a/frontend/src/components/pages/tenant/index.tsx +++ b/frontend/src/routes/tenant/index.tsx @@ -3,13 +3,14 @@ // SPDX-FileCopyrightText: 2019 Georg Schwarz import React, { useEffect, useState } from 'react' -import { useAppDispatch, useAppSelector } from '../../../store/hooks' +import { useAppDispatch, useAppSelector } from '../../store/hooks' import axios from 'axios' -import { selectCluster, selectTenant } from '../../../store/filterSlice' +import { selectCluster, selectTenant } from '../../store/filterSlice' import TenantView from './TenantView' import { selectTrigger } from '../requestTriggerSlice' -import config from '../../../config' +import config from '../../config' import { Masonry } from 'react-plock' +import FlushCacheButton from '../../components/buttons/FlushCacheButton' export interface ResponseTenant { tenants: TenantInfo[] @@ -18,6 +19,8 @@ export interface ResponseTenant { /** * Card group component for the tenant type. * Displays the TenantView cards, title, loading window and network error. + * + * @component * @returns Rendered tenant view cards for the dashboard component */ const TenantGroup: React.FC = () => { @@ -57,11 +60,16 @@ const TenantGroup: React.FC = () => { return (
-

Available Tenants ({data.length})

-

- Namespaces: {sumElements(data, 'numberOfNamespaces')}, Topics:{' '} - {sumElements(data, 'numberOfTopics')} -

+
+
+

Available Tenants ({data.length})

+

+ Namespaces: {sumElements(data, 'numberOfNamespaces')}, Topics:{' '} + {sumElements(data, 'numberOfTopics')} +

+
+ +
{loading ? (
Loading...
) : error ? ( diff --git a/frontend/src/components/pages/topic/TopicView.tsx b/frontend/src/routes/topic/TopicView.tsx similarity index 88% rename from frontend/src/components/pages/topic/TopicView.tsx rename to frontend/src/routes/topic/TopicView.tsx index 723a2cb0..a4030358 100644 --- a/frontend/src/components/pages/topic/TopicView.tsx +++ b/frontend/src/routes/topic/TopicView.tsx @@ -3,22 +3,28 @@ // SPDX-FileCopyrightText: 2019 Georg Schwarz import React, { useState } from 'react' -import ProducerModal from '../../modals/ProducerModal' +import ProducerModal from '../../components/modals/ProducerModal' import { Collapse, CardActions, Button } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ExpandLessIcon from '@mui/icons-material/ExpandLess' -import ChevronRight from '@mui/icons-material/ChevronRight' import { useNavigate } from 'react-router-dom' -import { - addFilterByDrilling, - resetAllFilters, -} from '../../../store/filterSlice' -import { useAppDispatch } from '../../../store/hooks' +import { addFilterByDrilling, resetAllFilters } from '../../store/filterSlice' +import { useAppDispatch } from '../../store/hooks' import axios from 'axios' -import SubscriptionModal from '../../modals/SubscriptionModal' -import config from '../../../config' -import MessageModal from '../../modals/MessageModal' +import SubscriptionModal from '../../components/modals/SubscriptionModal' +import config from '../../config' +import MessageModal from '../../components/modals/MessageModal' +import { Topology } from '../../enum' +/** + * TopicView is a React component for visualizing topic details. + * It shows key properties of a tenant such as its name, related tenant and namespace, + * and allows for the navigation to the detailed view. + * + * @component + * @param data - The data object containing the cluster information. + * @returns The rendered ClusterView component. + */ const TopicView: React.FC = ({ data }) => { const { name, tenant, namespace, producers, subscriptions }: TopicInfo = data const [expanded, setExpanded] = useState(false) @@ -48,12 +54,14 @@ const TopicView: React.FC = ({ data }) => { }*/ const handleDrillUpToNamespace = () => { dispatch(resetAllFilters()) - dispatch(addFilterByDrilling({ filterName: 'namespace', id: namespace })) + dispatch( + addFilterByDrilling({ filterName: Topology.NAMESPACE, id: namespace }) + ) navigate('/namespace') } const handleDrillUpToTenant = () => { dispatch(resetAllFilters()) - dispatch(addFilterByDrilling({ filterName: 'tenant', id: tenant })) + dispatch(addFilterByDrilling({ filterName: Topology.TENANT, id: tenant })) navigate('/tenant') } const handleExpand = () => { diff --git a/frontend/src/components/pages/topic/index.tsx b/frontend/src/routes/topic/index.tsx similarity index 87% rename from frontend/src/components/pages/topic/index.tsx rename to frontend/src/routes/topic/index.tsx index 265163ec..e2a88f32 100644 --- a/frontend/src/components/pages/topic/index.tsx +++ b/frontend/src/routes/topic/index.tsx @@ -3,7 +3,7 @@ // SPDX-FileCopyrightText: 2019 Georg Schwarz import React, { useEffect, useState } from 'react' -import { useAppDispatch, useAppSelector } from '../../../store/hooks' +import { useAppDispatch, useAppSelector } from '../../store/hooks' import axios from 'axios' import { selectCluster, @@ -12,13 +12,14 @@ import { selectSubscription, selectTenant, selectTopic, -} from '../../../store/filterSlice' +} from '../../store/filterSlice' import TopicView from './TopicView' import { selectTrigger } from '../requestTriggerSlice' -import config from '../../../config' +import config from '../../config' import { Masonry } from 'react-plock' import { Pagination } from '@mui/material' import { Box } from '@mui/system' +import FlushCacheButton from '../../components/buttons/FlushCacheButton' export interface ResponseTopic { topics: TopicInfo[] @@ -27,6 +28,8 @@ export interface ResponseTopic { /** * Card group component for the topic type. * Displays the TopicView cards, title, loading window and network error. + * + * @component * @returns Rendered topic view cards for the dashboard component */ const TopicGroup: React.FC = () => { @@ -101,11 +104,16 @@ const TopicGroup: React.FC = () => { return (
-

Available Topics ({data.length})

-

- Producers: {sumElements(data, 'producers')}, Subscriptions:{' '} - {sumElements(data, 'subscriptions')} -

+
+
+

Available Topics ({data.length})

+

+ Producers: {sumElements(data, 'producers')}, Subscriptions:{' '} + {sumElements(data, 'subscriptions')} +

+
+ +
{loading ? (
Loading...
) : error ? ( diff --git a/frontend/src/store/filterSlice.ts b/frontend/src/store/filterSlice.ts index 5f340357..aade37df 100644 --- a/frontend/src/store/filterSlice.ts +++ b/frontend/src/store/filterSlice.ts @@ -5,30 +5,21 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' import { RootState } from '.' import axios from 'axios' -import { ResponseCluster } from '../components/pages/cluster' -import { ResponseTenant } from '../components/pages/tenant' -import { ResponseNamespace } from '../components/pages/namespace' -import { ResponseTopic } from '../components/pages/topic' +import { ResponseCluster } from '../routes/cluster' +import { ResponseTenant } from '../routes/tenant' +import { ResponseNamespace } from '../routes/namespace' +import { ResponseTopic } from '../routes/topic' import config from '../config' - -export type HierarchyInPulsar = - | 'cluster' - | 'tenant' - | 'namespace' - | 'topic' - | 'message' - | 'producer' - | 'subscription' +import { Topology } from '../enum' export type FilterState = { - // used to keep track of what curently is filtered and what not: + // used to keep track of what currently is filtered and what not: cluster: string[] tenant: string[] namespace: string[] topic: string[] producer: string[] subscription: string[] - message: string[] // used for displaying the options in the filter dropdowns: displayedOptions: { allClusters: string[] @@ -37,22 +28,15 @@ export type FilterState = { allTopics: string[] allProducers: string[] allSubscriptions: string[] - allMessages: string[] } view: UpdateSingleFilter['filterName'] } export type UpdateSingleFilter = { - filterName: HierarchyInPulsar + filterName: Topology id: string } -type UpdateDisplayedOptions = { - // topologyLevel: 'cluster' | 'tenant' | 'namespace' | 'topic' | 'message' - topologyLevel: HierarchyInPulsar - options: string[] -} - const initialState: FilterState = { cluster: [], tenant: [], @@ -60,7 +44,6 @@ const initialState: FilterState = { topic: [], producer: [], subscription: [], - message: [], displayedOptions: { allClusters: [], allTenants: [], @@ -68,9 +51,8 @@ const initialState: FilterState = { allTopics: [], allProducers: [], allSubscriptions: [], - allMessages: [], }, - view: 'cluster', + view: Topology.CLUSTER, } const backendInstance = axios.create({ @@ -186,35 +168,8 @@ const filterSlice = createSlice({ }, // Adds Id to a filter array while resetting all other Id's in it. Specifically needed for Drill Down Buttons addFilterByDrilling: (state, action: PayloadAction) => { - switch (action.payload.filterName) { - case 'cluster': - state.cluster = initialState.cluster - state.cluster = [action.payload.id] - break - case 'tenant': - state.tenant = initialState.tenant - state.tenant = [action.payload.id] - break - case 'namespace': - state.namespace = initialState.namespace - state.namespace = [action.payload.id] - break - case 'topic': - state.topic = initialState.topic - state.topic = [action.payload.id] - break - case 'message': - state.message = initialState.message - state.message = [action.payload.id] - break - default: - console.log( - 'wrong type for updateDisplayedOptions with' + - action.payload.filterName + - ' ' + - action.payload.id - ) - } + state[action.payload.filterName] = initialState[action.payload.filterName] + state[action.payload.filterName] = [action.payload.id] }, // Resets all filter arrays / applied filters to initial state resetAllFilters: (state) => { @@ -225,7 +180,6 @@ const filterSlice = createSlice({ state.topic = initialState.topic state.producer = initialState.producer state.subscription = initialState.subscription - state.message = initialState.message }, // the filtering of lower views does not apply to higher views, // those filters shall be reset when the user "goes up". @@ -236,13 +190,12 @@ const filterSlice = createSlice({ const lastView = state.view const currentView = action.payload const pulsarHierarchyArr: UpdateSingleFilter['filterName'][] = [ - 'cluster', - 'tenant', - 'namespace', - 'topic', - 'message', - 'producer', - 'subscription', + Topology.CLUSTER, + Topology.TENANT, + Topology.NAMESPACE, + Topology.TOPIC, + Topology.PRODUCER, + Topology.SUBSCRIPTION, ] const currentViewLevel = pulsarHierarchyArr.indexOf(currentView) const lastViewLevel = pulsarHierarchyArr.indexOf(lastView) @@ -277,18 +230,6 @@ const filterSlice = createSlice({ }) builder.addCase(topicOptionThunk.fulfilled, (state, action) => { const data: ResponseTopic = JSON.parse(JSON.stringify(action.payload)) - /*const producers: string[] = data.topics - .flatMap((item) => item.producers) - .flat() - .filter((element, index) => { - return producers.indexOf(element) === index - }) - const subscriptions: string[] = data.topics - .flatMap((item) => item.subscriptions) - .flat() - .filter((element, index) => { - return producers.indexOf(element) === index - })*/ data.topics.forEach((topic) => { if (topic.producers) { state.displayedOptions.allProducers.push(...topic.producers) @@ -344,6 +285,11 @@ const selectSubscription = (state: RootState): string[] => { return state.filterControl.subscription } +/** + * Selects/returns all available filter options from the state + * @param state - current state + * @returns Filter Options + */ const selectOptions = ( state: RootState ): { @@ -353,11 +299,14 @@ const selectOptions = ( allTopics: string[] allProducers: string[] allSubscriptions: string[] - allMessages: string[] } => { return state.filterControl.displayedOptions } - +/** + * Selects/returns all applied filters from the state + * @param state - current state + * @returns Applied filters + */ const selectAllFilters = ( state: RootState ): { @@ -367,7 +316,6 @@ const selectAllFilters = ( topic: string[] producer: string[] subscription: string[] - message: string[] } => { return { cluster: state.filterControl.cluster, @@ -376,7 +324,6 @@ const selectAllFilters = ( topic: state.filterControl.topic, producer: state.filterControl.producer, subscription: state.filterControl.subscription, - message: state.filterControl.message, } } diff --git a/frontend/src/store/globalSlice.tsx b/frontend/src/store/globalSlice.tsx index 3fb001fe..5b4720f1 100644 --- a/frontend/src/store/globalSlice.tsx +++ b/frontend/src/store/globalSlice.tsx @@ -2,178 +2,36 @@ // SPDX-FileCopyrightText: 2010-2021 Dirk Riehle -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' -import axios from 'axios' +import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { RootState } from '.' -import config from '../config' -//import { modifyData } from './modifyData-temp' +import { Topology } from '../enum' export type View = { - selectedNav: string | null + selectedNav: Topology filteredId: number | string | null } export type globalState = { view: View - endpoint: string - rawClusterData: any - rawTopicData: any - clusterData: any - messageList: Array } const initialState: globalState = { view: { - selectedNav: 'cluster', + selectedNav: Topology.CLUSTER, filteredId: null, }, - endpoint: '127.0.0.1:8080', - rawClusterData: [], - rawTopicData: {}, - clusterData: [], - messageList: [], } -const backendInstance = axios.create({ - baseURL: config.backendUrl + '/api', - timeout: 1000, -}) -/* -const fetchRawClusterDataThunk = createAsyncThunk( - 'globalController/fetchData', - async () => { - const response = await backendInstance.get('/cluster') - return response.data - } -) - -const fetchRawTopicDataThunk = createAsyncThunk( - 'globalController/fetchTopic', - async () => { - const response = await backendInstance.get('/topic/all') - return response.data - } -) - -const fetchMessageDataThunk = createAsyncThunk( - 'globalController/fetchMessage', - async ({ topic, subscription }: { topic: string; subscription: string }) => { - const response = await backendInstance.get('/messages', { - params: { topic: topic, subscription: subscription }, - }) - return response.data.messages - } -) - -const fetchAllMessagesThunk = createAsyncThunk( - 'globalController/fetchAllMessages', - async (_, thunkAPI) => { - const { dispatch } = thunkAPI - const state = thunkAPI.getState() as RootState - const promises: Promise[] = [] - const { clusterData } = state.globalControl - // iterates through every topic-subscription pair and dispatches a request for it - clusterData.forEach((cluster: SampleCluster) => { - cluster.tenants.forEach((tenant: SampleTenant) => { - tenant.namespaces.forEach((namespace: SampleNamespace) => { - namespace.topics.forEach((topic: SampleTopic) => { - topic.topicStatsDto.subscriptions.forEach((sub: string) => { - if (sub) - promises.push( - dispatch( - fetchMessageDataThunk({ - topic: topic.name, - subscription: sub, - }) - ) - ) - }) - }) - }) - }) - }) - //awaits all requests - const messagesResults = await Promise.all(promises) - - //combines all messages in an array - const messages = [].concat( - ...messagesResults.map((result) => result.payload) - ) - return messages - } -) - -const combineDataThunk = createAsyncThunk( - 'globalController/combineData', - async (_, thunkAPI) => { - const { dispatch } = thunkAPI - // dispatch both thunks and wait for them to complete - await Promise.all([ - dispatch(fetchRawClusterDataThunk()), - dispatch(fetchRawTopicDataThunk()), - ]) - } -) -*/ -// eslint-disable-next-line const globalSlice = createSlice({ name: 'globalControl', initialState, reducers: { - /** Old landing page logic */ - // moveToApp: (state) => { - // state.showLP = false - // console.log('sdsd') - // }, - // backToLP: () => initialState, - /** End of landing page logic */ - setNav: (state, action: PayloadAction) => { + setNav: (state, action: PayloadAction) => { state.view.selectedNav = action.payload }, setView: (state, action: PayloadAction) => { state.view = action.payload }, - /*setClusterDataTEST: ( - state: globalState, - action: PayloadAction> - ) => { - state.clusterData = action.payload - },*/ - /* - setEndpoint: (state: globalState, action: PayloadAction) => { - state.endpoint = action.payload - }, - */ - }, - extraReducers: (builder) => { - /*builder.addCase(fetchRawClusterDataThunk.fulfilled, (state, action) => { - state.rawClusterData = JSON.parse(JSON.stringify(action.payload)) - }) - builder.addCase(fetchRawClusterDataThunk.rejected, () => { - console.log('fetch raw cluster data failed') - }) - builder.addCase(fetchRawTopicDataThunk.fulfilled, (state, action) => { - state.rawTopicData = JSON.parse(JSON.stringify(action.payload)) - }) - builder.addCase(fetchRawTopicDataThunk.rejected, () => { - console.log('fetch raw topic data failed') - }) - builder.addCase(combineDataThunk.fulfilled, (state) => { - //combine raw data to meet dummy data structure and save it in clusterData - state.clusterData = modifyData(state.rawClusterData, state.rawTopicData) - }) - builder.addCase(combineDataThunk.rejected, (state) => { - console.log('combine Async thunk failed') - state.clusterData = [] - state.rawClusterData = [] - state.rawTopicData = {} - }) - builder.addCase(fetchAllMessagesThunk.fulfilled, (state, action) => { - state.messageList = action.payload - }) - builder.addCase(fetchAllMessagesThunk.rejected, () => { - console.log('fetch all messages thunk failed') - })*/ }, }) @@ -181,24 +39,8 @@ const { actions, reducer } = globalSlice const selectView = (state: RootState): View => state.globalControl.view -const selectEndpoint = (state: RootState): string => - state.globalControl.endpoint - -/*const selectClusterData = (state: RootState): any => - state.globalControl.clusterData +export const { setNav, setView } = actions -const selectMessages = (state: RootState): any => - state.globalControl.messageList -*/ -export const { setNav, setView /*, setClusterDataTEST*/ } = actions - -export { - selectEndpoint, - selectView, - /*selectClusterData, - combineDataThunk, - selectMessages, - fetchAllMessagesThunk,*/ -} +export { selectView } export default reducer diff --git a/frontend/src/store/modifyData-temp.tsx b/frontend/src/store/modifyData-temp.tsx deleted file mode 100644 index 9dbcf1f8..00000000 --- a/frontend/src/store/modifyData-temp.tsx +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: 2010-2021 Dirk Riehle -// SPDX-FileCopyrightText: 2019 Georg Schwarz - -interface Topic { - id: string - cluster: string - name: string - localName: string - namespace: string - tenant: string - ownerBroker: string - topicStatsDto: { - subscriptions: string[] - producers: string[] - numberSubscriptions: number - numberProducers: number - producedMesages: number - consumedMessages: number - averageMessageSize: number - storageSize: number - } - producedMessages: number - consumedMessages: number - messagesDto: [] - persistent: boolean -} - -interface TopicData { - topics: Array -} - -/** - * Takes in standard cluster data from sprint release 5 and converts it to dummy data structure - * @param cluster - * @returns modified data - */ -/*export const modifyData = ( - clusters: Array, - topics: TopicData -): Array => { - const newData = clusters.map((cluster: SampleCluster) => { - const clusterCopy = { ...cluster } - let newCluster = addClusterAndTenant(clusterCopy) - newCluster = replaceTopicStrings(clusterCopy, topics) - return newCluster - }) - - return newData -} -*/ -/** - * Takes in standard cluster from sprint release 5 and converts it to dummy data structure - * @param cluster - * @returns modified cluster - */ -/* -const addClusterAndTenant = (cluster: SampleCluster): SampleCluster => { - // Add 'cluster' property to tenants - cluster.tenants = cluster.tenants.map((tenant) => { - // Add 'cluster' property to tenant - const tenantWithCluster = { ...tenant, cluster: cluster.id } - - // Add 'cluster' and 'tenant' property to namespaces - tenantWithCluster.namespaces = tenant.namespaces.map((namespace) => ({ - ...namespace, - cluster: cluster.id, - tenant: tenant.id, - })) - - return tenantWithCluster - }) - - return cluster -} - -const replaceTopicStrings = ( - cluster: SampleCluster, - topics: TopicData -): SampleCluster => { - const topicsMap = new Map(topics.topics.map((topic) => [topic.name, topic])) - - // Replace topic strings with topic objects. non strings stay the same - cluster.tenants.forEach((tenant) => { - tenant.namespaces.forEach((namespace) => { - namespace.topics = namespace.topics.map((topicName) => { - if (typeof topicName !== 'string') return topicName - const topic = topicsMap.get(topicName) - if (topic) return topic - else return topicName - }) - const filteredTopics: string[] = [] - namespace.topics = namespace.topics.filter((topic) => { - const pass = typeof topic !== 'string' - if (!pass) { - filteredTopics.push(topic) - } - return pass - }) - if (!filteredTopics) console.log('Filtered out topics: ', filteredTopics) - }) - }) - - return cluster -} -*/ diff --git a/frontend/src/store/reducers.tsx b/frontend/src/store/reducers.tsx index 84f53f60..12d002f9 100644 --- a/frontend/src/store/reducers.tsx +++ b/frontend/src/store/reducers.tsx @@ -3,14 +3,12 @@ // SPDX-FileCopyrightText: 2019 Georg Schwarz import { combineReducers } from '@reduxjs/toolkit' -import formControllerSlice from '../components/form/formControllerSlice' import globalSlice from './globalSlice' import filterSlice from './filterSlice' -import requestTriggerSlice from '../components/pages/requestTriggerSlice' +import requestTriggerSlice from '../routes/requestTriggerSlice' export default combineReducers({ globalControl: globalSlice, - formControl: formControllerSlice, filterControl: filterSlice, triggerControl: requestTriggerSlice, }) diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index 39e28748..7386b93b 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -2,353 +2,221 @@ // SPDX-FileCopyrightText: 2010-2021 Dirk Riehle -// Demo interfaces (MessageList was for Topics, Message needs to be updated) -interface MessageList { - id: string - name: string - messages: Array -} - -interface Message { - id: string - value: string -} - -// Component Prop Interfaces -interface CardProps { - data: - | SampleCluster - | SampleTenant - | SampleNamespace - | SampleTopic - | SampleMessage - handleClick: ( - e: React.MouseEvent, - currentEl: SampleCluster | SampleTenant | SampleNamespace | SampleTopic - ) => void -} - -interface DashboardProps { - completeMessages: Array - children: ReactNode - view?: - | 'cluster' - | 'tenant' - | 'namespace' - | 'topic' - | 'message' - | string - | null -} - -interface CustomAccordionProps { - data: Array -} +import { Topology } from './enum' -interface CustomSelectProps { - data: Array - onChange: (event: SelectChangeEvent) => void - value: T - label: string - error: boolean -} - -interface CustomCheckboxProps { - id: string - text: string - typology: - | 'cluster' - | 'tenant' - | 'namespace' - | 'topic' - | 'message' - | 'producer' - | 'subscription' - selected: boolean -} - -interface formProps { - data: Array - triggerUpdate(message: string, topic: string): void -} - -interface ClusterInfo { - name: string - numberOfTenants: number - numberOfNamespaces: number -} - -interface TenantInfo { - name: string - tenantInfo: { - adminRoles: string[] - allowedClusters: string[] +declare global { + interface DashboardProps { + children: ReactNode } - numberOfNamespaces: number - numberOfTopics: number -} -interface NamespaceInfo { - id: string - tenant: string - numberOfTopics: number -} + interface FilterItem { + id: string + name: string + } -interface TopicInfo { - name: string - namespace: string - tenant: string - subscriptions: string[] - producers: string[] - messagesSendToTopic: number -} + interface CustomSelectProps { + data: Array + onChange: (event: SelectChangeEvent) => void + value: T + label: string + error: boolean + } -interface ClusterDetail { - name: string - tenants: string[] - brokers: string[] - amountOfBrokers: number - brokerServiceUrl: string - serviceUrl: string -} + interface CustomCheckboxProps { + id: string + text: string + topology: Topology + selected: boolean + } -interface TenantDetail { - name: string - namespaces: string[] - tenantInfo: { - adminRoles: string[] - allowedClusters: string[] + interface ClusterInfo { + name: string + numberOfTenants: number + numberOfNamespaces: number } -} -interface NamespaceDetail { - id: string - topics: string[] - tenant: string - bundlesData: { - boundaries: string[] - numBundles: number + interface TenantInfo { + name: string + tenantInfo: { + adminRoles: string[] + allowedClusters: string[] + } + numberOfNamespaces: number + numberOfTopics: number } - amountOfTopics: number - messagesTTL: number - retentionPolicies: { - retentionTimeInMinutes: number - retentionSizeInMB: number + + interface NamespaceInfo { + id: string + tenant: string + numberOfTopics: number } -} -interface TopicDetail { - name: string - localName: string - namespace: string - tenant: string - ownerBroker: string - topicStatsDto: { + interface TopicInfo { + name: string + namespace: string + tenant: string subscriptions: string[] producers: string[] - numberSubscriptions: number - numberProducers: number - producedMesages: number - consumedMessages: number - averageMessageSize: number - storageSize: number + messagesSendToTopic: number } - producedMessages: number - consumedMessages: number - // messagesDto: [ - // { - // messageId: string - // topic: string - // payload: string - // schema: string - // namespace: string - // tenant: string - // publishTime: number - // producer: string - // } - // ] - schemaInfos: SchemaInfo[] - persistent: boolean -} -interface SchemaInfo { - name: string - version: number - type: string - properties: { - additionalProp1: string - additionalProp2: string - additionalProp3: string + interface ClusterDetail { + name: string + tenants: string[] + brokers: string[] + amountOfBrokers: number + brokerServiceUrl: string + serviceUrl: string } - schemaDefinition: string - timestamp: string -} - -interface ProducerDetails { - id: number - name: string - messagesDto: MessageDto[] - amountOfMessages: number - address: string - averageMsgSize: number - clientVersion: string - connectedSince: string -} - -interface MessageDto { - messageId: string - topic: string - payload: string - schema: string - namespace: string - tenant: string - publishTime: number - producer: string -} -interface ConsumerDetails { - name: string - address: string - availablePermits: number - bytesOutCounter: number - clientVersion: string - connectedSince: string - lastAckedTimestamp: number - lastConsumedTimestamp: number - messageOutCounter: number - unackedMessages: number - blockedConsumerOnUnackedMsgs: boolean -} - -interface MessageStandard { - id: string -} + interface TenantDetail { + name: string + namespaces: string[] + tenantInfo: { + adminRoles: string[] + allowedClusters: string[] + } + } -interface ClusterViewProps { - data: ClusterInfo -} + interface NamespaceDetail { + id: string + topics: string[] + tenant: string + bundlesData: { + boundaries: string[] + numBundles: number + } + amountOfTopics: number + messagesTTL: number + retentionPolicies: { + retentionTimeInMinutes: number + retentionSizeInMB: number + } + } -interface TenantViewProps { - data: TenantInfo -} + interface TopicDetail { + name: string + localName: string + namespace: string + tenant: string + ownerBroker: string + topicStatsDto: { + subscriptions: string[] + producers: string[] + numberSubscriptions: number + numberProducers: number + producedMesages: number + consumedMessages: number + averageMessageSize: number + storageSize: number + } + producedMessages: number + consumedMessages: number + schemaInfos: SchemaInfo[] + persistent: boolean + } -interface NamespaceViewProps { - data: NamespaceInfo -} + interface SchemaInfo { + name: string + version: number + type: string + properties: { + additionalProp1: string + additionalProp2: string + additionalProp3: string + } + schemaDefinition: string + timestamp: string + } -interface TopicViewProps { - data: TopicInfo -} + interface ProducerDetails { + id: number + name: string + messagesDto: MessageDto[] + amountOfMessages: number + address: string + averageMsgSize: number + clientVersion: string + connectedSince: string + } -interface MessageViewProps { - data: MessageInfo -} -interface MessageInfo { - messageId: string - topic: string - payload: string - schema: string - namespace: string - tenant: string - publishTime: number - producer: string -} + interface MessageDto { + messageId: string + topic: string + payload: string + schema: string + namespace: string + tenant: string + publishTime: number + producer: string + } -interface CustomFilterProps { - messages: Array - currentView: - | 'cluster' - | 'tenant' - | 'namespace' - | 'topic' - | 'message' - | undefined - | null - | string -} + interface ConsumerDetails { + name: string + address: string + availablePermits: number + bytesOutCounter: number + clientVersion: string + connectedSince: string + lastAckedTimestamp: number + lastConsumedTimestamp: number + messageOutCounter: number + unackedMessages: number + blockedConsumerOnUnackedMsgs: boolean + } -interface CustomSearchProps { - setSearchQuery: React.Dispatch> - placeholder: string -} + interface MessageStandard { + id: string + } -interface UpdateForData { - message: string - topic: string -} + interface ClusterViewProps { + data: ClusterInfo + } -// Data Types -type SampleCluster = { - id: string - tenants: Array - brokers: Array - bookies?: Array - amountOfTenants: number - amountOfNamespaces: number - amountOfTopics: number - amountOfBrokers: number - brokerServiceUrl: string - serviceUrl: string -} + interface TenantViewProps { + data: TenantInfo + } -type SampleTenant = { - id: string - namespaces: Array - amountOfNamespaces: number - amountOfTopics: number - cluster: string - tenantInfo: { adminRoles: Array; allowedClusters: Array } -} + interface NamespaceViewProps { + data: NamespaceInfo + } -type SampleNamespace = { - id: string - topics: Array - cluster: string - tenant: string - amountOfTopics: number - bundlesData: { boundaries: Array; numBundles: number } - messagesTTL: number | null - retentionPolicies: { - retentionTimeInMinutes: number - retentionSizeInMB: number + interface TopicViewProps { + data: TopicInfo } -} -type SampleTopic = { - id: string - name: string - localName: string - namespace: string - tenant: string - cluster: string - topicStatsDto: SampleTopicStats - persistent: boolean -} + interface MessageViewProps { + data: MessageInfo + } + interface MessageInfo { + messageId: string + topic: string + payload: string + schema: string + namespace: string + tenant: string + publishTime: number + producer: string + } -type SampleTopicStats = { - subscriptions: Array - producers: Array - numberSubscriptions: number - numberProducers: number - producedMesages: number - consumedMessages: number - averageMessageSize: number - storageSize: number -} + interface CustomFilterProps { + currentView: + | Topology.CLUSTER + | Topology.TENANT + | Topology.NAMESPACE + | Topology.TOPIC + | undefined + | null + | string + } -type SampleSubscription = { - name: string - consumers: Array - numberConsumers: number -} + interface CustomSearchProps { + setSearchQuery: React.Dispatch> + placeholder: string + } -type SampleMessage = { - id: string - payload: string - schema: string - cluster: string - tenant: string - namespace: string - topic: string - publishTime: string + interface UpdateForData { + message: string + topic: string + } }