diff --git a/frontend/package.json b/frontend/package.json index 205a9e0ed2..7d1a0855e3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -84,6 +84,7 @@ "papaparse": "5.4.1", "react": "18.2.0", "react-addons-update": "15.6.3", + "react-beautiful-dnd": "13.1.1", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-dom": "18.2.0", @@ -157,6 +158,7 @@ "@types/papaparse": "5.3.7", "@types/react": "18.0.26", "@types/react-addons-update": "0.14.21", + "@types/react-beautiful-dnd": "13.1.8", "@types/react-dom": "18.0.10", "@types/react-grid-layout": "^1.1.2", "@types/react-helmet-async": "1.0.3", diff --git a/frontend/src/assets/Dashboard/List.tsx b/frontend/src/assets/Dashboard/List.tsx new file mode 100644 index 0000000000..1c4d1d04a9 --- /dev/null +++ b/frontend/src/assets/Dashboard/List.tsx @@ -0,0 +1,30 @@ +import { CSSProperties } from 'react'; + +function ListIcon({ + fillColor, +}: { + fillColor: CSSProperties['color']; +}): JSX.Element { + return ( + + + + + + + + + ); +} + +export default ListIcon; diff --git a/frontend/src/assets/Dashboard/Table.tsx b/frontend/src/assets/Dashboard/Table.tsx index e1ade19994..60effdbfc0 100644 --- a/frontend/src/assets/Dashboard/Table.tsx +++ b/frontend/src/assets/Dashboard/Table.tsx @@ -1,18 +1,48 @@ -function Table(): JSX.Element { +import { CSSProperties } from 'react'; + +function TableIcon({ + fillColor, +}: { + fillColor: CSSProperties['color']; +}): JSX.Element { return ( + + + ); } -export default Table; +export default TableIcon; diff --git a/frontend/src/assets/Dashboard/TimeSeries.tsx b/frontend/src/assets/Dashboard/TimeSeries.tsx index a9ddbc5717..afa9b5f095 100644 --- a/frontend/src/assets/Dashboard/TimeSeries.tsx +++ b/frontend/src/assets/Dashboard/TimeSeries.tsx @@ -1,15 +1,66 @@ -function TimeSeries(): JSX.Element { +import { CSSProperties } from 'react'; + +function TimeSeries({ + fillColor, +}: { + fillColor: CSSProperties['color']; +}): JSX.Element { return ( + + + + + + ); diff --git a/frontend/src/assets/Dashboard/Value.tsx b/frontend/src/assets/Dashboard/Value.tsx index 708e16d6b9..39ef8d9f44 100644 --- a/frontend/src/assets/Dashboard/Value.tsx +++ b/frontend/src/assets/Dashboard/Value.tsx @@ -1,19 +1,29 @@ -function Value(): JSX.Element { +import { CSSProperties } from 'react'; + +function Value({ + fillColor, +}: { + fillColor: CSSProperties['color']; +}): JSX.Element { return ( ); diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 991fb4488e..399e1dffb2 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -8,6 +8,7 @@ import { VIEWS } from './constants'; export type LogDetailProps = { log: ILog | null; selectedTab: VIEWS; + isListViewPanel?: boolean; } & Pick & Partial> & Pick; diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index ffa1c07e9c..0794ead980 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -35,6 +35,7 @@ function LogDetail({ onAddToQuery, onClickActionItem, selectedTab, + isListViewPanel = false, }: LogDetailProps): JSX.Element { const [, copyToClipboard] = useCopyToClipboard(); const [selectedView, setSelectedView] = useState(selectedTab); @@ -190,6 +191,7 @@ function LogDetail({ logData={log} onAddToQuery={onAddToQuery} onClickActionItem={onClickActionItem} + isListViewPanel={isListViewPanel} /> )} {selectedView === VIEW_TYPES.JSON && } diff --git a/frontend/src/components/Logs/TableView/config.ts b/frontend/src/components/Logs/TableView/config.ts index c532981ad7..73b5f9a4c3 100644 --- a/frontend/src/components/Logs/TableView/config.ts +++ b/frontend/src/components/Logs/TableView/config.ts @@ -22,6 +22,10 @@ export const defaultTableStyle: CSSProperties = { maxWidth: '40rem', }; +export const defaultListViewPanelStyle: CSSProperties = { + maxWidth: '40rem', +}; + export const tableScroll: TableProps>['scroll'] = { x: true, }; diff --git a/frontend/src/components/Logs/TableView/types.ts b/frontend/src/components/Logs/TableView/types.ts index 35ef198ac6..3176101d9d 100644 --- a/frontend/src/components/Logs/TableView/types.ts +++ b/frontend/src/components/Logs/TableView/types.ts @@ -24,6 +24,7 @@ export type UseTableViewProps = { onClickExpand?: (log: ILog) => void; activeLog?: ILog | null; activeContextLog?: ILog | null; + isListViewPanel?: boolean; } & LogsTableViewProps; export type ActionsColumnProps = { diff --git a/frontend/src/components/Logs/TableView/useTableView.tsx b/frontend/src/components/Logs/TableView/useTableView.tsx index 5fad6e3896..9db2332635 100644 --- a/frontend/src/components/Logs/TableView/useTableView.tsx +++ b/frontend/src/components/Logs/TableView/useTableView.tsx @@ -13,7 +13,11 @@ import { useMemo } from 'react'; import LogStateIndicator, { LogType, } from '../LogStateIndicator/LogStateIndicator'; -import { defaultTableStyle, getDefaultCellStyle } from './config'; +import { + defaultListViewPanelStyle, + defaultTableStyle, + getDefaultCellStyle, +} from './config'; import { TableBodyContent } from './styles'; import { ColumnTypeRender, @@ -31,6 +35,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { appendTo = 'center', activeContextLog, activeLog, + isListViewPanel, } = props; const isDarkMode = useIsDarkMode(); @@ -48,7 +53,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { key: name, render: (field): ColumnTypeRender> => ({ props: { - style: getDefaultCellStyle(isDarkMode), + style: isListViewPanel + ? defaultListViewPanelStyle + : getDefaultCellStyle(isDarkMode), }, children: ( @@ -58,6 +65,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { }), })); + if (isListViewPanel) { + return [...fieldColumns]; + } + return [ { title: 'timestamp', @@ -110,6 +121,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { ]; }, [ fields, + isListViewPanel, appendTo, isDarkMode, linesPerRow, diff --git a/frontend/src/constants/panelTypes.ts b/frontend/src/constants/panelTypes.ts index 87cc62eae3..d16e5bf92d 100644 --- a/frontend/src/constants/panelTypes.ts +++ b/frontend/src/constants/panelTypes.ts @@ -1,6 +1,9 @@ import Uplot from 'components/Uplot'; import GridTableComponent from 'container/GridTableComponent'; import GridValueComponent from 'container/GridValueComponent'; +import LogsPanelComponent from 'container/LogsPanelTable/LogsPanelComponent'; +import TracesTableComponent from 'container/TracesTableComponent/TracesTableComponent'; +import { DataSource } from 'types/common/queryBuilder'; import { PANEL_TYPES } from './queryBuilder'; @@ -9,10 +12,27 @@ export const PANEL_TYPES_COMPONENT_MAP = { [PANEL_TYPES.VALUE]: GridValueComponent, [PANEL_TYPES.TABLE]: GridTableComponent, [PANEL_TYPES.TRACE]: null, - [PANEL_TYPES.LIST]: null, + [PANEL_TYPES.LIST]: LogsPanelComponent, [PANEL_TYPES.EMPTY_WIDGET]: null, } as const; +export const getComponentForPanelType = ( + panelType: PANEL_TYPES, + dataSource?: DataSource, +): React.ComponentType | null => { + const componentsMap = { + [PANEL_TYPES.TIME_SERIES]: Uplot, + [PANEL_TYPES.VALUE]: GridValueComponent, + [PANEL_TYPES.TABLE]: GridTableComponent, + [PANEL_TYPES.TRACE]: null, + [PANEL_TYPES.LIST]: + dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent, + [PANEL_TYPES.EMPTY_WIDGET]: null, + }; + + return componentsMap[panelType]; +}; + export const AVAILABLE_EXPORT_PANEL_TYPES = [ PANEL_TYPES.TIME_SERIES, PANEL_TYPES.TABLE, diff --git a/frontend/src/container/Controls/index.tsx b/frontend/src/container/Controls/index.tsx index 5618a7344a..c7389398ba 100644 --- a/frontend/src/container/Controls/index.tsx +++ b/frontend/src/container/Controls/index.tsx @@ -16,16 +16,21 @@ function Controls({ handleNavigatePrevious, handleNavigateNext, handleCountItemsPerPageChange, + isLogPanel = false, }: ControlsProps): JSX.Element | null { const isNextAndPreviousDisabled = useMemo( () => isLoading || countPerPage < 0 || totalCount === 0, [isLoading, countPerPage, totalCount], ); - const isPreviousDisabled = useMemo(() => offset <= 0, [offset]); - const isNextDisabled = useMemo(() => totalCount < countPerPage, [ - countPerPage, - totalCount, - ]); + const isPreviousDisabled = useMemo( + () => (isLogPanel ? false : offset <= 0 || isNextAndPreviousDisabled), + [isLogPanel, isNextAndPreviousDisabled, offset], + ); + const isNextDisabled = useMemo( + () => + isLogPanel ? false : totalCount < countPerPage || isNextAndPreviousDisabled, + [countPerPage, isLogPanel, isNextAndPreviousDisabled, totalCount], + ); return ( @@ -33,7 +38,7 @@ function Controls({ loading={isLoading} size="small" type="link" - disabled={isPreviousDisabled || isNextAndPreviousDisabled} + disabled={isPreviousDisabled} onClick={handleNavigatePrevious} > Previous @@ -42,7 +47,7 @@ function Controls({ loading={isLoading} size="small" type="link" - disabled={isNextDisabled || isNextAndPreviousDisabled} + disabled={isNextDisabled} onClick={handleNavigateNext} > Next @@ -68,6 +73,7 @@ function Controls({ Controls.defaultProps = { offset: 0, perPageOptions: DEFAULT_PER_PAGE_OPTIONS, + isLogPanel: false, }; export interface ControlsProps { @@ -79,6 +85,7 @@ export interface ControlsProps { handleNavigatePrevious: () => void; handleNavigateNext: () => void; handleCountItemsPerPageChange: (value: Pagination['limit']) => void; + isLogPanel?: boolean; } export default memo(Controls); diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss index 31ec0040d8..9efb621385 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss @@ -10,7 +10,6 @@ .graph-container { height: calc(60% - 40px); min-height: 300px; - border: 1px solid #333; width: 100%; padding: 12px; box-sizing: border-box; @@ -18,6 +17,11 @@ border-radius: 3px; } + .list-graph-container { + height: calc(100% - 40px); + overflow-y: auto; + } + .disabled { height: calc(100% - 65px); } diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index 786ef738d6..feaf19ffad 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -2,9 +2,11 @@ import './WidgetFullView.styles.scss'; import { SyncOutlined } from '@ant-design/icons'; import { Button } from 'antd'; +import cx from 'classnames'; import { ToggleGraphProps } from 'components/Graph/types'; import Spinner from 'components/Spinner'; import TimePreference from 'components/TimePreferenceDropDown'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import GridPanelSwitch from 'container/GridPanelSwitch'; import { timeItems, @@ -96,7 +98,7 @@ function FullView({ }, { queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`, - enabled: !isDependedDataLoaded, + enabled: !isDependedDataLoaded && widget.panelTypes !== PANEL_TYPES.LIST, // Internally both the list view panel has it's own query range api call, so we don't need to call it again }, ); @@ -164,6 +166,8 @@ function FullView({ parentGraphVisibilityState(graphsVisibilityStates); }, [graphsVisibilityStates, parentGraphVisibilityState]); + const isListView = widget.panelTypes === PANEL_TYPES.LIST; + if (response.isFetching) { return ; } @@ -192,14 +196,17 @@ function FullView({
{chartOptions && ( )} diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index 0af1b9474f..a07a4197e9 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -5,6 +5,7 @@ import cx from 'classnames'; import { ToggleGraphProps } from 'components/Graph/types'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import GridPanelSwitch from 'container/GridPanelSwitch'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; @@ -47,6 +48,7 @@ function WidgetGraphComponent({ onClickHandler, onDragSelect, setGraphVisibility, + isFetchingResponse, }: WidgetGraphComponentProps): JSX.Element { const [deleteModal, setDeleteModal] = useState(false); const [hovered, setHovered] = useState(false); @@ -222,7 +224,11 @@ function WidgetGraphComponent({ }); }; - if (queryResponse.isLoading || queryResponse.status === 'idle') { + const loadingState = + (queryResponse.isLoading || queryResponse.status === 'idle') && + widget.panelTypes !== PANEL_TYPES.LIST; + + if (loadingState) { return (
{queryResponse.isLoading && } - {queryResponse.isSuccess && ( + {(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
)} diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx index c5dd100a03..d377282cdb 100644 --- a/frontend/src/container/GridCardLayout/GridCard/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -116,6 +116,12 @@ function GridCardGraph({ const isEmptyWidget = widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget); + const queryEnabledCondition = + isVisible && + !isEmptyWidget && + isQueryEnabled && + widget.panelTypes !== PANEL_TYPES.LIST; + const queryResponse = useGetQueryRange( { selectedTime: widget?.timePreferance, @@ -135,7 +141,7 @@ function GridCardGraph({ widget.timePreferance, ], keepPreviousData: true, - enabled: isVisible && !isEmptyWidget && isQueryEnabled, + enabled: queryEnabledCondition, refetchOnMount: false, onError: (error) => { setErrorMessage(error.message); @@ -159,7 +165,8 @@ function GridCardGraph({ const isDarkMode = useIsDarkMode(); const menuList = - widget.panelTypes === PANEL_TYPES.TABLE + widget.panelTypes === PANEL_TYPES.TABLE || + widget.panelTypes === PANEL_TYPES.LIST ? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts) : headerMenuList; @@ -222,6 +229,7 @@ function GridCardGraph({ onClickHandler={onClickHandler} graphVisibiltyState={graphVisibility} setGraphVisibility={setGraphVisibility} + isFetchingResponse={queryResponse.isFetching} /> )} diff --git a/frontend/src/container/GridCardLayout/GridCard/types.ts b/frontend/src/container/GridCardLayout/GridCard/types.ts index d71b529207..59711ef9e5 100644 --- a/frontend/src/container/GridCardLayout/GridCard/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/types.ts @@ -30,6 +30,7 @@ export interface WidgetGraphComponentProps extends UplotProps { isWarning: boolean; graphVisibiltyState: boolean[]; setGraphVisibility: Dispatch>; + isFetchingResponse: boolean; } export interface GridCardGraphProps { diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss b/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss index f65cda37cc..c8b1c72897 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss +++ b/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss @@ -16,8 +16,28 @@ } } +.widget-full-view { + .ant-modal-content { + background-color: var(--bg-ink-400); + + .ant-modal-header { + background-color: var(--bg-ink-400); + } + } +} + .lightMode { .fullscreen-grid-container { background-color: rgb(250, 250, 250); } + + .widget-full-view { + .ant-modal-content { + background-color: var(--bg-vanilla-100); + } + + .ant-modal-header { + background-color: var(--bg-vanilla-100); + } + } } diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss index 799f0ed2f2..2fcb3e8e6f 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss +++ b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss @@ -2,7 +2,7 @@ display: flex; justify-content: space-between; align-items: center; - height: 40px; + height: 30px; width: 100%; padding: 0.5rem; box-sizing: border-box; diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index 820c3b8a07..63312a5225 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -45,6 +45,7 @@ interface IWidgetHeaderProps { threshold?: ReactNode; headerMenuList?: MenuItemKeys[]; isWarning: boolean; + isFetchingResponse: boolean; } function WidgetHeader({ @@ -59,6 +60,7 @@ function WidgetHeader({ threshold, headerMenuList, isWarning, + isFetchingResponse, }: IWidgetHeaderProps): JSX.Element | null { const onEditHandler = useCallback((): void => { const widgetId = widget.id; @@ -170,7 +172,7 @@ function WidgetHeader({
{threshold}
- {queryResponse.isFetching && !queryResponse.isError && ( + {isFetchingResponse && !queryResponse.isError && ( )} {queryResponse.isError && ( diff --git a/frontend/src/container/GridCardLayout/styles.ts b/frontend/src/container/GridCardLayout/styles.ts index 273a9a8035..0d79ea0f3b 100644 --- a/frontend/src/container/GridCardLayout/styles.ts +++ b/frontend/src/container/GridCardLayout/styles.ts @@ -17,7 +17,7 @@ export const Card = styled(CardComponent)` } .ant-card-body { - height: calc(100% - 40px); + height: calc(100% - 30px); padding: 0; } `; diff --git a/frontend/src/container/GridPanelSwitch/index.tsx b/frontend/src/container/GridPanelSwitch/index.tsx index f054fabb5e..09d133f0a0 100644 --- a/frontend/src/container/GridPanelSwitch/index.tsx +++ b/frontend/src/container/GridPanelSwitch/index.tsx @@ -1,8 +1,9 @@ import { ToggleGraphProps } from 'components/Graph/types'; -import { PANEL_TYPES_COMPONENT_MAP } from 'constants/panelTypes'; +import { getComponentForPanelType } from 'constants/panelTypes'; import { PANEL_TYPES } from 'constants/queryBuilder'; import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config'; import { FC, forwardRef, memo, useMemo } from 'react'; +import { DataSource } from 'types/common/queryBuilder'; import { GridPanelSwitchProps, PropsTypePropsMap } from './types'; @@ -11,7 +12,19 @@ const GridPanelSwitch = forwardRef< GridPanelSwitchProps >( ( - { panelType, data, yAxisUnit, panelData, query, options, thresholds }, + { + panelType, + data, + yAxisUnit, + panelData, + query, + options, + thresholds, + selectedLogFields, + selectedTracesFields, + dataSource, + selectedTime, + }, ref, ): JSX.Element | null => { const currentProps: PropsTypePropsMap = useMemo(() => { @@ -32,15 +45,38 @@ const GridPanelSwitch = forwardRef< query, thresholds, }, - [PANEL_TYPES.LIST]: null, + [PANEL_TYPES.LIST]: + dataSource === DataSource.LOGS + ? { + selectedLogsFields: selectedLogFields || [], + query, + selectedTime, + } + : { + selectedTracesFields: selectedTracesFields || [], + query, + selectedTime, + }, [PANEL_TYPES.TRACE]: null, [PANEL_TYPES.EMPTY_WIDGET]: null, }; return result; - }, [data, options, ref, yAxisUnit, thresholds, panelData, query]); + }, [ + data, + options, + ref, + yAxisUnit, + thresholds, + panelData, + query, + dataSource, + selectedLogFields, + selectedTime, + selectedTracesFields, + ]); - const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC< + const Component = getComponentForPanelType(panelType, dataSource) as FC< PropsTypePropsMap[typeof panelType] >; const componentProps = useMemo(() => currentProps[panelType], [ diff --git a/frontend/src/container/GridPanelSwitch/types.ts b/frontend/src/container/GridPanelSwitch/types.ts index df09d4b59d..8f8f0a2a1d 100644 --- a/frontend/src/container/GridPanelSwitch/types.ts +++ b/frontend/src/container/GridPanelSwitch/types.ts @@ -2,11 +2,15 @@ import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types'; import { UplotProps } from 'components/Uplot/Uplot'; import { GridTableComponentProps } from 'container/GridTableComponent/types'; import { GridValueComponentProps } from 'container/GridValueComponent/types'; +import { LogsPanelComponentProps } from 'container/LogsPanelTable/LogsPanelComponent'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; +import { TracesTableComponentProps } from 'container/TracesTableComponent/TracesTableComponent'; import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin'; import { ForwardedRef } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { QueryDataV3 } from 'types/api/widgets/getQuery'; +import { DataSource } from 'types/common/queryBuilder'; import uPlot from 'uplot'; import { PANEL_TYPES } from '../../constants/queryBuilder'; @@ -23,6 +27,10 @@ export type GridPanelSwitchProps = { panelData: QueryDataV3[]; query: Query; thresholds?: Widgets['thresholds']; + dataSource?: DataSource; + selectedLogFields?: Widgets['selectedLogFields']; + selectedTracesFields?: Widgets['selectedTracesFields']; + selectedTime?: timePreferance; }; export type PropsTypePropsMap = { @@ -32,6 +40,6 @@ export type PropsTypePropsMap = { [PANEL_TYPES.VALUE]: GridValueComponentProps; [PANEL_TYPES.TABLE]: GridTableComponentProps; [PANEL_TYPES.TRACE]: null; - [PANEL_TYPES.LIST]: null; + [PANEL_TYPES.LIST]: LogsPanelComponentProps | TracesTableComponentProps; [PANEL_TYPES.EMPTY_WIDGET]: null; }; diff --git a/frontend/src/container/LogDetailedView/Overview.tsx b/frontend/src/container/LogDetailedView/Overview.tsx index b25423b4cb..ff2a22fa09 100644 --- a/frontend/src/container/LogDetailedView/Overview.tsx +++ b/frontend/src/container/LogDetailedView/Overview.tsx @@ -22,6 +22,7 @@ import TableView from './TableView'; interface OverviewProps { logData: ILog; + isListViewPanel?: boolean; } type Props = OverviewProps & @@ -32,6 +33,7 @@ function Overview({ logData, onAddToQuery, onClickActionItem, + isListViewPanel = false, }: Props): JSX.Element { const [isWrapWord, setIsWrapWord] = useState(false); const [isSearchVisible, setIsSearchVisible] = useState(false); @@ -199,6 +201,7 @@ function Overview({ onAddToQuery={onAddToQuery} fieldSearchInput={fieldSearchInput} onClickActionItem={onClickActionItem} + isListViewPanel={isListViewPanel} /> ), @@ -210,4 +213,8 @@ function Overview({ ); } +Overview.defaultProps = { + isListViewPanel: false, +}; + export default Overview; diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 21774291f5..2095051fc7 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -40,6 +40,7 @@ const RESTRICTED_FIELDS = ['timestamp']; interface TableViewProps { logData: ILog; fieldSearchInput: string; + isListViewPanel?: boolean; } type Props = TableViewProps & @@ -51,6 +52,7 @@ function TableView({ fieldSearchInput, onAddToQuery, onClickActionItem, + isListViewPanel = false, }: Props): JSX.Element | null { const dispatch = useDispatch>(); const [isfilterInLoading, setIsFilterInLoading] = useState(false); @@ -218,38 +220,45 @@ function TableView({ {removeEscapeCharacters(fieldData.value)} - - -
); }, @@ -268,6 +277,10 @@ function TableView({ ); } +TableView.defaultProps = { + isListViewPanel: false, +}; + interface DataType { key: string; field: string; diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx index e1e7ca514d..b6d30f23ef 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx @@ -68,6 +68,7 @@ const InfinityTable = forwardRef( activeLog, activeContextLog, }); + const { draggedColumns, onDragColumns } = useDragColumns< Record >(LOCALSTORAGE.LOGS_LIST_COLUMNS); diff --git a/frontend/src/container/LogsPanelTable/LogsPanelComponent.styles.scss b/frontend/src/container/LogsPanelTable/LogsPanelComponent.styles.scss new file mode 100644 index 0000000000..96319b3ae2 --- /dev/null +++ b/frontend/src/container/LogsPanelTable/LogsPanelComponent.styles.scss @@ -0,0 +1,80 @@ +.logs-table { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + + .resize-table { + height: calc(92% - 5px); + overflow: scroll; + + .ant-table-wrapper .ant-table-tbody >tr >td { + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; + font-family: Inter; + .ant-typography { + background-color: transparent; + color: var(--bg-vanilla-100); + margin-bottom: 0; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; + font-family: Inter; + } + padding: 14px 8px; + border: none; + cursor: pointer; + } + + .ant-table-wrapper .ant-table-header { + border-bottom: 0.5px solid var(--bg-slate-400); + } + + .ant-table-wrapper .ant-table-thead > tr > th { + font-family: Inter; + color: var(--bg-vanilla-100); + background-color: transparent; + border: none; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 22px; + letter-spacing: 0.5px; + padding: 8px; + } + + .ant-table-wrapper .ant-table-thead > tr > th::before { + display: none; + } + } + + .controller { + position: absolute; + bottom: 5px; + right: 10px; + } +} + +.lightMode { + .logs-table { + .resize-table { + .ant-table-wrapper .ant-table-tbody >tr >td { + background-color: var(--bg-vanilla-100); + .ant-typography { + color: var(--bg-ink-500); + } + } + .ant-table-wrapper .ant-table-thead > tr > th { + background-color: var(--bg-vanilla-100); + color: var(--bg-ink-500); + } + + .ant-table-wrapper .ant-table-header { + border-bottom: 0.5px solid var(--bg-vanilla-400); + } + } + } +} \ No newline at end of file diff --git a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx new file mode 100644 index 0000000000..64be38c035 --- /dev/null +++ b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx @@ -0,0 +1,311 @@ +import './LogsPanelComponent.styles.scss'; + +import { Table } from 'antd'; +import LogDetail from 'components/LogDetail'; +import { VIEW_TYPES } from 'components/LogDetail/constants'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import Controls from 'container/Controls'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; +import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs'; +import { tableStyles } from 'container/TracesExplorer/ListView/styles'; +import { useActiveLog } from 'hooks/logs/useActiveLog'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { Pagination } from 'hooks/queryPagination'; +import { useLogsData } from 'hooks/useLogsData'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; +import { FlatLogData } from 'lib/logs/flatLogData'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { + HTMLAttributes, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { v4 as uuid } from 'uuid'; + +import { getLogPanelColumnsList } from './utils'; + +function LogsPanelComponent({ + selectedLogsFields, + query, + selectedTime, +}: LogsPanelComponentProps): JSX.Element { + const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const [pagination, setPagination] = useState({ + offset: 0, + limit: query.builder.queryData[0].limit || 0, + }); + + const [requestData, setRequestData] = useState(() => { + const updatedQuery = { ...query }; + updatedQuery.builder.queryData[0].pageSize = 10; + return { + query: updatedQuery, + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval: globalSelectedTime, + tableParams: { + pagination, + }, + }; + }); + + useEffect(() => { + setRequestData({ + ...requestData, + globalSelectedInterval: globalSelectedTime, + tableParams: { + pagination, + }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pagination]); + + const [pageSize, setPageSize] = useState(10); + const { selectedDashboard } = useDashboard(); + + const handleChangePageSize = (value: number): void => { + setPagination({ + ...pagination, + limit: 0, + offset: value, + }); + setPageSize(value); + const newQueryData = { ...requestData.query }; + newQueryData.builder.queryData[0].pageSize = value; + const newRequestData = { + ...requestData, + query: newQueryData, + tableParams: { + pagination, + }, + }; + setRequestData(newRequestData); + }; + + const { data, isFetching, isError } = useGetQueryRange( + { + ...requestData, + globalSelectedInterval: globalSelectedTime, + selectedTime: selectedTime?.enum || 'GLOBAL_TIME', + variables: getDashboardVariables(selectedDashboard?.data.variables), + }, + { + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + globalSelectedTime, + maxTime, + minTime, + requestData, + pagination, + selectedDashboard?.data.variables, + ], + enabled: !!requestData.query && !!selectedLogsFields?.length, + }, + ); + + const columns = getLogPanelColumnsList(selectedLogsFields); + + const dataLength = + data?.payload?.data?.newResult?.data?.result[0]?.list?.length; + const totalCount = useMemo(() => dataLength || 0, [dataLength]); + + const [firstLog, setFirstLog] = useState(); + const [lastLog, setLastLog] = useState(); + + const { logs } = useLogsData({ + result: data?.payload.data.newResult.data.result, + panelType: PANEL_TYPES.LIST, + stagedQuery: query, + }); + + useEffect(() => { + if (logs.length) { + setFirstLog(logs[0]); + setLastLog(logs[logs.length - 1]); + } + }, [logs]); + + const flattenLogData = useMemo( + () => logs.map((log) => FlatLogData(log) as RowData), + [logs], + ); + + const { + activeLog, + onSetActiveLog, + onClearActiveLog, + onAddToQuery, + } = useActiveLog(); + + const handleRow = useCallback( + (record: RowData): HTMLAttributes => ({ + onClick: (): void => { + const log = logs.find((item) => item.id === record.id); + if (log) onSetActiveLog(log); + }, + }), + [logs, onSetActiveLog], + ); + + const isOrderByTimeStamp = + query.builder.queryData[0].orderBy.length > 0 && + query.builder.queryData[0].orderBy[0].columnName === 'timestamp'; + + const handlePreviousPagination = (): void => { + if (isOrderByTimeStamp) { + setRequestData({ + ...requestData, + query: { + ...requestData.query, + builder: { + ...requestData.query.builder, + queryData: [ + { + ...requestData.query.builder.queryData[0], + filters: { + ...requestData.query.builder.queryData[0].filters, + items: [ + { + id: uuid(), + key: { + key: 'id', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: OPERATORS['>'], + value: firstLog?.id || '', + }, + ], + }, + }, + ], + }, + }, + }); + return; + } + setPagination({ + ...pagination, + limit: 0, + offset: pagination.offset - pageSize, + }); + }; + + const handleNextPagination = (): void => { + if (isOrderByTimeStamp) { + setRequestData({ + ...requestData, + query: { + ...requestData.query, + builder: { + ...requestData.query.builder, + queryData: [ + { + ...requestData.query.builder.queryData[0], + filters: { + ...requestData.query.builder.queryData[0].filters, + items: [ + { + id: uuid(), + key: { + key: 'id', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: OPERATORS['<'], + value: lastLog?.id || '', + }, + ], + }, + }, + ], + }, + }, + }); + return; + } + setPagination({ + ...pagination, + limit: 0, + offset: pagination.offset + pageSize, + }); + }; + + if (isError) { + return
{SOMETHING_WENT_WRONG}
; + } + + return ( + <> +
+
+ + + {!query.builder.queryData[0].limit && ( +
+ +
+ )} + + + + ); +} + +export type LogsPanelComponentProps = { + selectedLogsFields: Widgets['selectedLogFields']; + query: Query; + selectedTime?: timePreferance; +}; + +LogsPanelComponent.defaultProps = { + selectedTime: undefined, +}; + +export default LogsPanelComponent; diff --git a/frontend/src/container/LogsPanelTable/utils.tsx b/frontend/src/container/LogsPanelTable/utils.tsx new file mode 100644 index 0000000000..46701e763b --- /dev/null +++ b/frontend/src/container/LogsPanelTable/utils.tsx @@ -0,0 +1,38 @@ +import { ColumnsType } from 'antd/es/table'; +import { Typography } from 'antd/lib'; +// import Typography from 'antd/es/typography/Typography'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { ReactNode } from 'react'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { IField } from 'types/api/logs/fields'; + +export const getLogPanelColumnsList = ( + selectedLogFields: Widgets['selectedLogFields'], +): ColumnsType => { + const initialColumns: ColumnsType = []; + + const columns: ColumnsType = + selectedLogFields?.map((field: IField) => { + const { name } = field; + return { + title: name, + dataIndex: name, + key: name, + width: name === 'body' ? 350 : 100, + render: (value: ReactNode): JSX.Element => { + if (name === 'body') { + return ( + + {value} + + ); + } + + return {value}; + }, + responsive: ['md'], + }; + }) || []; + + return [...initialColumns, ...columns]; +}; diff --git a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts index 20becbb810..b974972d70 100644 --- a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts +++ b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts @@ -22,4 +22,6 @@ export const getWidgetQueryBuilder = ({ yAxisUnit, softMax: null, softMin: null, + selectedLogFields: [], + selectedTracesFields: [], }); diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts new file mode 100644 index 0000000000..44512e3a00 --- /dev/null +++ b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts @@ -0,0 +1,88 @@ +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; + +export const PANEL_TYPES_INITIAL_QUERY = { + [PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics, + [PANEL_TYPES.VALUE]: initialQueriesMap.metrics, + [PANEL_TYPES.TABLE]: initialQueriesMap.metrics, + [PANEL_TYPES.LIST]: initialQueriesMap.logs, + [PANEL_TYPES.TRACE]: initialQueriesMap.traces, + [PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics, +}; + +export const listViewInitialLogQuery: Query = { + ...initialQueriesMap.logs, + builder: { + ...initialQueriesMap.logs.builder, + queryData: [ + { + ...initialQueriesMap.logs.builder.queryData[0], + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + offset: 0, + pageSize: 100, + }, + ], + }, +}; + +export const listViewInitialTraceQuery = { + // it should be the above commented query + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [ + { + ...initialQueriesMap.traces.builder.queryData[0], + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + offset: 0, + pageSize: 10, + selectColumns: [ + { + key: 'serviceName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'serviceName--string--tag--true', + }, + { + key: 'name', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'name--string--tag--true', + }, + { + key: 'durationNano', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'durationNano--float64--tag--true', + }, + { + key: 'httpMethod', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'httpMethod--string--tag--true', + }, + { + key: 'responseStatusCode', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'responseStatusCode--string--tag--true', + }, + ] as BaseAutocompleteData[], + }, + ], + }, +}; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index d355edfd1a..80f1e745da 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,6 +1,6 @@ import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; @@ -8,8 +8,14 @@ import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { CSSProperties } from 'react'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; import { v4 as uuid } from 'uuid'; +import { + listViewInitialLogQuery, + listViewInitialTraceQuery, + PANEL_TYPES_INITIAL_QUERY, +} from './constants'; import menuItems from './menuItems'; import { Card, Container, Text } from './styles'; @@ -28,6 +34,7 @@ function DashboardGraphSlider(): JSX.Element { const updateDashboardMutation = useUpdateDashboard(); + // eslint-disable-next-line sonarjs/cognitive-complexity const onClickHandler = (name: PANEL_TYPES) => (): void => { const id = uuid(); @@ -61,10 +68,28 @@ function DashboardGraphSlider(): JSX.Element { nullZeroValues: '', opacity: '', panelTypes: name, - query: initialQueriesMap.metrics, + query: + name === PANEL_TYPES.LIST + ? listViewInitialLogQuery + : PANEL_TYPES_INITIAL_QUERY[name], timePreferance: 'GLOBAL_TIME', softMax: null, softMin: null, + selectedLogFields: [ + { + dataType: 'string', + type: '', + name: 'body', + }, + { + dataType: 'string', + type: '', + name: 'timestamp', + }, + ], + selectedTracesFields: [ + ...listViewInitialTraceQuery.builder.queryData[0].selectColumns, + ], }, ], }, @@ -73,16 +98,43 @@ function DashboardGraphSlider(): JSX.Element { onSuccess: (data) => { if (data.payload) { handleToggleDashboardSlider(false); + const queryParamsLog = { + graphType: name, + widgetId: id, + [QueryParams.compositeQuery]: JSON.stringify({ + ...PANEL_TYPES_INITIAL_QUERY[name], + builder: { + ...PANEL_TYPES_INITIAL_QUERY[name].builder, + queryData: [ + { + ...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0], + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + offset: 0, + pageSize: 100, + }, + ], + }, + }), + }; const queryParams = { graphType: name, widgetId: id, - [QueryParams.compositeQuery]: JSON.stringify(initialQueriesMap.metrics), + [QueryParams.compositeQuery]: JSON.stringify( + PANEL_TYPES_INITIAL_QUERY[name], + ), }; - history.push( - `${history.location.pathname}/new?${createQueryParams(queryParams)}`, - ); + if (name === PANEL_TYPES.LIST) { + history.push( + `${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`, + ); + } else { + history.push( + `${history.location.pathname}/new?${createQueryParams(queryParams)}`, + ); + } } }, onError: () => { diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts index 560da8deef..1aaa3a71ea 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts @@ -1,3 +1,4 @@ +import List from 'assets/Dashboard/List'; import TableIcon from 'assets/Dashboard/Table'; import TimeSeriesIcon from 'assets/Dashboard/TimeSeries'; import ValueIcon from 'assets/Dashboard/Value'; @@ -16,6 +17,7 @@ const Items: ItemsProps[] = [ display: 'Value', }, { name: PANEL_TYPES.TABLE, Icon: TableIcon, display: 'Table' }, + { name: PANEL_TYPES.LIST, Icon: List, display: 'List' }, ]; interface ItemsProps { diff --git a/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss new file mode 100644 index 0000000000..f9652e859a --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss @@ -0,0 +1,184 @@ +.explorer-columns-renderer { + margin-top: 10px; + + .title { + display: flex; + align-items: center; + gap: 4px; + } + + .ant-typography { + color: var(rgba(255, 255, 255, 0.85)); + font-family: "Inter"; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 22px; + letter-spacing: 0.5px; + } + + .ant-divider { + margin: 8px 0 !important; + border: 0.5px solid var(--bg-slate-400); + } + + .explorer-columns-contents { + display: flex; + justify-content: space-between; + align-items: center; + + .explorer-columns { + display: flex; + align-items: center; + gap: 12px; + overflow-x: scroll; + min-width: 90%; + + .explorer-columns-list { + display: flex !important; + } + + .explorer-column-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px; + min-width: 200px; + border-radius: 2px; + border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); + background: var(--bg-slate-500); + cursor: unset; + + .explorer-column-title { + display: flex; + align-items: center; + gap: 8px; + font-family: Inter; + font-size: 12px; + cursor: grab; + } + + .lucide-trash2 { + cursor: pointer !important; + } + + } + } + + .explorer-columns::-webkit-scrollbar { + height: 0px; /* Height of the scrollbar */ + } + + .action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0px 16px; + border-radius: 2px; + background: var(--bg-robin-400); + } + } +} + +.explorer-columns-search { + border: 1px solid rgba(118, 136, 201, 0.12); + border-radius: 6px; + padding: 0px; + background:#141414; + > input { + height: 32px; + padding: 0 6px; + } + + +} + +.explorer-columns-dropdown { + height: 200px; + background-color: var(--bg-slate-500); + overflow: hidden !important; + .ant-dropdown-menu { + padding: 0; + + .ant-dropdown-menu-item { + padding: 4px; + .ant-checkbox-wrapper { + padding: 2px 8px !important; + } + + .attribute-columns { + display: flex; + flex-direction: column; + height: 160px; + overflow: scroll; + } + + .attribute-columns::-webkit-scrollbar { + width: 3px; /* Width of the scrollbar */ + } + + .attribute-columns::-webkit-scrollbar-track { + background: var(--bg-slate-500); /* Color of the track */ + } + + .attribute-columns::-webkit-scrollbar-thumb { + background: var(--bg-vanilla-400); /* Color of the thumb */ + border-radius: 4px; /* Roundness of the thumb */ + } + + .attribute-columns::-webkit-scrollbar-thumb:hover { + background: var(--bg-vanilla-300); /* Color of the thumb on hover */ + } + } + } +} + +.lightMode { + .explorer-columns-renderer { + + .ant-divider { + border: 0.5px solid var(--bg-vanilla-300); + } + + .explorer-columns { + .explorer-column-card { + border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); + background: var(--bg-vanilla-200); + } + } + + .explorer-columns-search { + border: 1px solid rgba(118, 136, 201, 0.12); + } + } + + .explorer-columns-dropdown { + background-color: var(--bg-vanilla-100); + + .ant-dropdown-menu-item { + .attribute-columns { + &::-webkit-scrollbar { + width: 3px; /* Width of the scrollbar */ + } + + &::-webkit-scrollbar-track { + background: var(--bg-vanilla-200); /* Color of the track */ + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-vanilla-400); /* Color of the thumb */ + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-vanilla-300); /* Color of the thumb on hover */ + } + } + } + } + + .explorer-columns-search { + background: var(--bg-vanilla-100); + } +} diff --git a/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx new file mode 100644 index 0000000000..a9a8d9ceb2 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx @@ -0,0 +1,328 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +/* eslint-disable react/jsx-props-no-spreading */ +import './ExplorerColumnsRenderer.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { + Button, + Checkbox, + Divider, + Dropdown, + Input, + Tooltip, + Typography, +} from 'antd'; +import { MenuProps } from 'antd/lib'; +import Spinner from 'components/Spinner'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { + AlertCircle, + GripVertical, + PlusCircle, + Search, + Trash2, +} from 'lucide-react'; +import { useState } from 'react'; +import { + DragDropContext, + Draggable, + Droppable, + DropResult, +} from 'react-beautiful-dnd'; +import { DataSource } from 'types/common/queryBuilder'; + +import { WidgetGraphProps } from '../types'; + +type LogColumnsRendererProps = { + setSelectedLogFields: WidgetGraphProps['setSelectedLogFields']; + selectedLogFields: WidgetGraphProps['selectedLogFields']; + selectedTracesFields: WidgetGraphProps['selectedTracesFields']; + setSelectedTracesFields: WidgetGraphProps['setSelectedTracesFields']; +}; + +function ExplorerColumnsRenderer({ + selectedLogFields, + setSelectedLogFields, + selectedTracesFields, + setSelectedTracesFields, +}: LogColumnsRendererProps): JSX.Element { + const { currentQuery } = useQueryBuilder(); + const [searchText, setSearchText] = useState(''); + const [open, setOpen] = useState(false); + + const initialDataSource = currentQuery.builder.queryData[0].dataSource; + + const { data, isLoading, isError } = useGetAggregateKeys( + { + aggregateAttribute: '', + dataSource: currentQuery.builder.queryData[0].dataSource, + aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator, + searchText: '', + tagType: '', + }, + { + queryKey: [ + currentQuery.builder.queryData[0].dataSource, + currentQuery.builder.queryData[0].aggregateOperator, + ], + }, + ); + + const isAttributeKeySelected = (key: string): boolean => { + if (initialDataSource === DataSource.LOGS && selectedLogFields) { + return selectedLogFields.some((field) => field.name === key); + } + if (initialDataSource === DataSource.TRACES && selectedTracesFields) { + return selectedTracesFields.some((field) => field.key === key); + } + return false; + }; + + const handleCheckboxChange = (key: string): void => { + if ( + initialDataSource === DataSource.LOGS && + setSelectedLogFields !== undefined + ) { + if (selectedLogFields) { + if (isAttributeKeySelected(key)) { + setSelectedLogFields( + selectedLogFields.filter((field) => field.name !== key), + ); + } else { + setSelectedLogFields([ + ...selectedLogFields, + { dataType: 'string', name: key, type: '' }, + ]); + } + } else { + setSelectedLogFields([{ dataType: 'string', name: key, type: '' }]); + } + } else if ( + initialDataSource === DataSource.TRACES && + setSelectedTracesFields !== undefined + ) { + const selectedField = data?.payload?.attributeKeys?.find( + (attributeKey) => attributeKey.key === key, + ); + if (selectedTracesFields) { + if (isAttributeKeySelected(key)) { + setSelectedTracesFields( + selectedTracesFields.filter((field) => field.key !== key), + ); + } else if (selectedField) { + setSelectedTracesFields([...selectedTracesFields, selectedField]); + } + } else if (selectedField) setSelectedTracesFields([selectedField]); + } + setOpen(false); + }; + + const handleSearchChange = (e: React.ChangeEvent): void => { + setSearchText(e.target.value); + }; + + const items: MenuProps['items'] = [ + { + key: 'search', + label: ( + } + /> + ), + }, + { + key: 'columns', + label: ( +
+ {data?.payload?.attributeKeys + ?.filter((attributeKey) => + attributeKey.key.toLowerCase().includes(searchText.toLowerCase()), + ) + ?.map((attributeKey) => ( + handleCheckboxChange(attributeKey.key)} + style={{ padding: 0 }} + key={attributeKey.key} + > + {attributeKey.key} + + ))} +
+ ), + }, + ]; + + const removeSelectedLogField = (name: string): void => { + if ( + initialDataSource === DataSource.LOGS && + setSelectedLogFields && + selectedLogFields + ) { + setSelectedLogFields( + selectedLogFields.filter((field) => field.name !== name), + ); + } + if ( + initialDataSource === DataSource.TRACES && + setSelectedTracesFields && + selectedTracesFields + ) { + setSelectedTracesFields( + selectedTracesFields.filter((field) => field.key !== name), + ); + } + }; + + const onDragEnd = (result: DropResult): void => { + if (!result.destination) { + return; + } + + if ( + initialDataSource === DataSource.LOGS && + selectedLogFields && + setSelectedLogFields + ) { + const items = [...selectedLogFields]; + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + setSelectedLogFields(items); + } + if ( + initialDataSource === DataSource.TRACES && + selectedTracesFields && + setSelectedTracesFields + ) { + const items = [...selectedTracesFields]; + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + setSelectedTracesFields(items); + } + }; + + const toggleDropdown = (): void => { + setOpen(!open); + if (!open) { + setSearchText(''); + } + }; + + const isDarkMode = useIsDarkMode(); + + if (isLoading) { + return ; + } + + return ( +
+
+ Columns + {isError && ( + + + + )} +
+ + {!isError && ( +
+ + + {(provided): JSX.Element => ( +
+ {initialDataSource === DataSource.LOGS && + selectedLogFields && + selectedLogFields.map((field, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {(dragProvided): JSX.Element => ( +
+
+ + {field.name} +
+ removeSelectedLogField(field.name)} + /> +
+ )} +
+ ))} + {initialDataSource === DataSource.TRACES && + selectedTracesFields && + selectedTracesFields.map((field, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {(dragProvided): JSX.Element => ( +
+
+ + {field.key} +
+ removeSelectedLogField(field.key)} + /> +
+ )} +
+ ))} +
+ )} +
+
+
+ +
+
+ )} +
+ ); +} + +export default ExplorerColumnsRenderer; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index 9fe20bd8bc..4c66c70b83 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -18,7 +18,7 @@ import { getPreviousWidgets, getSelectedWidgetIndex, } from 'providers/Dashboard/util'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { Widgets } from 'types/api/dashboard/getAll'; @@ -35,7 +35,6 @@ function QuerySection({ selectedTime, }: QueryProps): JSX.Element { const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); - const [currentTab, setCurrentTab] = useState(currentQuery.queryType); const urlQuery = useUrlQuery(); const { minTime, maxTime } = useSelector( @@ -100,7 +99,6 @@ function QuerySection({ ], }, }); - redirectWithQueryBuilderData(updatedQuery); }, [ @@ -114,11 +112,13 @@ function QuerySection({ ); const handleQueryCategoryChange = (qCategory: string): void => { - const currentQueryType = qCategory as EQueryType; - setCurrentTab(qCategory as EQueryType); + const currentQueryType = qCategory; featureResponse.refetch().then(() => { - handleStageQuery({ ...currentQuery, queryType: currentQueryType }); + handleStageQuery({ + ...currentQuery, + queryType: currentQueryType as EQueryType, + }); }); }; @@ -134,6 +134,27 @@ function QuerySection({ return config; }, []); + const listItems = [ + { + key: EQueryType.QUERY_BUILDER, + label: ( + + + + ), + tab: Query Builder, + children: ( + + ), + }, + ]; + const items = [ { key: EQueryType.QUERY_BUILDER, @@ -180,8 +201,12 @@ function QuerySection({ @@ -197,7 +222,7 @@ function QuerySection({ } - items={items} + items={selectedGraph === PANEL_TYPES.LIST ? listItems : items} /> ); diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx index 219d4e011b..99da2e517e 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx @@ -10,7 +10,7 @@ interface IPlotTagProps { } function PlotTag({ queryType, panelType }: IPlotTagProps): JSX.Element | null { - if (queryType === undefined) { + if (queryType === undefined || panelType === PANEL_TYPES.LIST) { return null; } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx index c3d47880d3..2472d94092 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx @@ -1,5 +1,6 @@ import { Card, Typography } from 'antd'; import Spinner from 'components/Spinner'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { WidgetGraphProps } from 'container/NewWidget/types'; import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange'; import useUrlQuery from 'hooks/useUrlQuery'; @@ -16,6 +17,8 @@ function WidgetGraphContainer({ fillSpans = false, softMax, softMin, + selectedLogFields, + selectedTracesFields, }: WidgetGraphProps): JSX.Element { const { selectedDashboard } = useDashboard(); @@ -46,7 +49,21 @@ function WidgetGraphContainer({ if (getWidgetQueryRange.isLoading) { return ; } - if (getWidgetQueryRange.data?.payload.data.result.length === 0) { + + if ( + selectedGraph !== PANEL_TYPES.LIST && + getWidgetQueryRange.data?.payload.data.result.length === 0 + ) { + return ( + + No Data + + ); + } + if ( + selectedGraph === PANEL_TYPES.LIST && + getWidgetQueryRange.data?.payload.data.newResult.data.result.length === 0 + ) { return ( No Data @@ -63,6 +80,9 @@ function WidgetGraphContainer({ fillSpans={fillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} + selectedTime={selectedTime} /> ); } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx index f4a8ed1b22..ccd0a91ea3 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx @@ -1,6 +1,7 @@ import { QueryParams } from 'constants/query'; import GridPanelSwitch from 'container/GridPanelSwitch'; import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useResizeObserver } from 'hooks/useDimensions'; @@ -30,8 +31,11 @@ function WidgetGraph({ fillSpans, softMax, softMin, + selectedLogFields, + selectedTracesFields, + selectedTime, }: WidgetGraphProps): JSX.Element { - const { stagedQuery } = useQueryBuilder(); + const { stagedQuery, currentQuery } = useQueryBuilder(); const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< AppState, @@ -156,6 +160,10 @@ function WidgetGraph({ } query={stagedQuery || selectedWidget.query} thresholds={thresholds} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} + dataSource={currentQuery.builder.queryData[0].dataSource} + selectedTime={selectedTime} /> ); @@ -172,6 +180,9 @@ interface WidgetGraphProps { >; softMax: number | null; softMin: number | null; + selectedLogFields: Widgets['selectedLogFields']; + selectedTracesFields: Widgets['selectedTracesFields']; + selectedTime: timePreferance; } export default WidgetGraph; diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx index 56846c8dec..1f306a41a2 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx @@ -19,6 +19,8 @@ function WidgetGraph({ fillSpans, softMax, softMin, + selectedLogFields, + selectedTracesFields, }: WidgetGraphProps): JSX.Element { const { currentQuery } = useQueryBuilder(); const { selectedDashboard } = useDashboard(); @@ -57,6 +59,8 @@ function WidgetGraph({ fillSpans={fillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} /> ); diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts index b1bf36b588..a5d030e27c 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts @@ -13,8 +13,10 @@ export const Container = styled(Card)` .ant-card-body { padding: ${({ $panelType }): string => - $panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'}; - height: 57vh; + $panelType === PANEL_TYPES.TABLE || $panelType === PANEL_TYPES.LIST + ? '0 0' + : '1.5rem 0'}; + height: 60vh; display: flex; flex-direction: column; } diff --git a/frontend/src/container/NewWidget/LeftContainer/index.tsx b/frontend/src/container/NewWidget/LeftContainer/index.tsx index 0a78a084d8..5dd429d83f 100644 --- a/frontend/src/container/NewWidget/LeftContainer/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/index.tsx @@ -1,6 +1,8 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; import { memo } from 'react'; import { WidgetGraphProps } from '../types'; +import ExplorerColumnsRenderer from './ExplorerColumnsRenderer'; import QuerySection from './QuerySection'; import { QueryContainer } from './styles'; import WidgetGraph from './WidgetGraph'; @@ -13,6 +15,10 @@ function LeftContainer({ fillSpans, softMax, softMin, + selectedLogFields, + setSelectedLogFields, + selectedTracesFields, + setSelectedTracesFields, }: WidgetGraphProps): JSX.Element { return ( <> @@ -24,9 +30,19 @@ function LeftContainer({ fillSpans={fillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} /> + {selectedGraph === PANEL_TYPES.LIST && ( + + )} ); diff --git a/frontend/src/container/NewWidget/RightContainer/constants.ts b/frontend/src/container/NewWidget/RightContainer/constants.ts index 0030f6927b..a6663ac75c 100644 --- a/frontend/src/container/NewWidget/RightContainer/constants.ts +++ b/frontend/src/container/NewWidget/RightContainer/constants.ts @@ -47,3 +47,41 @@ export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; + +export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: false, + [PANEL_TYPES.TABLE]: false, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: true, + [PANEL_TYPES.TABLE]: true, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsCreateAlert: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: true, + [PANEL_TYPES.TABLE]: false, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsPanelTimePreferences: { + [key in PANEL_TYPES]: boolean; +} = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: true, + [PANEL_TYPES.TABLE]: true, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index 5ab1a965fc..60e90c866e 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -17,7 +17,14 @@ import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts'; import { Dispatch, SetStateAction, useCallback } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; -import { panelTypeVsSoftMinMax, panelTypeVsThreshold } from './constants'; +import { + panelTypeVsCreateAlert, + panelTypeVsFillSpan, + panelTypeVsPanelTimePreferences, + panelTypeVsSoftMinMax, + panelTypeVsThreshold, + panelTypeVsYAxisUnit, +} from './constants'; import { Container, Title } from './styles'; import ThresholdSelector from './Threshold/ThresholdSelector'; import { ThresholdProps } from './Threshold/types'; @@ -62,6 +69,11 @@ function RightContainer({ const allowThreshold = panelTypeVsThreshold[selectedGraph]; const allowSoftMinMax = panelTypeVsSoftMinMax[selectedGraph]; + const allowFillSpans = panelTypeVsFillSpan[selectedGraph]; + const allowYAxisUnit = panelTypeVsYAxisUnit[selectedGraph]; + const allowCreateAlerts = panelTypeVsCreateAlert[selectedGraph]; + const allowPanelTimePreference = + panelTypeVsPanelTimePreferences[selectedGraph]; const softMinHandler = useCallback( (value: number | null) => { @@ -117,32 +129,40 @@ function RightContainer({ } /> - - Fill gaps + {allowFillSpans && ( + + Fill gaps - setIsFillSpans(checked)} - /> - + setIsFillSpans(checked)} + /> + + )} - Panel Time Preference + {allowPanelTimePreference && ( + Panel Time Preference + )} - + {allowPanelTimePreference && ( + + )} - + {allowYAxisUnit && ( + + )} - {selectedWidget?.panelTypes !== PANEL_TYPES.TABLE && ( + {allowCreateAlerts && ( diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 5192644a5e..f4c62ae601 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -24,6 +24,7 @@ import { useSelector } from 'react-redux'; import { generatePath, useLocation, useParams } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; +import { IField } from 'types/api/logs/fields'; import { EQueryType } from 'types/common/dashboard'; import { DataSource } from 'types/common/queryBuilder'; import AppReducer from 'types/reducer/app'; @@ -110,6 +111,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { : selectedWidget?.softMin || 0, ); + const [selectedLogFields, setSelectedLogFields] = useState( + selectedWidget?.selectedLogFields || null, + ); + + const [selectedTracesFields, setSelectedTracesFields] = useState( + selectedWidget?.selectedTracesFields || null, + ); + const [softMax, setSoftMax] = useState( selectedWidget?.softMax === null || selectedWidget?.softMax === undefined ? null @@ -189,10 +198,13 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { title, yAxisUnit, panelTypes: graphType, + query: currentQuery, thresholds, softMin, softMax, fillSpans: isFillSpans, + selectedLogFields, + selectedTracesFields, }, ...afterWidgets, ], @@ -226,10 +238,13 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { title, yAxisUnit, graphType, + currentQuery, thresholds, softMin, softMax, isFillSpans, + selectedLogFields, + selectedTracesFields, afterWidgets, updateDashboardMutation, setSelectedDashboard, @@ -336,6 +351,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { fillSpans={isFillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + setSelectedLogFields={setSelectedLogFields} + selectedTracesFields={selectedTracesFields} + setSelectedTracesFields={setSelectedTracesFields} /> diff --git a/frontend/src/container/NewWidget/types.ts b/frontend/src/container/NewWidget/types.ts index 21d2268d76..31cdf0c87c 100644 --- a/frontend/src/container/NewWidget/types.ts +++ b/frontend/src/container/NewWidget/types.ts @@ -1,4 +1,5 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; +import { Dispatch, SetStateAction } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; import { ThresholdProps } from './RightContainer/Threshold/types'; @@ -15,4 +16,10 @@ export interface WidgetGraphProps extends NewWidgetProps { thresholds: ThresholdProps[]; softMin: number | null; softMax: number | null; + selectedLogFields: Widgets['selectedLogFields']; + setSelectedLogFields?: Dispatch>; + selectedTracesFields: Widgets['selectedTracesFields']; + setSelectedTracesFields?: Dispatch< + SetStateAction + >; } diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts index 0923395296..ef18d8ce39 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts +++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts @@ -26,4 +26,5 @@ export type QueryBuilderProps = { actions?: ReactNode; filterConfigs?: Partial; queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode }; + isListViewPanel?: boolean; }; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/QueryBuilder/QueryBuilder.tsx index 39875a7c90..f67533e427 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/QueryBuilder/QueryBuilder.tsx @@ -1,7 +1,12 @@ import './QueryBuilder.styles.scss'; import { Button, Col, Divider, Row, Tooltip } from 'antd'; -import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder'; +import { + MAX_FORMULAS, + MAX_QUERIES, + OPERATORS, + PANEL_TYPES, +} from 'constants/queryBuilder'; // ** Hooks import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { DatabaseZap, Sigma } from 'lucide-react'; @@ -19,6 +24,7 @@ export const QueryBuilder = memo(function QueryBuilder({ panelType: newPanelType, filterConfigs = {}, queryComponents, + isListViewPanel = false, }: QueryBuilderProps): JSX.Element { const { currentQuery, @@ -84,6 +90,33 @@ export const QueryBuilder = memo(function QueryBuilder({ } }; + const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { + const config: QueryBuilderProps['filterConfigs'] = { + stepInterval: { isHidden: true, isDisabled: true }, + having: { isHidden: true, isDisabled: true }, + filters: { + customKey: 'body', + customOp: OPERATORS.CONTAINS, + }, + }; + + return config; + }, []); + + const listViewTracesFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { + const config: QueryBuilderProps['filterConfigs'] = { + stepInterval: { isHidden: true, isDisabled: true }, + having: { isHidden: true, isDisabled: true }, + limit: { isHidden: true, isDisabled: true }, + filters: { + customKey: 'body', + customOp: OPERATORS.CONTAINS, + }, + }; + + return config; + }, []); + return ( -
- - - - + {!isListViewPanel && ( +
+ + + + - - - - -
+ + + +
+
+ )}
@@ -119,49 +154,66 @@ export const QueryBuilder = memo(function QueryBuilder({ className="query-builder-queries-formula-container" ref={containerRef} > - {currentQuery.builder.queryData.map((query, index) => ( - - - - ))} - {currentQuery.builder.queryFormulas.map((formula, index) => { - const isAllMetricDataSource = currentQuery.builder.queryData.every( - (query) => query.dataSource === DataSource.METRICS, - ); - - const query = - currentQuery.builder.queryData[index] || - currentQuery.builder.queryData[0]; - - return ( + {panelType === PANEL_TYPES.LIST && isListViewPanel && ( + + )} + {!isListViewPanel && + currentQuery.builder.queryData.map((query, index) => ( - - ); - })} + ))} + {!isListViewPanel && + currentQuery.builder.queryFormulas.map((formula, index) => { + const isAllMetricDataSource = currentQuery.builder.queryData.every( + (query) => query.dataSource === DataSource.METRICS, + ); + + const query = + currentQuery.builder.queryData[index] || + currentQuery.builder.queryData[0]; + + return ( + + + + ); + })} @@ -171,29 +223,31 @@ export const QueryBuilder = memo(function QueryBuilder({ - - {currentQuery.builder.queryData.map((query) => ( - - ))} + {!isListViewPanel && ( + + {currentQuery.builder.queryData.map((query) => ( + + ))} - {currentQuery.builder.queryFormulas.map((formula) => ( - - ))} - + {currentQuery.builder.queryFormulas.map((formula) => ( + + ))} + + )} ); }); diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts index 0a34d52c6f..acbe586220 100644 --- a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts +++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts @@ -3,4 +3,5 @@ import { DataSource } from 'types/common/queryBuilder'; export type QueryLabelProps = { onChange: (value: DataSource) => void; + isListViewPanel?: boolean; } & Omit; diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx index 52b3d030cb..81efc3b9d7 100644 --- a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx +++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx @@ -10,18 +10,22 @@ import { QueryLabelProps } from './DataSourceDropdown.interfaces'; const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES]; +const exploreDataSourceMap = [DataSource.LOGS, DataSource.TRACES]; + export const DataSourceDropdown = memo(function DataSourceDropdown( props: QueryLabelProps, ): JSX.Element { - const { onChange, value, style } = props; + const { onChange, value, style, isListViewPanel = false } = props; - const dataSourceOptions: SelectOption< - DataSource, - string - >[] = dataSourceMap.map((source) => ({ - label: transformToUpperCase(source), - value: source, - })); + const dataSourceOptions: SelectOption[] = isListViewPanel + ? exploreDataSourceMap.map((source) => ({ + label: transformToUpperCase(source), + value: source, + })) + : dataSourceMap.map((source) => ({ + label: transformToUpperCase(source), + value: source, + })); return ( void; + isListViewPanel?: boolean; }; export type OrderByFilterValue = { diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx index 2f707ec7a1..dc6fb964fa 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx @@ -11,6 +11,7 @@ import { useOrderByFilter } from './useOrderByFilter'; export function OrderByFilter({ query, onChange, + isListViewPanel = false, }: OrderByFilterProps): JSX.Element { const { debouncedSearchText, @@ -30,7 +31,7 @@ export function OrderByFilter({ searchText: debouncedSearchText, }, { - enabled: !!query.aggregateAttribute.key, + enabled: !!query.aggregateAttribute.key || isListViewPanel, keepPreviousData: true, }, ); diff --git a/frontend/src/container/TracesExplorer/Controls/styles.ts b/frontend/src/container/TracesExplorer/Controls/styles.ts index be74f2db18..d9810d1f4c 100644 --- a/frontend/src/container/TracesExplorer/Controls/styles.ts +++ b/frontend/src/container/TracesExplorer/Controls/styles.ts @@ -4,6 +4,5 @@ export const Container = styled.div` display: flex; align-items: center; justify-content: flex-end; - gap: 0.5rem; - margin: 4px 0; + gap: 0.3rem; `; diff --git a/frontend/src/container/TracesExplorer/ListView/index.tsx b/frontend/src/container/TracesExplorer/ListView/index.tsx index c9dd78df2f..4f18bb3a27 100644 --- a/frontend/src/container/TracesExplorer/ListView/index.tsx +++ b/frontend/src/container/TracesExplorer/ListView/index.tsx @@ -4,6 +4,7 @@ import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { useOptionsMenu } from 'container/OptionsMenu'; +import TraceExplorerControls from 'container/TracesExplorer/Controls'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { Pagination } from 'hooks/queryPagination'; @@ -18,7 +19,6 @@ import { AppState } from 'store/reducers'; import { DataSource } from 'types/common/queryBuilder'; import { GlobalReducer } from 'types/reducer/globalTime'; -import TraceExplorerControls from '../Controls'; import { defaultSelectedColumns, PER_PAGE_OPTIONS } from './configs'; import { Container, ErrorText, tableStyles } from './styles'; import { getListColumns, getTraceLink, transformDataWithDate } from './utils'; diff --git a/frontend/src/container/TracesExplorer/ListView/styles.ts b/frontend/src/container/TracesExplorer/ListView/styles.ts index 292b04b1f9..9410a9b90e 100644 --- a/frontend/src/container/TracesExplorer/ListView/styles.ts +++ b/frontend/src/container/TracesExplorer/ListView/styles.ts @@ -3,7 +3,7 @@ import { CSSProperties } from 'react'; import styled from 'styled-components'; export const tableStyles: CSSProperties = { - cursor: 'pointer', + cursor: 'unset', }; export const Container = styled.div` diff --git a/frontend/src/container/TracesExplorer/ListView/utils.tsx b/frontend/src/container/TracesExplorer/ListView/utils.tsx index 6a28acfba9..6a8ef4fc4c 100644 --- a/frontend/src/container/TracesExplorer/ListView/utils.tsx +++ b/frontend/src/container/TracesExplorer/ListView/utils.tsx @@ -3,14 +3,11 @@ import { ColumnsType } from 'antd/es/table'; import ROUTES from 'constants/routes'; import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util'; import { formUrlParams } from 'container/TraceDetail/utils'; -import dayjs from 'dayjs'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { ILog } from 'types/api/logs/log'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { QueryDataV3 } from 'types/api/widgets/getQuery'; -import { DateText } from './styles'; - export const transformDataWithDate = ( data: QueryDataV3[], ): Omit[] => @@ -27,28 +24,14 @@ export const getTraceLink = (record: RowData): string => export const getListColumns = ( selectedColumns: BaseAutocompleteData[], ): ColumnsType => { - const initialColumns: ColumnsType = [ - { - title: 'date', - dataIndex: 'date', - key: 'date', - width: 145, - render: (date: string): JSX.Element => { - const day = dayjs(date); - return ( - - {day.format('YYYY/MM/DD HH:mm:ss')} - - ); - }, - }, - ]; + const initialColumns: ColumnsType = []; const columns: ColumnsType = selectedColumns.map(({ dataType, key, type }) => ({ title: key, dataIndex: key, key: `${key}-${dataType}-${type}`, + width: 145, render: (value): JSX.Element => { if (value === '') { return N/A; @@ -68,6 +51,7 @@ export const getListColumns = ( return {value}; }, + responsive: ['md'], })) || []; return [...initialColumns, ...columns]; diff --git a/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss b/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss new file mode 100644 index 0000000000..e1ff9ba437 --- /dev/null +++ b/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss @@ -0,0 +1,65 @@ +.traces-table { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + + .resize-table { + height: calc(90% - 5px); + overflow: scroll; + + .ant-table-wrapper .ant-table-tbody >tr >td { + border: none; + background-color: transparent; + color: var(--bg-vanilla-100); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; + padding: 10px 8px; + font-family: Inter; + cursor: pointer; + } + + .ant-table-wrapper .ant-table-thead > tr > th { + font-family: Inter; + color: var(--bg-vanilla-100); + background-color: transparent; + border: none; + border-bottom: 0.5px solid var(--bg-slate-400); + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 22px; + letter-spacing: 0.5px; + padding: 8px; + } + + .ant-table-wrapper .ant-table-thead > tr > th::before { + display: none; + } + } + + .controller { + position: absolute; + bottom: 5px; + right: 10px; + } +} + +.lightMode { + .traces-table { + .resize-table { + .ant-table-wrapper .ant-table-tbody >tr >td { + background-color: var(--bg-vanilla-100); + color: var(--bg-ink-500); + border-color: rgba(0, 0, 0, 0.06); + } + .ant-table-wrapper .ant-table-thead > tr > th { + background-color: var(--bg-vanilla-300); + color: var(--bg-ink-500); + border-color: rgba(0, 0, 0, 0.06); + } + } + } +} \ No newline at end of file diff --git a/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx b/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx new file mode 100644 index 0000000000..a59303780c --- /dev/null +++ b/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx @@ -0,0 +1,170 @@ +import './TracesTableComponent.styles.scss'; + +import { Table } from 'antd'; +// import { ResizeTable } from 'components/ResizeTable'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import Controls from 'container/Controls'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; +import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs'; +import { tableStyles } from 'container/TracesExplorer/ListView/styles'; +import { + getListColumns, + getTraceLink, + transformDataWithDate, +} from 'container/TracesExplorer/ListView/utils'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { Pagination } from 'hooks/queryPagination'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; +import history from 'lib/history'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { HTMLAttributes, useCallback, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +function TracesTableComponent({ + selectedTracesFields, + query, + selectedTime, +}: TracesTableComponentProps): JSX.Element { + const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const [pagination, setPagination] = useState({ + offset: 0, + limit: 10, + }); + + const { selectedDashboard } = useDashboard(); + + const { data, isFetching, isError } = useGetQueryRange( + { + query, + graphType: PANEL_TYPES.LIST, + selectedTime: selectedTime?.enum || 'GLOBAL_TIME', + globalSelectedInterval: globalSelectedTime, + params: { + dataSource: 'traces', + }, + tableParams: { + pagination, + selectColumns: selectedTracesFields, + }, + variables: getDashboardVariables(selectedDashboard?.data.variables), + }, + { + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + globalSelectedTime, + maxTime, + minTime, + query, + pagination, + selectedTracesFields?.length, + selectedTime?.enum, + selectedDashboard?.data.variables, + ], + enabled: !!query && !!selectedTracesFields?.length, + }, + ); + + const columns = getListColumns(selectedTracesFields || []); + + const dataLength = + data?.payload?.data?.newResult?.data?.result[0]?.list?.length; + const totalCount = useMemo(() => dataLength || 0, [dataLength]); + + const queryTableDataResult = data?.payload.data.newResult.data.result; + const queryTableData = useMemo(() => queryTableDataResult || [], [ + queryTableDataResult, + ]); + + const transformedQueryTableData = useMemo( + () => ((transformDataWithDate(queryTableData) || []) as unknown) as RowData[], + [queryTableData], + ); + + const handleRow = useCallback( + (record: RowData): HTMLAttributes => ({ + onClick: (event): void => { + event.preventDefault(); + event.stopPropagation(); + if (event.metaKey || event.ctrlKey) { + window.open(getTraceLink(record), '_blank'); + } else { + history.push(getTraceLink(record)); + } + }, + }), + [], + ); + + if (isError) { + return
{SOMETHING_WENT_WRONG}
; + } + + return ( +
+
+
+ +
+ { + setPagination({ + ...pagination, + offset: pagination.offset - pagination.limit, + }); + }} + handleNavigateNext={(): void => { + setPagination({ + ...pagination, + offset: pagination.offset + pagination.limit, + }); + }} + handleCountItemsPerPageChange={(value): void => { + setPagination({ + ...pagination, + limit: value, + offset: 0, + }); + }} + /> +
+ + ); +} + +export type TracesTableComponentProps = { + selectedTracesFields: Widgets['selectedTracesFields']; + query: Query; + selectedTime?: timePreferance; +}; + +TracesTableComponent.defaultProps = { + selectedTime: undefined, +}; + +export default TracesTableComponent; diff --git a/frontend/src/hooks/dashboard/utils.ts b/frontend/src/hooks/dashboard/utils.ts index 4dfb8ce9c3..a66204ae62 100644 --- a/frontend/src/hooks/dashboard/utils.ts +++ b/frontend/src/hooks/dashboard/utils.ts @@ -35,6 +35,8 @@ export const addEmptyWidgetInDashboardJSONWithQuery = ( panelTypes: panelTypes || PANEL_TYPES.TIME_SERIES, softMax: null, softMin: null, + selectedLogFields: [], + selectedTracesFields: [], }, ], }, diff --git a/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts b/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts index 8e883852cb..799640da4e 100644 --- a/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts +++ b/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts @@ -6,6 +6,10 @@ import { mapOfQueryFilters, PANEL_TYPES, } from 'constants/queryBuilder'; +import { + listViewInitialLogQuery, + listViewInitialTraceQuery, +} from 'container/NewDashboard/ComponentsSlider/constants'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType'; import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator'; @@ -29,6 +33,7 @@ export const useQueryOperations: UseQueryOperations = ({ index, filterConfigs, formula, + isListViewPanel = false, }) => { const { handleSetQueryData, @@ -37,6 +42,7 @@ export const useQueryOperations: UseQueryOperations = ({ panelType, initialDataSource, currentQuery, + redirectWithQueryBuilderData, } = useQueryBuilder(); const [operators, setOperators] = useState[]>([]); @@ -125,6 +131,14 @@ export const useQueryOperations: UseQueryOperations = ({ const handleChangeDataSource = useCallback( (nextSource: DataSource): void => { + if (isListViewPanel) { + if (nextSource === DataSource.LOGS) { + redirectWithQueryBuilderData(listViewInitialLogQuery); + } else if (nextSource === DataSource.TRACES) { + redirectWithQueryBuilderData(listViewInitialTraceQuery); + } + } + const newOperators = getOperatorsBySourceAndPanelType({ dataSource: nextSource, panelType: panelType || PANEL_TYPES.TIME_SERIES, @@ -146,7 +160,14 @@ export const useQueryOperations: UseQueryOperations = ({ setOperators(newOperators); handleSetQueryData(index, newQuery); }, - [index, query, panelType, handleSetQueryData], + [ + isListViewPanel, + panelType, + query, + handleSetQueryData, + index, + redirectWithQueryBuilderData, + ], ); const handleDeleteQuery = useCallback(() => { diff --git a/frontend/src/hooks/useLogsData.ts b/frontend/src/hooks/useLogsData.ts new file mode 100644 index 0000000000..6105c03cbf --- /dev/null +++ b/frontend/src/hooks/useLogsData.ts @@ -0,0 +1,196 @@ +import { QueryParams } from 'constants/query'; +import { + initialQueryBuilderFormValues, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config'; +import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; +import { useEffect, useMemo, useState } from 'react'; +import { ILog } from 'types/api/logs/log'; +import { + IBuilderQuery, + OrderByPayload, + Query, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; +import { QueryDataV3 } from 'types/api/widgets/getQuery'; + +import { LogTimeRange } from './logs/types'; +import { useCopyLogLink } from './logs/useCopyLogLink'; +import { useGetExplorerQueryRange } from './queryBuilder/useGetExplorerQueryRange'; +import useUrlQueryData from './useUrlQueryData'; + +export const useLogsData = ({ + result, + panelType, + stagedQuery, +}: { + result: QueryDataV3[] | undefined; + panelType: PANEL_TYPES; + stagedQuery: Query | null; +}): { + logs: ILog[]; + handleEndReached: (index: number) => void; + isFetching: boolean; +} => { + const [logs, setLogs] = useState([]); + const [page, setPage] = useState(1); + const [requestData, setRequestData] = useState(null); + const [shouldLoadMoreLogs, setShouldLoadMoreLogs] = useState(false); + + const { queryData: pageSize } = useUrlQueryData( + QueryParams.pageSize, + DEFAULT_PER_PAGE_VALUE, + ); + + const listQuery = useMemo(() => { + if (!stagedQuery || stagedQuery?.builder?.queryData?.length < 1) return null; + + return stagedQuery.builder?.queryData.find((item) => !item.disabled) || null; + }, [stagedQuery]); + + const isLimit: boolean = useMemo(() => { + if (!listQuery) return false; + if (!listQuery.limit) return false; + + return logs.length >= listQuery.limit; + }, [logs.length, listQuery]); + + const orderByTimestamp: OrderByPayload | null = useMemo(() => { + const timestampOrderBy = listQuery?.orderBy.find( + (item) => item.columnName === 'timestamp', + ); + + return timestampOrderBy || null; + }, [listQuery]); + + useEffect(() => { + if (panelType !== PANEL_TYPES.LIST) return; + const currentData = result || []; + if (currentData.length > 0 && currentData[0].list) { + const currentLogs: ILog[] = currentData[0].list.map((item) => ({ + ...item.data, + timestamp: item.timestamp, + })); + const newLogs = [...currentLogs]; + + setLogs(newLogs); + } else { + setLogs([]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [result]); + + const getRequestData = ( + query: Query | null, + params: { + page: number; + log: ILog | null; + pageSize: number; + filters: TagFilter; + }, + ): Query | null => { + if (!query) return null; + + const paginateData = getPaginationQueryData({ + filters: params.filters, + listItemId: params.log ? params.log.id : null, + orderByTimestamp, + page: params.page, + pageSize: params.pageSize, + }); + + const queryData: IBuilderQuery[] = + query.builder.queryData.length > 1 + ? query.builder.queryData + : [ + { + ...(listQuery || initialQueryBuilderFormValues), + ...paginateData, + }, + ]; + + const data: Query = { + ...query, + builder: { + ...query.builder, + queryData, + }, + }; + + return data; + }; + + const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink(); + + const { data, isFetching } = useGetExplorerQueryRange( + requestData, + panelType, + { + keepPreviousData: true, + enabled: !isLimit && !!requestData, + }, + { + ...(timeRange && + activeLogId && + !logs.length && { + start: timeRange.start, + end: timeRange.end, + }), + }, + shouldLoadMoreLogs, + ); + + useEffect(() => { + const currentParams = data?.params as Omit; + const currentData = data?.payload.data.newResult.data.result || []; + if (currentData.length > 0 && currentData[0].list) { + const currentLogs: ILog[] = currentData[0].list.map((item) => ({ + ...item.data, + timestamp: item.timestamp, + })); + const newLogs = [...logs, ...currentLogs]; + + setLogs(newLogs); + onTimeRangeChange({ + start: currentParams?.start, + end: timeRange?.end || currentParams?.end, + pageSize: newLogs.length, + }); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + + const handleEndReached = (index: number): void => { + if (!listQuery) return; + + if (isLimit) return; + if (logs.length < pageSize) return; + + const { limit, filters } = listQuery; + + const lastLog = logs[index]; + + const nextLogsLength = logs.length + pageSize; + + const nextPageSize = + limit && nextLogsLength >= limit ? limit - logs.length : pageSize; + + if (!stagedQuery) return; + + const newRequestData = getRequestData(stagedQuery, { + filters, + page: page + 1, + log: orderByTimestamp ? lastLog : null, + pageSize: nextPageSize, + }); + + setPage((prevPage) => prevPage + 1); + + setRequestData(newRequestData); + setShouldLoadMoreLogs(true); + }; + + return { logs, handleEndReached, isFetching }; +}; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index bb302c152b..2111d3d57b 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -5,6 +5,9 @@ import { ReactNode } from 'react'; import { Layout } from 'react-grid-layout'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { IField } from '../logs/fields'; +import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse'; + export type PayloadProps = Dashboard[]; export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; @@ -76,6 +79,8 @@ export interface IBaseWidget { softMin: number | null; softMax: number | null; fillSpans?: boolean; + selectedLogFields: IField[] | null; + selectedTracesFields: BaseAutocompleteData[] | null; } export interface Widgets extends IBaseWidget { query: Query; diff --git a/frontend/src/types/common/operations.types.ts b/frontend/src/types/common/operations.types.ts index b1a21ce385..58fd4533b9 100644 --- a/frontend/src/types/common/operations.types.ts +++ b/frontend/src/types/common/operations.types.ts @@ -12,6 +12,7 @@ import { SelectOption } from './select'; type UseQueryOperationsParams = Pick & Pick & { formula?: IBuilderFormula; + isListViewPanel?: boolean; }; export type HandleChangeQueryData = < diff --git a/frontend/yarn.lock b/frontend/yarn.lock index fd1b081eb0..8e1c80fad2 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4222,6 +4222,13 @@ dependencies: "@types/react" "*" +"@types/react-beautiful-dnd@13.1.8": + version "13.1.8" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz#f52d3ea07e1e19159d6c3c4a48c8da3d855e60b4" + integrity sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ== + dependencies: + "@types/react" "*" + "@types/react-dom@18.0.10", "@types/react-dom@^18.0.0": version "18.0.10" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz" @@ -6809,7 +6816,7 @@ critters@^0.0.16: cross-env@^7.0.3: version "7.0.3" - resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== dependencies: cross-spawn "^7.0.1" @@ -6846,6 +6853,13 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" @@ -11697,6 +11711,11 @@ memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -13749,6 +13768,11 @@ quickselect@^2.0.0: resolved "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" @@ -14179,6 +14203,19 @@ react-addons-update@15.6.3: dependencies: object-assign "^4.1.0" +react-beautiful-dnd@13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-dnd-html5-backend@16.0.1: version "16.0.1" resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6" @@ -14361,7 +14398,7 @@ react-query@3.39.3: broadcast-channel "^3.4.1" match-sorter "^6.0.2" -react-redux@^7.2.2: +react-redux@^7.2.0, react-redux@^7.2.2: version "7.2.9" resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz" integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== @@ -14556,7 +14593,7 @@ redux-thunk@^2.3.0: resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@^4.0.0, redux@^4.0.5, redux@^4.2.0: +redux@^4.0.0, redux@^4.0.4, redux@^4.0.5, redux@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -16162,7 +16199,7 @@ timestamp-nano@^1.0.0: resolved "https://registry.npmjs.org/timestamp-nano/-/timestamp-nano-1.0.1.tgz" integrity sha512-4oGOVZWTu5sl89PtCDnhQBSt7/vL1zVEwAfxH1p49JhTosxzVQWYBYFRFZ8nJmo0G6f824iyP/44BFAwIoKvIA== -tiny-invariant@^1.0.2: +tiny-invariant@^1.0.2, tiny-invariant@^1.0.6: version "1.3.1" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== @@ -16731,6 +16768,11 @@ use-isomorphic-layout-effect@^1.1.2: resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== +use-memo-one@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"