Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow switching timezone for date rendering. Fixes #3474 #10120

Merged
merged 4 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"lint": "tslint --fix -p ./src/app",
"test": "jest"
},
"engines" : {
"node" : ">=16"
"engines": {
"node": ">=16"
},
"dependencies": {
"argo-ui": "https://github.com/argoproj/argo-ui.git#v2.5.0",
Expand All @@ -26,6 +26,7 @@
"js-yaml": "^4.1.0",
"json-merge-patch": "^0.2.3",
"moment": "^2.29.4",
"moment-timezone": "^0.5.39",
"monaco-editor": "0.20.0",
"prop-types": "^15.8.1",
"react": "^16.14.0",
Expand Down
42 changes: 42 additions & 0 deletions ui/src/app/shared/hooks/uselocalstorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {useState} from 'react';

export function useCustomLocalStorage<T>(key: string, initial: T, onError: (err: any) => T | undefined): [T | undefined, React.Dispatch<T>] {
const [storedValue, setStoredValue]: [T | undefined, React.Dispatch<T>] = useState(() => {
if (window === undefined) {
return initial;
}
try {
const item = window.localStorage.getItem(key);
// try retrieve if none present, default to initial
return item ? JSON.parse(item) : initial;
} catch (err) {
const val = onError(err) || undefined;
if (val === undefined) {
return undefined;
}
return val;
}
});

const setValue = (value: T | ((oldVal: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
if (window !== undefined) {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
setStoredValue(valueToStore);
}
} catch (err) {
const val = onError(err) || undefined;
if (val === undefined) {
return undefined;
}
return val;
}
};

return [storedValue, setValue];
}

export function useLocalStorage<T>(key: string, initial: T): [T, React.Dispatch<T>] {
return useCustomLocalStorage(key, initial, _ => initial);
}
3 changes: 2 additions & 1 deletion ui/src/app/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ const config = {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
SYSTEM_INFO: JSON.stringify({
version: process.env.VERSION || "latest"
})
}),
"process.env.DEFAULT_TZ": JSON.stringify("UTC"),
}),
new HtmlWebpackPlugin({ template: "src/app/index.html" }),
new CopyWebpackPlugin([{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,12 @@
padding: 10px;
margin: 10px;
background-color: white;
}

.log-menu {
display: flex;
column-gap: 12px;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import {useEffect, useState} from 'react';

import {Autocomplete} from 'argo-ui';
import moment = require('moment-timezone');
import {Observable} from 'rxjs';
import {map, publishReplay, refCount} from 'rxjs/operators';
import * as models from '../../../../models';
Expand All @@ -10,10 +11,15 @@ import {ANNOTATION_KEY_POD_NAME_VERSION} from '../../../shared/annotations';
import {ErrorNotice} from '../../../shared/components/error-notice';
import {InfoIcon, WarningIcon} from '../../../shared/components/fa-icons';
import {Links} from '../../../shared/components/links';
import {useLocalStorage} from '../../../shared/hooks/uselocalstorage';
import {getPodName, getTemplateNameFromNode} from '../../../shared/pod-name';
import {services} from '../../../shared/services';
import {FullHeightLogsViewer} from './full-height-logs-viewer';

const TZ_LOCALSTORAGE_KEY = 'DEFAULT_TZ';

const DEFAULT_TZ = process.env.DEFAULT_TZ || 'UTC';

interface WorkflowLogsViewerProps {
workflow: models.Workflow;
nodeId?: string;
Expand All @@ -26,13 +32,61 @@ function identity<T>(value: T) {
return () => value;
}

// USED FOR MANUAL TESTING
// const timeSpammer:Observable<string> = new Observable((subscriber) => {
// setInterval(() => {
// subscriber.next('time="2022-11-27T04:07:37.291Z" level=info msg="running spammer" argo=true\n');
// }, 2000);
// });

interface ParsedTime {
quoted: string;
fullstring: string;
}
// extract the time field from a string
const parseTime = (formattedString: string): undefined | ParsedTime => {
const re = new RegExp('time="(.*?)"');
const table = re.exec(formattedString);
if (table === null || table.length !== 2) {
return undefined;
}
return {quoted: table[1], fullstring: table[0]};
};

const parseAndTransform = (formattedString: string, timezone: string) => {
const maybeTime = parseTime(formattedString);
if (maybeTime === undefined) {
return formattedString;
}

try {
const newTime = moment.tz(maybeTime.quoted, timezone).format('YYYY-MM-DDTHH:mm:ss z');
const newFormattedTime = `time=\"${newTime}\"`;
const newFormattedString = formattedString.replace(maybeTime.fullstring, newFormattedTime);
return newFormattedString;
} catch {
return formattedString;
}
};

export const WorkflowLogsViewer = ({workflow, nodeId, initialPodName, container, archived}: WorkflowLogsViewerProps) => {
const [podName, setPodName] = useState(initialPodName || '');
const [selectedContainer, setContainer] = useState(container);
const [grep, setGrep] = useState('');
const [error, setError] = useState<Error>();
const [loaded, setLoaded] = useState(false);
const [logsObservable, setLogsObservable] = useState<Observable<string>>();
// timezone used for ui rendering only
const [uiTimezone, setUITimezone] = useState<string>(DEFAULT_TZ);
// timezone used for timezone formatting
const [timezone, setTimezone] = useLocalStorage<string>(TZ_LOCALSTORAGE_KEY, DEFAULT_TZ);
// list of timezones the moment-timezone library supports
const [timezones, setTimezones] = useState<string[]>([]);
isubasinghe marked this conversation as resolved.
Show resolved Hide resolved

// update the UI everytime the timezone changes
useEffect(() => {
setUITimezone(timezone);
}, [timezone]);

useEffect(() => {
setError(null);
Expand All @@ -46,17 +100,24 @@ export const WorkflowLogsViewer = ({workflow, nodeId, initialPodName, container,
}
return x;
}),
map((x: string) => parseAndTransform(x, timezone)),
publishReplay(),
refCount()
);

// const source = timeSpammer.pipe(
// map((x)=> parseAndTransform(x, timezone)),
// publishReplay(),
// refCount()
// );
const subscription = source.subscribe(
() => setLoaded(true),
setError,
() => setLoaded(true)
);
setLogsObservable(source);
return () => subscription.unsubscribe();
}, [workflow.metadata.namespace, workflow.metadata.name, podName, selectedContainer, grep, archived]);
}, [workflow.metadata.namespace, workflow.metadata.name, podName, selectedContainer, grep, archived, timezone]);

// filter allows us to introduce a short delay, before we actually change grep
const [logFilter, setLogFilter] = useState('');
Expand All @@ -65,6 +126,16 @@ export const WorkflowLogsViewer = ({workflow, nodeId, initialPodName, container,
return () => clearTimeout(x);
}, [logFilter]);

useEffect(() => {
const tzs = moment.tz.names();
const tzsSet = new Set<string>();
tzs.forEach(item => {
tzsSet.add(item);
});
const flatTzs = [...tzsSet];
setTimezones(flatTzs);
}, []);

const annotations = workflow.metadata.annotations || {};
const podNameVersion = annotations[ANNOTATION_KEY_POD_NAME_VERSION];

Expand Down Expand Up @@ -96,7 +167,7 @@ export const WorkflowLogsViewer = ({workflow, nodeId, initialPodName, container,
)
)
];

