Logs Panel: Table UI - Misc UI tweaks (#78150)

* Miscellaneous UI tweaks for logs table UI in explore
pull/78354/head
Galen Kistler 2 years ago committed by GitHub
parent 0b65f900aa
commit fd863cfc93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      public/app/features/explore/Logs/Logs.tsx
  2. 4
      public/app/features/explore/Logs/LogsColumnSearch.tsx
  3. 23
      public/app/features/explore/Logs/LogsTableMultiSelect.tsx
  4. 99
      public/app/features/explore/Logs/LogsTableNavColumn.tsx
  5. 19
      public/app/features/explore/Logs/LogsTableWrap.tsx

@ -613,13 +613,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
) )
) : null, ) : null,
]} ]}
title={ title={'Logs'}
config.featureToggles.logsExploreTableVisualisation
? this.state.visualisationType === 'logs'
? 'Logs'
: 'Table'
: 'Logs'
}
actions={ actions={
<> <>
{config.featureToggles.logsExploreTableVisualisation && ( {config.featureToggles.logsExploreTableVisualisation && (
@ -627,16 +621,16 @@ class UnthemedLogs extends PureComponent<Props, State> {
<RadioButtonGroup <RadioButtonGroup
className={styles.visualisationTypeRadio} className={styles.visualisationTypeRadio}
options={[ options={[
{
label: 'Table',
value: 'table',
description: 'Show results in table visualisation',
},
{ {
label: 'Logs', label: 'Logs',
value: 'logs', value: 'logs',
description: 'Show results in logs visualisation', description: 'Show results in logs visualisation',
}, },
{
label: 'Table',
value: 'table',
description: 'Show results in table visualisation',
},
]} ]}
size="sm" size="sm"
value={this.state.visualisationType} 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 theme = useTheme2();
const styles = getStyles(theme); const styles = getStyles(theme);
return ( return (
<Field className={styles.searchWrap}> <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> </Field>
); );
} }

@ -13,7 +13,15 @@ function getStyles(theme: GrafanaTheme2) {
overflowY: 'scroll', overflowY: 'scroll',
height: 'calc(100% - 50px)', height: 'calc(100% - 50px)',
}), }),
columnHeaderButton: css({
appearance: 'none',
background: 'none',
border: 'none',
fontSize: theme.typography.pxToRem(11),
}),
columnHeader: css({ columnHeader: css({
display: 'flex',
justifyContent: 'space-between',
fontSize: theme.typography.h6.fontSize, fontSize: theme.typography.h6.fontSize,
background: theme.colors.background.secondary, background: theme.colors.background.secondary,
position: 'sticky', position: 'sticky',
@ -33,6 +41,7 @@ export const LogsTableMultiSelect = (props: {
toggleColumn: (columnName: string) => void; toggleColumn: (columnName: string) => void;
filteredColumnsWithMeta: Record<string, fieldNameMeta> | undefined; filteredColumnsWithMeta: Record<string, fieldNameMeta> | undefined;
columnsWithMeta: Record<string, fieldNameMeta>; columnsWithMeta: Record<string, fieldNameMeta>;
clear: () => void;
}) => { }) => {
const theme = useTheme2(); const theme = useTheme2();
const styles = getStyles(theme); const styles = getStyles(theme);
@ -41,11 +50,23 @@ export const LogsTableMultiSelect = (props: {
<div className={styles.sidebarWrap}> <div className={styles.sidebarWrap}>
{/* Sidebar columns */} {/* 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> <div className={styles.columnHeader}>Fields</div>
<LogsTableNavColumn <LogsTableNavColumn
toggleColumn={props.toggleColumn} toggleColumn={props.toggleColumn}
labels={props.filteredColumnsWithMeta ?? props.columnsWithMeta} labels={props.filteredColumnsWithMeta ?? props.columnsWithMeta}
valueFilter={(value) => !!value} valueFilter={(value) => !props.columnsWithMeta[value]?.active}
/> />
</> </>
</div> </div>

@ -11,6 +11,10 @@ function getStyles(theme: GrafanaTheme2) {
labelCount: css({ labelCount: css({
marginLeft: theme.spacing(0.5), marginLeft: theme.spacing(0.5),
marginRight: theme.spacing(0.5), marginRight: theme.spacing(0.5),
appearance: 'none',
background: 'none',
border: 'none',
fontSize: theme.typography.pxToRem(11),
}), }),
wrap: css({ wrap: css({
display: 'flex', display: 'flex',
@ -29,13 +33,11 @@ function getStyles(theme: GrafanaTheme2) {
top: 0, top: 0,
}, },
'> span': { '> span': {
overflow: 'scroll', overflow: 'hidden',
'&::-webkit-scrollbar': { textOverflow: 'ellipsis',
display: 'none', whiteSpace: 'nowrap',
}, display: 'block',
'&::-moz-scrollbar': { maxWidth: '100%',
display: 'none',
},
}, },
}), }),
columnWrapper: css({ columnWrapper: css({
@ -51,70 +53,19 @@ function getStyles(theme: GrafanaTheme2) {
}; };
} }
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
function sortLabels(labels: Record<string, fieldNameMeta>) { function sortLabels(labels: Record<string, fieldNameMeta>) {
return (a: string, b: string) => { return (a: string, b: string) => {
// First sort by active const la = labels[a];
if (labels[a].active && labels[b].active) { const lb = labels[b];
// 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;
}
}
if (labels[b].active && labels[a].active) {
// Sort alphabetically
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
}
// If just one label is active, sort it first if (la != null && lb != null) {
if (labels[b].active) { return (
return 1; Number(lb.type === 'TIME_FIELD') - Number(la.type === 'TIME_FIELD') ||
} Number(lb.type === 'BODY_FIELD') - Number(la.type === 'BODY_FIELD') ||
if (labels[a].active) { collator.compare(a, b)
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 // otherwise do not sort
return 0; return 0;
}; };
@ -122,25 +73,31 @@ function sortLabels(labels: Record<string, fieldNameMeta>) {
export const LogsTableNavColumn = (props: { export const LogsTableNavColumn = (props: {
labels: Record<string, fieldNameMeta>; labels: Record<string, fieldNameMeta>;
valueFilter: (value: number) => boolean; valueFilter: (value: string) => boolean;
toggleColumn: (columnName: string) => void; toggleColumn: (columnName: string) => void;
}): JSX.Element => { }): JSX.Element => {
const { labels, valueFilter, toggleColumn } = props; const { labels, valueFilter, toggleColumn } = props;
const theme = useTheme2(); const theme = useTheme2();
const styles = getStyles(theme); 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) { if (labelKeys.length) {
return ( return (
<div className={styles.columnWrapper}> <div className={styles.columnWrapper}>
{labelKeys.sort(sortLabels(labels)).map((labelName) => ( {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 <Checkbox
className={styles.checkboxLabel} className={styles.checkboxLabel}
label={labelName} label={labelName}
onChange={() => toggleColumn(labelName)} onChange={() => toggleColumn(labelName)}
checked={labels[labelName]?.active ?? false} 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>
))} ))}
</div> </div>

@ -1,5 +1,4 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { debounce } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { import {
@ -51,6 +50,7 @@ export function LogsTableWrap(props: Props) {
// Filtered copy of columnsWithMeta that only includes matching results // Filtered copy of columnsWithMeta that only includes matching results
const [filteredColumnsWithMeta, setFilteredColumnsWithMeta] = useState<fieldNameMetaStore | undefined>(undefined); const [filteredColumnsWithMeta, setFilteredColumnsWithMeta] = useState<fieldNameMetaStore | undefined>(undefined);
const [searchValue, setSearchValue] = useState<string>('');
const height = getLogsTableHeight(); const height = getLogsTableHeight();
const panelStateRefId = props?.panelState?.refId; 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 // Toggle a column on or off when the user interacts with an element in the multi-select sidebar
const toggleColumn = (columnName: fieldName) => { const toggleColumn = (columnName: fieldName) => {
if (!columnsWithMeta || !(columnName in columnsWithMeta)) { if (!columnsWithMeta || !(columnName in columnsWithMeta)) {
@ -320,14 +328,12 @@ export function LogsTableWrap(props: Props) {
fuzzySearch(Object.keys(columnsWithMeta), needle, dispatcher); fuzzySearch(Object.keys(columnsWithMeta), needle, dispatcher);
}; };
// Debounce fuzzy search
const debouncedSearch = debounce(search, 500);
// onChange handler for search input // onChange handler for search input
const onSearchInputChange = (e: React.FormEvent<HTMLInputElement>) => { const onSearchInputChange = (e: React.FormEvent<HTMLInputElement>) => {
const value = e.currentTarget?.value; const value = e.currentTarget?.value;
setSearchValue(value);
if (value) { if (value) {
debouncedSearch(value); search(value);
} else { } else {
// If the search input is empty, reset the local search state. // If the search input is empty, reset the local search state.
setFilteredColumnsWithMeta(undefined); setFilteredColumnsWithMeta(undefined);
@ -376,11 +382,12 @@ export function LogsTableWrap(props: Props) {
</div> </div>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<section className={styles.sidebar}> <section className={styles.sidebar}>
<LogsColumnSearch onChange={onSearchInputChange} /> <LogsColumnSearch value={searchValue} onChange={onSearchInputChange} />
<LogsTableMultiSelect <LogsTableMultiSelect
toggleColumn={toggleColumn} toggleColumn={toggleColumn}
filteredColumnsWithMeta={filteredColumnsWithMeta} filteredColumnsWithMeta={filteredColumnsWithMeta}
columnsWithMeta={columnsWithMeta} columnsWithMeta={columnsWithMeta}
clear={clearSelection}
/> />
</section> </section>
<LogsTable <LogsTable

Loading…
Cancel
Save