feat: User Preferences modal (#413)

This commit is contained in:
Ernest Iliiasov 2024-05-28 14:48:31 -07:00 committed by GitHub
parent 63e7d301d5
commit e26a6d2ee6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 499 additions and 242 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---
Add User Preferences modal

View file

@ -1,10 +1,10 @@
{
"editor.formatOnSave.eslint": true,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"cssVariables.lookupFiles": [
"**/*.css",

View file

@ -18,7 +18,7 @@ import { apiConfigs } from '../src/api';
import * as config from '../src/config';
import { useConfirmModal } from '../src/useConfirm';
import { QueryParamProvider as HDXQueryParamProvider } from '../src/useQueryParam';
import { UserPreferencesProvider } from '../src/useUserPreferences';
import { useBackground, useUserPreferences } from '../src/useUserPreferences';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
@ -29,12 +29,17 @@ import '../src/LandingPage.scss';
const queryClient = new QueryClient();
import HyperDX from '@hyperdx/browser';
const mantineTheme: MantineThemeOverride = {
fontFamily: 'IBM Plex Mono, sans-serif',
const makeTheme = ({
fontFamily,
}: {
fontFamily: string;
}): MantineThemeOverride => ({
fontFamily,
primaryColor: 'green',
primaryShade: 8,
white: '#fff',
fontSizes: {
xxs: '11px',
xs: '12px',
sm: '13px',
md: '15px',
@ -68,13 +73,13 @@ const mantineTheme: MantineThemeOverride = {
],
},
headings: {
fontFamily: 'IBM Plex Mono, sans-serif',
fontFamily,
},
components: {
Modal: {
styles: {
header: {
fontFamily: 'IBM Plex Mono, sans-serif',
fontFamily,
fontWeight: 'bold',
},
},
@ -108,7 +113,7 @@ const mantineTheme: MantineThemeOverride = {
},
},
},
};
});
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: React.ReactElement) => React.ReactNode;
@ -119,7 +124,9 @@ type AppPropsWithLayout = AppProps & {
};
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const { userPreferences } = useUserPreferences();
const confirmModal = useConfirmModal();
const background = useBackground(userPreferences);
// port to react query ? (needs to wrap with QueryClientProvider)
useEffect(() => {
@ -162,6 +169,20 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
});
}, []);
useEffect(() => {
document.documentElement.className =
userPreferences.theme === 'dark' ? 'hdx-theme-dark' : 'hdx-theme-light';
// TODO: Remove after migration to Mantine
document.body.style.fontFamily = userPreferences.font
? `"${userPreferences.font}", sans-serif`
: '"IBM Plex Mono"';
}, [userPreferences.theme, userPreferences.font]);
const mantineTheme = React.useMemo(
() => makeTheme({ fontFamily: userPreferences.font }),
[userPreferences.font],
);
const getLayout = Component.getLayout ?? (page => page);
return (
@ -195,14 +216,13 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<HDXQueryParamProvider>
<QueryParamProvider adapter={NextAdapter}>
<QueryClientProvider client={queryClient}>
<UserPreferencesProvider>
<MantineProvider forceColorScheme="dark" theme={mantineTheme}>
<Notifications />
{getLayout(<Component {...pageProps} />)}
</MantineProvider>
<ReactQueryDevtools initialIsOpen={false} />
{confirmModal}
</UserPreferencesProvider>
<MantineProvider forceColorScheme="dark" theme={mantineTheme}>
<Notifications />
{getLayout(<Component {...pageProps} />)}
</MantineProvider>
<ReactQueryDevtools initialIsOpen={false} />
{confirmModal}
{background}
</QueryClientProvider>
</QueryParamProvider>
</HDXQueryParamProvider>

View file

@ -21,6 +21,7 @@ import {
Loader,
ScrollArea,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { version } from '../package.json';
@ -31,6 +32,7 @@ import Icon from './Icon';
import Logo from './Logo';
import { KubernetesFlatIcon } from './SVGIcons';
import type { Dashboard, LogView } from './types';
import { UserPreferencesModal } from './UserPreferencesModal';
import { useLocalStorage, useWindowSize } from './utils';
import styles from '../styles/AppNav.module.scss';
@ -902,6 +904,11 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
[dashboards, refetchDashboards, updateDashboard],
);
const [
UserPreferencesOpen,
{ close: closeUserPreferences, open: openUserPreferences },
] = useDisclosure(false);
return (
<>
<AuthLoadingBlocker />
@ -1348,6 +1355,19 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
</span>
</Link>
</div>
<div className="my-3">
<span
onClick={openUserPreferences}
className={cx(
'text-decoration-none d-flex justify-content-between align-items-center text-muted-hover cursor-pointer',
)}
>
<span>
<i className="bi bi-person-gear text-slate-300" />{' '}
{!isCollapsed && <span>User Preferences</span>}
</span>
</span>
</div>
<div className="my-3">
<Link
href="https://hyperdx.io/docs"
@ -1379,6 +1399,10 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
</>
)}
</ScrollArea>
<UserPreferencesModal
opened={UserPreferencesOpen}
onClose={closeUserPreferences}
/>
</>
);
}

