diff --git a/plugins/grafana-custom-plugins/grafana-http-table-plugin/src/TablePanel.test.tsx b/plugins/grafana-custom-plugins/grafana-http-table-plugin/src/TablePanel.test.tsx
new file mode 100644
index 00000000..4ab2c03e
--- /dev/null
+++ b/plugins/grafana-custom-plugins/grafana-http-table-plugin/src/TablePanel.test.tsx
@@ -0,0 +1,59 @@
+import { configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import { TablePanel } from './TablePanel';
+import { LoadingState, PanelProps, TimeRange, toDataFrame } from '@grafana/data';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import React from 'react';
+
+configure({ adapter: new Adapter() });
+
+describe('Table Plugin Test', () => {
+ it('Should render Table', () => {
+ let props = {} as PanelProps;
+ let timeRange = {} as TimeRange;
+ props.data = {
+ series: [
+ toDataFrame({
+ refId: 'A',
+ fields: [
+ { name: 'sourceIP', values: ['10.10.1.4'] },
+ { name: 'sourceTransportPort', values: ['42162'] },
+ { name: 'destinationIP', values: ['93.184.216.34'] },
+ { name: 'destinationTransportPort', values: ['80'] },
+ { name: 'httpVals', values: ['{"0":{"hostname":"example.com","url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1256},"1":{"hostname":"example.com","url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1256},"2":{"hostname":"example.com","url":"/","http_user_agent":"curl/7.88.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1256}}'] },
+ ],
+ }),
+ ],
+ state: LoadingState.Done,
+ timeRange: timeRange,
+ };
+ props.width = 600,
+ props.height = 600;
+ props.options = {};
+ let { unmount } = render();
+ React.useLayoutEffect = React.useEffect;
+
+ // check each column of the row is in the document and exists three times
+ expect(screen.getAllByText('10.10.1.4:42162').length).toEqual(3);
+ expect(screen.getAllByText('10.10.1.4:42162')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('93.184.216.34:80').length).toEqual(3);
+ expect(screen.getAllByText('93.184.216.34:80')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('example.com').length).toEqual(3);
+ expect(screen.getAllByText('example.com')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('/').length).toEqual(3);
+ expect(screen.getAllByText('/')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('curl/7.88.1').length).toEqual(3);
+ expect(screen.getAllByText('curl/7.88.1')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('text/html').length).toEqual(3);
+ expect(screen.getAllByText('text/html')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('GET').length).toEqual(3);
+ expect(screen.getAllByText('GET')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('200').length).toEqual(3);
+ expect(screen.getAllByText('200')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('1256').length).toEqual(3);
+ expect(screen.getAllByText('1256')[0]).toBeInTheDocument();
+
+ unmount();
+ });
+});
diff --git a/plugins/grafana-custom-plugins/grafana-http-table-plugin/src/TablePanel.tsx b/plugins/grafana-custom-plugins/grafana-http-table-plugin/src/TablePanel.tsx
new file mode 100644
index 00000000..83d0d950
--- /dev/null
+++ b/plugins/grafana-custom-plugins/grafana-http-table-plugin/src/TablePanel.tsx
@@ -0,0 +1,136 @@
+import React, { useMemo } from 'react';
+import {config} from '@grafana/runtime'
+import { PanelProps } from '@grafana/data';
+import { SimpleOptions } from 'types';
+import { MaterialReactTable, type MRT_ColumnDef} from 'material-react-table';
+import { ThemeProvider, createTheme } from '@mui/material';
+
+interface Props extends PanelProps {}
+
+export const TablePanel: React.FC = ({ options, data, width, height }) => {
+ const frame = data.series[0];
+ const columns = useMemo>>(
+ () => [
+ {header: 'Source', accessorKey: 'source'},
+ {header: 'Destination', accessorKey: 'destination'},
+ {header: 'Transaction ID', accessorKey: 'txId'},
+ {header: 'Hostname', accessorKey: 'hostname'},
+ {header: 'URL', accessorKey: 'url'},
+ {header: 'HTTP User Agent', accessorKey: 'http_user_agent'},
+ {header: 'HTTP Content Type', accessorKey: 'http_content_type'},
+ {header: 'HTTP Method', accessorKey: 'http_method'},
+ {header: 'Protocol', accessorKey: 'protocol'},
+ {header: 'Status', accessorKey: 'status'},
+ {header: 'Length', accessorKey: 'length'},
+ ],
+ [],
+ );
+ const sourceIPs = frame.fields.find((field) => field.name === 'sourceIP');
+ const destinationIPs = frame.fields.find((field) => field.name === 'destinationIP');
+ const sourceTransportPorts = frame.fields.find((field) => field.name === 'sourceTransportPort');
+ const destinationTransportPorts = frame.fields.find((field) => field.name === 'destinationTransportPort');
+ const httpValsList = frame.fields.find((field) => field.name === 'httpVals');
+ const defaultMaterialTheme = createTheme({palette: {
+ mode: config.theme2.isDark ? 'dark': 'light',
+ }});
+
+ let tableData: FlowRow[] = [];
+ interface FlowRow {
+ source: string,
+ destination: string,
+ txId: number,
+ hostname: string,
+ url: string,
+ http_user_agent: string,
+ http_content_type: string,
+ http_method: string,
+ protocol: string,
+ status: number,
+ length: number,
+ subRows: FlowRow[],
+ }
+ for (let i = 0; i < frame.length; i++) {
+ const sourceIP = sourceIPs?.values.get(i);
+ const destinationIP = destinationIPs?.values.get(i);
+ const sourcePort = sourceTransportPorts?.values.get(i);
+ const destinationPort = destinationTransportPorts?.values.get(i);
+ const httpVals = httpValsList?.values.get(i);
+ let httpValsJSON: any;
+ if (httpVals !== undefined) {
+ httpValsJSON = JSON.parse(httpVals);
+ }
+ const rowData: FlowRow = {
+ source: '',
+ destination: '',
+ txId: 0,
+ hostname: '',
+ url: '',
+ http_user_agent: '',
+ http_content_type: '',
+ http_method: '',
+ protocol: '',
+ status: 0,
+ length: 0,
+ subRows: [],
+ };
+ function setTableRow(source: string, destination: string, txId: number, hostname: string, url: string, http_user_agent: string, http_content_type: string, http_method: string, protocol: string, status: number, length: number) {
+ if (txId === 0) {
+ rowData.source = source;
+ rowData.destination = destination;
+ rowData.txId = txId;
+ rowData.hostname = hostname;
+ rowData.url = url;
+ rowData.http_user_agent = http_user_agent;
+ rowData.http_content_type = http_content_type;
+ rowData.http_method = http_method;
+ rowData.protocol = protocol;
+ rowData.status = status;
+ rowData.length = length;
+ } else {
+ const row: FlowRow = {
+ source: source,
+ destination: destination,
+ txId: txId,
+ hostname: hostname,
+ url: url,
+ http_user_agent: http_user_agent,
+ http_content_type: http_content_type,
+ http_method: http_method,
+ protocol: protocol,
+ status: status,
+ length: length,
+ subRows: [],
+ }
+ rowData.subRows.push(row);
+ }
+ }
+ for (const txId in httpValsJSON) {
+ setTableRow(sourceIP+':'+sourcePort, destinationIP+':'+destinationPort, +txId, httpValsJSON[txId].hostname, httpValsJSON[txId].url, httpValsJSON[txId].http_user_agent, httpValsJSON[txId].http_content_type, httpValsJSON[txId].http_method, httpValsJSON[txId].protocol, httpValsJSON[txId].status, httpValsJSON[txId].length);
+ tableData.push(rowData);
+ }
+ }
+
+ return (
+
+
+
+
+
+ );
+};