mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 08:35:43 -06:00
Logs Panel: Table UI - Misc UI tweaks (#78150)
* Miscellaneous UI tweaks for logs table UI in explore
This commit is contained in:
parent
0b65f900aa
commit
fd863cfc93
@ -613,13 +613,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
)
|
||||
) : null,
|
||||
]}
|
||||
title={
|
||||
config.featureToggles.logsExploreTableVisualisation
|
||||
? this.state.visualisationType === 'logs'
|
||||
? 'Logs'
|
||||
: 'Table'
|
||||
: 'Logs'
|
||||
}
|
||||
title={'Logs'}
|
||||
actions={
|
||||
<>
|
||||
{config.featureToggles.logsExploreTableVisualisation && (
|
||||
@ -627,16 +621,16 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
<RadioButtonGroup
|
||||
className={styles.visualisationTypeRadio}
|
||||
options={[
|
||||
{
|
||||
label: 'Table',
|
||||
value: 'table',
|
||||
description: 'Show results in table visualisation',
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
value: 'logs',
|
||||
description: 'Show results in logs visualisation',
|
||||
},
|
||||
{
|
||||
label: 'Table',
|
||||
value: 'table',
|
||||
description: 'Show results in table visualisation',
|
||||
},
|
||||
]}
|
||||
size="sm"
|
||||
value={this.state.visualisationType}
|
||||
|
@ -12,12 +12,12 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
};
|
||||
}
|
||||
|
||||
export function LogsColumnSearch(props: { onChange: (e: React.FormEvent<HTMLInputElement>) => void }) {
|
||||
export function LogsColumnSearch(props: { onChange: (e: React.FormEvent<HTMLInputElement>) => void; value: string }) {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<Field className={styles.searchWrap}>
|
||||
<Input type={'text'} placeholder={'Search fields by name'} onChange={props.onChange} />
|
||||
<Input value={props.value} type={'text'} placeholder={'Search fields by name'} onChange={props.onChange} />
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
@ -13,7 +13,15 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
overflowY: 'scroll',
|
||||
height: 'calc(100% - 50px)',
|
||||
}),
|
||||
columnHeaderButton: css({
|
||||
appearance: 'none',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: theme.typography.pxToRem(11),
|
||||
}),
|
||||
columnHeader: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
background: theme.colors.background.secondary,
|
||||
position: 'sticky',
|
||||
@ -33,6 +41,7 @@ export const LogsTableMultiSelect = (props: {
|
||||
toggleColumn: (columnName: string) => void;
|
||||
filteredColumnsWithMeta: Record<string, fieldNameMeta> | undefined;
|
||||
columnsWithMeta: Record<string, fieldNameMeta>;
|
||||
clear: () => void;
|
||||
}) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
@ -41,11 +50,23 @@ export const LogsTableMultiSelect = (props: {
|
||||
<div className={styles.sidebarWrap}>
|
||||
{/* Sidebar columns */}
|
||||
<>
|
||||
<div className={styles.columnHeader}>
|
||||
Selected fields
|
||||
<button onClick={props.clear} className={styles.columnHeaderButton}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<LogsTableNavColumn
|
||||
toggleColumn={props.toggleColumn}
|
||||
labels={props.filteredColumnsWithMeta ?? props.columnsWithMeta}
|
||||
valueFilter={(value) => props.columnsWithMeta[value]?.active ?? false}
|
||||
/>
|
||||
|
||||
<div className={styles.columnHeader}>Fields</div>
|
||||
<LogsTableNavColumn
|
||||
toggleColumn={props.toggleColumn}
|
||||
labels={props.filteredColumnsWithMeta ?? props.columnsWithMeta}
|
||||
valueFilter={(value) => !!value}
|
||||
valueFilter={(value) => !props.columnsWithMeta[value]?.active}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
|
@ -11,6 +11,10 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
labelCount: css({
|
||||
marginLeft: theme.spacing(0.5),
|
||||
marginRight: theme.spacing(0.5),
|
||||
appearance: 'none',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: theme.typography.pxToRem(11),
|
||||
}),
|
||||
wrap: css({
|
||||
display: 'flex',
|
||||
@ -29,13 +33,11 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
top: 0,
|
||||
},
|
||||
'> span': {
|
||||
overflow: 'scroll',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
'&::-moz-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'block',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
}),
|
||||
columnWrapper: css({
|
||||
@ -51,70 +53,19 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
};
|
||||
}
|
||||
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
function sortLabels(labels: Record<string, fieldNameMeta>) {
|
||||
return (a: string, b: string) => {
|
||||
// First sort by active
|
||||
if (labels[a].active && labels[b].active) {
|
||||
// If both fields are active, sort time first
|
||||
if (labels[a]?.type === 'TIME_FIELD') {
|
||||
return -1;
|
||||
}
|
||||
if (labels[b]?.type === 'TIME_FIELD') {
|
||||
return 1;
|
||||
}
|
||||
// And then line second
|
||||
if (labels[a]?.type === 'BODY_FIELD') {
|
||||
return -1;
|
||||
}
|
||||
// special fields are next
|
||||
if (labels[b]?.type === 'BODY_FIELD') {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const la = labels[a];
|
||||
const lb = labels[b];
|
||||
|
||||
if (labels[b].active && labels[a].active) {
|
||||
// Sort alphabetically
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
if (la != null && lb != null) {
|
||||
return (
|
||||
Number(lb.type === 'TIME_FIELD') - Number(la.type === 'TIME_FIELD') ||
|
||||
Number(lb.type === 'BODY_FIELD') - Number(la.type === 'BODY_FIELD') ||
|
||||
collator.compare(a, b)
|
||||
);
|
||||
}
|
||||
|
||||
// If just one label is active, sort it first
|
||||
if (labels[b].active) {
|
||||
return 1;
|
||||
}
|
||||
if (labels[a].active) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If both fields are special, and not selected, sort time first
|
||||
if (labels[a]?.type && labels[b]?.type) {
|
||||
if (labels[a]?.type === 'TIME_FIELD') {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If only one special field, stick to the top of inactive fields
|
||||
if (labels[a]?.type && !labels[b]?.type) {
|
||||
return -1;
|
||||
}
|
||||
// if the b field is special, sort it first
|
||||
if (!labels[a]?.type && labels[b]?.type) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Finally sort by name
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// otherwise do not sort
|
||||
return 0;
|
||||
};
|
||||
@ -122,25 +73,31 @@ function sortLabels(labels: Record<string, fieldNameMeta>) {
|
||||
|
||||
export const LogsTableNavColumn = (props: {
|
||||
labels: Record<string, fieldNameMeta>;
|
||||
valueFilter: (value: number) => boolean;
|
||||
valueFilter: (value: string) => boolean;
|
||||
toggleColumn: (columnName: string) => void;
|
||||
}): JSX.Element => {
|
||||
const { labels, valueFilter, toggleColumn } = props;
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
const labelKeys = Object.keys(labels).filter((labelName) => valueFilter(labels[labelName].percentOfLinesWithLabel));
|
||||
const labelKeys = Object.keys(labels).filter((labelName) => valueFilter(labelName));
|
||||
if (labelKeys.length) {
|
||||
return (
|
||||
<div className={styles.columnWrapper}>
|
||||
{labelKeys.sort(sortLabels(labels)).map((labelName) => (
|
||||
<div className={styles.wrap} key={labelName}>
|
||||
<div
|
||||
title={`${labelName} appears in ${labels[labelName]?.percentOfLinesWithLabel}% of log lines`}
|
||||
className={styles.wrap}
|
||||
key={labelName}
|
||||
>
|
||||
<Checkbox
|
||||
className={styles.checkboxLabel}
|
||||
label={labelName}
|
||||
onChange={() => toggleColumn(labelName)}
|
||||
checked={labels[labelName]?.active ?? false}
|
||||
/>
|
||||
<span className={styles.labelCount}>({labels[labelName]?.percentOfLinesWithLabel}%)</span>
|
||||
<button className={styles.labelCount} onClick={() => toggleColumn(labelName)}>
|
||||
{labels[labelName]?.percentOfLinesWithLabel}%
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
@ -51,6 +50,7 @@ export function LogsTableWrap(props: Props) {
|
||||
|
||||
// Filtered copy of columnsWithMeta that only includes matching results
|
||||
const [filteredColumnsWithMeta, setFilteredColumnsWithMeta] = useState<fieldNameMetaStore | undefined>(undefined);
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
|
||||
const height = getLogsTableHeight();
|
||||
const panelStateRefId = props?.panelState?.refId;
|
||||
@ -251,6 +251,14 @@ export function LogsTableWrap(props: Props) {
|
||||
});
|
||||
}
|
||||
|
||||
const clearSelection = () => {
|
||||
const pendingLabelState = { ...columnsWithMeta };
|
||||
Object.keys(pendingLabelState).forEach((key) => {
|
||||
pendingLabelState[key].active = !!pendingLabelState[key].type;
|
||||
});
|
||||
setColumnsWithMeta(pendingLabelState);
|
||||
};
|
||||
|
||||
// Toggle a column on or off when the user interacts with an element in the multi-select sidebar
|
||||
const toggleColumn = (columnName: fieldName) => {
|
||||
if (!columnsWithMeta || !(columnName in columnsWithMeta)) {
|
||||
@ -320,14 +328,12 @@ export function LogsTableWrap(props: Props) {
|
||||
fuzzySearch(Object.keys(columnsWithMeta), needle, dispatcher);
|
||||
};
|
||||
|
||||
// Debounce fuzzy search
|
||||
const debouncedSearch = debounce(search, 500);
|
||||
|
||||
// onChange handler for search input
|
||||
const onSearchInputChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = e.currentTarget?.value;
|
||||
setSearchValue(value);
|
||||
if (value) {
|
||||
debouncedSearch(value);
|
||||
search(value);
|
||||
} else {
|
||||
// If the search input is empty, reset the local search state.
|
||||
setFilteredColumnsWithMeta(undefined);
|
||||
@ -376,11 +382,12 @@ export function LogsTableWrap(props: Props) {
|
||||
</div>
|
||||
<div className={styles.wrapper}>
|
||||
<section className={styles.sidebar}>
|
||||
<LogsColumnSearch onChange={onSearchInputChange} />
|
||||
<LogsColumnSearch value={searchValue} onChange={onSearchInputChange} />
|
||||
<LogsTableMultiSelect
|
||||
toggleColumn={toggleColumn}
|
||||
filteredColumnsWithMeta={filteredColumnsWithMeta}
|
||||
columnsWithMeta={columnsWithMeta}
|
||||
clear={clearSelection}
|
||||
/>
|
||||
</section>
|
||||
<LogsTable
|
||||
|
Loading…
Reference in New Issue
Block a user