View file

@ -137,7 +137,6 @@ function GraphPage() {
const { isReady, searchedTimeRange, displayedTimeInputValue, onSearch } =
useNewTimeQuery({
isUTC: false,
initialDisplayValue: 'Past 1h',
initialTimeRange: defaultTimeRange,
});

View file

@ -34,7 +34,6 @@ export default function DBQuerySidePanel() {
);
const { searchedTimeRange: dateRange } = useTimeQuery({
isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,

View file

@ -351,8 +351,6 @@ const Tile = forwardRef(
<LogTableWithSidePanel
config={config}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
onSettled={onSettled}
/>
@ -692,7 +690,6 @@ export default function DashboardPage() {
const { searchedTimeRange, displayedTimeInputValue, onSearch } =
useNewTimeQuery({
isUTC: false,
initialDisplayValue: 'Past 1h',
initialTimeRange: defaultTimeRange,
});

View file

@ -329,8 +329,6 @@ export const EditSearchChartForm = ({
where: previewConfig.where,
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
</div>
@ -486,8 +484,6 @@ export const EditNumberChartForm = ({
}`,
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
</div>
@ -691,8 +687,6 @@ export const EditTableChartForm = ({
}`,
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
</div>
@ -887,8 +881,6 @@ export const EditHistogramChartForm = ({
}`,
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
</div>
@ -1426,8 +1418,6 @@ export const EditLineChartForm = ({
})}`,
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
</div>

View file

@ -34,7 +34,6 @@ export default function EndpointSidePanel() {
);
const { searchedTimeRange: dateRange } = useTimeQuery({
isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,

View file

@ -28,7 +28,7 @@ import {
seriesToUrlSearchQueryParam,
} from './ChartUtils';
import type { ChartSeries, NumberFormat } from './types';
import useUserPreferences, { TimeFormat } from './useUserPreferences';
import { useUserPreferences } from './useUserPreferences';
import { formatNumber } from './utils';
import { semanticKeyedColor, TIME_TOKENS, truncateMiddle } from './utils';
@ -38,7 +38,9 @@ const MAX_LEGEND_ITEMS = 4;
const HDXLineChartTooltip = withErrorBoundary(
memo((props: any) => {
const timeFormat: TimeFormat = useUserPreferences().timeFormat;
const {
userPreferences: { timeFormat },
} = useUserPreferences();
const tsFormat = TIME_TOKENS[timeFormat];
const { active, payload, label, numberFormat } = props;
if (active && payload && payload.length) {
@ -217,7 +219,9 @@ const MemoChart = memo(function MemoChart({
}, [groupKeys, displayType, lineNames, graphResults]);
const sizeRef = useRef<[number, number]>([0, 0]);
const timeFormat: TimeFormat = useUserPreferences().timeFormat;
const {
userPreferences: { timeFormat },
} = useUserPreferences();
const tsFormat = TIME_TOKENS[timeFormat];
// Gets the preffered time format from User Preferences, then converts it to a formattable token

View file

@ -753,7 +753,6 @@ export default function KubernetesDashboardPage() {
setDisplayedTimeInputValue,
onSearch,
} = useTimeQuery({
isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,
@ -991,8 +990,6 @@ export default function KubernetesDashboardPage() {
'object.regarding.name': 'Name',
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
showServiceColumn={false}
/>

View file

@ -28,8 +28,7 @@ import InstallInstructionsModal from './InstallInstructionsModal';
import LogLevel from './LogLevel';
import { useSearchEventStream } from './search';
import { UNDEFINED_WIDTH } from './tableUtils';
import type { TimeFormat } from './useUserPreferences';
import useUserPreferences from './useUserPreferences';
import { useUserPreferences } from './useUserPreferences';
import { useLocalStorage, usePrevious, useWindowSize } from './utils';
import { TIME_TOKENS } from './utils';
@ -142,19 +141,16 @@ function LogTableSettingsModal({
onHide,
onDone,
initialAdditionalColumns,
initialIsUTC,
initialWrapLines,
downloadCSVButton,
}: {
initialAdditionalColumns: string[];
initialIsUTC: boolean;
initialWrapLines: boolean;
show: boolean;
onHide: () => void;
onDone: (settings: {
additionalColumns: string[];
wrapLines: boolean;
isUTC: boolean;
}) => void;
downloadCSVButton: JSX.Element;
}) {
@ -162,7 +158,6 @@ function LogTableSettingsModal({
initialAdditionalColumns,
);
const [wrapLines, setWrapLines] = useState(initialWrapLines);
const [isUTC, setIsUTC] = useState(initialIsUTC);
return (
<Modal
@ -188,14 +183,9 @@ function LogTableSettingsModal({
onChange={() => setWrapLines(!wrapLines)}
label="Wrap Lines"
/>
<Checkbox
id="utc"
className="mt-4"
labelClassName="fs-7"
checked={isUTC}
onChange={() => setIsUTC(!isUTC)}
label="Use UTC time instead of local time"
/>
<div className="mt-4 text-muted fs-8">
UTC setting moved to User Preferences
</div>
<div className="mt-4">
<div className="mb-2">Download Search Results</div>
{downloadCSVButton}
@ -205,7 +195,7 @@ function LogTableSettingsModal({
variant="outline-success"
className="fs-7 text-muted-hover"
onClick={() => {
onDone({ additionalColumns, wrapLines, isUTC });
onDone({ additionalColumns, wrapLines });
onHide();
}}
>
@ -225,7 +215,6 @@ export const RawLogTable = memo(
tableId,
displayedColumns,
fetchNextPage,
formatUTC,
hasNextPage,
highlightedLineId,
isLive,
@ -261,7 +250,6 @@ export const RawLogTable = memo(
// value: string | number | boolean,
// ) => void;
hasNextPage: boolean;
formatUTC: boolean;
highlightedLineId: string | undefined;
onScroll: (scrollTop: number) => void;
isLive: boolean;
@ -283,7 +271,9 @@ export const RawLogTable = memo(
const { width } = useWindowSize();
const isSmallScreen = (width ?? 1000) < 900;
const timeFormat: TimeFormat = useUserPreferences().timeFormat;
const {
userPreferences: { timeFormat, isUTC },
} = useUserPreferences();
const tsFormat = TIME_TOKENS[timeFormat];
const [columnSizeStorage, setColumnSizeStorage] = useLocalStorage<
@ -340,13 +330,13 @@ export const RawLogTable = memo(
header: () =>
isSmallScreen
? 'Time'
: `Timestamp${formatUTC ? ' (UTC)' : ' (Local)'}`,
: `Timestamp${isUTC ? ' (UTC)' : ' (Local)'}`,
cell: info => {
// FIXME: since original timestamp doesn't come with timezone info
const date = new Date(info.getValue<string>());
return (
<span className="text-muted">
{formatUTC
{isUTC
? formatInTimeZone(
date,
'Etc/UTC',
@ -436,7 +426,7 @@ export const RawLogTable = memo(
},
],
[
formatUTC,
isUTC,
highlightedLineId,
onRowExpandClick,
displayedColumns,
@ -795,10 +785,8 @@ export default function LogTable({
highlightedLineId,
onPropertySearchClick,
onRowExpandClick,
formatUTC,
isLive,
onScroll,
setIsUTC,
onEnd,
onShowPatternsClick,
tableId,
@ -817,10 +805,8 @@ export default function LogTable({
value: string | number | boolean,
) => void;
onRowExpandClick: (logId: string, sortKey: string) => void;
formatUTC: boolean;
onScroll: (scrollTop: number) => void;
isLive: boolean;
setIsUTC: (isUTC: boolean) => void;
onEnd?: () => void;
onShowPatternsClick?: () => void;
tableId?: string;
@ -837,6 +823,10 @@ export default function LogTable({
const resultsKey = [searchedQuery, displayedColumns, isLive].join(':');
const {
userPreferences: { isUTC },
} = useUserPreferences();
const {
results: searchResults,
resultsKey: searchResultsKey,
@ -891,16 +881,14 @@ export default function LogTable({
onHide={() => setInstructionsOpen(false)}
/>
<LogTableSettingsModal
key={`${formatUTC} ${displayedColumns} ${wrapLines}`}
key={`${isUTC} ${displayedColumns} ${wrapLines}`}
show={settingsOpen}
initialIsUTC={formatUTC}
initialAdditionalColumns={displayedColumns}
initialWrapLines={wrapLines}
onHide={() => setSettingsOpen(false)}
onDone={({ additionalColumns, wrapLines, isUTC }) => {
onDone={({ additionalColumns, wrapLines }) => {
setDisplayedColumns(additionalColumns);
setWrapLines(wrapLines);
setIsUTC(isUTC);
}}
downloadCSVButton={
<DownloadCSVButton
@ -934,7 +922,6 @@ export default function LogTable({
)}
// onPropertySearchClick={onPropertySearchClick}
hasNextPage={hasNextPageWhenNotLive}
formatUTC={formatUTC}
onRowExpandClick={onRowExpandClick}
onScroll={onScroll}
onShowPatternsClick={onShowPatternsClick}

View file

@ -8,14 +8,12 @@ import { useDisplayedColumns } from './useDisplayedColumns';
export function LogTableWithSidePanel({
config,
isUTC,
isLive,
onScroll,
selectedSavedSearch,
onPropertySearchClick,
onRowExpandClick,
onPropertyAddClick,
setIsUTC,
onSettled,
columnNameMap,
showServiceColumn,
@ -25,7 +23,6 @@ export function LogTableWithSidePanel({
dateRange: [Date, Date];
columns?: string[];
};
isUTC: boolean;
isLive: boolean;
columnNameMap?: Record<string, string>;
showServiceColumn?: boolean;
@ -34,7 +31,6 @@ export function LogTableWithSidePanel({
property: string,
value: string | number | boolean,
) => void;
setIsUTC: (isUTC: boolean) => void;
onPropertyAddClick?: (name: string, value: string | boolean | number) => void;
onRowExpandClick?: (logId: string, sortKey: string) => void;
@ -111,12 +107,10 @@ export function LogTableWithSidePanel({
) : null}
<LogTable
isLive={isLive}
setIsUTC={setIsUTC}
onScroll={onScroll ?? voidFn}
highlightedLineId={openedLog?.id}
config={config}
onPropertySearchClick={onPropertySearchClick}
formatUTC={isUTC}
onRowExpandClick={useCallback(
(id: string, sortKey: string) => {
setOpenedLog({ id, sortKey });

View file

@ -166,8 +166,6 @@ function NamespaceLogs({
where: _where,
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
</Card.Section>
@ -192,7 +190,6 @@ export default function NamespaceDetailsSidePanel() {
}, [namespaceName]);
const { searchedTimeRange: dateRange } = useTimeQuery({
isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,

View file

@ -181,8 +181,6 @@ function NodeLogs({
where: _where,
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
</Card.Section>
@ -207,7 +205,6 @@ export default function NodeDetailsSidePanel() {
}, [nodeName]);
const { searchedTimeRange: dateRange } = useTimeQuery({
isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,

View file

@ -160,7 +160,6 @@ export default function PatternSidePanel({
isLoading={false}
hasNextPage={false}
wrapLines={false}
formatUTC={false}
fetchNextPage={useCallback(() => {}, [])}
onScroll={useCallback(() => {}, [])}
/>

View file

@ -132,7 +132,6 @@ const MemoPatternTable = memo(
({
dateRange,
patterns,
formatUTC,
highlightedPatternId,
isLoading,
onRowExpandClick,
@ -144,7 +143,6 @@ const MemoPatternTable = memo(
wrapLines: boolean;
isLoading: boolean;
onRowExpandClick: (pattern: Pattern) => void;
formatUTC: boolean;
highlightedPatternId: string | undefined;
onShowEventsClick?: () => void;
}) => {
@ -254,7 +252,6 @@ const MemoPatternTable = memo(
},
],
[
// formatUTC,
highlightedPatternId,
onRowExpandClick,
isSmallScreen,
@ -437,7 +434,6 @@ const MemoPatternTable = memo(
export default function PatternTable({
config: { where, dateRange },
onRowExpandClick,
isUTC,
onShowEventsClick,
highlightedPatternId,
}: {
@ -447,7 +443,6 @@ export default function PatternTable({
};
highlightedPatternId: undefined | string;
onRowExpandClick: (pattern: Pattern) => void;
isUTC: boolean;
onShowEventsClick?: () => void;
}) {
const { data: histogramResults, isLoading: isHistogramResultsLoading } =
@ -495,7 +490,6 @@ export default function PatternTable({
highlightedPatternId={highlightedPatternId}
patterns={patterns?.data ?? []}
isLoading={isLoading}
formatUTC={isUTC}
onRowExpandClick={onRowExpandClick}
onShowEventsClick={onShowEventsClick}
/>

View file

@ -7,15 +7,12 @@ import PatternTable from './PatternTable';
function PatternTableWithSidePanel({
config,
isUTC,
onShowEventsClick,
}: {
config: {
where: string;
dateRange: [Date, Date];
};
isUTC: boolean;
onShowEventsClick?: () => void;
}) {
const [openedPattern, setOpenedPattern] = useState<Pattern | undefined>();
@ -38,7 +35,6 @@ function PatternTableWithSidePanel({
</Portal>
) : null}
<PatternTable
isUTC={isUTC}
config={config}
highlightedPatternId={openedPattern?.id}
onShowEventsClick={onShowEventsClick}

View file

@ -145,8 +145,6 @@ function PodLogs({
columns: ['k8s.container.name'],
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
columnNameMap={{
'k8s.container.name': 'Container',
@ -179,7 +177,6 @@ export default function PodDetailsSidePanel() {
}, [podName]);
const { searchedTimeRange: dateRange } = useTimeQuery({
isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,

View file

@ -48,6 +48,7 @@ import SearchTimeRangePicker from './SearchTimeRangePicker';
import { Tags } from './Tags';
import { useTimeQuery } from './timeQuery';
import { useDisplayedColumns } from './useDisplayedColumns';
import { useUserPreferences } from './useUserPreferences';
import 'react-modern-drawer/dist/index.css';
import styles from '../styles/SearchPage.module.scss';
@ -90,7 +91,6 @@ const HDXHistogram = memo(
config: { dateRange, where },
onTimeRangeSelect,
isLive,
isUTC,
}: {
config: {
dateRange: [Date, Date];
@ -98,8 +98,11 @@ const HDXHistogram = memo(
};
onTimeRangeSelect: (start: Date, end: Date) => void;
isLive: boolean;
isUTC: boolean;
}) => {
const {
userPreferences: { isUTC },
} = useUserPreferences();
const { data: histogramResults, isLoading: isHistogramResultsLoading } =
api.useLogHistogram(
where,
@ -216,7 +219,7 @@ const HDXHistogram = memo(
}
tick={{ fontSize: 12, fontFamily: 'IBM Plex Mono, monospace' }}
/>
<Tooltip content={<HistogramBarChartTooltip isUTC={isUTC} />} />
<Tooltip content={<HistogramBarChartTooltip />} />
<Bar dataKey="info" stackId="a" fill="#50FA7B" maxBarSize={24} />
<Bar dataKey="error" stackId="a" fill="#FF5D5B" maxBarSize={24} />
{highlightStart && highlightEnd ? (
@ -272,8 +275,6 @@ const LogViewerContainer = memo(function LogViewerContainer({
generateChartUrl,
isLive,
setIsLive,
isUTC,
setIsUTC,
onShowPatternsClick,
}: {
config: {
@ -293,8 +294,6 @@ const LogViewerContainer = memo(function LogViewerContainer({
onPropertyAddClick: (name: string, value: string | boolean | number) => void;
isLive: boolean;
setIsLive: (isLive: boolean) => void;
isUTC: boolean;
setIsUTC: (isUTC: boolean) => void;
onShowPatternsClick: () => void;
}) {
const [openedLogQuery, setOpenedLogQuery] = useQueryParams(
@ -362,7 +361,6 @@ const LogViewerContainer = memo(function LogViewerContainer({
<LogTable
tableId="search-table"
isLive={isLive}
setIsUTC={setIsUTC}
onScroll={useCallback(
(scrollTop: number) => {
// If the user scrolls a bit down, kick out of live mode
@ -375,7 +373,6 @@ const LogViewerContainer = memo(function LogViewerContainer({
highlightedLineId={openedLog?.id}
config={config}
onPropertySearchClick={onPropertySearchClick}
formatUTC={isUTC}
onRowExpandClick={useCallback(
(id: string, sortKey: string) => {
setOpenedLog({ id, sortKey });
@ -399,7 +396,6 @@ function SearchPage() {
'search',
);
const [isUTC, setIsUTC] = useState(false);
const {
isReady,
isLive,
@ -409,7 +405,7 @@ function SearchPage() {
onSearch,
setIsLive,
onTimeRangeSelect,
} = useTimeQuery({ isUTC });
} = useTimeQuery({});
const [isFirstLoad, setIsFirstLoad] = useState(true);
useEffect(() => {
@ -462,6 +458,10 @@ function SearchPage() {
[searchInput],
);
const {
userPreferences: { isUTC },
} = useUserPreferences();
const [saveSearchModalMode, setSaveSearchModalMode] = useState<
'update' | 'save' | 'hidden'
>('hidden');
@ -975,7 +975,6 @@ function SearchPage() {
config={chartsConfig}
onTimeRangeSelect={onTimeRangeSelect}
isLive={isLive}
isUTC={isUTC}
/>
) : null}
</div>
@ -1008,8 +1007,6 @@ function SearchPage() {
onPropertySearchClick={onPropertySearchClick}
isLive={isLive}
setIsLive={setIsLive}
isUTC={isUTC}
setIsUTC={setIsUTC}
onShowPatternsClick={() => {
setIsLive(false);
setResultsMode('patterns');
@ -1017,7 +1014,6 @@ function SearchPage() {
/>
) : (
<MemoPatternTableWithSidePanel
isUTC={isUTC}
config={chartsConfig}
onShowEventsClick={onShowEventsClick}
/>

View file

@ -7,7 +7,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import DatePicker from 'react-datepicker';
import { useHotkeys } from 'react-hotkeys-hook';
import { TimeFormat } from './useUserPreferences';
import { useUserPreferences } from './useUserPreferences';
import 'react-datepicker/dist/react-datepicker.css';
@ -39,14 +39,12 @@ export default function SearchTimeRangePicker({
onSearch,
onSubmit,
showLive = false,
timeFormat = '12h',
}: {
inputValue: string;
setInputValue: (str: string) => any;
onSearch: (rangeStr: string) => void;
onSubmit?: (rangeStr: string) => void;
showLive?: boolean;
timeFormat?: TimeFormat;
}) {
const inputRef = useRef<HTMLInputElement>(null);
@ -71,6 +69,10 @@ export default function SearchTimeRangePicker({
}
}, [isDatePickerOpen]);
const {
userPreferences: { timeFormat },
} = useUserPreferences();
return (
<>
<OverlayTrigger

View file

@ -107,7 +107,6 @@ export default function ServiceDashboardPage() {
setDisplayedTimeInputValue,
onSearch,
} = useTimeQuery({
isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,
@ -361,8 +360,6 @@ export default function ServiceDashboardPage() {
'object.regarding.name': 'Name',
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
showServiceColumn={false}
/>
@ -639,7 +636,6 @@ export default function ServiceDashboardPage() {
</Card.Section>
<Card.Section p="md" py="sm">
<MemoPatternTableWithSidePanel
isUTC={false}
config={{
where: scopeWhereQuery('level:"error"'),
dateRange,

View file

@ -241,7 +241,6 @@ export default function SessionsPage() {
setDisplayedTimeInputValue,
onSearch,
} = useTimeQuery({
isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,

View file

@ -61,8 +61,6 @@ export default function SlowestEventsTile({
columns: ['duration'],
}}
isLive={false}
isUTC={false}
setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
)}

View file

@ -42,7 +42,6 @@ import api from './api';
import { withAppNav } from './layout';
import { WebhookFlatIcon } from './SVGIcons';
import { WebhookService } from './types';
import useUserPreferences, { TimeFormat } from './useUserPreferences';
import { truncateMiddle } from './utils';
import { isValidJson, isValidUrl } from './utils';
@ -167,9 +166,6 @@ export default function TeamPage() {
const deleteTeamInvitation = api.useDeleteTeamInvitation();
const saveWebhook = api.useSaveWebhook();
const deleteWebhook = api.useDeleteWebhook();
const setTimeFormat = useUserPreferences().setTimeFormat;
const timeFormat = useUserPreferences().timeFormat;
const handleTimeButtonClick = (val: TimeFormat) => setTimeFormat(val);
const hasAdminAccess = true;
@ -1112,43 +1108,6 @@ export default function TeamPage() {
isSubmitting={saveTeamInvitation.isLoading}
/>
</MModal>
<div className={styles.sectionHeader}>Time Format</div>
<div className="mb-4">
<div className="text-slate-400 fs-8 mb-4">
Note: Only affects your own view and does not propagate to
other team members.
</div>
<ToggleButtonGroup
type="radio"
value={timeFormat}
onChange={handleTimeButtonClick}
name="buttons"
>
<ToggleButton
id="tbg-btn-1"
value="24h"
variant={
timeFormat === '24h'
? 'outline-success'
: 'outline-secondary'
}
>
24h
</ToggleButton>
<ToggleButton
id="tbg-btn-2"
value="12h"
variant={
timeFormat === '12h'
? 'outline-success'
: 'outline-secondary'
}
>
12h
</ToggleButton>
</ToggleButtonGroup>
</div>
</Stack>
)}
</Container>

View file

@ -0,0 +1,283 @@
import * as React from 'react';
import {
Autocomplete,
Badge,
Button,
Divider,
Group,
Input,
Modal,
Select,
Slider,
Stack,
Switch,
Text,
} from '@mantine/core';
import { UserPreferences, useUserPreferences } from './useUserPreferences';
const OPTIONS_FONTS = [
'IBM Plex Mono',
'Roboto Mono',
'Inter',
{ value: 'or use your own font', disabled: true },
];
const OPTIONS_THEMES = [
{ label: 'Dark', value: 'dark' },
{ label: 'Light', value: 'light' },
];
const OPTIONS_MIX_BLEND_MODE = [
'normal',
'multiply',
'screen',
'overlay',
'darken',
'lighten',
'color-dodge',
'color-burn',
'hard-light',
'soft-light',
'difference',
'exclusion',
'hue',
'saturation',
'color',
'luminosity',
'plus-darker',
'plus-lighter',
];
const SettingContainer = ({
label,
description,
children,
}: {
label: React.ReactNode;
description?: React.ReactNode;
children: React.ReactNode;
}) => {
return (
<Group align="center" justify="space-between">
<div style={{ flex: 1 }}>
{label}
{description && (
<Text c="gray.6" size="xs" mt={2}>
{description}
</Text>
)}
</div>
<div style={{ flex: 0.8 }}>{children}</div>
</Group>
);
};
export const UserPreferencesModal = ({
opened,
onClose,
}: {
opened: boolean;
onClose: () => void;
}) => {
const { userPreferences, setUserPreference } = useUserPreferences();
return (
<Modal
title={
<>
<span>Preferences</span>
<Text size="xs" c="gray.6" mt={6}>
Customize your experience
</Text>
</>
}
size="lg"
padding="lg"
keepMounted={false}
opened={opened}
onClose={onClose}
>
<Stack gap="lg">
<Divider label="Date and Time" labelPosition="left" />
<SettingContainer label="Time format">
<Select
value={userPreferences.timeFormat}
onChange={value =>
value &&
setUserPreference({
timeFormat: value as UserPreferences['timeFormat'],
})
}
data={['12h', '24h']}
allowDeselect={false}
/>
</SettingContainer>
<SettingContainer
label="Use UTC time in log tables"
description="Currently only applied to log table timestamps. Charts and other areas will be updated soon"
>
<Switch
size="md"
onLabel="UTC"
checked={userPreferences.isUTC}
onChange={e =>
setUserPreference({
isUTC: e.currentTarget.checked,
})
}
/>
</SettingContainer>
<Divider
label={
<Group align="center" gap="xs">
Appearance
<Badge variant="light" fw="normal" size="xs">
Experimental
</Badge>
</Group>
}
labelPosition="left"
mt="sm"
/>
<SettingContainer
label="Theme"
description="Switch between light and dark mode"
>
<Select
value={userPreferences.theme}
onChange={value =>
value &&
setUserPreference({
theme: value as UserPreferences['theme'],
})
}
data={OPTIONS_THEMES}
allowDeselect={false}
/>
</SettingContainer>
<SettingContainer
label="Font"
description="If using custom font, make sure it's installed on your system"
>
<Autocomplete
value={userPreferences.font}
filter={({ options }) => options}
onChange={value =>
setUserPreference({
font: value as UserPreferences['font'],
})
}
data={OPTIONS_FONTS}
/>
</SettingContainer>
<SettingContainer label="Background overlay">
<Switch
size="md"
variant="default"
onClick={() =>
setUserPreference({
backgroundEnabled: !userPreferences.backgroundEnabled,
})
}
checked={userPreferences.backgroundEnabled}
/>
</SettingContainer>
{userPreferences.backgroundEnabled && (
<>
<Divider label={<>Background</>} labelPosition="left" />
<SettingContainer
label="Background URL"
description={
<Group gap={4}>
<Button
variant="light"
color="gray"
size="compact-xs"
onClick={() =>
setUserPreference({
backgroundUrl: 'https://i.imgur.com/CrHYfTG.jpeg',
})
}
>
Try this
</Button>
<Button
variant="light"
color="gray"
size="compact-xs"
onClick={() =>
setUserPreference({
backgroundUrl: 'https://i.imgur.com/hnkdzAX.jpeg',
})
}
>
or this
</Button>
</Group>
}
>
<Input
placeholder="https:// or data:"
value={userPreferences.backgroundUrl}
leftSection={<i className="bi bi-globe" />}
onChange={e =>
setUserPreference({
backgroundUrl: e.currentTarget.value,
})
}
/>
</SettingContainer>
<SettingContainer label="Opacity">
<Slider
defaultValue={0.1}
step={0.01}
max={1}
min={0}
value={userPreferences.backgroundOpacity}
onChange={value =>
setUserPreference({
backgroundOpacity: value,
})
}
/>
</SettingContainer>
<SettingContainer label="Blur">
<Slider
defaultValue={0}
step={0.01}
max={90}
min={0}
value={userPreferences.backgroundBlur}
onChange={value =>
setUserPreference({
backgroundBlur: value,
})
}
/>
</SettingContainer>
<SettingContainer label="Blend mode">
<Select
value={userPreferences.backgroundBlendMode}
defaultValue="plus-lighter"
onChange={value =>
value &&
setUserPreference({
backgroundBlendMode:
value as UserPreferences['backgroundBlendMode'],
})
}
data={OPTIONS_MIX_BLEND_MODE}
allowDeselect={false}
/>
</SettingContainer>
</>
)}
</Stack>
</Modal>
);
};

View file

@ -12,6 +12,7 @@ import {
type UseTimeQueryInputType,
type UseTimeQueryReturnType,
} from '../timeQuery';
import { useUserPreferences } from '../useUserPreferences';
import { TestRouter } from './fixtures';
@ -23,7 +24,18 @@ jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));
function TestWrapper({ children }: { children: React.ReactNode }) {
function TestWrapper({
children,
isUTC,
}: {
children: React.ReactNode;
isUTC?: boolean;
}) {
const { setUserPreference } = useUserPreferences();
React.useEffect(() => {
setUserPreference({ isUTC });
}, [setUserPreference, isUTC]);
return (
<QueryParamProvider adapter={NextAdapter}>{children}</QueryParamProvider>
);
@ -72,7 +84,6 @@ describe('useTimeQuery tests', () => {
render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -89,9 +100,8 @@ describe('useTimeQuery tests', () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
render(
<TestWrapper>
<TestWrapper isUTC={true}>
<TestComponent
isUTC={true}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -111,7 +121,6 @@ describe('useTimeQuery tests', () => {
const { rerender } = render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -122,7 +131,6 @@ describe('useTimeQuery tests', () => {
rerender(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -151,7 +159,6 @@ describe('useTimeQuery tests', () => {
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -165,7 +172,6 @@ describe('useTimeQuery tests', () => {
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -189,7 +195,6 @@ describe('useTimeQuery tests', () => {
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -200,7 +205,6 @@ describe('useTimeQuery tests', () => {
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -228,7 +232,6 @@ describe('useTimeQuery tests', () => {
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -243,7 +246,6 @@ describe('useTimeQuery tests', () => {
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -269,7 +271,6 @@ describe('useTimeQuery tests', () => {
render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -289,7 +290,6 @@ describe('useTimeQuery tests', () => {
render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -313,7 +313,6 @@ describe('useTimeQuery tests', () => {
const result = render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -324,7 +323,6 @@ describe('useTimeQuery tests', () => {
result.rerender(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
@ -347,7 +345,6 @@ describe('useTimeQuery tests', () => {
render(
<TestWrapper>
<TestComponent
isUTC={false}
initialDisplayValue={initialDisplayValue}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}

View file

@ -19,6 +19,7 @@ import {
withDefault,
} from 'use-query-params';
import { useUserPreferences } from './useUserPreferences';
import { usePrevious } from './utils';
const LIVE_TAIL_TIME_QUERY = 'Live Tail';
@ -91,12 +92,10 @@ export function parseValidTimeRange(
}
export function useTimeQuery({
isUTC,
defaultValue = LIVE_TAIL_TIME_QUERY,
defaultTimeRange = [-1, -1],
isLiveEnabled = true,
}: {
isUTC: boolean;
defaultValue?: string;
defaultTimeRange?: [number, number];
isLiveEnabled?: boolean;
@ -106,6 +105,10 @@ export function useTimeQuery({
const isReady = typeof window === 'undefined' ? true : router.isReady;
const prevIsReady = usePrevious(isReady);
const {
userPreferences: { isUTC },
} = useUserPreferences();
const [displayedTimeInputValue, setDisplayedTimeInputValue] = useState<
undefined | string
>(undefined);
@ -401,8 +404,6 @@ export function useTimeQuery({
}
export type UseTimeQueryInputType = {
/** Whether the displayed value should be in UTC */
isUTC: boolean;
/**
* Optional initial value to be set as the `displayedTimeInputValue`.
* If no value is provided it will return a date string for the initial
@ -423,7 +424,6 @@ export type UseTimeQueryReturnType = {
};
export function useNewTimeQuery({
isUTC,
initialDisplayValue,
initialTimeRange,
}: UseTimeQueryInputType): UseTimeQueryReturnType {
@ -431,6 +431,10 @@ export function useNewTimeQuery({
// We need to return true in SSR to prevent mismatch issues
const isReady = typeof window === 'undefined' ? true : router.isReady;
const {
userPreferences: { isUTC },
} = useUserPreferences();
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
useState<string>(() => {
return initialDisplayValue ?? dateRangeToString(initialTimeRange, isUTC);

View file

@ -1,58 +1,67 @@
import React, { useContext, useEffect, useState } from 'react';
import React from 'react';
import produce from 'immer';
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { useLocalStorage } from './utils';
export type TimeFormat = '12h' | '24h';
export const UserPreferences = React.createContext({
isUTC: false,
timeFormat: '24h' as TimeFormat,
setTimeFormat: (timeFormat: TimeFormat) => {},
setIsUTC: (isUTC: boolean) => {},
});
export const UserPreferencesProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [storedTF, setTF] = useLocalStorage('timeFormat', '24h');
const setTimeFormat = (timeFormat: TimeFormat) => {
setState(state => ({ ...state, timeFormat }));
setTF(timeFormat);
};
const initState = {
isUTC: false,
timeFormat: '24h' as TimeFormat,
setTimeFormat,
setIsUTC: (isUTC: boolean) => setState(state => ({ ...state, isUTC })),
};
const [state, setState] = useState(initState);
// This only runs once in order to grab and set the initial timeFormat from localStorage
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
let timeFormat = window.localStorage.getItem('timeFormat') as TimeFormat;
if (timeFormat !== null) timeFormat = JSON.parse(timeFormat);
if (timeFormat !== null) {
setState(state => ({ ...state, timeFormat }));
}
} catch (error) {
console.log(error);
}
}, []);
return (
<UserPreferences.Provider value={state}>
{children}
</UserPreferences.Provider>
);
export type UserPreferences = {
isUTC: boolean;
timeFormat: '12h' | '24h';
theme: 'light' | 'dark';
font: 'IBM Plex Mono' | 'Inter';
backgroundEnabled?: boolean;
backgroundUrl?: string;
backgroundBlur?: number;
backgroundOpacity?: number;
backgroundBlendMode?: string;
};
export default function useUserPreferences() {
return useContext(UserPreferences);
}
export const userPreferencesAtom = atomWithStorage<UserPreferences>(
'hdx-user-preferences',
{
isUTC: false,
timeFormat: '12h',
theme: 'dark',
font: 'IBM Plex Mono',
},
);
export const useUserPreferences = () => {
const [userPreferences, setUserPreferences] = useAtom(userPreferencesAtom);
const setUserPreference = React.useCallback(
(preference: Partial<UserPreferences>) => {
setUserPreferences(
produce((draft: UserPreferences) => {
return { ...draft, ...preference };
}),
);
},
[setUserPreferences],
);
return { userPreferences, setUserPreference };
};
export const useBackground = (prefs: UserPreferences) => {
if (!prefs.backgroundEnabled || !prefs.backgroundUrl) {
return null;
}
const blurOffset = -1.5 * (prefs.backgroundBlur || 0) + 'px';
return (
<div
className="hdx-background-image"
style={{
backgroundImage: `url(${prefs.backgroundUrl})`,
opacity: prefs.backgroundOpacity ?? 0.1,
top: blurOffset,
left: blurOffset,
right: blurOffset,
bottom: blurOffset,
filter: `blur(${prefs.backgroundBlur}px)`,
mixBlendMode: (prefs.backgroundBlendMode as any) ?? 'screen',
}}
/>
);
};

View file

@ -3,7 +3,8 @@
@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&display=swap');
@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');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100..700&display=swap');
.inter {
font-family: Inter, Roboto, 'Helvetica Neue', sans-serif;
@ -767,6 +768,24 @@ div.react-datepicker {
}
}
// html {
// filter: invert(1) hue-rotate(180deg);
// }
.hdx-background-image {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
mix-blend-mode: screen;
z-index: 9999999;
pointer-events: none;
}
html.hdx-theme-light {
filter: invert(1) hue-rotate(180deg) brightness(1.05);
}
html.hdx-theme-light .hdx-background-image {
display: none;
}

View file

@ -18,7 +18,7 @@ $bg-dark-grey: #1f2429;
$bg-grey: #21272e;
$bg-purple: #2e273b;
$bg-purple-active: #4a4eb5;
$body-bg: #0f1216;
$body-bg: #0f1215;
$code-color: #4bb74a;
$info: $purple;