mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: User Preferences modal (#413)
This commit is contained in:
parent
63e7d301d5
commit
e26a6d2ee6
31 changed files with 499 additions and 242 deletions
5
.changeset/fair-buttons-camp.md
Normal file
5
.changeset/fair-buttons-camp.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/app': patch
|
||||
---
|
||||
|
||||
Add User Preferences modal
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ function GraphPage() {
|
|||
|
||||
const { isReady, searchedTimeRange, displayedTimeInputValue, onSearch } =
|
||||
useNewTimeQuery({
|
||||
isUTC: false,
|
||||
initialDisplayValue: 'Past 1h',
|
||||
initialTimeRange: defaultTimeRange,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ export default function DBQuerySidePanel() {
|
|||
);
|
||||
|
||||
const { searchedTimeRange: dateRange } = useTimeQuery({
|
||||
isUTC: false,
|
||||
defaultValue: 'Past 1h',
|
||||
defaultTimeRange: [
|
||||
defaultTimeRange?.[0]?.getTime() ?? -1,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ export default function EndpointSidePanel() {
|
|||
);
|
||||
|
||||
const { searchedTimeRange: dateRange } = useTimeQuery({
|
||||
isUTC: false,
|
||||
defaultValue: 'Past 1h',
|
||||
defaultTimeRange: [
|
||||
defaultTimeRange?.[0]?.getTime() ?? -1,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -160,7 +160,6 @@ export default function PatternSidePanel({
|
|||
isLoading={false}
|
||||
hasNextPage={false}
|
||||
wrapLines={false}
|
||||
formatUTC={false}
|
||||
fetchNextPage={useCallback(() => {}, [])}
|
||||
onScroll={useCallback(() => {}, [])}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -241,7 +241,6 @@ export default function SessionsPage() {
|
|||
setDisplayedTimeInputValue,
|
||||
onSearch,
|
||||
} = useTimeQuery({
|
||||
isUTC: false,
|
||||
defaultValue: 'Past 1h',
|
||||
defaultTimeRange: [
|
||||
defaultTimeRange?.[0]?.getTime() ?? -1,
|
||||
|
|
|
|||
|
|
@ -61,8 +61,6 @@ export default function SlowestEventsTile({
|
|||
columns: ['duration'],
|
||||
}}
|
||||
isLive={false}
|
||||
isUTC={false}
|
||||
setIsUTC={() => {}}
|
||||
onPropertySearchClick={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
283
packages/app/src/UserPreferencesModal.tsx
Normal file
283
packages/app/src/UserPreferencesModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue