diff --git a/package.json b/package.json index 8fe2b4fa..b131bde6 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "just-compare": "^2.3.0", "ms": "^2.1.3", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.10", "react-resizable-panels": "^0.0.53", @@ -39,6 +40,7 @@ "@types/ms": "^0.7.31", "@types/node": "^20.3.2", "@types/react": "^18.2.14", + "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.6", "@types/react-window": "^1.8.5", "@typescript-eslint/eslint-plugin": "^5.60.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 745ee72c..7c4cce6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@emotion/react': specifier: ^11.11.1 @@ -49,6 +53,9 @@ dependencies: react: specifier: ^18.2.0 version: 18.2.0 + react-beautiful-dnd: + specifier: ^13.1.1 + version: 13.1.1(react-dom@18.2.0)(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -75,6 +82,9 @@ devDependencies: '@types/react': specifier: ^18.2.14 version: 18.2.14 + '@types/react-beautiful-dnd': + specifier: ^13.1.4 + version: 13.1.4 '@types/react-dom': specifier: ^18.2.6 version: 18.2.6 @@ -947,6 +957,13 @@ packages: resolution: {integrity: sha512-dU54aBwaxG0H+jQ4BdrqtYFN5L7PZevvlnzyL6XeOZgfDS3+sVNCtuG3JmpTEqQSwGLYC1IEwogPGA/Iit2bOA==} dev: false + /@types/hoist-non-react-statics@3.3.1: + resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} + dependencies: + '@types/react': 18.2.14 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/json-schema@7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true @@ -974,12 +991,27 @@ packages: /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + /@types/react-beautiful-dnd@13.1.4: + resolution: {integrity: sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==} + dependencies: + '@types/react': 18.2.14 + dev: true + /@types/react-dom@18.2.6: resolution: {integrity: sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==} dependencies: '@types/react': 18.2.14 dev: true + /@types/react-redux@7.1.25: + resolution: {integrity: sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==} + dependencies: + '@types/hoist-non-react-statics': 3.3.1 + '@types/react': 18.2.14 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + /@types/react-window@1.8.5: resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==} dependencies: @@ -1400,6 +1432,12 @@ packages: which: 2.0.2 dev: true + /css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + dependencies: + tiny-invariant: 1.3.1 + dev: false + /csstype@3.0.9: resolution: {integrity: sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==} dev: false @@ -2746,6 +2784,29 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + dev: false + + /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.21.5 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.2.0) + transitivePeerDependencies: + - react-native + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -2768,6 +2829,32 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: false + + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.21.5 + '@types/react-redux': 7.1.25 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 17.0.2 + dev: false + /react-remove-scroll-bar@2.3.4(@types/react@18.2.14)(react@18.2.0): resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} engines: {node: '>=10'} @@ -2901,6 +2988,12 @@ packages: loose-envify: 1.4.0 dev: false + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.21.5 + dev: false + /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: false @@ -3121,6 +3214,10 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: false + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -3251,6 +3348,14 @@ packages: use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.14)(react@18.2.0) dev: false + /use-memo-one@1.1.3(react@18.2.0): + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /use-sidecar@1.1.2(@types/react@18.2.14)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} diff --git a/src/hooks/useGetLogStreamSchema.ts b/src/hooks/useGetLogStreamSchema.ts index ed333630..28df383b 100644 --- a/src/hooks/useGetLogStreamSchema.ts +++ b/src/hooks/useGetLogStreamSchema.ts @@ -2,6 +2,7 @@ import { LogStreamSchemaData } from '@/@types/parseable/api/stream'; import { getLogStreamSchema } from '@/api/logStream'; import { StatusCodes } from 'http-status-codes'; import useMountedState from './useMountedState'; +import { Field } from '@/@types/parseable/dataType'; export const useGetLogStreamSchema = () => { const [data, setData] = useMountedState(null); @@ -36,5 +37,33 @@ export const useGetLogStreamSchema = () => { setData(null); }; - return { data, error, loading, getDataSchema, resetData }; + const reorderSchemaFields = (destination: number, source: number) => { + setData((prev) => { + if (prev != null) { + if (destination >= prev.fields.length || source >= prev.fields.length) { + setError('Unable to reorder fields'); + return prev; + } + if (destination === source) return prev; + + const newFields: Field[] = [...prev.fields]; + for (let i = 0; i < prev.fields.length; i++) { + let offset = 0; + if (source < destination && i >= source && i < destination) offset = 1; + else if (destination < source && i > destination && i <= source) offset = -1; + newFields[i] = prev.fields[i + offset]; + } + newFields[destination] = prev.fields[source]; + + return { + ...prev, + fields: newFields, + }; + } + + return null; + }); + }; + + return { data, error, loading, getDataSchema, resetData, reorderSchemaFields }; }; diff --git a/src/pages/Logs/LogTable.tsx b/src/pages/Logs/LogTable.tsx index e4517993..c9e62d67 100644 --- a/src/pages/Logs/LogTable.tsx +++ b/src/pages/Logs/LogTable.tsx @@ -10,7 +10,8 @@ import LogRow from './LogRow'; import { useLogTableStyles } from './styles'; import useMountedState from '@/hooks/useMountedState'; import ErrorText from '@/components/Text/ErrorText'; -import { IconDotsVertical, IconSelector } from '@tabler/icons-react'; +import { IconDotsVertical, IconSelector, IconGripVertical } from '@tabler/icons-react'; +import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import { Field } from '@/@types/parseable/dataType'; import EmptyBox from '@/components/Empty'; import { RetryBtn } from '@/components/Button/Retry'; @@ -36,6 +37,7 @@ const LogTable: FC = () => { const { data: logsSchema, getDataSchema, + reorderSchemaFields, resetData: resetStreamData, loading, error: logStreamSchemaError, @@ -51,7 +53,7 @@ const LogTable: FC = () => { loading: logsLoading, error: logsError, resetData: resetLogsData, - sort + sort, } = useQueryLogs(); const appliedFilter = (key: string) => { @@ -89,8 +91,8 @@ const LogTable: FC = () => { setQuerySearch((prev) => { const sort = { field: 'p_timestamp', - order: SortOrder.DESCENDING - } + order: SortOrder.DESCENDING, + }; if (order !== null) { sort.field = columName; sort.order = order; @@ -98,11 +100,11 @@ const LogTable: FC = () => { return { ...prev, - sort - } - }) - } - } + sort, + }; + }); + }; + }; const onRetry = () => { const query = subLogQuery.get(); @@ -221,6 +223,7 @@ const LogTable: FC = () => { columnToggles={columnToggles} toggleColumn={toggleColumn} isColumnActive={isColumnActive} + reorderColumn={reorderSchemaFields} /> @@ -248,7 +251,7 @@ const LogTable: FC = () => { ) : ( - + ) ) : ( @@ -263,15 +266,53 @@ const LogTable: FC = () => { ); }; +type ThColumnMenuItemProps = { + field: Field; + index: number; + toggleColumn: (columnName: string, value: boolean) => void; + isColumnActive: (columnName: string) => boolean; +}; + +const ThColumnMenuItem: FC = (props) => { + const { field, index, toggleColumn, isColumnActive } = props; + const { classes } = useLogTableStyles(); + if (skipFields.includes(field.name)) return null; + + return ( + + {(provided) => ( + +
+
+ +
+ toggleColumn(field.name, event.currentTarget.checked)} + /> +
+
+ )} +
+ ); +}; + type ThColumnMenuProps = { logSchemaFields: Array; columnToggles: Map; toggleColumn: (columnName: string, value: boolean) => void; + reorderColumn: (destination: number, source: number) => void; isColumnActive: (columnName: string) => boolean; }; const ThColumnMenu: FC = (props) => { - const { logSchemaFields, isColumnActive, toggleColumn } = props; + const { logSchemaFields, isColumnActive, toggleColumn, reorderColumn } = props; const { classes } = useLogTableStyles(); const { thColumnMenuBtn, thColumnMenuDropdown } = classes; @@ -286,22 +327,30 @@ const ThColumnMenu: FC = (props) => { - - {logSchemaFields.map((field) => { - if (skipFields.includes(field.name)) return null; - - return ( - - toggleColumn(field.name, event.currentTarget.checked)} - /> - - ); - })} - + { + reorderColumn(destination?.index || 0, source.index); + }}> + + + {(provided) => ( +
+ {logSchemaFields.map((field, index) => { + return ( + + ); + })} + {provided.placeholder} +
+ )} +
+
+
); diff --git a/src/pages/Logs/styles.tsx b/src/pages/Logs/styles.tsx index f52cca49..228a9639 100644 --- a/src/pages/Logs/styles.tsx +++ b/src/pages/Logs/styles.tsx @@ -141,6 +141,20 @@ export const useLogTableStyles = createStyles((theme) => { overflowY: 'scroll', }, + thColumnMenuDragHandle: { + ...theme.fn.focusStyles(), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + color: theme.colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.gray[6], + paddingRight: theme.spacing.md, + }, + + thColumnMenuDraggable: { + display: 'flex', + }, + footerContainer: { padding: spacing.md, display: 'flex',