diff --git a/package.json b/package.json index a36f67ed..c4df4c91 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@apache-arrow/ts": "^14.0.2", "@emotion/react": "^11.11.1", "@mantine/carousel": "^7.5.1", + "@mantine/charts": "^7.5.3", "@mantine/code-highlight": "^7.5.1", "@mantine/core": "^7.5.1", "@mantine/dates": "^7.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bef580c..d97d47aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@mantine/carousel': specifier: ^7.5.1 version: 7.5.1(@mantine/core@7.5.1)(@mantine/hooks@7.5.1)(embla-carousel-react@7.1.0)(react-dom@18.2.0)(react@18.2.0) + '@mantine/charts': + specifier: ^7.5.3 + version: 7.5.3(@mantine/core@7.5.1)(@mantine/hooks@7.5.1)(react-dom@18.2.0)(react@18.2.0)(recharts@2.12.1) '@mantine/code-highlight': specifier: ^7.5.1 version: 7.5.1(@mantine/core@7.5.1)(@mantine/hooks@7.5.1)(react-dom@18.2.0)(react@18.2.0) @@ -651,6 +654,22 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@mantine/charts@7.5.3(@mantine/core@7.5.1)(@mantine/hooks@7.5.1)(react-dom@18.2.0)(react@18.2.0)(recharts@2.12.1): + resolution: {integrity: sha512-TgoBVACbmAxCHZQOL/K8DlijPZ4Ogv8S4hVXdxFgwUnKUzvnLZanaan2Vu3s7111HYCY2qHwgJwuCNtKTGuARQ==} + peerDependencies: + '@mantine/core': 7.5.3 + '@mantine/hooks': 7.5.3 + react: ^18.2.0 + react-dom: ^18.2.0 + recharts: ^2.10.3 + dependencies: + '@mantine/core': 7.5.1(@mantine/hooks@7.5.1)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@mantine/hooks': 7.5.1(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + recharts: 2.12.1(react-dom@18.2.0)(react@18.2.0) + dev: false + /@mantine/code-highlight@7.5.1(@mantine/core@7.5.1)(@mantine/hooks@7.5.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-zCdJ911r7WacTC8aQb66oB299jhcnQCQ8uS1kXYioUvJ/lOc0aiFh659KokmUvIL6ZdowCvffwaodiUFDZcqBw==} peerDependencies: @@ -971,6 +990,48 @@ packages: resolution: {integrity: sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==} dev: false + /@types/d3-array@3.2.1: + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + dev: false + + /@types/d3-color@3.1.3: + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + dev: false + + /@types/d3-ease@3.0.2: + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + dev: false + + /@types/d3-interpolate@3.0.4: + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + dependencies: + '@types/d3-color': 3.1.3 + dev: false + + /@types/d3-path@3.1.0: + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + dev: false + + /@types/d3-scale@4.0.8: + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + dependencies: + '@types/d3-time': 3.0.3 + dev: false + + /@types/d3-shape@3.1.6: + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + dependencies: + '@types/d3-path': 3.1.0 + dev: false + + /@types/d3-time@3.0.3: + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + dev: false + + /@types/d3-timer@3.0.2: + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + dev: false + /@types/hoist-non-react-statics@3.3.1: resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} dependencies: @@ -1533,6 +1594,77 @@ packages: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} dev: false + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + /dayjs@1.11.10: resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} dev: false @@ -1560,6 +1692,10 @@ packages: ms: 2.1.2 dev: true + /decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -1987,6 +2123,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + /execa@4.1.0: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} engines: {node: '>=10'} @@ -2009,6 +2149,11 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true + /fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + dev: false + /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -2342,6 +2487,11 @@ packages: side-channel: 1.0.4 dev: true + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} dependencies: @@ -2589,6 +2739,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} dev: false @@ -3234,6 +3388,19 @@ packages: react: 18.2.0 dev: false + /react-smooth@4.0.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2NMXOBY1uVUQx1jBeENGA497HK20y6CPGYL1ZnJLeoQ8rrc3UfmOM82sRxtzpcoCkUMy4CS0RGylfuVhuFjBgg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-style-singleton@2.2.1(@types/react@18.2.14)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -3299,6 +3466,31 @@ packages: loose-envify: 1.4.0 dev: false + /recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + dependencies: + decimal.js-light: 2.5.1 + dev: false + + /recharts@2.12.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-35vUCEBPf+pM+iVgSgVTn86faKya5pc4JO6cYJL63qOK2zDEyzDn20Tdj+CDI/3z+VcpKyQ8ZBQ9OiQ+vuAbjg==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + clsx: 2.0.0 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 16.13.1 + react-smooth: 4.0.0(react-dom@18.2.0)(react@18.2.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.1 + victory-vendor: 36.9.1 + dev: false + /redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} dependencies: @@ -3748,6 +3940,25 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /victory-vendor@36.9.1: + resolution: {integrity: sha512-+pZIP+U3pEJdDCeFmsXwHzV7vNHQC/eIbHklfe2ZCZqayYRH7lQbHcVgsJ0XOOv27hWs4jH4MONgXxHMObTMSA==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + dev: false + /vite@4.3.9(@types/node@20.3.2): resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/src/api/axios.ts b/src/api/axios.ts index f47e04f5..5a07d9a0 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -16,15 +16,15 @@ instance.interceptors.request.use( instance.interceptors.response.use( (response) => { - return response; + return response; }, (error) => { const status = error.status || (error.response ? error.response.status : 0); - if (status === 403 || status === 401) { + if (status === 401) { signOutHandler(); } return Promise.reject(error); - } - ); + }, +); export const Axios = () => instance; diff --git a/src/assets/images/brand/icon.svg b/src/assets/images/brand/icon.svg new file mode 100644 index 00000000..36297700 --- /dev/null +++ b/src/assets/images/brand/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Button/Button.module.css b/src/components/Button/Button.module.css index dd021477..a421a163 100644 --- a/src/components/Button/Button.module.css +++ b/src/components/Button/Button.module.css @@ -20,3 +20,36 @@ background-color: #545BEB; color: #FFFFFF; } + +.iconBtn { + background: #fff; + padding: 0; + width: 36px; + color: #211F1F; + border: 1px #e9ecef solid; + border-radius: rem(8px); + margin-right: 0.675rem; + + &.iconBtnActive { + color: white; + background-color: var(--mantine-color-brandPrimary-4); + } +} + + + +.iconBtnIcon { + color: var(--mantine-color-brandPrimary-6); +} + +.iconBtnLabel { + font-size: 0.8rem; +} + +.iconBtn:hover { + background-color: #E0E0E0; + &.iconBtnActive { + color: white; + background-color: var(--mantine-color-brandPrimary-4); + } +} \ No newline at end of file diff --git a/src/components/Button/IconButton.tsx b/src/components/Button/IconButton.tsx new file mode 100644 index 00000000..788c4af2 --- /dev/null +++ b/src/components/Button/IconButton.tsx @@ -0,0 +1,28 @@ +import { Button, Tooltip } from '@mantine/core'; +import type { FC, ReactNode } from 'react'; +import classes from './Button.module.css' +import React from 'react'; + +type IconButtonProps = { + onClick?: () => void; + renderIcon: () => ReactNode; + icon?: ReactNode; + active?: boolean; + tooltipLabel?: string; +} + +const IconButton: FC = (props) => { + const { renderIcon, tooltipLabel } = props; + const Wrapper = tooltipLabel ? Tooltip : React.Fragment + return ( + + + + ); +}; + +export default IconButton; diff --git a/src/components/Button/ToggleButton.tsx b/src/components/Button/ToggleButton.tsx index 9edaf360..099d017c 100644 --- a/src/components/Button/ToggleButton.tsx +++ b/src/components/Button/ToggleButton.tsx @@ -7,17 +7,19 @@ type ToggleButtonProps = ButtonProps & { toggled: boolean; renderIcon?: () => ReactNode; label?: string; + iconPosition?: 'left' | 'right'; + customClassName?: string | null; }; export const ToggleButton: FC = (props) => { - const { onClick, toggled, label = '', renderIcon } = props; + const { onClick, toggled, label = '', renderIcon, customClassName = '' } = props; const { toggleBtn, toggleBtnActive } = classes; - + const iconPosition = props.iconPosition === 'right' ? 'rightSection' : 'leftSection' return ( ); diff --git a/src/components/Header/HelpModal.tsx b/src/components/Header/HelpModal.tsx new file mode 100644 index 00000000..680f4710 --- /dev/null +++ b/src/components/Header/HelpModal.tsx @@ -0,0 +1,91 @@ +import { Box, Button, Modal, Text, Tooltip, px } from '@mantine/core'; +import { FC, useEffect } from 'react'; +import { useAbout } from '@/hooks/useGetAbout'; +import { IconBook2, IconBrandGithub, IconBrandSlack, IconBusinessplan } from '@tabler/icons-react'; +import { useHeaderContext } from '@/layouts/MainLayout/Context'; +import styles from './styles/HelpModal.module.css'; + +const helpResources = [ + { + icon: IconBusinessplan, + title: 'Production support', + description: 'Get production support', + href: 'mailto:sales@parseable.io?subject=Production%20Support%20Query', //https://www.parseable.io/pricing + }, + { + icon: IconBrandSlack, + title: 'Slack', + description: 'Join the Slack community', + href: 'https://join.slack.com/t/parseable/shared_invite/zt-23t505gz7-zX4T10OvkS8RAhnme4gDZQ', + }, + { + icon: IconBrandGithub, + title: 'GitHub', + description: 'Find resources on GitHub', + href: 'https://github.com/parseablehq/parseable', + }, + { + icon: IconBook2, + title: 'Documentation', + description: 'Refer the documentation', + href: 'https://www.parseable.com/docs', + }, +]; + +type HelpCardProps = { + data: (typeof helpResources)[number]; +}; + +const HelpCard: FC = (props) => { + const { data } = props; + + const classes = styles; + const { HelpIconBox } = classes; + + return ( + + + + ); +}; + +type HelpModalProps = { + opened: boolean; + close(): void; +}; + +const HelpModal: FC = (props) => { + const { opened, close } = props; + const { + state: { subInstanceConfig }, + } = useHeaderContext(); + + const { getAboutData } = useAbout(); + + useEffect(() => { + if (getAboutData?.data) { + subInstanceConfig.set(getAboutData?.data); + } + }, [getAboutData?.data]); + + const classes = styles; + const { container, aboutTitle, aboutDescription, helpIconContainer } = classes; + + return ( + + + Need help? + Ensure uninterrupted deployment + + {helpResources.map((data) => ( + + ))} + + + + ); +}; + +export default HelpModal; diff --git a/src/components/Header/PrimaryHeader.tsx b/src/components/Header/PrimaryHeader.tsx index 8596af3f..7b031a19 100644 --- a/src/components/Header/PrimaryHeader.tsx +++ b/src/components/Header/PrimaryHeader.tsx @@ -1,47 +1,51 @@ -import logoInvert from '@/assets/images/brand/logo-invert.svg'; -import { HOME_ROUTE } from '@/constants/routes'; -import { HEADER_HEIGHT, NAVBAR_WIDTH } from '@/constants/theme'; -import { Box, Button, Image, Tooltip } from '@mantine/core'; -import { FC } from 'react'; -import styles from './styles/Header.module.css' + import icon from '@/assets/images/brand/icon.svg'; + import { HOME_ROUTE } from '@/constants/routes'; + import { NAVBAR_WIDTH, PRIMARY_HEADER_HEIGHT } from '@/constants/theme'; + import { Button, Image, Stack } from '@mantine/core'; + import { FC } from 'react'; + import styles from './styles/Header.module.css'; + import { useHeaderContext } from '@/layouts/MainLayout/Context'; + import HelpModal from './HelpModal'; + const PrimaryHeader: FC = () => { + const classes = styles; + const { logoContainer, imageSty } = classes; + const { + state: { maximized, helpModalOpen }, + methods: { toggleHelpModal }, + } = useHeaderContext(); -const PrimaryHeader: FC = () => { - const classes = styles; - const { container, logoContainer, navContainer, imageSty, actionBtn } = classes; + if (maximized) return null; - return ( - - - - Parseable Logo - - - - - - - - - - - ); -}; + return ( + + + + + Parseable Logo + + + + + + + + + ); + }; -export default PrimaryHeader; + export default PrimaryHeader; diff --git a/src/components/Header/RefreshInterval.tsx b/src/components/Header/RefreshInterval.tsx index b9f1f140..0e2be3fa 100644 --- a/src/components/Header/RefreshInterval.tsx +++ b/src/components/Header/RefreshInterval.tsx @@ -1,6 +1,6 @@ import useMountedState from '@/hooks/useMountedState'; import { useHeaderContext } from '@/layouts/MainLayout/Context'; -import { Button, Menu, Text, px } from '@mantine/core'; +import { Button, Menu, Text, Tooltip, px } from '@mantine/core'; import { IconRefresh, IconRefreshOff } from '@tabler/icons-react'; import ms from 'ms'; import type { FC } from 'react'; @@ -36,9 +36,11 @@ const RefreshInterval: FC = () => { return ( - + + + void; -} +}; const RefreshNow: FC = (props) => { const { refreshNowBtn } = classes; return ( - + + + ); }; diff --git a/src/components/Header/StreamDropdown.tsx b/src/components/Header/StreamDropdown.tsx new file mode 100644 index 00000000..6fee99b7 --- /dev/null +++ b/src/components/Header/StreamDropdown.tsx @@ -0,0 +1,42 @@ +import { Select } from '@mantine/core'; +import { useCallback, useEffect, useRef } from 'react'; +import classes from './styles/LogQuery.module.css'; +import { useHeaderContext } from '@/layouts/MainLayout/Context'; +import { useNavigate, useParams } from 'react-router-dom'; + +const StreamDropdown = () => { + const { + state: { userSpecficStreams, subLogQuery }, + } = useHeaderContext(); + const { streamName } = useParams(); + + const selectedStream = subLogQuery.get().streamName; + const valueRef = useRef(selectedStream); + const navigate = useNavigate(); + + const handleChange: (value: string | null) => void = useCallback((value: string | null) => { + if (value === null) return; + + valueRef.current = value; + navigate(`/${value}/logs`); + }, []); + + useEffect(() => { + valueRef.current = streamName || null; + }, [streamName]) + + return ( + { - const targetValue = value === null ? selectedStream : value; - handleChange(targetValue || ''); - }} - nothingFoundMessage="No options" - value={selectedStream} - data={userSepecficStreams?.map((stream: any) => ({ value: stream.name, label: stream.name })) ?? []} - searchable - required - className={styles.selectStreambtn} - classNames={{ option: styles.option }} - /> + + {navItems.map((navItem, index) => { + if (navItem.route === USERS_MANAGEMENT_ROUTE && !userSpecificAccessMap.hasUserAccess) return null; - {getLogStreamListIsError &&
{getLogStreamListIsError}
} - {getLogStreamListIsError && ( - } - component="button" - onClick={() => getLogStreamListRefetch()} - style={{ paddingLeft: 0 }} - /> - )} - {links.map((link) => { - if ( - (link.requiredAccess && - !userSepecficAccess?.some((access: string) => link.requiredAccess.includes(access))) || - selectedStream === '' - ) { - return null; - } + const isActiveItem = navItem.route === currentRoute; return ( - } - disabled={disableLink} - onClick={() => { - handleChange(selectedStream, link.pathname); - }} - style={{ paddingLeft: 20, paddingRight: 20 }} - key={link.label} - className={(currentPage === link.pathname && styles.linkBtnActive) || styles.linkBtn} - /> + navigateToPage(navItem.route)} + key={index}> + + + + ); })} - {!userSepecficAccess?.some((access: string) => ['DeleteStream'].includes(access)) || - selectedStream === '' ? null : ( - } - onClick={openDelete} - style={{ paddingLeft: 20, paddingRight: 20 }} - disabled={disableLink} - /> - )} - {!userSepecficAccess?.some((access: string) => ['ListUser'].includes(access)) ? null : ( - } - onClick={() => { - navigate('/users'); - setCurrentPage(USERS_MANAGEMENT_ROUTE); - }} - /> - )}
- - } - className={styles.userBtn} - component="a" - /> - } - className={styles.actionBtn} - component="a" - onClick={open} - /> - } - className={styles.actionBtn} - component="a" - onClick={signOutHandler} - /> - - - { - setDeleteStream(e.target.value); - }} - placeholder={`Type the name of the stream to confirm. i.e. ${selectedStream}`} - required - /> - - - - - - - + + {navActions.map((navAction, index) => { + const isActiveItem = false; + const onClick = + navAction.key === 'about' + ? toggleInfoModal + : navAction.key === 'user' + ? toggleUserModal + : navAction.key === 'logout' + ? signOutHandler + : () => {}; + return ( + + + + + + ); + })} + + + diff --git a/src/components/Navbar/infoModal.tsx b/src/components/Navbar/infoModal.tsx index 0659c361..11caba53 100644 --- a/src/components/Navbar/infoModal.tsx +++ b/src/components/Navbar/infoModal.tsx @@ -1,56 +1,10 @@ -import { Box, Button, Modal, Text, Tooltip, px } from '@mantine/core'; +import { Box, Button, Modal, Text, px } from '@mantine/core'; import { FC, useEffect, useMemo } from 'react'; import { useAbout } from '@/hooks/useGetAbout'; -import { IconAlertCircle, IconBook2, IconBrandGithub, IconBrandSlack, IconBusinessplan } from '@tabler/icons-react'; +import { IconAlertCircle } from '@tabler/icons-react'; import { useHeaderContext } from '@/layouts/MainLayout/Context'; import styles from './styles/InfoModal.module.css' -const helpResources = [ - { - icon: IconBusinessplan, - title: 'Production support', - description: 'Get production support', - href: 'mailto:sales@parseable.io?subject=Production%20Support%20Query', //https://www.parseable.io/pricing - }, - { - icon: IconBrandSlack, - title: 'Slack', - description: 'Join the Slack community', - href: 'https://join.slack.com/t/parseable/shared_invite/zt-23t505gz7-zX4T10OvkS8RAhnme4gDZQ', - }, - { - icon: IconBrandGithub, - title: 'GitHub', - description: 'Find resources on GitHub', - href: 'https://github.com/parseablehq/parseable', - }, - { - icon: IconBook2, - title: 'Documentation', - description: 'Refer the documentation', - href: 'https://www.parseable.com/docs', - }, -]; - -type HelpCardProps = { - data: (typeof helpResources)[number]; -}; - -const HelpCard: FC = (props) => { - const { data } = props; - - const classes = styles; - const { HelpIconBox } = classes; - - return ( - - - - ); -}; - type InfoModalProps = { opened: boolean; close(): void; @@ -84,7 +38,6 @@ const InfoModal: FC = (props) => { aboutTitle, aboutDescription, actionBtn, - helpIconContainer, aboutTextBox, aboutTextKey, aboutTextValue, @@ -172,15 +125,6 @@ const InfoModal: FC = (props) => { ) : null} - - Need help? - Ensure uninterrupted deployment - - - {helpResources.map((data) => ( - - ))} - ); diff --git a/src/components/Navbar/styles/Navbar.module.css b/src/components/Navbar/styles/Navbar.module.css index 12f031b4..fa3df0af 100644 --- a/src/components/Navbar/styles/Navbar.module.css +++ b/src/components/Navbar/styles/Navbar.module.css @@ -8,23 +8,9 @@ font-size: 16px; font-weight: 400; line-height: normal; - - & .mantine-Select-group { - padding: 23rem !important; - } + position: relative; } -.streamsBtn { - cursor: default; - color: #4d4d4d; - padding-left: 0; - padding-right: 0; - &:hover { - background-color: white; - } -} - - .userManagementBtn { color: #4d4d4d; padding-left: 0; @@ -100,12 +86,12 @@ .navbar { height: calc(100vh - 50px); - padding: var(--mantine-spacing-md) var(--mantine-spacing-md); display: flex; flex-direction: column; border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); transition: width 0.4s ease-in-out; flex: '0 0 auto'; + background-color: var(--mantine-color-gray-0); } .navbarMain { @@ -126,6 +112,39 @@ } .option[aria-selected="true"] { - background-color: var(--mantine-color-brandPrimary-4); + background-color: var(--mantine-color-brandPrimary-6); color: white; } + +.navItemContainer { + width: 100%; + align-items: center; + padding: 0.8rem 0; + cursor: pointer; + color: var(--mantine-color-gray-6); +} + +.navItemContainer:last-child { + border: none; +} + +.navItemLabel { + font-size: 0.75rem; +} + +.navItemActive { + /* color: #ff0000; */ + border-color: white; + color: var(--mantine-color-gray-8); +} + +.lowerContainer { + color: var(--mantine-color-gray-5); + align-items: center; +} + +.maximizeToggleContainer { + top: 4%; + right: -14%; + cursor: pointer; +} \ No newline at end of file diff --git a/src/constants/routes.ts b/src/constants/routes.ts index e9ac2b98..e23b1e37 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -7,3 +7,15 @@ export const STATS_ROUTE = '/:streamName/stats'; export const CONFIG_ROUTE = '/:streamName/config'; export const USERS_MANAGEMENT_ROUTE = '/users'; export const OIDC_NOT_CONFIGURED_ROUTE = '/oidc-not-configured'; + +export const PATHS = { + all: '/*', + home: '/', + logs: '/:streamName/logs', + login: '/login', + liveTail: '/:streamName/live-tail', + stats: '/:streamName/stats', + config: '/:streamName/config', + users: '/users', + oidcNotConfigured: '/oidc-not-configured' +} as {[key: string]: string} \ No newline at end of file diff --git a/src/constants/theme.ts b/src/constants/theme.ts index 9276131b..0546b4ab 100644 --- a/src/constants/theme.ts +++ b/src/constants/theme.ts @@ -1,3 +1,6 @@ export const HEADER_HEIGHT = 50; -export const NAVBAR_WIDTH = 200; +export const PRIMARY_HEADER_HEIGHT = 52; +export const NAVBAR_WIDTH = 60; export const APP_MIN_WIDTH = 650; +export const LOGS_PRIMARY_TOOLBAR_HEIGHT = 80; +export const LOGS_SECONDARY_TOOLBAR_HEIGHT = 68; diff --git a/src/constants/timeConstants.ts b/src/constants/timeConstants.ts index 398c0604..e8cf74f0 100644 --- a/src/constants/timeConstants.ts +++ b/src/constants/timeConstants.ts @@ -3,6 +3,7 @@ import dayjs from 'dayjs'; type FixedDuration = { name: string; milliseconds: number; + label: string; }; export const REFRESH_INTERVALS: number[] = [10000, 30000, 60000, 300000, 600000, 1200000]; @@ -11,25 +12,34 @@ export const FIXED_DURATIONS: ReadonlyArray = [ { name: 'last 10 minutes', milliseconds: dayjs.duration({ minutes: 10 }).asMilliseconds(), + label: '10M' }, { name: 'last 1 hour', milliseconds: dayjs.duration({ hours: 1 }).asMilliseconds(), + label: '1H' }, { name: 'last 5 hours', milliseconds: dayjs.duration({ hours: 5 }).asMilliseconds(), + label: '5H' }, { name: 'last 24 hours', milliseconds: dayjs.duration({ days: 1 }).asMilliseconds(), + label: '1D' }, { name: 'last 3 days', milliseconds: dayjs.duration({ days: 3 }).asMilliseconds(), - }, - { - name: 'last 7 days', - milliseconds: dayjs.duration({ days: 7 }).asMilliseconds(), + label: '3D' }, ] as const; + +export const FIXED_DURATIONS_LABEL: { [key: string]: string } = { + 'last 10 minutes': '10M', + 'last 1 hour': '1H', + 'last 5 hours': '5H', + 'last 24 hours': '1D', + 'last 3 days': '3D', +} as const; \ No newline at end of file diff --git a/src/hooks/useCurrentRoute.ts b/src/hooks/useCurrentRoute.ts new file mode 100644 index 00000000..bc2bfe8d --- /dev/null +++ b/src/hooks/useCurrentRoute.ts @@ -0,0 +1,20 @@ +import { PATHS } from "@/constants/routes" +import { matchRoutes, useLocation } from "react-router-dom" + +const routes = Object.keys(PATHS).map((key: string) => { + const value = PATHS[key]; + return {path: value}; +}); + +const useCurrentRoute = () => { + const location = useLocation(); + const match = matchRoutes(routes, location); + + if (!match) { + return ''; + } else { + return match[0].route.path; + } +}; + +export default useCurrentRoute; \ No newline at end of file diff --git a/src/hooks/useQueryLogs.ts b/src/hooks/useQueryLogs.ts index 29cea13d..0ea6500c 100644 --- a/src/hooks/useQueryLogs.ts +++ b/src/hooks/useQueryLogs.ts @@ -3,7 +3,7 @@ import { getQueryLogs, getQueryResult } from '@/api/query'; import { StatusCodes } from 'http-status-codes'; import useMountedState from './useMountedState'; import { useCallback, useEffect, useMemo, useRef, useTransition } from 'react'; -import { LOG_QUERY_LIMITS, useLogsPageContext } from '@/pages/Logs/Context'; +import { LOG_QUERY_LIMITS, useLogsPageContext } from '@/pages/Logs/logsContextProvider'; import { parseLogData } from '@/utils'; type QueryLogs = { diff --git a/src/hooks/useUser.tsx b/src/hooks/useUser.tsx index 28ec8e41..296a5b91 100644 --- a/src/hooks/useUser.tsx +++ b/src/hooks/useUser.tsx @@ -14,14 +14,20 @@ export const useUser = () => { isLoading: createUserIsLoading, data: createUserData, reset: createUserReset, - } = useMutation((data: { userName: string; roles: object[] }) => postUser(data.userName, data.roles), { - onError: (data: AxiosError) => { - if (isAxiosError(data) && data.response) { - const error = data.response.data as string; - setCreateUserError(error); - } + } = useMutation( + (data: { userName: string; roles: object[]; onSuccess?: () => void }) => postUser(data.userName, data.roles), + { + onError: (data: AxiosError) => { + if (isAxiosError(data) && data.response) { + const error = data.response.data as string; + setCreateUserError(error); + } + }, + onSuccess: (_data, variables) => { + variables.onSuccess && variables.onSuccess(); + }, }, - }); + ); const { mutate: deleteUserMutation, @@ -54,16 +60,6 @@ export const useUser = () => { }, }); - const { - data: getUserData, - isError: getUserIsError, - isSuccess: getUserIsSuccess, - isLoading: getUserIsLoading, - refetch: getUserRefetch, - } = useQuery(['fetch-user', createUserIsSuccess, deleteUserIsSuccess], () => getUsers(), { - retry: false, - }); - const { data: getUserRolesData, isError: getUserRolesIsError, @@ -81,11 +77,6 @@ export const useUser = () => { createUserIsLoading, createUserData, createUserReset, - getUserRefetch, - getUserData, - getUserIsError, - getUserIsSuccess, - getUserIsLoading, deleteUserMutation, deleteUserIsSuccess, deleteUserIsError, @@ -110,3 +101,22 @@ export const useUser = () => { createUserError, }; }; + +export const useGetUser = () => { + const { + data: getUserData, + isError: getUserIsError, + isSuccess: getUserIsSuccess, + isLoading: getUserIsLoading, + refetch: getUserRefetch, + } = useQuery(['fetch-user'], () => getUsers(), { + retry: false, + }); + return { + getUserRefetch, + getUserData, + getUserIsError, + getUserIsSuccess, + getUserIsLoading, + }; +}; diff --git a/src/layouts/MainLayout/Context.tsx b/src/layouts/MainLayout/Context.tsx index 584d7d25..c2d48fbb 100644 --- a/src/layouts/MainLayout/Context.tsx +++ b/src/layouts/MainLayout/Context.tsx @@ -4,9 +4,10 @@ import { LogStreamData } from '@/@types/parseable/api/stream'; import { getStreamsSepcificAccess } from '@/components/Navbar/rolesHandler'; import { FIXED_DURATIONS } from '@/constants/timeConstants'; import useSubscribeState, { SubData } from '@/hooks/useSubscribeState'; +import { useDisclosure } from '@mantine/hooks'; import dayjs from 'dayjs'; -import type { FC } from 'react'; -import { ReactNode, createContext, useCallback, useContext } from 'react'; +import type { Dispatch, FC, SetStateAction } from 'react'; +import { ReactNode, createContext, useCallback, useContext, useEffect, useState } from 'react'; const Context = createContext({}); @@ -33,6 +34,10 @@ interface HeaderContextState { subCreateUserModalTogle: SubData; subInstanceConfig: SubData; subAppContext: SubData; + maximized: boolean; + userSpecficStreams: LogStreamData | null; + userSpecificAccessMap: { [key: string]: boolean }; + helpModalOpen: boolean; } export type UserRoles = { @@ -61,6 +66,10 @@ interface HeaderContextMethods { streamChangeCleanup: (streamName: string) => void; setUserRoles: (userRoles: UserRoles) => void; setSelectedStream: (stream: string) => void; + setUserSpecficStreams: Dispatch>; + toggleMaximize: () => void; + updateUserSpecificAccess: (accessRoles: string[] | null) => void; + toggleHelpModal: () => void; } interface HeaderContextValue { @@ -72,6 +81,23 @@ interface HeaderProviderProps { children: ReactNode; } +const accessKeyMap: { [key: string]: string } = { + hasUserAccess: 'ListUser', + hasDeleteAccess: 'DeleteStream', + hasUpdateAlertAccess: 'PutAlert', + hasGetAlertAccess: 'GetAlert', +}; + +const generateUserAcccessMap = (accessRoles: string[] | null) => { + return Object.keys(accessKeyMap).reduce((acc, accessKey: string) => { + return { + ...acc, + [accessKey]: + accessRoles !== null && accessKeyMap.hasOwnProperty(accessKey) && accessRoles.includes(accessKeyMap[accessKey]), + }; + }, {}); +}; + const MainLayoutPageProvider: FC = ({ children }) => { const subAppContext = useSubscribeState({ selectedStream: '', @@ -108,6 +134,10 @@ const MainLayoutPageProvider: FC = ({ children }) => { const subNavbarTogle = useSubscribeState(false); const subCreateUserModalTogle = useSubscribeState(false); const subInstanceConfig = useSubscribeState(null); + const [maximized, { toggle: toggleMaximize }] = useDisclosure(false); + const [helpModalOpen, { toggle: toggleHelpModal }] = useDisclosure(false); + const [userSpecficStreams, setUserSpecficStreams] = useState(null); + const [userSpecificAccessMap, setUserSpecificMap] = useState<{ [key: string]: boolean }>({}); const state: HeaderContextState = { subLogQuery, @@ -119,8 +149,26 @@ const MainLayoutPageProvider: FC = ({ children }) => { subInstanceConfig, subAppContext, subLiveTailsData, + maximized, + userSpecficStreams, + userSpecificAccessMap, + helpModalOpen, }; + useEffect(() => { + const handleEscKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + maximized && toggleMaximize(); + } + }; + + window.addEventListener('keydown', handleEscKeyPress); + + return () => { + window.removeEventListener('keydown', handleEscKeyPress); + }; + }, [maximized]); + const resetTimeInterval = useCallback(() => { if (subLogSelectedTimeRange.get().state === 'fixed') { const now = dayjs(); @@ -166,7 +214,20 @@ const MainLayoutPageProvider: FC = ({ children }) => { }); }, []); - const methods: HeaderContextMethods = { resetTimeInterval, streamChangeCleanup, setUserRoles, setSelectedStream }; + const updateUserSpecificAccess = useCallback((accessRoles: string[] | null) => { + setUserSpecificMap(generateUserAcccessMap(accessRoles)); + }, []); + + const methods: HeaderContextMethods = { + resetTimeInterval, + streamChangeCleanup, + setUserRoles, + setSelectedStream, + toggleMaximize, + setUserSpecficStreams, + updateUserSpecificAccess, + toggleHelpModal, + }; return {children}; }; diff --git a/src/layouts/MainLayout/index.tsx b/src/layouts/MainLayout/index.tsx index 317f9f71..52596ffb 100644 --- a/src/layouts/MainLayout/index.tsx +++ b/src/layouts/MainLayout/index.tsx @@ -1,22 +1,29 @@ import { PrimaryHeader } from '@/components/Header'; import Navbar from '@/components/Navbar'; -import { HEADER_HEIGHT, NAVBAR_WIDTH } from '@/constants/theme'; +import { NAVBAR_WIDTH, PRIMARY_HEADER_HEIGHT } from '@/constants/theme'; import { Box } from '@mantine/core'; import type { FC } from 'react'; import { Outlet } from 'react-router-dom'; import { heights } from '@/components/Mantine/sizing'; +import { useHeaderContext } from './Context'; const MainLayout: FC = () => { + const { + state: { maximized }, + } = useHeaderContext(); + const primaryHeaderHeight = !maximized ? PRIMARY_HEADER_HEIGHT : 0; + const navbarWidth = !maximized ? NAVBAR_WIDTH : 0; return ( - + diff --git a/src/main.tsx b/src/main.tsx index 7bd65527..dc1193ab 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,7 @@ import '@mantine/notifications/styles.css'; import '@mantine/notifications/styles.css'; import '@mantine/dates/styles.css'; import '@mantine/code-highlight/styles.css'; +import '@mantine/charts/styles.css'; import './utils/dayjsLoader'; import React from 'react'; import ReactDOM from 'react-dom/client'; diff --git a/src/pages/AccessManagement/Roles.tsx b/src/pages/AccessManagement/Roles.tsx index 2733282d..b4bce033 100644 --- a/src/pages/AccessManagement/Roles.tsx +++ b/src/pages/AccessManagement/Roles.tsx @@ -4,9 +4,16 @@ import { FC, useEffect, useState } from 'react'; import { useGetLogStreamList } from '@/hooks/useGetLogStreamList'; import { useHeaderContext } from '@/layouts/MainLayout/Context'; import PrivilegeTR from './PrivilegeTR'; -import { IconPencil, IconUserPlus } from '@tabler/icons-react'; +import { IconBook2, IconPencil, IconUserPlus } from '@tabler/icons-react'; import { useRole } from '@/hooks/useRole'; import styles from './styles/AccessManagement.module.css' +import IconButton from '@/components/Button/IconButton'; + +const navigateToDocs = () => { + return window.open('https://www.parseable.io/docs/rbac', '_blank'); +} + +const renderDocsIcon = () => const Roles: FC = () => { useDocumentTitle('Parseable | Users'); @@ -178,37 +185,32 @@ const Roles: FC = () => { const classes = styles; return ( - - + + Roles - + - {oidcActive ? ( + {oidcActive && ( - ) : ( - '' )} - - + + + @@ -259,7 +261,13 @@ const Roles: FC = () => { - + { + return window.open('https://www.parseable.io/docs/rbac', '_blank'); +} + +const renderDocsIcon = () => const Users: FC = () => { useDocumentTitle('Parseable | Users'); @@ -32,9 +39,6 @@ const Users: FC = () => { const [roleSearchValue, setRoleSearchValue] = useState(''); const { - getUserData, - getUserIsSuccess, - getUserIsLoading, createUserMutation, createUserIsError, createUserIsLoading, @@ -51,6 +55,8 @@ const Users: FC = () => { createUserReset, } = useUser(); + const { getUserData, getUserIsSuccess, getUserIsLoading, getUserRefetch } = useGetUser(); + const { getRolesData } = useRole(); const rows = @@ -94,7 +100,7 @@ const Users: FC = () => { if (SelectedRole !== '') { userRole.push(SelectedRole); } - createUserMutation({ userName: createUserInput, roles: userRole }); + createUserMutation({ userName: createUserInput, roles: userRole, onSuccess: getUserRefetch }); }; const createVaildtion = () => { @@ -111,23 +117,23 @@ const Users: FC = () => { return false; }; + + return ( - - - + + + Users - - + + + + +
@@ -141,7 +147,13 @@ const Users: FC = () => { {rows}
- + { ) : createUserData?.data ? ( Password - { - useDocumentTitle('Parseable | Config'); - - const { - state: { subLogQuery }, - } = useHeaderContext(); - - const [streamName, setStreamName] = useMountedState(subLogQuery.get().streamName ?? ''); - - useEffect(() => { - const subQuery = subLogQuery.subscribe((value: any) => { - setStreamName(value.streamName); - }); - - return () => { - subQuery(); - }; - }, [subLogQuery]); - - const { handleCacheToggle, isCacheEnabled } = useCacheToggle(streamName); - - const { handleAlertQueryChange, submitAlertQuery, getLogAlertData } = useAlertsEditor(streamName); - - const { handleRetentionQueryChange, submitRetentionQuery, getLogRetentionData } = useRetentionEditor(streamName); - - // const { classes } = useConfigStyles(); - const classes = configStyles; - const { container, submitBtn, accordionSt, innerContainer, containerWrapper, trackStyle } = classes; - - const switchStyles = { - track: isCacheEnabled ? trackStyle : {}, - }; - - return ( - - - - - - - Alert - - - - - - - - - - - - {!subLogQuery.get().access?.some((access: string) => ['PutRetention'].includes(access)) ? null : ( - - - - Retention - - - - - - - - - - - - )} - - - ); -}; - -export default Config; diff --git a/src/pages/Config/styles/Config.module.css b/src/pages/Config/styles/Config.module.css deleted file mode 100644 index fa6bc5d6..00000000 --- a/src/pages/Config/styles/Config.module.css +++ /dev/null @@ -1,46 +0,0 @@ -.container { - display: flex; - flex-direction: column; - flex: 1; - margin: 1.25rem; - gap: 1.25rem; -} - -.trackStyle { - background-color: #545BEB; -} - -.containerWrapper { - display: flex; - gap: 20px; -} - -.primaryBtn { - margin-top: 1rem; - background-color: #545BEB; - color: #fff; - width: max-content; -} - -.submitBtn { - margin-top: 1rem; - background-color: #545BEB; - color: #fff; -} - -.accordionSt { - border-radius: 60px; - border: none; -} - -.accordionSt { - .mantine-Accordion-item { - border: 10px #f8f9fa solid; - } -} - -.innerContainer { - width: 50%; - justify-content: center; - display: flex; -} diff --git a/src/pages/LiveTail/LogTable.tsx b/src/pages/LiveTail/LogTable.tsx index fb4c00b9..4a5084c2 100644 --- a/src/pages/LiveTail/LogTable.tsx +++ b/src/pages/LiveTail/LogTable.tsx @@ -7,12 +7,13 @@ import Column from './Column'; import { useHeaderContext } from '@/layouts/MainLayout/Context'; import { useDoGetLiveTail } from '@/hooks/useDoGetLiveTail'; import EmptyBox from '@/components/Empty'; -import styles from './styles/Logs.module.css' +import styles from './styles/Logs.module.css'; +import { LOGS_PRIMARY_TOOLBAR_HEIGHT, LOGS_SECONDARY_TOOLBAR_HEIGHT, PRIMARY_HEADER_HEIGHT } from '@/constants/theme'; const LogTable: FC = () => { const { finalData: data, doGetLiveTail, resetData, abort, loading, schema } = useDoGetLiveTail(); const { - state: { subInstanceConfig, subLogQuery, subLiveTailsData }, + state: { subInstanceConfig, subLogQuery, subLiveTailsData, maximized }, } = useHeaderContext(); const [grpcPort, setGrpcPort] = useMountedState(subInstanceConfig.get()?.grpcPort ?? null); @@ -88,10 +89,20 @@ const LogTable: FC = () => { const { container, tableStyle, theadStyle, tableContainer, innerContainer } = classes; + const primaryHeaderHeight = !maximized + ? PRIMARY_HEADER_HEIGHT + LOGS_PRIMARY_TOOLBAR_HEIGHT + LOGS_SECONDARY_TOOLBAR_HEIGHT + : 0; + return ( - - - + + + {data.length > 0 ? ( ({ diff --git a/src/pages/LiveTail/styles/Logs.module.css b/src/pages/LiveTail/styles/Logs.module.css index 89639950..bfb4d64d 100644 --- a/src/pages/LiveTail/styles/Logs.module.css +++ b/src/pages/LiveTail/styles/Logs.module.css @@ -161,6 +161,7 @@ flex-direction: row; justify-content: space-between; border-top: 0.0625rem solid rgba(0, 0, 0, 0.1); + align-items: center; } .errorContainer { diff --git a/src/pages/Logs/AlertsModal.tsx b/src/pages/Logs/AlertsModal.tsx new file mode 100644 index 00000000..be9b1f5e --- /dev/null +++ b/src/pages/Logs/AlertsModal.tsx @@ -0,0 +1,58 @@ +import { Box, Button, Modal, Stack } from '@mantine/core'; +import { useLogsPageContext } from './logsContextProvider'; +import { Text } from '@mantine/core'; +import classes from './styles/Logs.module.css'; +import { Editor } from '@monaco-editor/react'; + +const ModalTitle = () => { + return Alerts; +}; + +type AlertsModalProps = { + data: any; + handleChange: (value: string | undefined) => void; + handleSubmit: () => void; +}; + +const AlertsModal = (props: AlertsModalProps) => { + const { + state: { alertsModalOpen }, + methods: { closeAlertsModal }, + } = useLogsPageContext(); + + return ( + }> + + + + + + + + + + ); +}; + +export default AlertsModal; diff --git a/src/pages/Logs/CarouselSlide.tsx b/src/pages/Logs/CarouselSlide.tsx index b411b789..0f229c73 100644 --- a/src/pages/Logs/CarouselSlide.tsx +++ b/src/pages/Logs/CarouselSlide.tsx @@ -3,7 +3,7 @@ import { useHeaderContext } from '@/layouts/MainLayout/Context'; import { Box, Button, Modal, Text, Tooltip } from '@mantine/core'; import dayjs from 'dayjs'; import { useEffect } from 'react'; -import { useLogsPageContext } from './Context'; +import { useLogsPageContext } from './logsContextProvider'; import useMountedState from '@/hooks/useMountedState'; import { Carousel } from '@mantine/carousel'; import carouselStyles from './styles/CarouselSlide.module.css'; diff --git a/src/pages/Logs/Context.tsx b/src/pages/Logs/Context.tsx deleted file mode 100644 index d120882a..00000000 --- a/src/pages/Logs/Context.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import type { Log } from '@/@types/parseable/api/query'; -import useSubscribeState, { SubData } from '@/hooks/useSubscribeState'; -import type { Dispatch, FC, SetStateAction } from 'react'; -import { ReactNode, createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { LogStreamSchemaData } from '@/@types/parseable/api/stream'; -import { sanitizeCSVData } from '@/utils/exportHelpers'; - -const Context = createContext({}); - -const { Provider } = Context; - -export const LOG_QUERY_LIMITS = [30, 50, 100, 150, 200]; -export const LOAD_LIMIT = 9000; - -type GapTime = { - startTime: Date; - endTime: Date; - id: number | null; -}; -interface LogsPageContextState { - subLogStreamError: SubData; - subViewLog: SubData; - subGapTime: SubData; - subLogQueryData: SubData; - subLogStreamSchema: SubData; - subSchemaToggle: SubData; - pageOffset: number; - custQuerySearchState: CustQuerySearchState; -} - -type LogQueryData = { - rawData: Log[]; - filteredData: Log[]; -}; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface LogsPageContextMethods { - makeExportData: (type: string) => Log[]; - toggleShowQueryEditor: () => void; - resetQuerySearch: () => void; - setPageOffset: Dispatch>; - setCustSearchQuery: (query: string, mode: custQuerySearchMode) => void; -} - -interface LogsPageContextValue { - state: LogsPageContextState; - methods: LogsPageContextMethods; -} - -interface LogsPageProviderProps { - children: ReactNode; -} - -type custQuerySearchMode = null | 'sql' | 'filters' - -type CustQuerySearchState = { - showQueryEditor: boolean; - isQuerySearchActive: boolean; - custSearchQuery: string; - mode: custQuerySearchMode; -}; - -export const defaultQueryResult = ''; - -const defaultCustQuerySearchState = { showQueryEditor: false, isQuerySearchActive: false, custSearchQuery: '', mode: null }; - -const LogsPageProvider: FC = ({ children }) => { - const subLogStreamError = useSubscribeState(null); - const subViewLog = useSubscribeState(null); - const subGapTime = useSubscribeState(null); - const subLogQueryData = useSubscribeState({ - rawData: [], - filteredData: [], - }); - const subLogStreamSchema = useSubscribeState(null); - const subSchemaToggle = useSubscribeState(false); - const [pageOffset, setPageOffset] = useState(0); - const [custQuerySearchState, setCustQuerySearchState] = useState(defaultCustQuerySearchState); - - // state - const state: LogsPageContextState = { - subLogStreamError, - subViewLog, - subGapTime, - subLogQueryData, - subLogStreamSchema, - subSchemaToggle, - custQuerySearchState, - pageOffset, - }; - - // getters & setters - const toggleShowQueryEditor = useCallback(() => { - setCustQuerySearchState((prev) => ({ ...prev, showQueryEditor: !prev.showQueryEditor })); - }, []); - - const resetQuerySearch = useCallback(() => { - setCustQuerySearchState(defaultCustQuerySearchState); - // setPageOffset(0); wont the LogTable handle this ? - }, []); - - const setCustSearchQuery = useCallback((query: string, mode: custQuerySearchMode) => { - setCustQuerySearchState((prev) => ({ ...prev, mode, custSearchQuery: query, isQuerySearchActive: true, showQueryEditor: false})); - }, []) - - // handlers - const makeExportData = useCallback((type: string): Log[] => { - const { rawData, filteredData: _filteredData } = subLogQueryData.get(); // filteredData - records filtered with in-page search - if (type === 'JSON') { - return rawData; - } else if (type === 'CSV') { - const fields = subLogStreamSchema.get()?.fields; - const headers = !custQuerySearchState.isQuerySearchActive - ? Array.isArray(fields) - ? fields.map((field) => field.name) - : [] - : typeof rawData[0] === 'object' - ? Object.keys(rawData[0]) - : []; - - const sanitizedCSVData = sanitizeCSVData(rawData, headers); - return [headers, ...sanitizedCSVData]; - } else { - return []; - } - }, [custQuerySearchState.isQuerySearchActive]); - - const methods = { - makeExportData, - toggleShowQueryEditor, - resetQuerySearch, - setPageOffset, - setCustSearchQuery, - }; - - const value = useMemo(() => ({ state, methods }), [state, methods]); - - return {children}; -}; - -export const useLogsPageContext = () => useContext(Context) as LogsPageContextValue; - -export default LogsPageProvider; diff --git a/src/pages/Logs/DeleteStreamModal.tsx b/src/pages/Logs/DeleteStreamModal.tsx new file mode 100644 index 00000000..3974d008 --- /dev/null +++ b/src/pages/Logs/DeleteStreamModal.tsx @@ -0,0 +1,56 @@ +import { Button, Group, Modal, TextInput } from '@mantine/core'; +import { useLogsPageContext } from './logsContextProvider'; +import styles from './styles/Logs.module.css'; +import { useCallback, useState } from 'react'; +import { useLogStream } from '@/hooks/useLogStream'; + +const DeleteStreamModal = () => { + const { + state: { deleteModalOpen, currentStream }, + methods: { closeDeleteModal }, + } = useLogsPageContext(); + const [confirmInputValue, setConfirmInputValue] = useState(''); + const handleInputChange = useCallback((e: React.ChangeEvent) => { + setConfirmInputValue(e.target.value); + }, []); + + const { deleteLogStreamMutation } = useLogStream(); + + const handleDeleteStream = useCallback(() => { + deleteLogStreamMutation({ deleteStream: currentStream }); + }, [currentStream]); + + return ( + + + + + + + + ); +}; + +export default DeleteStreamModal; diff --git a/src/pages/Logs/EventTimeLineGraph.tsx b/src/pages/Logs/EventTimeLineGraph.tsx new file mode 100644 index 00000000..7b487117 --- /dev/null +++ b/src/pages/Logs/EventTimeLineGraph.tsx @@ -0,0 +1,81 @@ +import { Skeleton, Stack, Text } from '@mantine/core'; +import classes from './styles/EventTimeLineGraph.module.css'; +import { useQueryResult } from '@/hooks/useQueryResult'; +import { useEffect } from 'react'; +import { useLogsPageContext } from './logsContextProvider'; +import dayjs from 'dayjs'; +import { AreaChart } from '@mantine/charts'; +import { HumanizeNumber } from '@/utils/formatBytes'; + +const generateCountQuery = (streamName: string, startTime: string, endTime: string) => { + return `SELECT DATE_TRUNC('minute', p_timestamp) AS minute_range, COUNT(*) AS log_count FROM ${streamName} WHERE p_timestamp BETWEEN '${startTime}' AND '${endTime}' GROUP BY minute_range ORDER BY minute_range`; +}; + +const NoDataView = () => { + return ( + + + No new events in the last 10 minutes. + + + ); +}; + +const EventTimeLineGraph = () => { + const { fetchQueryMutation } = useQueryResult(); + const { + state: { currentStream }, + } = useLogsPageContext(); + const endTime = dayjs().subtract(1, 'minute').startOf('minute'); + const startTime = endTime.subtract(10, 'minute').startOf('minute'); + + useEffect(() => { + if (!currentStream || currentStream.length === 0) return; + + const logsQuery = { + streamName: currentStream, + startTime: startTime.toDate(), + endTime: endTime.toDate(), + access: [], + }; + const query = generateCountQuery(currentStream, startTime.toISOString(), endTime.toISOString()); + fetchQueryMutation.mutate({ + logsQuery, + query, + }); + }, [currentStream]); + + const graphData = fetchQueryMutation?.data; + const isLoading = fetchQueryMutation.isLoading; + const hasData = Array.isArray(graphData) && graphData.length !== 0; + + return ( + + + {hasData ? ( + new Intl.NumberFormat('en-US').format(value)} + withXAxis={false} + withYAxis={hasData} + curveType="linear" + yAxisProps={{ tickCount: 2, tickFormatter: (value) => `${HumanizeNumber(value)}` }} + gridAxis="xy" + withGradient={false} + /> + ) : ( + + )} + + + ); +}; + +export default EventTimeLineGraph; diff --git a/src/components/Header/Querier.tsx b/src/pages/Logs/FilterQueryBuilder.tsx similarity index 75% rename from src/components/Header/Querier.tsx rename to src/pages/Logs/FilterQueryBuilder.tsx index e44ba5a1..7e48573e 100644 --- a/src/components/Header/Querier.tsx +++ b/src/pages/Logs/FilterQueryBuilder.tsx @@ -1,25 +1,34 @@ import { - Stack, + Button, Group, ScrollArea, + Stack, Box, ThemeIcon, - Text, Select, Input, - Button, CloseIcon, Pill, ActionIcon, - Modal, } from '@mantine/core'; -import { useCallback } from 'react'; -import classes from './styles/QueryBuilder.module.css'; -import { useLogsPageContext } from '@/pages/Logs/Context'; +import { useLogsPageContext } from './logsContextProvider'; import { IconFilter, IconPlus } from '@tabler/icons-react'; -import { operatorLabelMap, useQueryFilterContext } from '@/providers/QueryFilterProvider'; +import classes from './styles/Querier.module.css'; +import { Text } from '@mantine/core'; +import { useQueryFilterContext, operatorLabelMap } from '@/providers/QueryFilterProvider'; + +export const FilterPlaceholder = () => { + return ( + + + Click to add filter + + ); +}; + +import { useCallback } from 'react'; import { noValueOperators, textFieldOperators, numberFieldOperators } from '@/providers/QueryFilterProvider'; -import { RuleTypeOverride, RuleGroupTypeOverride, QueryType, Combinator } from '@/providers/QueryFilterProvider'; +import { RuleTypeOverride, RuleGroupTypeOverride, Combinator } from '@/providers/QueryFilterProvider'; type RuleSetProps = { ruleSet: RuleGroupTypeOverride; @@ -81,7 +90,7 @@ const RuleView = (props: RuleViewType) => { type={type} disabled={isDisabled} /> - + @@ -163,10 +172,6 @@ const AddRuleGroupBtn = ({ createRuleGroup }: { createRuleGroup: () => void }) = ); -type QueryPillProps = { - query: QueryType; -}; - type RuleSetPillProps = { ruleSet: RuleGroupTypeOverride; }; @@ -196,9 +201,11 @@ const RuleSetPills = (props: RuleSetPillProps) => { ); }; -const QueryPills = (props: QueryPillProps) => { - const { query } = props; - const { combinator, rules: ruleSets } = query; +export const QueryPills = () => { + const { + state: { appliedQuery }, + } = useQueryFilterContext(); + const { combinator, rules: ruleSets } = appliedQuery; return ( @@ -216,60 +223,35 @@ const QueryPills = (props: QueryPillProps) => { ); }; -const FilterBtnPlaceholder = () => { - return ( - - - Click to add filter - - ); -}; - -const ModalTitle = () => { - return Filters; -}; - -const Querier = () => { +export const FilterQueryBuilder = () => { const { state: queryBuilderState, methods: queryBuilderMethods } = useQueryFilterContext(); - const { isModalOpen, query, isSumbitDisabled, appliedQuery } = queryBuilderState; - const { createRuleGroup, clearFilters, applyQuery, closeBuilderModal, openBuilderModal } = queryBuilderMethods; + const { query, isSumbitDisabled } = queryBuilderState; + const { createRuleGroup, clearFilters, applyQuery } = queryBuilderMethods; const { - state: { custQuerySearchState }, - methods: {}, + state: { + custQuerySearchState: { isQuerySearchActive, mode }, + }, } = useLogsPageContext(); + const isFiltersApplied = isQuerySearchActive && mode === 'filters'; - const isFiltersApplied = custQuerySearchState.mode === 'filters' && custQuerySearchState.isQuerySearchActive; return ( - <> - - {!isFiltersApplied ? : } - - }> - - - - {query.rules.map((ruleSet) => { - return ; - })} - - - + + + + {query.rules.map((ruleSet) => { + return ; + })} + - - - - - - + + + + + + ); }; - -export default Querier; diff --git a/src/pages/Logs/HeaderPagination.tsx b/src/pages/Logs/HeaderPagination.tsx index e1ce6e68..342387d3 100644 --- a/src/pages/Logs/HeaderPagination.tsx +++ b/src/pages/Logs/HeaderPagination.tsx @@ -6,7 +6,7 @@ import { Box, Button, Text, Tooltip, px } from '@mantine/core'; import dayjs from 'dayjs'; import { FC, useEffect } from 'react'; import FillCarousel from './CarouselSlide'; -import { useLogsPageContext } from './Context'; +import { useLogsPageContext } from './logsContextProvider'; import Loading from '@/components/Loading'; import { IconZoomIn, IconZoomOut } from '@tabler/icons-react'; import headerPaginationStyles from './styles/HeaderPagination.module.css'; diff --git a/src/pages/Logs/LogRow.tsx b/src/pages/Logs/LogRow.tsx index 2f9529a9..6a35c82d 100644 --- a/src/pages/Logs/LogRow.tsx +++ b/src/pages/Logs/LogRow.tsx @@ -2,7 +2,7 @@ import { parseLogData } from '@/utils'; import { Box, px } from '@mantine/core'; import { IconArrowNarrowRight } from '@tabler/icons-react'; import { FC, Fragment } from 'react'; -import { useLogsPageContext } from './Context'; +import { useLogsPageContext } from './logsContextProvider'; import { Log } from '@/@types/parseable/api/query'; import tableStyles from './styles/Logs.module.css' diff --git a/src/pages/Logs/LogTable.tsx b/src/pages/Logs/LogTable.tsx index 05556459..16c47a2e 100644 --- a/src/pages/Logs/LogTable.tsx +++ b/src/pages/Logs/LogTable.tsx @@ -16,10 +16,11 @@ import { Pagination, Loader, Group, + Stack, } from '@mantine/core'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import type { FC } from 'react'; -import { LOG_QUERY_LIMITS, useLogsPageContext, LOAD_LIMIT as loadLimit } from './Context'; +import { LOG_QUERY_LIMITS, useLogsPageContext, LOAD_LIMIT as loadLimit, LOAD_LIMIT } from './logsContextProvider'; import LogRow from './LogRow'; import useMountedState from '@/hooks/useMountedState'; import { IconSelector, IconGripVertical, IconPin, IconPinFilled, IconSettings } from '@tabler/icons-react'; @@ -30,11 +31,13 @@ import Column from './Column'; import FilterPills from './FilterPills'; import { useHeaderContext } from '@/layouts/MainLayout/Context'; import dayjs from 'dayjs'; -import { Log, SortOrder } from '@/@types/parseable/api/query'; +import { SortOrder } from '@/@types/parseable/api/query'; import { usePagination } from '@mantine/hooks'; import { LogStreamSchemaData } from '@/@types/parseable/api/stream'; import tableStyles from './styles/Logs.module.css'; -import { HEADER_HEIGHT } from '@/constants/theme'; +import { LOGS_PRIMARY_TOOLBAR_HEIGHT, LOGS_SECONDARY_TOOLBAR_HEIGHT, PRIMARY_HEADER_HEIGHT } from '@/constants/theme'; +import { useQueryResult } from '@/hooks/useQueryResult'; +import { HumanizeNumber } from '@/utils/formatBytes'; const skipFields = ['p_metadata', 'p_tags']; @@ -47,12 +50,33 @@ const makeHeadersFromSchema = (schema: LogStreamSchemaData | null): string[] => } }; -const makeHeadersfromData = (data: Log[] | null): string[] => { - if (Array.isArray(data) && data.length > 0) { - return typeof data[0] === 'object' ? Object.keys(data[0]) : []; - } else { - return []; - } +const makeHeadersfromData = (schema: LogStreamSchemaData | null, custSearchQuery: string | null): string[] => { + const allColumns = makeHeadersFromSchema(schema); + if (custSearchQuery === null) return allColumns; + + const selectClause = custSearchQuery.match(/SELECT(.*?)FROM/i)?.[1]; + if (!selectClause || selectClause.includes('*')) return allColumns; + + const commonColumns = allColumns.filter((column) => selectClause.includes(column)); + return commonColumns; +}; + +type TotalLogsCountProps = { + totalCount: number | null; + loadedCount: number | null; +}; + +const TotalLogsCount = (props: TotalLogsCountProps) => { + const { totalCount, loadedCount } = props; + if (typeof totalCount !== 'number' || typeof loadedCount !== 'number') return ; + + return ( + + {`Showing ${loadedCount < LOAD_LIMIT ? loadedCount : LOAD_LIMIT} out of ${HumanizeNumber( + totalCount, + )} records`} + + ); }; const LogTable: FC = () => { @@ -66,7 +90,7 @@ const LogTable: FC = () => { methods: { setPageOffset, resetQuerySearch }, } = useLogsPageContext(); const { - state: { subLogSearch, subLogQuery, subRefreshInterval, subLogSelectedTimeRange }, + state: { subLogSearch, subLogQuery, subRefreshInterval, subLogSelectedTimeRange, maximized }, } = useHeaderContext(); const [refreshInterval, setRefreshInterval] = useMountedState(null); const [logStreamError, setLogStreamError] = useMountedState(null); @@ -95,15 +119,38 @@ const LogTable: FC = () => { sort, } = useQueryLogs(); - const tableHeaders = isQuerySearchActive ? makeHeadersfromData(logs) : makeHeadersFromSchema(logsSchema); + const tableHeaders = isQuerySearchActive + ? makeHeadersfromData(logsSchema, custSearchQuery) + : makeHeadersFromSchema(logsSchema); const appliedFilter = (key: string) => { return subLogSearch.get().filters[key] ?? []; }; const currentStreamName = subLogQuery.get().streamName; + const { fetchQueryMutation } = useQueryResult(); + const fetchCount = useCallback(() => { + const queryContext = subLogQuery.get(); + const defaultQuery = `select count(*) as count from ${currentStreamName}`; + const query = isQuerySearchActive + ? custSearchQuery.replace(/SELECT[\s\S]*?FROM/i, 'SELECT COUNT(*) as count FROM') + : defaultQuery; + if (queryContext && query?.length > 0) { + const logsQuery = { + streamName: queryContext.streamName, + startTime: queryContext.startTime, + endTime: queryContext.endTime, + access: [], + }; + fetchQueryMutation.mutate({ + logsQuery, + query, + }); + } + }, [currentStreamName, isQuerySearchActive, custSearchQuery]); + useEffect(() => { resetQuerySearch(); - }, [currentStreamName]) + }, [currentStreamName]); const applyFilter = (key: string, value: string[]) => { subLogSearch.set((state) => { @@ -206,6 +253,12 @@ const LogTable: FC = () => { } }, [custSearchQuery]); + useEffect(() => { + if (pageOffset === 0 && subLogQuery.get()) { + fetchCount(); + } + }, [currentStreamName, isQuerySearchActive, custSearchQuery]); + useEffect(() => { const streamErrorListener = subLogStreamError.subscribe(setLogStreamError); const logSearchListener = subLogSearch.subscribe(setQuerySearch); @@ -340,19 +393,25 @@ const LogTable: FC = () => { } }, [pinnedContianerRef, pinnedColumns]); + const primaryHeaderHeight = !maximized + ? PRIMARY_HEADER_HEIGHT + LOGS_PRIMARY_TOOLBAR_HEIGHT + LOGS_SECONDARY_TOOLBAR_HEIGHT + : 0; + + const totalCount = Array.isArray(fetchQueryMutation?.data) ? fetchQueryMutation.data[0]?.count : null; + const loadedCount = pageLogData?.data.length || null; return ( {!(logStreamError || logStreamSchemaError || logsError) ? ( Boolean(tableHeaders.length) && Boolean(pageLogData?.data.length) ? ( - + + style={{ display: 'flex', flexDirection: 'row', maxHeight: `calc(100vh - ${primaryHeaderHeight}px )` }}> { )} - + {!loading && !logsLoading ? ( ; +const renderSettingsIcon = () => ; +const renderLiveTailIcon = () => ; +const renderDeleteIcon = () => ; + +const PrimaryToolbar = () => { + const { + methods: { openDeleteModal, openAlertsModal, openRetentionModal, toggleLiveTail }, + state: { liveTailToggled }, + } = useLogsPageContext(); + const { + state: { userSpecificAccessMap }, + } = useHeaderContext(); + const isSecureConnection = window.location.protocol === 'https:'; + return ( + + + + + {!isSecureConnection && ( + + )} + {userSpecificAccessMap.hasUpdateAlertAccess && ( + + )} + + {userSpecificAccessMap.hasDeleteAccess && ( + + )} + + + ); +}; + +export default PrimaryToolbar; diff --git a/src/pages/Logs/Querier.tsx b/src/pages/Logs/Querier.tsx new file mode 100644 index 00000000..4514227a --- /dev/null +++ b/src/pages/Logs/Querier.tsx @@ -0,0 +1,107 @@ +import { Group, Menu, Modal, Stack, px } from '@mantine/core'; +import { useLogsPageContext } from './logsContextProvider'; +import { ToggleButton } from '@/components/Button/ToggleButton'; +import { IconChevronDown, IconCodeCircle, IconFilter } from '@tabler/icons-react'; +import classes from './styles/Querier.module.css'; +import { Text } from '@mantine/core'; +import { FilterQueryBuilder, QueryPills } from './FilterQueryBuilder'; +import { AppliedSQLQuery } from './QueryEditor'; +import QueryCodeEditor from './QueryCodeEditor'; + +const getLabel = (mode: string | null) => { + return mode === 'filters' ? 'Filters' : mode === 'sql' ? 'SQL' : ''; +}; + +const FilterPlaceholder = () => { + return ( + + + Click to add filter + + ); +}; + +const SQLEditorPlaceholder = () => { + return ( + + + Click to write query + + ); +}; + +const ModalTitle = ({ title }: { title: string }) => { + return {title}; +}; + +const QuerierModal = () => { + const { + methods: { toggleBuilderModal }, + state: { + custQuerySearchState: { viewMode }, + builderModalOpen, + }, + } = useLogsPageContext(); + + return ( + }> + + {viewMode === 'filters' ? : } + + + ); +}; + +const Querier = () => { + const { + methods: { toggleCustQuerySearchMode, toggleBuilderModal }, + state: { + custQuerySearchState: { isQuerySearchActive, mode, viewMode }, + }, + } = useLogsPageContext(); + const isFiltersApplied = mode === 'filters' && isQuerySearchActive; + const isSqlSearchActive = mode === 'sql' && isQuerySearchActive; + return ( + + + + +
+ {}} + toggled={false} + renderIcon={() => } + label={getLabel(viewMode)} + iconPosition="right" + customClassName={classes.modeButton} + /> +
+
+ + toggleCustQuerySearchMode('filters')} + style={{ padding: '0.5rem 2.25rem 0.5rem 0.75rem' }}> + Filters + + toggleCustQuerySearchMode('sql')} + style={{ padding: '0.5rem 2.25rem 0.5rem 0.75rem' }}> + SQL + + +
+ + {viewMode === 'filters' && (isFiltersApplied ? : )} + {viewMode === 'sql' && (isSqlSearchActive ? : )} + +
+ ); +}; + +export default Querier; diff --git a/src/pages/Logs/QueryCodeEditor.tsx b/src/pages/Logs/QueryCodeEditor.tsx index 518e7b5d..ed841e42 100644 --- a/src/pages/Logs/QueryCodeEditor.tsx +++ b/src/pages/Logs/QueryCodeEditor.tsx @@ -1,20 +1,15 @@ -import React, { FC, MutableRefObject, useCallback, useEffect } from 'react'; +import React, { FC, useCallback, useEffect } from 'react'; import Editor from '@monaco-editor/react'; import { useHeaderContext } from '@/layouts/MainLayout/Context'; -import { Box, Button, Flex, Text, TextInput, Tooltip, px } from '@mantine/core'; +import { Box, Button, Flex, ScrollArea, Stack, Text, TextInput } from '@mantine/core'; import { ErrorMarker, errChecker } from './ErrorMarker'; -import { IconPlayerPlayFilled, IconRotate } from '@tabler/icons-react'; import useMountedState from '@/hooks/useMountedState'; import { notify } from '@/utils/notification'; import { usePostLLM } from '@/hooks/usePostLLM'; import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; -import { LOAD_LIMIT, useLogsPageContext } from '../Logs/Context'; +import { LOAD_LIMIT, useLogsPageContext } from './logsContextProvider'; import { Field } from '@/@types/parseable/dataType'; -import queryCodeStyles from './styles/QueryCode.module.css' - -type QueryCodeEditorProps = { - inputRef: MutableRefObject; -}; +import queryCodeStyles from './styles/QueryCode.module.css'; const genColumnConfig = (fields: Field[]) => { const columnConfig = { leftColumns: [], rightColumns: [] }; @@ -33,16 +28,17 @@ const genColumnConfig = (fields: Field[]) => { }, columnConfig); }; -const QueryCodeEditor: FC = (props) => { +const QueryCodeEditor: FC = () => { const { state: { subLogQuery, subInstanceConfig }, } = useHeaderContext(); const { state: { - custQuerySearchState: { isQuerySearchActive }, + custQuerySearchState: { isQuerySearchActive, mode }, subLogStreamSchema, + queryCodeEditorRef, }, - methods: { resetQuerySearch, setCustSearchQuery }, + methods: { resetQuerySearch, setCustSearchQuery, closeBuilderModal }, } = useLogsPageContext(); const fields = subLogStreamSchema.get()?.fields || []; @@ -55,11 +51,12 @@ const QueryCodeEditor: FC = (props) => { const { data: resAIQuery, postLLMQuery } = usePostLLM(); const currentStreamName = subLogQuery.get().streamName; const isLlmActive = !!subInstanceConfig.get()?.llmActive; + const isSqlSearchActive = isQuerySearchActive && mode === 'sql'; const updateQuery = useCallback((query: string) => { - props.inputRef.current = query; - setQuery(query) - }, []) + queryCodeEditorRef.current = query; + setQuery(query); + }, []); const handleAIGenerate = useCallback(() => { if (!aiQuery?.length) { @@ -73,7 +70,7 @@ const QueryCodeEditor: FC = (props) => { if (resAIQuery) { const warningMsg = '-- LLM generated query is experimental and may produce incorrect answers\n-- Always verify the generated SQL before executing\n\n'; - updateQuery(warningMsg + resAIQuery); + updateQuery(warningMsg + resAIQuery); } }, [resAIQuery]); @@ -86,21 +83,21 @@ const QueryCodeEditor: FC = (props) => { useEffect(() => { if (currentStreamName !== localStreamName) { setlocalStreamName(currentStreamName); - const query = `SELECT * FROM ${currentStreamName} LIMIT ${LOAD_LIMIT}; ` + const query = `SELECT * FROM ${currentStreamName} LIMIT ${LOAD_LIMIT}; `; updateQuery(query); } setlocalLlmActive(isLlmActive); }, [currentStreamName, isLlmActive]); useEffect(() => { - updateQuery(props.inputRef.current); + updateQuery(queryCodeEditorRef.current); }, []); function handleEditorDidMount(editor: any, monaco: any) { editorRef.current = editor; monacoRef.current = monaco; editor.addCommand(monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter, async () => { - runQuery(props.inputRef.current); + runQuery(queryCodeEditorRef.current); }); } @@ -108,104 +105,63 @@ const QueryCodeEditor: FC = (props) => { const query = sanitiseSqlString(inputQuery); const parsedQuery = query.replace(/(\r\n|\n|\r)/gm, ''); setCustSearchQuery(parsedQuery, 'sql'); + closeBuilderModal(); }; - const classes = queryCodeStyles; - const { container, runQueryBtn, textContext, clearQueryBtn } = classes; - return ( - - - Search Query - - - - - - - - - - - {localLlmActive ? ( - setAiQuery(e.target.value)} - placeholder="Enter plain text to generate SQL query using OpenAI" - rightSectionWidth={'auto'} - style={{ - '& .mantine-Input-input': { - border: 'none', - borderRadius: 0, - backgroundColor: 'rgba(84,91,235,.2)', - '::placeholder': {}, - }, - '& .mantine-TextInput-rightSection ': { - height: '100%', - }, - }} - rightSection={ - - } - /> - ) : ( - - - Know More: How to enable SQL generation with OpenAI ? +
+ ) : ( + + + Know More: How to enable SQL generation with OpenAI ? + -
- )} -
- + )} + -
- - - -
+ + + + + + + + + ); }; @@ -225,12 +181,20 @@ const SchemaList = (props: { currentStreamName: string; fields: Field[] }) => { {leftColumns.map((config, index) => { - return {`${config}\n\n`}; + return ( + {`${config}\n\n`} + ); })} {rightColumns.map((config, index) => { - return {`${config}\n\n`}; + return ( + {`${config}\n\n`} + ); })} diff --git a/src/pages/Logs/QueryEditor.tsx b/src/pages/Logs/QueryEditor.tsx index bc5d7afb..4852604f 100644 --- a/src/pages/Logs/QueryEditor.tsx +++ b/src/pages/Logs/QueryEditor.tsx @@ -1,41 +1,18 @@ -import { Box, Drawer } from '@mantine/core'; -import type { FC } from 'react'; -import { LOAD_LIMIT, useLogsPageContext } from './Context'; -import React, { useEffect } from 'react'; -import { useHeaderContext } from '@/layouts/MainLayout/Context'; -import QueryCodeEditor from './QueryCodeEditor'; -import viewLogStyles from './styles/ViewLogs.module.css' +import { useLogsPageContext } from './logsContextProvider'; +import { CodeHighlight } from '@mantine/code-highlight'; -const QueryEditor: FC = () => { +export const AppliedSQLQuery = () => { const { state: { - custQuerySearchState: { showQueryEditor }, + custQuerySearchState: { custSearchQuery }, }, - methods: { toggleShowQueryEditor }, } = useLogsPageContext(); - const { - state: { subLogQuery }, - } = useHeaderContext(); - const onClose = () => toggleShowQueryEditor(); - const classes = viewLogStyles; - const inputRef = React.useRef(); // to store input value even after the editor unmounts - const currentStreamName = subLogQuery.get().streamName; - useEffect(() => { - if (currentStreamName) { - const defaultSearchQuery = `SELECT * FROM ${currentStreamName} LIMIT ${LOAD_LIMIT};`; - inputRef.current = defaultSearchQuery; - } else { - inputRef.current = ''; - } - }, [currentStreamName]); - return ( - - - - - + ); }; - -export default QueryEditor; diff --git a/src/pages/Logs/RetentionModal.tsx b/src/pages/Logs/RetentionModal.tsx new file mode 100644 index 00000000..2576d79c --- /dev/null +++ b/src/pages/Logs/RetentionModal.tsx @@ -0,0 +1,75 @@ +import { Box, Button, Modal, Stack, Switch } from '@mantine/core'; +import { useLogsPageContext } from './logsContextProvider'; +import { Text } from '@mantine/core'; +import classes from './styles/Logs.module.css'; +import { Editor } from '@monaco-editor/react'; + +const ModalTitle = () => { + return Settings; +}; + +type RetentionModalProps = { + data: any; + handleChange: (value: string | undefined) => void; + handleSubmit: () => void; + handleCacheToggle: () => void; + isCacheEnabled: boolean; +}; + +const RententionModal = (props: RetentionModalProps) => { + const { + state: { retentionModalOpen }, + methods: { closeRetentionModal }, + } = useLogsPageContext(); + const { isCacheEnabled, handleCacheToggle } = props; + const switchStyles = { + track: isCacheEnabled ? classes.trackStyle : {}, + }; + + return ( + }> + + + Allow Cache + + + Retention + + + + + + + + + ); +}; + +export default RententionModal; diff --git a/src/pages/Logs/SecondaryToolbar.tsx b/src/pages/Logs/SecondaryToolbar.tsx new file mode 100644 index 00000000..7a8edee5 --- /dev/null +++ b/src/pages/Logs/SecondaryToolbar.tsx @@ -0,0 +1,72 @@ +import { Menu, Stack, px } from '@mantine/core'; +import IconButton from '@/components/Button/IconButton'; +import { useLogsPageContext } from './logsContextProvider'; +import { useHeaderContext } from '@/layouts/MainLayout/Context'; +import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers'; +import classes from './styles/Toolbar.module.css'; +import { IconDownload, IconMaximize } from '@tabler/icons-react'; +import { LOGS_SECONDARY_TOOLBAR_HEIGHT } from '@/constants/theme'; +import TimeRange from '@/components/Header/TimeRange'; +import RefreshInterval from '@/components/Header/RefreshInterval'; +import RefreshNow from '@/components/Header/RefreshNow'; +import StreamingButton from '@/components/Header/StreamingButton'; +import Querier from './Querier'; + +const renderExportIcon = () => ; +const renderMaximizeIcon = () => ; + +const SecondaryToolbar = () => { + const { + methods: { makeExportData }, + state: { liveTailToggled }, + } = useLogsPageContext(); + const { + state: { subLogQuery }, + methods: { resetTimeInterval, toggleMaximize }, + } = useHeaderContext(); + const exportHandler = (fileType: string | null) => { + const query = subLogQuery.get(); + const filename = `${query.streamName}-logs`; + if (fileType === 'CSV') { + downloadDataAsCSV(makeExportData('CSV'), filename); + } else if (fileType === 'JSON') { + downloadDataAsJson(makeExportData('JSON'), filename); + } + }; + return ( + + {!liveTailToggled && ( + + + + + + +
+ +
+
+ + exportHandler('CSV')} style={{ padding: '0.5rem 2.25rem 0.5rem 0.75rem' }}> + CSV + + exportHandler('JSON')} style={{ padding: '0.5rem 2.25rem 0.5rem 0.75rem' }}> + JSON + + +
+ + +
+ )} + {liveTailToggled && ( + + + + + )} +
+ ); +}; + +export default SecondaryToolbar; diff --git a/src/pages/Logs/ViewLog.tsx b/src/pages/Logs/ViewLog.tsx index 62475d3d..aa9a7d07 100644 --- a/src/pages/Logs/ViewLog.tsx +++ b/src/pages/Logs/ViewLog.tsx @@ -2,7 +2,7 @@ import useMountedState from '@/hooks/useMountedState'; import { Box, Chip, CloseButton, Divider, Drawer, Text, px } from '@mantine/core'; import type { FC } from 'react'; import { useEffect, Fragment, useMemo } from 'react'; -import { useLogsPageContext } from './Context'; +import { useLogsPageContext } from './logsContextProvider'; import dayjs from 'dayjs'; import viewLogStyles from './styles/ViewLogs.module.css' import { CodeHighlight } from '@mantine/code-highlight'; diff --git a/src/pages/Logs/index.tsx b/src/pages/Logs/index.tsx index aa7d804c..9cef4972 100644 --- a/src/pages/Logs/index.tsx +++ b/src/pages/Logs/index.tsx @@ -1,20 +1,53 @@ import { Box } from '@mantine/core'; import { useDocumentTitle } from '@mantine/hooks'; import { FC } from 'react'; -import LogTable from './LogTable'; +import StaticLogTable from './LogTable'; +import LiveLogTable from '../LiveTail/LogTable'; import ViewLog from './ViewLog'; -import QueryEditor from './QueryEditor'; -// import HeaderPagination from './HeaderPagination'; +import DeleteStreamModal from './DeleteStreamModal'; +import AlertsModal from './AlertsModal'; +import RententionModal from './RetentionModal'; +import { useLogsPageContext } from './logsContextProvider'; +import PrimaryToolbar from './PrimaryToolbar'; +import SecondaryToolbar from './SecondaryToolbar'; +import { useHeaderContext } from '@/layouts/MainLayout/Context'; +import { useAlertsEditor } from '@/hooks/useAlertsEditor'; +import { useRetentionEditor } from '@/hooks/useRetentionEditor'; +import { useCacheToggle } from '@/hooks/useCacheToggle'; const Logs: FC = () => { useDocumentTitle('Parseable | Logs'); + const { + state: { maximized }, + } = useHeaderContext(); + const { + state: { liveTailToggled, currentStream }, + } = useLogsPageContext(); + + const { handleAlertQueryChange, submitAlertQuery, getLogAlertData } = useAlertsEditor(currentStream); + const { handleRetentionQueryChange, submitRetentionQuery, getLogRetentionData } = useRetentionEditor(currentStream); + const { handleCacheToggle, isCacheEnabled } = useCacheToggle(currentStream); return ( - {/* */} - + + + + {!maximized && ( + <> + + + + )} + {liveTailToggled ? : } + {/* TODO: need to move the live logtable into the Logs folder */} - ); }; diff --git a/src/pages/Logs/logsContextProvider.tsx b/src/pages/Logs/logsContextProvider.tsx new file mode 100644 index 00000000..6ed456b6 --- /dev/null +++ b/src/pages/Logs/logsContextProvider.tsx @@ -0,0 +1,264 @@ +import type { Log } from '@/@types/parseable/api/query'; +import useSubscribeState, { SubData } from '@/hooks/useSubscribeState'; +import type { Dispatch, FC, MutableRefObject, SetStateAction } from 'react'; +import React, { ReactNode, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { LogStreamSchemaData } from '@/@types/parseable/api/stream'; +import { sanitizeCSVData } from '@/utils/exportHelpers'; +import { useHeaderContext } from '@/layouts/MainLayout/Context'; +import { useDisclosure } from '@mantine/hooks'; + +const Context = createContext({}); + +const { Provider } = Context; + +export const LOG_QUERY_LIMITS = [30, 50, 100, 150, 200]; +export const LOAD_LIMIT = 9000; + +type GapTime = { + startTime: Date; + endTime: Date; + id: number | null; +}; +interface LogsPageContextState { + subLogStreamError: SubData; + subViewLog: SubData; + subGapTime: SubData; + subLogQueryData: SubData; + subLogStreamSchema: SubData; + subSchemaToggle: SubData; + pageOffset: number; + custQuerySearchState: CustQuerySearchState; + deleteModalOpen: boolean; + currentStream: string; + alertsModalOpen: boolean; + retentionModalOpen: boolean; + maximized: boolean; + liveTailToggled: boolean; + builderModalOpen: boolean; + queryCodeEditorRef: MutableRefObject; +} + +type LogQueryData = { + rawData: Log[]; + filteredData: Log[]; +}; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface LogsPageContextMethods { + makeExportData: (type: string) => Log[]; + toggleShowQueryEditor: () => void; + resetQuerySearch: () => void; + setPageOffset: Dispatch>; + setCustSearchQuery: (query: string, mode: custQuerySearchMode) => void; + closeRetentionModal: () => void; + openDeleteModal: () => void; + openAlertsModal: () => void; + openRetentionModal: () => void; + toggleLiveTail: () => void; + closeAlertsModal: () => void; + toggleBuilderModal: () => void; + toggleCustQuerySearchMode: (mode: custQuerySearchMode) => void; + closeBuilderModal: () => void; + closeDeleteModal: () => void; +} + +interface LogsPageContextValue { + state: LogsPageContextState; + methods: LogsPageContextMethods; +} + +interface LogsPageProviderProps { + children: ReactNode; +} + +type custQuerySearchMode = 'sql' | 'filters'; + +type CustQuerySearchState = { + showQueryEditor: boolean; + isQuerySearchActive: boolean; + custSearchQuery: string; + mode: string; + viewMode: string; +}; + +export const defaultQueryResult = ''; + +const defaultCustQuerySearchState = { + showQueryEditor: false, + isQuerySearchActive: false, + custSearchQuery: '', + mode: 'filters', + viewMode: 'filters', +}; + +const defaultCustSQLQuery = (streamName: string) => { + if (streamName && streamName.length > 0) { + return `SELECT * FROM ${streamName} LIMIT ${LOAD_LIMIT};` + } else { + return '' + } +} + +const LogsPageProvider: FC = ({ children }) => { + const { + state: { subLogQuery }, + } = useHeaderContext(); + const subLogStreamError = useSubscribeState(null); + const subViewLog = useSubscribeState(null); + const subGapTime = useSubscribeState(null); + const subLogQueryData = useSubscribeState({ + rawData: [], + filteredData: [], + }); + const subLogStreamSchema = useSubscribeState(null); + const subSchemaToggle = useSubscribeState(false); + const [pageOffset, setPageOffset] = useState(0); + const [custQuerySearchState, setCustQuerySearchState] = useState(defaultCustQuerySearchState); + const [currentStream, setCurrentStream] = useState(subLogQuery.get().streamName); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [alertsModalOpen, setAlertsModalOpen] = useState(false); + const [retentionModalOpen, setRetentionModalOpen] = useState(false); + + const [maximized, { toggle: toggleMaximize }] = useDisclosure(false); + const [liveTailToggled, { toggle: toggleLiveTail }] = useDisclosure(false); + const [builderModalOpen, { toggle: toggleBuilderModal, close: closeBuilderModal }] = useDisclosure(false); + const queryCodeEditorRef = React.useRef(defaultCustSQLQuery(subLogQuery.get().streamName)); // to store input value even after the editor unmounts + + // TODO: rm this after context refactor + useEffect(() => { + const streamlistener = subLogQuery.subscribe((state) => { + if (state.streamName) { + setCurrentStream(state.streamName); + const defaultSearchQuery = `SELECT * FROM ${state.streamName} LIMIT ${LOAD_LIMIT};`; + queryCodeEditorRef.current = defaultSearchQuery; + } else { + queryCodeEditorRef.current = ''; + } + }); + + return () => { + streamlistener(); + }; + }, [subLogQuery]); + + // state + const state: LogsPageContextState = { + subLogStreamError, + subViewLog, + subGapTime, + subLogQueryData, + subLogStreamSchema, + subSchemaToggle, + custQuerySearchState, + pageOffset, + deleteModalOpen, + currentStream, + alertsModalOpen, + retentionModalOpen, + maximized, + liveTailToggled, + builderModalOpen, + queryCodeEditorRef + }; + + // getters & setters + const toggleShowQueryEditor = useCallback(() => { + setCustQuerySearchState((prev) => ({ ...prev, showQueryEditor: !prev.showQueryEditor })); + }, []); + + const resetQuerySearch = useCallback(() => { + closeBuilderModal(); + setCustQuerySearchState((prev) => ({ ...defaultCustQuerySearchState, viewMode: prev.viewMode })); + // setPageOffset(0); wont the LogTable handle this ? + }, []); + + const setCustSearchQuery = useCallback((query: string, mode: custQuerySearchMode) => { + setCustQuerySearchState((prev) => ({ + ...prev, + mode, + custSearchQuery: query, + isQuerySearchActive: true, + showQueryEditor: false, + })); + }, []); + + const toggleCustQuerySearchMode = useCallback((viewMode: custQuerySearchMode) => { + setCustQuerySearchState((prev) => ({ ...prev, viewMode })); + }, []); + + const closeDeleteModal = useCallback(() => { + return setDeleteModalOpen(false); + }, []); + + const openDeleteModal = useCallback(() => { + return setDeleteModalOpen(true); + }, []); + + const closeAlertsModal = useCallback(() => { + return setAlertsModalOpen(false); + }, []); + + const openAlertsModal = useCallback(() => { + return setAlertsModalOpen(true); + }, []); + + const closeRetentionModal = useCallback(() => { + return setRetentionModalOpen(false); + }, []); + + const openRetentionModal = useCallback(() => { + return setRetentionModalOpen(true); + }, []); + + // handlers + const makeExportData = useCallback( + (type: string): Log[] => { + const { rawData, filteredData: _filteredData } = subLogQueryData.get(); // filteredData - records filtered with in-page search + if (type === 'JSON') { + return rawData; + } else if (type === 'CSV') { + const fields = subLogStreamSchema.get()?.fields; + const headers = !custQuerySearchState.isQuerySearchActive + ? Array.isArray(fields) + ? fields.map((field) => field.name) + : [] + : typeof rawData[0] === 'object' + ? Object.keys(rawData[0]) + : []; + + const sanitizedCSVData = sanitizeCSVData(rawData, headers); + return [headers, ...sanitizedCSVData]; + } else { + return []; + } + }, + [custQuerySearchState.isQuerySearchActive], + ); + + const methods = { + makeExportData, + toggleShowQueryEditor, + resetQuerySearch, + setPageOffset, + setCustSearchQuery, + closeDeleteModal, + openDeleteModal, + openAlertsModal, + closeAlertsModal, + openRetentionModal, + closeRetentionModal, + toggleMaximize, + toggleLiveTail, + toggleCustQuerySearchMode, + toggleBuilderModal, + closeBuilderModal, + }; + + const value = useMemo(() => ({ state, methods }), [state, methods]); + + return {children}; +}; + +export const useLogsPageContext = () => useContext(Context) as LogsPageContextValue; + +export default LogsPageProvider; diff --git a/src/pages/Logs/styles/EventTimeLineGraph.module.css b/src/pages/Logs/styles/EventTimeLineGraph.module.css new file mode 100644 index 00000000..1b826222 --- /dev/null +++ b/src/pages/Logs/styles/EventTimeLineGraph.module.css @@ -0,0 +1,19 @@ +.graphContainer { + width: 100%; + height: 100%; + margin-left: -2.8rem; +} + +.noDataContainer { + width: 98%; + height: 100%; + align-items: center; + justify-content: center; + border: 1px dashed var(--mantine-color-gray-4); +} + +.noDataText { + text-align: center; + color: var(--mantine-color-gray-6); + font-size: var(--mantine-font-size-sm); +} \ No newline at end of file diff --git a/src/pages/Logs/styles/Logs.module.css b/src/pages/Logs/styles/Logs.module.css index 13b077d0..7eaf42e7 100644 --- a/src/pages/Logs/styles/Logs.module.css +++ b/src/pages/Logs/styles/Logs.module.css @@ -206,4 +206,8 @@ &:hover { background: #E0E0E0; } +} + +.trackStyle { + background-color: #545BEB; } \ No newline at end of file diff --git a/src/pages/Logs/styles/Querier.module.css b/src/pages/Logs/styles/Querier.module.css new file mode 100644 index 00000000..f4ecbfc0 --- /dev/null +++ b/src/pages/Logs/styles/Querier.module.css @@ -0,0 +1,182 @@ +.filterContainer { + padding-left: 0.6rem; + background-color: white; + color: #211F1F; + cursor: pointer; + flex: 1; + flex-direction: row; + overflow-y: hidden; + align-items: center; +} + +.container { + background-color: white; + color: #211F1F; + border: 1px var(--mantine-color-gray-3 ) solid; + border-radius: rem(8px); + cursor: pointer; + flex: 1; + overflow-y: hidden; + flex-direction: row; + margin-right: 0.675rem; +} + +.modeContainer { + width: 5rem; + height: 100%; + text-align: center; + align-items: center; + justify-content: center; + border-right: 1px solid var(--mantine-color-gray-4); +} + +.modeLabel { + /* color: white; */ + font-weight: 600; +} + +.modeButton { + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; + width: 6rem; + border-top: none !important; + border-left: none !important; + border-bottom: none !important; + margin: 0px !important; +} + +.placeholderText { + color: var(--mantine-color-gray-5); + font-size: 1rem; + font-weight: 500; +} + +.addRuleContainer { + border: rem(2px) solid var(--mantine-color-gray-3); + height: 6rem; + width: 100%; + border-style: dashed; + border-spacing: 400px; + cursor: pointer; + align-items: center; + justify-content: center; + border-radius: rem(6px); + color: var(--mantine-primary-color-3); + font-weight: 400; +} + +.ruleSet { + border-radius: rem(6px); + padding: 1rem; + width: 100%; + background-color: var(--mantine-color-gray-1); +} + +.toggleBtnContainer { + flex-direction: row; + border: 1px solid black; + width: fit-content; + border-radius: rem(6px); + background-color: white; + border-color: transparent; + display: flex; + flex-direction: row; + cursor: pointer; + align-self: flex-end; +} + +.toggleBtnText { + padding: 0.2rem 0.6rem; + border-radius: rem(6px); + font-weight: 500; + font-size: small; + &.toggleBtnActive { + background-color: var(--mantine-primary-color-4) !important; + color: white; + } +} + +.ruleContainer { + flex-direction: row; + width: 100%; + align-items: center; +} + +.deleteRulebtn { + cursor: pointer; +} + +.addConditionBtn { + width: fit-content; + font-size: small; +} + +.parentCombinatorToggleContainer { + width: fit-content; + background-color: var(--mantine-color-gray-2); + border-radius: rem(6px); + position: 'relative'; + margin-left: 16px; + border: 1px solid var(--mantine-color-gray-4); + padding: 0.2rem; +} + +.ruleSetConnector { + width: 0; + height: 100%; + margin-left: 60px; + border: 1px solid var(--mantine-color-gray-4); +} + +.modalHeader { + font-weight: 500; + font-size: large; + margin-bottom: 1rem; +} + +.queryBuilderBtn { + background-color: white; + padding: 4px 12px; + color: #211F1F; + border: 1px var(--mantine-color-gray-4) solid; + border-radius: rem(8px); + cursor: pointer; + height: 2.2rem; + overflow: auto; + flex: 1; + margin-right: 0.625rem; +} + +.parentCombinatorPill { + color: white; + background: var(--mantine-color-teal-4); + text-transform: uppercase; + font-weight: 700; +} + +.childCombinatorPill { + color: white; + background: var(--mantine-color-indigo-4); + text-transform: uppercase; + font-weight: 700; +} + +.footer { + flex-direction: row; + justify-content: flex-end; + padding: 1.5rem; + padding-right: 0rem; + padding-top: 0.75rem; +} + +.queryBuilderContainer { + border-top-left-radius: rem(6px); + border-top-right-radius: rem(6px); + /* border-bottom: 1px solid var(--mantine-color-gray-3); */ +} + +.queryBuilderBtnPlaceholder { + color: var(--mantine-color-gray-5); + font-size: 1rem; + font-weight: 500; +} diff --git a/src/pages/Logs/styles/QueryCode.module.css b/src/pages/Logs/styles/QueryCode.module.css index e7c52353..2edc53b9 100644 --- a/src/pages/Logs/styles/QueryCode.module.css +++ b/src/pages/Logs/styles/QueryCode.module.css @@ -54,3 +54,11 @@ font-size: 1rem; font-weight: 600; } + +.footer { + border-top: 1px solid var(--mantine-color-gray-4) ; + flex-direction: row; + justify-content: flex-end; + padding: 1.5rem; + padding-right: 0rem; +} \ No newline at end of file diff --git a/src/pages/Logs/styles/Toolbar.module.css b/src/pages/Logs/styles/Toolbar.module.css new file mode 100644 index 00000000..5b8e70a4 --- /dev/null +++ b/src/pages/Logs/styles/Toolbar.module.css @@ -0,0 +1,42 @@ + +.streamSelectDescription { + font-size: 12px; + font-weight: 500; + color: var(--mantine-color-gray-6); +} + +.streamInput { + border: none; + padding-left: 0; + padding-right: 0; + height: 50px; + font-size: 24px; + font-weight: 600; + margin-top: -10px; + background-color: transparent; + cursor: pointer; + width: 200px; +} + +.chevronDown { + color: var(--mantine-color-gray-9); +} + +.streamSelect { + margin-left: 0.625rem; +} + +.logsPrimaryToolbar { + padding: 0.625rem 0; + border-bottom: 1px solid var(--mantine-color-gray-3); + width: 100%; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.logsSecondaryToolbar { + width: '100%'; + flex-direction: row; + padding: 1rem 0.625rem; +} \ No newline at end of file diff --git a/src/pages/Stats/Alerts.tsx b/src/pages/Stats/Alerts.tsx deleted file mode 100644 index 8ce949ac..00000000 --- a/src/pages/Stats/Alerts.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useHeaderContext } from '@/layouts/MainLayout/Context'; -import { Box, Button, Modal, ScrollArea, Text, px } from '@mantine/core'; -import { useDisclosure } from '@mantine/hooks'; -import { FC, useEffect, useRef } from 'react'; -import { IconArrowsMaximize } from '@tabler/icons-react'; -import useMountedState from '@/hooks/useMountedState'; -import { heights } from '@/components/Mantine/sizing'; -import { useAlertsEditor } from '@/hooks/useAlertsEditor'; -import { useParams } from 'react-router-dom'; -import alertStyles from './styles/Alerts.module.css' -import { CodeHighlight } from '@mantine/code-highlight'; -import { useStatsPageContext } from './Context'; - -const Alerts: FC = () => { - const { - state: { subLogQuery }, - } = useHeaderContext(); - const { streamName } = useParams(); - const { - state: { fetchStartTime }, - } = useStatsPageContext(); - const { getLogAlertData, getLogAlertIsError, getLogAlertIsLoading } = useAlertsEditor(streamName || '', fetchStartTime); - - const [opened, { open, close }] = useDisclosure(false); - const [Alert, setAlert] = useMountedState({ name: 'Loading....' }); - const AlertsWrapper = useRef(null); - const [editorHeight, setEditorHeight] = useMountedState(0); - - useEffect(() => { - setEditorHeight(AlertsWrapper.current?.offsetTop ? AlertsWrapper.current?.offsetTop + 15 : 0); - }, [heights.full, AlertsWrapper]); - - const classes = alertStyles; - const { container, headContainer, alertsText, alertsContainer, alertContainer, expandButton } = classes; - - return ( - - - Alerts - - - {!getLogAlertIsLoading ? ( - getLogAlertIsError ? ( - 'ERROR' - ) : getLogAlertData?.data && getLogAlertData?.data.alerts.length > 0 ? ( - getLogAlertData?.data.alerts.map((item: any, index: number) => { - return ( - - Name: {item.name} - - - ); - }) - ) : ( - No Alert set for {subLogQuery.get().streamName} - ) - ) : ( - 'Loading' - )} - - - - - - ); -}; - -export default Alerts; diff --git a/src/pages/Stats/Context.tsx b/src/pages/Stats/Context.tsx deleted file mode 100644 index bf78004f..00000000 --- a/src/pages/Stats/Context.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type { Dispatch, FC, SetStateAction } from 'react'; -import { ReactNode, createContext, useCallback, useContext, useMemo, useState } from 'react'; -import dayjs, { Dayjs } from 'dayjs'; - -const Context = createContext({}); - -const { Provider } = Context; - -interface StatsPageProvider { - children: ReactNode; -} - -interface LogsPageContextValue { - state: StatsPageContextState; - methods: StatsPageContextMethods; -} - -type StatsPageContextState = { - fetchStartTime: Dayjs; - statusFixedDurations: number; -} - -type StatsPageContextMethods = { - resetFetchStartTime: () => void; - setStatusFixedDurations: Dispatch>; -} - -const StatsPageProvider: FC = ({ children }) => { - const [fetchStartTime, setFetchStartTime] = useState(dayjs()); - const [statusFixedDurations, setStatusFixedDurations] = useState(0); - - const resetFetchStartTime = useCallback(() => { - setFetchStartTime(dayjs()) - }, []) - - const state: StatsPageContextState = { - fetchStartTime, - statusFixedDurations - }; - - const methods: StatsPageContextMethods = { - resetFetchStartTime, - setStatusFixedDurations - } - - const value = useMemo(() => ({ state, methods }), [state, methods]); - - return {children}; -}; - -export const useStatsPageContext = () => useContext(Context) as LogsPageContextValue; - -export default StatsPageProvider; diff --git a/src/pages/Stats/Status.tsx b/src/pages/Stats/Status.tsx deleted file mode 100644 index 87a5ee17..00000000 --- a/src/pages/Stats/Status.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { FC, useEffect } from 'react'; -import { Box, Stack, Text, ThemeIcon, Tooltip, px } from '@mantine/core'; -import dayjs from 'dayjs'; -import { - IconClockStop, - IconDatabase, - IconInfoCircle, - IconTimelineEventText, - IconTransferIn, - IconWindowMinimize, -} from '@tabler/icons-react'; -import { useQueryResult } from '@/hooks/useQueryResult'; -import useMountedState from '@/hooks/useMountedState'; -import { convertToReadableScale } from '@/utils/convertToReadableScale'; -import { formatBytes } from '@/utils/formatBytes'; -import { FIXED_DURATIONS } from '@/constants/timeConstants'; -import { useRetentionEditor } from '@/hooks/useRetentionEditor'; -import { useLogStreamStats } from '@/hooks/useLogStreamStats'; -import { useParams } from 'react-router-dom'; -import statusStyles from './styles/Status.module.css'; -import statCardStyles from './styles/StatsCard.module.css'; -import { useStatsPageContext } from './Context'; - -const Status: FC = () => { - const { streamName } = useParams(); - - const { - state: { statusFixedDurations, fetchStartTime }, - methods: { setStatusFixedDurations }, - } = useStatsPageContext(); - - const [fetchQueryStatus, setFetchQueryStatus] = useMountedState(''); - - const { getLogRetentionIsError, getLogRetentionData, getLogRetentionIsSuccess, getLogRetentionIsLoading } = - useRetentionEditor(streamName || '', fetchStartTime); - - const { - getLogStreamStatsData, - getLogStreamStatsDataIsSuccess, - getLogStreamStatsDataIsLoading, - getLogStreamStatsDataIsError, - } = useLogStreamStats(streamName || '', fetchStartTime); - const { fetchQueryMutation } = useQueryResult(); - - useEffect(() => { - if (streamName) { - getStatus(); - setStatusFixedDurations(0); - } - }, [streamName, fetchStartTime]); - - const getStatus = async () => { - setStatusFixedDurations(statusFixedDurations + 1); - const LogQuery = { - streamName: streamName || '', - startTime: fetchStartTime.subtract(FIXED_DURATIONS[statusFixedDurations].milliseconds, 'milliseconds').toDate(), - endTime: fetchStartTime.toDate(), - access: [], - }; - fetchQueryMutation.mutate({ - logsQuery: LogQuery, - query: `SELECT count(*) as count FROM ${streamName} ;`, - }); - }; - - useEffect(() => { - const updateStatus = async () => { - if (fetchQueryMutation.isLoading) { - setFetchQueryStatus('Loading...'); - } else if (fetchQueryMutation.isError) { - setFetchQueryStatus( - `Not Received any events in ${FIXED_DURATIONS[statusFixedDurations]?.name} and error occurred`, - ); - } else if (fetchQueryMutation.isSuccess && fetchQueryMutation?.data[0].count) { - setFetchQueryStatus( - `${fetchQueryMutation?.data[0].count} events in ${FIXED_DURATIONS[statusFixedDurations]?.name}`, - ); - } else { - if (FIXED_DURATIONS.length > statusFixedDurations && fetchQueryMutation?.data[0].count === 0) { - try { - await getStatus(); - } catch (error: unknown) { - let errorMessage = 'An unknown error occurred'; - if (error instanceof Error) { - errorMessage = error.message; - } else if (typeof error === 'string') { - errorMessage = error; - } - setFetchQueryStatus(`Error in fetching status: ${errorMessage}`); - } - } else { - setFetchQueryStatus(`No events received ${FIXED_DURATIONS[statusFixedDurations]?.name}`); - } - } - }; - - updateStatus(); - }, [fetchQueryMutation.isLoading, fetchQueryMutation.isError, fetchQueryMutation.isSuccess]); - - const generatedOn = getLogStreamStatsDataIsLoading - ? 'Loading...' - : getLogStreamStatsDataIsError - ? 'ERROR' - : getLogStreamStatsDataIsSuccess && getLogStreamStatsData?.data?.time - ? dayjs(getLogRetentionData?.data?.time).format('HH:mm DD-MM-YYYY') - : 'Not Found'; - - const retentionValue = getLogRetentionIsLoading - ? 'Loading...' - : getLogRetentionIsError - ? 'ERROR' - : getLogRetentionIsSuccess && getLogRetentionData?.data[0] && getLogRetentionData?.data[0].duration - ? `${getLogRetentionData?.data[0].duration.split('d')[0]} Days` - : 'Not Set'; - - const compressionValue = getLogStreamStatsDataIsLoading - ? 'Loading..' - : getLogStreamStatsDataIsError - ? 'ERROR' - : getLogStreamStatsDataIsSuccess && - getLogStreamStatsData?.data?.ingestion?.size && - getLogStreamStatsData?.data?.storage?.size - ? `${( - 100 - - (parseInt(getLogStreamStatsData?.data?.storage?.size.split(' ')[0]) / - parseInt(getLogStreamStatsData?.data?.ingestion?.size.split(' ')[0])) * - 100 - ).toPrecision(4)} %` - : 'Not Found'; - - const storageValue = getLogStreamStatsDataIsLoading - ? 'Loading..' - : getLogStreamStatsDataIsError - ? 'ERROR' - : getLogStreamStatsDataIsSuccess && getLogStreamStatsData?.data?.storage?.size - ? formatBytes(Number(getLogStreamStatsData?.data?.storage.size.split(' ')[0])) - : '0'; - - const ingestionValue = getLogStreamStatsDataIsLoading - ? 'Loading..' - : getLogStreamStatsDataIsError - ? 'ERROR' - : getLogStreamStatsDataIsSuccess && getLogStreamStatsData?.data?.ingestion?.size - ? formatBytes(Number(getLogStreamStatsData?.data?.ingestion.size.split(' ')[0])) - : '0'; - - const eventsValue = getLogStreamStatsDataIsLoading - ? 'Loading..' - : getLogStreamStatsDataIsError - ? 'ERROR' - : getLogStreamStatsDataIsSuccess && getLogStreamStatsData?.data?.ingestion?.count - ? convertToReadableScale(getLogStreamStatsData?.data.ingestion.count) - : '0'; - - const classes = statusStyles; - const { - container, - headContainer, - statusText, - statusTextResult, - genterateContiner, - genterateText, - genterateTextResult, - StatsContainer, - statusTextFailed, - } = classes; - return ( - - - - {fetchQueryStatus} - - - - - Generated at [{generatedOn}] - - - - - - - - - - - - - ); -}; - -type statCardProps = { - data: { Icon: any; title: string; description: string; value: string }; -}; - -const StatCard: FC = (props) => { - const { data } = props; - const classes = statCardStyles; - const { statCard, statCardTitle, statCardDescription, statCardDescriptionIcon, statCardIcon, statCardTitleValue } = - classes; - - return ( - - - - - - - - - - {data.value} - {data.title} - - ); -}; -export default Status; diff --git a/src/pages/Stats/index.tsx b/src/pages/Stats/index.tsx deleted file mode 100644 index bfe7f9d5..00000000 --- a/src/pages/Stats/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Box } from '@mantine/core'; -import { useDocumentTitle } from '@mantine/hooks'; -import { FC } from 'react'; -import Status from './Status'; -import Alerts from './Alerts'; - -const Stats: FC = () => { - useDocumentTitle('Parseable | Stats'); - - return ( - - - - - ); -}; - -export default Stats; diff --git a/src/pages/Stats/styles/Alerts.module.css b/src/pages/Stats/styles/Alerts.module.css deleted file mode 100644 index 000ff75e..00000000 --- a/src/pages/Stats/styles/Alerts.module.css +++ /dev/null @@ -1,53 +0,0 @@ -.container { - flex: 1 1 auto; - overflow-y: auto; - border-radius: 0.5rem; - margin: 1rem; - border: 1px solid rgba(82, 82, 82, 0.15); -} - -.headContainer { - padding: 1rem; - width: 100%; - height: 55px; - display: flex; - align-items: center; - justify-content: space-between; - top: 0; - position: sticky; - background-color: #ffffff; - z-index: 1; - border-bottom: 1px solid rgba(82, 82, 82, 0.15); -} - -.alertsText { - font-size: 1rem; - font-weight: 500; - color: #141414; -} - -.alertsContainer { - overflow: scroll; - color: #141414; -} - -.alertContainer { - border-bottom: 1px solid rgba(82, 82, 82, 0.15); - padding: 1rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.expandButton { - background: #ffffff; - padding: 0; - margin-right: 0.625rem; - width: 36px; - color: #868e96; - border: 1px solid rgba(82, 82, 82, 0.15); -} - -.expandButton:hover { - background: #f1f3f5; -} diff --git a/src/pages/Stats/styles/StatsCard.module.css b/src/pages/Stats/styles/StatsCard.module.css deleted file mode 100644 index 97d19087..00000000 --- a/src/pages/Stats/styles/StatsCard.module.css +++ /dev/null @@ -1,39 +0,0 @@ -.statCard { - border: 1px solid rgba(82, 82, 82, 0.15); - width: 100%; - border-radius: 8px; - text-align: center; - padding: 0.5rem 1rem; -} - -.statCardDescription { - text-align: right; -} - -.statCardDescriptionIcon { - color: #141414; -} - -.statCardIcon { - background-color: #e7eeff; - color: #545BEB; -} - -.statCardText { - display: flex; - flex-direction: column; - justify-content: space-around; - align-items: center; -} - -.statCardTitleValue { - font-size: 1.5rem; - font-weight: 600; - color: #545BEB; -} - -.statCardTitle { - font-size: 1rem; - font-weight: 600; - color: #141414; -} diff --git a/src/pages/Stats/styles/Status.module.css b/src/pages/Stats/styles/Status.module.css deleted file mode 100644 index 6089895a..00000000 --- a/src/pages/Stats/styles/Status.module.css +++ /dev/null @@ -1,50 +0,0 @@ -.container { - flex: 0 1 auto; -} - -.headContainer { - display: flex; - justify-content: space-between; - padding: 1rem; - height: 55px; - align-items: center; - border-bottom: 1px solid rgba(82, 82, 82, 0.15); -} - -.statusText { - font-size: 1rem; - font-weight: 600; - color: #141414; -} - -.statusTextResult { - color: #00cc14; -} - -.statusTextFailed { - color: #ff0000; -} - -.genterateContiner { - margin-right: 0.75rem; -} - -.genterateText { - font-size: 1rem; - font-weight: 600; - color: #141414; -} - -.genterateTextResult { - font-size: 1rem; - font-weight: 400; - color: #141414; -} - -.StatsContainer { - display: flex; - flex-direction: row; - padding: 1rem; - padding-bottom: 0; - justify-content: space-between; -} diff --git a/src/providers/QueryFilterProvider.tsx b/src/providers/QueryFilterProvider.tsx index 46ca283c..25154abf 100644 --- a/src/providers/QueryFilterProvider.tsx +++ b/src/providers/QueryFilterProvider.tsx @@ -1,5 +1,5 @@ import { useHeaderContext } from '@/layouts/MainLayout/Context'; -import { useLogsPageContext } from '@/pages/Logs/Context'; +import { useLogsPageContext } from '@/pages/Logs/logsContextProvider'; import { generateRandomId } from '@/utils'; import { ReactNode, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Field, RuleGroupType, RuleType, formatQuery } from 'react-querybuilder'; @@ -36,7 +36,6 @@ type QueryFilterContextMethods = { applyQuery: () => void; clearFilters: () => void; closeBuilderModal: () => void; - openBuilderModal: () => void; }; type QueryFilterContextValue = { @@ -154,11 +153,10 @@ const defaultQuery = { }; const QueryFilterProvider = (props: QueryFilterProviderProps) => { - const [isModalOpen, setModalOpen] = useState(false); const [isSumbitDisabled, setSubmitDisabled] = useState(true); const { - state: { subLogStreamSchema, custQuerySearchState }, - methods: { setCustSearchQuery, resetQuerySearch }, + state: { subLogStreamSchema, custQuerySearchState, builderModalOpen: isModalOpen }, + methods: { setCustSearchQuery, resetQuerySearch, closeBuilderModal }, } = useLogsPageContext(); const { state: { subAppContext }, @@ -250,14 +248,6 @@ const QueryFilterProvider = (props: QueryFilterProviderProps) => { }); }, []); - const openBuilderModal = useCallback(() => { - return setModalOpen(true); - }, []); - - const closeBuilderModal = useCallback(() => { - return setModalOpen(false); - }, []); - const updateParentCombinator = useCallback((combinator: Combinator) => { return setQuery((prev) => { return { ...prev, combinator: combinator }; @@ -274,12 +264,12 @@ const QueryFilterProvider = (props: QueryFilterProviderProps) => { const parsedQuery = parseQuery(); setCustSearchQuery(parsedQuery, 'filters'); setAppliedQuery(query); - setModalOpen(false); + closeBuilderModal() }, [query]); const clearFilters = useCallback(() => { resetQuerySearch(); - setModalOpen(false); + closeBuilderModal(); setAppliedQuery(defaultQuery); setQuery(defaultQuery); }, []); @@ -361,7 +351,6 @@ const QueryFilterProvider = (props: QueryFilterProviderProps) => { applyQuery, clearFilters, closeBuilderModal, - openBuilderModal, }; const value = useMemo(() => ({ state, methods }), [state, methods]); diff --git a/src/routes/elements.tsx b/src/routes/elements.tsx index 0eb92e7b..650824b4 100644 --- a/src/routes/elements.tsx +++ b/src/routes/elements.tsx @@ -4,18 +4,9 @@ import { lazy } from 'react'; import SuspensePage from './SuspensePage'; import MainLayoutPageProvider from '@/layouts/MainLayout/Context'; import MainLayout from '@/layouts/MainLayout'; -import { - ConfigHeader, - HomeHeader, - LiveTailHeader, - LogsHeader, - StatsHeader, - UsersManagementHeader, -} from '@/components/Header/SubHeader'; // page-wise providers -import LogsPageProvider from '@/pages/Logs/Context'; -import StatsPageProvider from '@/pages/Stats/Context'; +import LogsPageProvider from '@/pages/Logs/logsContextProvider'; // component-wise providers import QueryFilterProvider from '@/providers/QueryFilterProvider'; @@ -23,7 +14,6 @@ import QueryFilterProvider from '@/providers/QueryFilterProvider'; export const HomeElement: FC = () => { return ( - ); @@ -46,7 +36,6 @@ export const LogsElement: FC = () => { - @@ -62,47 +51,11 @@ export const MainLayoutElement: FC = () => { ); }; -const LiveTail = lazy(() => import('@/pages/LiveTail')); - -export const LiveTailElement: FC = () => { - return ( - - - - - ); -}; - -const Stats = lazy(() => import('@/pages/Stats')); - -export const StatsElement: FC = () => { - return ( - - - - - - - ); -}; - -const Config = lazy(() => import('@/pages/Config')); - -export const ConfigElement: FC = () => { - return ( - - - - - ); -}; - const Users = lazy(() => import('@/pages/AccessManagement')); export const UsersElement: FC = () => { return ( - ); diff --git a/src/routes/index.tsx b/src/routes/index.tsx index d5be1fbf..b07b8b7e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,12 +1,9 @@ import { ALL_ROUTE, - CONFIG_ROUTE, HOME_ROUTE, - LIVE_TAIL_ROUTE, LOGIN_ROUTE, LOGS_ROUTE, OIDC_NOT_CONFIGURED_ROUTE, - STATS_ROUTE, USERS_MANAGEMENT_ROUTE, } from '@/constants/routes'; import FullPageLayout from '@/layouts/FullPageLayout'; @@ -14,48 +11,23 @@ import NotFound from '@/pages/Errors/NotFound'; import type { FC } from 'react'; import { Route, Routes } from 'react-router-dom'; import PrivateRoute from './PrivateRoute'; -import { - HomeElement, - LoginElement, - LogsElement, - MainLayoutElement, - StatsElement, - ConfigElement, - UsersElement, - LiveTailElement, -} from './elements'; +import { HomeElement, LoginElement, LogsElement, MainLayoutElement, UsersElement } from './elements'; import AccessSpecificRoute from './AccessSpecificRoute'; import OIDCNotConFigured from '@/pages/Errors/OIDC'; const AppRouter: FC = () => { - const isSecureConnection = window.location.protocol === 'https:'; return ( }> }> - {/* Cuurently working Empty Stream page sooner change to HomeElement */} } /> - - {/* Users Management Route */} }> } /> - }> } /> - {!isSecureConnection && ( - }> - } /> - - )} - }> - } /> - - }> - } /> - } /> diff --git a/src/utils/sanitiseSqlString.ts b/src/utils/sanitiseSqlString.ts index d41dc84d..ec8c8fbe 100644 --- a/src/utils/sanitiseSqlString.ts +++ b/src/utils/sanitiseSqlString.ts @@ -1,5 +1,5 @@ import { notify } from './notification'; -import { LOAD_LIMIT } from '@/pages/Logs/Context'; +import { LOAD_LIMIT } from '@/pages/Logs/logsContextProvider'; export const sanitiseSqlString = (sqlString: string): string => { const withoutComments = sqlString.replace(/--.*$/gm, '');