LogTable and LogSidePanel UI tweaks (#88)

* Tweak colors and contrast for log table row hover and selected state
* Styling tweaks for LogSidePanel – header, shadow, borders, etc
* Add [X] button to LogSidePanel
* Use CSS modules for styling
* LogTable – use chevron icon instead of `>` symbol
* Use `col-resize` cursor when resizing LogTable columns
* Use correct env var for HDX_API_KEY in `app` to fix client sessions 

![Screenshot 2023-11-08 at 9 18 01 PM](https://github.com/hyperdxio/hyperdx/assets/149748269/8ebe6065-002f-4c02-ae9f-e436d92a5983)

![Screenshot 2023-11-08 at 9 18 09 PM](https://github.com/hyperdxio/hyperdx/assets/149748269/1f96406a-d88e-4344-aee2-533b77102e5c)

![Screenshot 2023-11-08 at 9 18 12 PM](https://github.com/hyperdxio/hyperdx/assets/149748269/43c57e20-1941-4eb3-9e82-30539c009b1f)
This commit is contained in:
Shorpo 2023-11-09 17:54:32 -07:00 committed by GitHub
parent b1a537d88c
commit 04f82d71db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 351 additions and 168 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/app': minor
---
LogTable and LogSidePanel UI tweaks

View file

@ -27,6 +27,7 @@ import SearchInput from './SearchInput';
import TabBar from './TabBar';
import TimelineChart from './TimelineChart';
import SessionSubpanel from './SessionSubpanel';
import LogSidePanelKbdShortcuts from './LogSidePanelKbdShortcuts';
import {
formatDistanceToNowStrictShort,
useFirstNonNullValue,
@ -41,6 +42,8 @@ import { CurlGenerator } from './curlGenerator';
import { Dictionary } from './types';
import { ZIndexContext, useZIndex } from './zIndex';
import styles from '../styles/LogSidePanel.module.scss';
const HDX_BODY_FIELD = '_hdx_body';
// https://github.com/reduxjs/redux-devtools/blob/f11383d294c1139081f119ef08aa1169bd2ad5ff/packages/react-json-tree/src/createStylingFromTheme.ts
@ -1765,8 +1768,10 @@ function SidePanelHeader({
logData,
onPropertyAddClick,
generateSearchUrl,
onClose,
}: {
logData: any;
onClose: VoidFunction;
onPropertyAddClick?: (name: string, value: string) => void;
generateSearchUrl: (
query?: string,
@ -1800,10 +1805,10 @@ function SidePanelHeader({
return (
<div>
<div className="d-flex justify-content-between align-items-center">
<div className={styles.panelHeader}>
<div>
{logData.severity_text != null ? (
<span className="me-2">
<span className={styles.severityChip}>
<LogLevel level={logData?.severity_text ?? ''} />
</span>
) : null}
@ -1817,8 +1822,8 @@ function SidePanelHeader({
<span className="text-muted">at</span>{' '}
{format(new Date(logData.timestamp), 'MMM d HH:mm:ss.SSS')}{' '}
<span className="text-muted">
({formatDistanceToNowStrictShort(new Date(logData.timestamp))}{' '}
ago)
&middot;{' '}
{formatDistanceToNowStrictShort(new Date(logData.timestamp))} ago
</span>
</span>
</div>
@ -1864,57 +1869,67 @@ function SidePanelHeader({
Share Event
</Button>
</CopyToClipboard>
<Button
variant="dark"
className="text-muted-hover d-flex align-items-center"
size="sm"
onClick={onClose}
>
<i className="bi bi-x-lg" />
</Button>
</div>
</div>
<div className="mt-3">
<div
className="bg-hdx-dark p-3 overflow-auto"
style={{ maxHeight: 300 }}
>
{stripAnsi(logData.body)}
<div className={styles.panelDetails}>
<div>
<div
className="bg-hdx-dark p-3 overflow-auto"
style={{ maxHeight: 300 }}
>
{stripAnsi(logData.body)}
</div>
</div>
<div className="d-flex flex-wrap">
{logData._service ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="service"
value={logData._service}
/>
) : null}
{logData._host ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="host"
value={logData._host}
/>
) : null}
{userEmail ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="userEmail"
value={userEmail}
/>
) : null}
{userName ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="userName"
value={userName}
/>
) : null}
{teamName ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="teamName"
value={teamName}
/>
) : null}
</div>
</div>
<div className="d-flex mt-2">
{logData._service ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="service"
value={logData._service}
/>
) : null}
{logData._host ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="host"
value={logData._host}
/>
) : null}
{userEmail ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="userEmail"
value={userEmail}
/>
) : null}
{userName ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="userName"
value={userName}
/>
) : null}
{teamName ? (
<EventTag
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
name="teamName"
value={teamName}
/>
) : null}
</div>
</div>
);
@ -2109,6 +2124,7 @@ export default function LogSidePanel({
// We'll need to handle this properly eventually...
const tab = isNestedPanel ? stateTab : queryTab;
const setTab = isNestedPanel ? setStateTab : setQueryTab;
const _onClose = useCallback(() => {
// Reset tab to undefined when unmounting, so that when we open the drawer again, it doesn't open to the last tab
// (which might not be valid, ex session replay)
@ -2170,9 +2186,10 @@ export default function LogSidePanel({
return (
<Drawer
enableOverlay
overlayOpacity={0.1}
customIdSuffix={`log-side-panel-${logId}`}
duration={0}
overlayOpacity={0.2}
open={logId != null}
onClose={() => {
if (!subDrawerOpen) {
@ -2181,26 +2198,22 @@ export default function LogSidePanel({
}}
direction="right"
size={displayedTab === 'replay' || isSmallScreen ? '80vw' : '60vw'}
style={{ background: '#0F1216' }}
className="border-start border-dark"
zIndex={drawerZIndex}
// enableOverlay={subDrawerOpen}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div
className="p-3 h-100 d-flex flex-column fs-8"
style={{ marginTop: 20 }}
>
{isLoading && <h3>Loading...</h3>}
<div className={styles.panel}>
{isLoading && <div className={styles.loadingState}>Loading...</div>}
{logData != null && !isLoading ? (
<>
<SidePanelHeader
logData={logData}
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
onClose={_onClose}
/>
<TabBar
className="fs-8 mb-2 mt-4"
className="fs-8 mb-2 mt-2"
items={[
{
text: 'Parsed Properties',
@ -2243,13 +2256,14 @@ export default function LogSidePanel({
console.error(err);
}}
fallbackRender={() => (
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent">
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent p-4">
An error occurred while rendering this event.
</div>
)}
>
{/* Parsed Properties */}
{displayedTab === 'parsed' ? (
<div className="flex-grow-1 mt-1 pt-2 bg-body overflow-auto">
<div className="flex-grow-1 px-4 bg-body overflow-auto">
<PropertySubpanel
logData={logData}
onPropertyAddClick={onPropertyAddClick}
@ -2266,9 +2280,11 @@ export default function LogSidePanel({
/>
</div>
) : null}
{/* Original Line */}
{displayedTab === 'original' ? (
<div
className="mt-3 flex-grow-1 overflow-auto"
className="flex-grow-1 px-4 overflow-auto"
style={{ minHeight: 0 }}
>
<div className="my-2">
@ -2276,9 +2292,11 @@ export default function LogSidePanel({
</div>
</div>
) : null}
{/* Trace */}
{displayedTab === 'trace' ? (
<div
className="flex-grow-1 mt-3 bg-body overflow-auto"
className="flex-grow-1 px-4 mt-3 bg-body overflow-auto"
style={{ minHeight: 0 }}
>
<TraceSubpanel
@ -2292,15 +2310,19 @@ export default function LogSidePanel({
/>
</div>
) : null}
{/* Debug */}
{displayedTab === 'debug' ? (
<div className="mt-3 overflow-auto">
<div className="px-4 overflow-auto">
<code>
<pre>{JSON.stringify(logData, undefined, 4)}</pre>
</code>
</div>
) : null}
{/* Session Replay */}
{displayedTab === 'replay' ? (
<div className="mt-3 overflow-hidden">
<div className="px-4 overflow-hidden flex-grow-1">
{rumSessionId != null ? (
<SessionSubpanel
start={start}
@ -2320,6 +2342,7 @@ export default function LogSidePanel({
</div>
) : null}
</ErrorBoundary>
<LogSidePanelKbdShortcuts />
</>
) : null}
</div>

View file

@ -0,0 +1,45 @@
import styles from '../styles/LogSidePanel.module.scss';
import { CloseButton } from 'react-bootstrap';
import { useLocalStorage } from './utils';
import * as React from 'react';
const Kbd = ({ children }: { children: string }) => (
<div className={styles.kbd}>{children}</div>
);
export default function LogSidePanelKbdShortcuts() {
const [isDismissed, setDismissed] = useLocalStorage<boolean>(
'kbd-shortcuts-dismissed',
false,
);
const handleDismiss = React.useCallback(() => {
setDismissed(true);
}, []);
if (isDismissed) {
return null;
}
return (
<div className={styles.kbdShortcuts}>
<div className="d-flex justify-content-between align-items-center ">
<div className="d-flex align-items-center gap-3">
<div>
Use <Kbd></Kbd>
<Kbd></Kbd> arrow keys to move through events
</div>
<div className={styles.kbdDivider} />
<div>
<Kbd>ESC</Kbd> to close
</div>
</div>
<CloseButton
variant="white"
aria-label="Hide"
onClick={handleDismiss}
/>
</div>
</div>
);
}

View file

@ -26,6 +26,8 @@ import { useLocalStorage, usePrevious, useWindowSize } from './utils';
import { useSearchEventStream } from './search';
import { useHotkeys } from 'react-hotkeys-hook';
import styles from '../styles/LogTable.module.scss';
type Row = Record<string, any> & { duration: number };
type AccessorFn = (row: Row, column: string) => any;
@ -316,7 +318,7 @@ export const RawLogTable = memo(
onRowExpandClick(id, sort_key);
}}
>
{'> '}
<span className="bi bi-chevron-right" />
</div>
);
},
@ -564,7 +566,7 @@ export const RawLogTable = memo(
return (
<div
className="overflow-auto h-100 fs-8 bg-inherit py-2"
className="overflow-auto h-100 fs-8 bg-inherit"
onScroll={e => {
fetchMoreOnBottomReached(e.target as HTMLDivElement);
@ -582,14 +584,7 @@ export const RawLogTable = memo(
id={tableId}
style={{ tableLayout: 'fixed' }}
>
<thead
className="bg-inherit"
style={{
background: 'inherit',
position: 'sticky',
top: 0,
}}
>
<thead className={styles.tableHead}>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, headerIndex) => {
@ -623,7 +618,7 @@ export const RawLogTable = memo(
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={`resizer text-gray-600 cursor-grab ${
className={`resizer text-gray-600 cursor-col-resize ${
header.column.getIsResizing() ? 'isResizing' : ''
}`}
style={{
@ -690,8 +685,9 @@ export const RawLogTable = memo(
}}
role="button"
key={virtualRow.key}
className={cx('bg-default-dark-grey-hover', {
'bg-dark-grey': highlightedLineId === row.original.id,
className={cx(styles.tableRow, {
[styles.tableRow__selected]:
highlightedLineId === row.original.id,
})}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}

View file

@ -0,0 +1,73 @@
@import './variables';
// TODO: Use variables for spacings
$padding-x: 24px;
.panel {
display: flex;
flex-direction: column;
font-size: 12px;
height: 100%;
background-color: $body-bg;
box-shadow: 0 0 100px 40px rgba(0, 0, 0, 0.7);
border-left: 1px solid $slate-900;
}
.panelHeader {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid $slate-900;
padding: 12px $padding-x;
}
.panelDetails {
padding: $padding-x;
display: flex;
flex-direction: column;
gap: 12px;
}
.panelContent {
padding: 0 $padding-x;
}
.severityChip {
background: $slate-800;
padding: 4px 12px;
border-radius: 16px;
margin-right: 8px;
}
.loadingState {
color: $slate-600;
width: 100%;
margin: $padding-x;
font-size: 16px;
text-align: center;
flex-grow: 1;
}
.kbdShortcuts {
background: $slate-950;
border-top: 1px solid $slate-900;
padding: 12px $padding-x;
color: $slate-300;
font-size: 11px;
}
.kbd {
display: inline-block;
background: $slate-900;
letter-spacing: 0.5px;
border: 1px solid $slate-500;
border-radius: 4px;
padding: 1px 6px;
margin: 0 2px;
}
.kbdDivider {
height: 21px;
width: 1px;
background: $slate-800;
}

View file

@ -0,0 +1,17 @@
@import './variables';
.tableHead {
background: inherit;
position: sticky;
top: 0;
}
.tableRow {
&:hover {
background-color: $slate-800;
}
&__selected {
background-color: $slate-800;
font-weight: bold;
}
}

View file

@ -1,90 +1,4 @@
$text-muted: #c4c4c4;
$text-purple: #9357ff; // This isn't a bootstrap variable
$text-light: #f8f8f2;
$green: #50fa7b;
$red: #ff5d5b;
$blue: #3788f6;
$purple: #8f57f3;
$gray-100: #f8f9fa;
$body-color: $gray-100;
$bg-hdx-dark: #1a1d23;
$bg-dark-grey: #1f2429;
$bg-grey: #21272e;
$bg-purple: #2e273b;
$bg-purple-active: #4a4eb5;
$body-bg: #0f1216;
$code-color: #4bb74a;
$info: $purple;
$spacer: 1rem;
$spacers: (
0: 0,
0\.5: $spacer * 0.125,
1: $spacer * 0.25,
2: $spacer * 0.5,
2\.5: $spacer * 0.75,
3: $spacer,
4: $spacer * 1.5,
4\.5: $spacer * 2,
5: $spacer * 3,
6: $spacer * 4,
7: $spacer * 5,
8: $spacer * 8,
10: $spacer * 10,
14: $spacer * 14,
16: $spacer * 16,
);
$navbar-dark-active-color: $green;
$navbar-dark-color: rgba(#fff, 0.75);
$navbar-dark-hover-color: rgba(#fff, 1);
$dropdown-dark-bg: $bg-grey;
$popover-bg: $bg-purple;
$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`
$h1-font-size: $font-size-base * 2.5 !default;
$h2-font-size: $font-size-base * 2 !default;
$h3-font-size: $font-size-base * 1.75 !default;
$h4-font-size: $font-size-base * 1.5 !default;
$h5-font-size: $font-size-base * 1.25 !default;
$h6-font-size: $font-size-base !default;
// scss-docs-end font-variables
// scss-docs-start font-sizes
$font-sizes: (
1: $h1-font-size,
2: $h2-font-size,
3: $h3-font-size,
4: $h4-font-size,
5: $h5-font-size,
5\.5: $font-size-base * 1.125,
6: $h6-font-size,
7: $font-size-base * 0.875,
7\.5: $font-size-base * 0.8125,
8: $font-size-base * 0.75,
8\.5: $font-size-base * 0.625,
9: $font-size-base * 0.5,
) !default;
// scss-docs-end font-sizes
$input-bg: #ffffff1a;
$input-border-width: 0px;
$input-group-addon-bg: $input-bg;
$form-select-indicator-color: $body-color;
$accordion-button-bg: $bg-purple;
$accordion-button-active-bg: $bg-purple-active;
$accordion-button-active-color: $body-color;
$accordion-bg: $body-bg;
$accordion-border-radius: 5px;
@import './variables';
@import '~bootstrap/scss/bootstrap';
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;700&display=swap');
@ -164,7 +78,7 @@ html[class~='dark'] body {
color: $text-muted;
transition: color 0.2s ease;
&:hover {
color: #e5e5e5;
color: #fff;
}
}
@ -237,6 +151,10 @@ body {
cursor: grab;
}
.cursor-col-resize {
cursor: col-resize;
}
.cursor-zoom-in {
cursor: zoom-in !important;
}

View file

@ -0,0 +1,106 @@
/**
Colors
*/
$text-muted: #dadada;
$text-purple: #9357ff; // This isn't a bootstrap variable
$text-light: #f8f8f2;
$green: #50fa7b;
$red: #ff5d5b;
$blue: #3788f6;
$purple: #8f57f3;
$gray-100: #f8f9fa;
$body-color: $gray-100;
$bg-hdx-dark: #1a1d23;
$bg-dark-grey: #1f2429;
$bg-grey: #21272e;
$bg-purple: #2e273b;
$bg-purple-active: #4a4eb5;
$body-bg: #0f1216;
$code-color: #4bb74a;
$info: $purple;
// TODO: Adjust contrast ratios
$slate-50: #f6f7f9;
$slate-100: #eceef2;
$slate-200: #d5dae2;
$slate-300: #b1bbc8;
$slate-400: #8695aa;
$slate-500: #677890;
$slate-600: #526077;
$slate-700: #434e61;
$slate-800: #3a4352;
$slate-900: #343a46;
$slate-950: #1a1d23;
/**
Spacing
*/
$spacer: 1rem;
$spacers: (
0: 0,
0\.5: $spacer * 0.125,
1: $spacer * 0.25,
2: $spacer * 0.5,
2\.5: $spacer * 0.75,
3: $spacer,
4: $spacer * 1.5,
4\.5: $spacer * 2,
5: $spacer * 3,
6: $spacer * 4,
7: $spacer * 5,
8: $spacer * 8,
10: $spacer * 10,
14: $spacer * 14,
16: $spacer * 16,
);
$navbar-dark-active-color: $green;
$navbar-dark-color: rgba(#fff, 0.75);
$navbar-dark-hover-color: rgba(#fff, 1);
$dropdown-dark-bg: $bg-grey;
$popover-bg: $bg-purple;
$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`
$h1-font-size: $font-size-base * 2.5 !default;
$h2-font-size: $font-size-base * 2 !default;
$h3-font-size: $font-size-base * 1.75 !default;
$h4-font-size: $font-size-base * 1.5 !default;
$h5-font-size: $font-size-base * 1.25 !default;
$h6-font-size: $font-size-base !default;
// scss-docs-end font-variables
// scss-docs-start font-sizes
$font-sizes: (
1: $h1-font-size,
2: $h2-font-size,
3: $h3-font-size,
4: $h4-font-size,
5: $h5-font-size,
5\.5: $font-size-base * 1.125,
6: $h6-font-size,
7: $font-size-base * 0.875,
7\.5: $font-size-base * 0.8125,
8: $font-size-base * 0.75,
8\.5: $font-size-base * 0.625,
9: $font-size-base * 0.5,
) !default;
// scss-docs-end font-sizes
$input-bg: #ffffff1a;
$input-border-width: 0px;
$input-group-addon-bg: $input-bg;
$form-select-indicator-color: $body-color;
$accordion-button-bg: $bg-purple;
$accordion-button-active-bg: $bg-purple-active;
$accordion-button-active-color: $body-color;
$accordion-bg: $body-bg;
$accordion-border-radius: 5px;