const filteredTimezones = timezones.filter(tz => tz.startsWith(uiTimezone) || uiTimezone === '');
return (
<div className='workflow-logs-viewer'>
<h3>Logs</h3>
Expand All @@ -116,7 +187,18 @@ export const WorkflowLogsViewer = ({workflow, nodeId, initialPodName, container,
/>{' '}
/ <Autocomplete items={containers} value={selectedContainer} onSelect={setContainer} />
<span className='fa-pull-right'>
<i className='fa fa-filter' /> <input type='search' defaultValue={logFilter} onChange={v => setLogFilter(v.target.value)} placeholder='Filter (regexp)...' />
<div className='log-menu'>
<i className='fa fa-filter' />{' '}
<input type='search' defaultValue={logFilter} onChange={v => setLogFilter(v.target.value)} placeholder='Filter (regexp)...' />
<i className='fa fa-globe' />{' '}
<Autocomplete
items={filteredTimezones}
value={uiTimezone}
onChange={v => setUITimezone(v.target.value)}
// useEffect ensures UITimezone is also changed
onSelect={setTimezone}
/>
</div>
</span>
</div>
<ErrorNotice error={error} />
Expand Down
7 changes: 7 additions & 0 deletions ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6579,6 +6579,13 @@ moment-timezone@^0.5.33:
dependencies:
moment ">= 2.9.0"

moment-timezone@^0.5.39:
version "0.5.39"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.39.tgz#342625a3b98810f04c8f4ea917e448d3525e600b"
integrity sha512-hoB6suq4ISDj7BDgctiOy6zljBsdYT0++0ZzZm9rtxIvJhIbQ3nmbgSWe7dNFGurl6/7b1OUkHlmN9JWgXVz7w==
dependencies:
moment ">= 2.9.0"

"moment@>= 2.9.0", moment@^2.10.2, moment@^2.24.0, moment@^2.25.3, moment@^2.29.1, moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
Expand Down