codeant-ai-for-open-source[bot] commented on code in PR #36486:
URL: https://github.com/apache/superset/pull/36486#discussion_r2604983820
##########
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx:
##########
@@ -37,25 +37,55 @@ afterEach(() => {
});
test('render timezones in correct order for daylight saving time', async () =>
{
+ // Set system time BEFORE loading component to ensure cache uses correct date
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date('2022-07-01'));
+
const TimezoneSelector = await loadComponent('2022-07-01');
const onTimezoneChange = jest.fn();
- render(
+ const { container } = render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="America/Nassau"
/>,
);
+ // Trigger data loading by clicking the select
const searchInput = screen.getByRole('combobox');
await userEvent.click(searchInput);
+ // Run timers to execute queueMicrotask/setTimeout callback
+ jest.runAllTimers();
+
+ // Wait for timezone data to load
+ await waitFor(() => {
+ expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
+ });
+
+ // Verify the selected timezone is displayed correctly (in DST)
+ const selectionItem = container.querySelector('.ant-select-selection-item');
+ expect(selectionItem).toHaveTextContent('GMT -04:00 (Eastern Daylight
Time)');
+
+ // Verify options are sorted by UTC offset (lowest/most negative first)
const options = await waitFor(() =>
document.querySelectorAll('.ant-select-item-option-content'),
);
Review Comment:
**Suggestion:** The call to `waitFor(() => document.querySelectorAll(...))`
will resolve immediately because an empty NodeList is truthy, causing the test
to proceed before options are rendered and making
`options[0]`/`options[1]`/`options[2]` undefined; change the wait condition to
assert on the NodeList length (e.g., wait until length > 2) so the test only
continues once the expected items exist. [possible bug]
**Severity Level:** Critical 🚨
```suggestion
const options = await waitFor(() => {
const opts =
document.querySelectorAll('.ant-select-item-option-content');
expect(opts.length).toBeGreaterThan(2);
return opts;
});
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
Correct. waitFor will resolve as soon as the callback returns (a NodeList is
truthy even when empty),
so the test can proceed with an empty NodeList and lead to undefined
indexing. The proposed change
asserts on opts.length inside the waitFor, which creates a proper
synchronization point.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx
**Line:** 70:72
**Comment:**
*Possible Bug: The call to `waitFor(() =>
document.querySelectorAll(...))` will resolve immediately because an empty
NodeList is truthy, causing the test to proceed before options are rendered and
making `options[0]`/`options[1]`/`options[2]` undefined; change the wait
condition to assert on the NodeList length (e.g., wait until length > 2) so the
test only continues once the expected items exist.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx:
##########
@@ -45,117 +63,160 @@ const offsetsToName = {
'060': ['GMT Standard Time - London', 'British Summer Time'],
};
-export type TimezoneSelectorProps = {
- onTimezoneChange: (value: string) => void;
- timezone?: string | null;
- minWidth?: string;
-};
+let cachedTimezoneOptions: TimezoneOption[] | null = null;
+let computePromise: Promise<TimezoneOption[]> | null = null;
+
+function getOffsetKey(timezoneName: string): string {
+ return (
+ JANUARY_REF.tz(timezoneName).utcOffset().toString() +
+ JULY_REF.tz(timezoneName).utcOffset().toString()
+ );
+}
+
+function getTimezoneDisplayName(
+ timezoneName: string,
+ currentDate: ReturnType<typeof extendedDayjs>,
+): string {
+ const offsetKey = getOffsetKey(timezoneName);
+ const dateInZone = currentDate.tz(timezoneName);
+ const isDSTActive = isDST(dateInZone, timezoneName);
+ const namePair = offsetsToName[offsetKey];
+ return namePair ? (isDSTActive ? namePair[1] : namePair[0]) : timezoneName;
+}
+
+function computeTimezoneOptions(): TimezoneOption[] {
+ const currentDate = extendedDayjs(new Date());
+ const allZones = Intl.supportedValuesOf('timeZone');
+ const seenLabels = new Set<string>();
+ const options: TimezoneOption[] = [];
+
+ for (const zone of allZones) {
+ const offsetKey = getOffsetKey(zone);
+ const displayName = getTimezoneDisplayName(zone, currentDate);
+ const offset = extendedDayjs.tz(currentDate, zone).format('Z');
+ const label = `GMT ${offset} (${displayName})`;
+
+ if (!seenLabels.has(label)) {
+ seenLabels.add(label);
+ options.push({
+ label,
+ value: zone,
+ offsets: offsetKey,
+ timezoneName: zone,
+ });
+ }
+ }
+
+ // Pre-sort timezone options by time offset
+ options.sort(
+ (a, b) =>
+ extendedDayjs.tz(currentDate, a.timezoneName).utcOffset() -
+ extendedDayjs.tz(currentDate, b.timezoneName).utcOffset(),
+ );
+
+ cachedTimezoneOptions = options;
+ return options;
+}
+
+function getTimezoneOptionsAsync(): Promise<TimezoneOption[]> {
+ if (cachedTimezoneOptions) {
+ return Promise.resolve(cachedTimezoneOptions);
+ }
+
+ if (computePromise) {
+ return computePromise;
+ }
+
+ // Use queueMicrotask for better performance than setTimeout(0)
+ // Falls back to setTimeout for older browsers
+ computePromise = new Promise<TimezoneOption[]>(resolve => {
+ if (typeof queueMicrotask === 'function') {
+ queueMicrotask(() => {
+ resolve(computeTimezoneOptions());
+ });
+ } else {
+ setTimeout(() => {
+ resolve(computeTimezoneOptions());
+ }, 0);
Review Comment:
**Suggestion:** `getTimezoneOptionsAsync` assigns `computePromise` but never
clears it after resolution or rejection; if the promise rejects or future
recomputations are needed, callers may receive a rejected or stale promise —
clear `computePromise` once the async work completes (in finally) to allow
retries and avoid stuck state. [possible bug]
**Severity Level:** Critical 🚨
```suggestion
computePromise = new Promise<TimezoneOption[]>((resolve, reject) => {
const run = () => {
try {
const result = computeTimezoneOptions();
resolve(result);
} catch (err) {
reject(err);
} finally {
computePromise = null;
}
};
if (typeof queueMicrotask === 'function') {
queueMicrotask(run);
} else {
setTimeout(run, 0);
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
If computeTimezoneOptions throws in some environment, computePromise would
remain a rejected promise and future calls would keep returning that rejected
promise, preventing retries. Clearing computePromise in a finally (or after
resolution/rejection) improves robustness. The improved snippet handles errors
and resets computePromise so callers can retry.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx
**Line:** 132:140
**Comment:**
*Possible Bug: `getTimezoneOptionsAsync` assigns `computePromise` but
never clears it after resolution or rejection; if the promise rejects or future
recomputations are needed, callers may receive a rejected or stale promise —
clear `computePromise` once the async work completes (in finally) to allow
retries and avoid stuck state.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx:
##########
@@ -45,117 +63,160 @@ const offsetsToName = {
'060': ['GMT Standard Time - London', 'British Summer Time'],
};
-export type TimezoneSelectorProps = {
- onTimezoneChange: (value: string) => void;
- timezone?: string | null;
- minWidth?: string;
-};
+let cachedTimezoneOptions: TimezoneOption[] | null = null;
+let computePromise: Promise<TimezoneOption[]> | null = null;
+
+function getOffsetKey(timezoneName: string): string {
+ return (
+ JANUARY_REF.tz(timezoneName).utcOffset().toString() +
+ JULY_REF.tz(timezoneName).utcOffset().toString()
+ );
+}
+
+function getTimezoneDisplayName(
+ timezoneName: string,
+ currentDate: ReturnType<typeof extendedDayjs>,
+): string {
+ const offsetKey = getOffsetKey(timezoneName);
+ const dateInZone = currentDate.tz(timezoneName);
+ const isDSTActive = isDST(dateInZone, timezoneName);
+ const namePair = offsetsToName[offsetKey];
+ return namePair ? (isDSTActive ? namePair[1] : namePair[0]) : timezoneName;
+}
+
+function computeTimezoneOptions(): TimezoneOption[] {
+ const currentDate = extendedDayjs(new Date());
+ const allZones = Intl.supportedValuesOf('timeZone');
+ const seenLabels = new Set<string>();
+ const options: TimezoneOption[] = [];
+
+ for (const zone of allZones) {
+ const offsetKey = getOffsetKey(zone);
+ const displayName = getTimezoneDisplayName(zone, currentDate);
+ const offset = extendedDayjs.tz(currentDate, zone).format('Z');
+ const label = `GMT ${offset} (${displayName})`;
+
+ if (!seenLabels.has(label)) {
+ seenLabels.add(label);
+ options.push({
+ label,
+ value: zone,
+ offsets: offsetKey,
+ timezoneName: zone,
+ });
+ }
+ }
+
+ // Pre-sort timezone options by time offset
+ options.sort(
+ (a, b) =>
+ extendedDayjs.tz(currentDate, a.timezoneName).utcOffset() -
+ extendedDayjs.tz(currentDate, b.timezoneName).utcOffset(),
Review Comment:
**Suggestion:** `computeTimezoneOptions` uses `extendedDayjs.tz(currentDate,
zone)` where `currentDate` is a Dayjs instance; the dayjs timezone API expects
either `date.tz(zone)` or `dayjs.tz(dateString, zone)`, so calling
`extendedDayjs.tz` with a Dayjs object can be unsupported and cause runtime
errors; use `currentDate.tz(zone)` and `currentDate.tz(...).format(...)`
consistently. [possible bug]
**Severity Level:** Critical 🚨
```suggestion
const offset = currentDate.tz(zone).format('Z');
const label = `GMT ${offset} (${displayName})`;
if (!seenLabels.has(label)) {
seenLabels.add(label);
options.push({
label,
value: zone,
offsets: offsetKey,
timezoneName: zone,
});
}
}
// Pre-sort timezone options by time offset
options.sort(
(a, b) =>
currentDate.tz(a.timezoneName).utcOffset() -
currentDate.tz(b.timezoneName).utcOffset(),
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
The current code mixes extendedDayjs.tz(currentDate, zone) with other usages
like currentDate.tz(zone). The safe, consistent API is to call .tz on a Dayjs
instance (currentDate.tz(zone)). The improved code correctly normalizes these
uses and avoids potential runtime errors depending on the dayjs-timezone API
surface.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx
**Line:** 96:114
**Comment:**
*Possible Bug: `computeTimezoneOptions` uses
`extendedDayjs.tz(currentDate, zone)` where `currentDate` is a Dayjs instance;
the dayjs timezone API expects either `date.tz(zone)` or `dayjs.tz(dateString,
zone)`, so calling `extendedDayjs.tz` with a Dayjs object can be unsupported
and cause runtime errors; use `currentDate.tz(zone)` and
`currentDate.tz(...).format(...)` consistently.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx:
##########
@@ -45,117 +63,160 @@ const offsetsToName = {
'060': ['GMT Standard Time - London', 'British Summer Time'],
};
-export type TimezoneSelectorProps = {
- onTimezoneChange: (value: string) => void;
- timezone?: string | null;
- minWidth?: string;
-};
+let cachedTimezoneOptions: TimezoneOption[] | null = null;
+let computePromise: Promise<TimezoneOption[]> | null = null;
+
+function getOffsetKey(timezoneName: string): string {
+ return (
+ JANUARY_REF.tz(timezoneName).utcOffset().toString() +
+ JULY_REF.tz(timezoneName).utcOffset().toString()
+ );
+}
+
+function getTimezoneDisplayName(
+ timezoneName: string,
+ currentDate: ReturnType<typeof extendedDayjs>,
+): string {
+ const offsetKey = getOffsetKey(timezoneName);
+ const dateInZone = currentDate.tz(timezoneName);
+ const isDSTActive = isDST(dateInZone, timezoneName);
+ const namePair = offsetsToName[offsetKey];
+ return namePair ? (isDSTActive ? namePair[1] : namePair[0]) : timezoneName;
+}
+
+function computeTimezoneOptions(): TimezoneOption[] {
+ const currentDate = extendedDayjs(new Date());
+ const allZones = Intl.supportedValuesOf('timeZone');
+ const seenLabels = new Set<string>();
+ const options: TimezoneOption[] = [];
+
+ for (const zone of allZones) {
+ const offsetKey = getOffsetKey(zone);
+ const displayName = getTimezoneDisplayName(zone, currentDate);
+ const offset = extendedDayjs.tz(currentDate, zone).format('Z');
+ const label = `GMT ${offset} (${displayName})`;
+
+ if (!seenLabels.has(label)) {
+ seenLabels.add(label);
+ options.push({
+ label,
+ value: zone,
+ offsets: offsetKey,
+ timezoneName: zone,
+ });
+ }
+ }
+
+ // Pre-sort timezone options by time offset
+ options.sort(
+ (a, b) =>
+ extendedDayjs.tz(currentDate, a.timezoneName).utcOffset() -
+ extendedDayjs.tz(currentDate, b.timezoneName).utcOffset(),
+ );
+
+ cachedTimezoneOptions = options;
+ return options;
+}
+
+function getTimezoneOptionsAsync(): Promise<TimezoneOption[]> {
+ if (cachedTimezoneOptions) {
+ return Promise.resolve(cachedTimezoneOptions);
+ }
+
+ if (computePromise) {
+ return computePromise;
+ }
+
+ // Use queueMicrotask for better performance than setTimeout(0)
+ // Falls back to setTimeout for older browsers
+ computePromise = new Promise<TimezoneOption[]>(resolve => {
+ if (typeof queueMicrotask === 'function') {
+ queueMicrotask(() => {
+ resolve(computeTimezoneOptions());
+ });
+ } else {
+ setTimeout(() => {
+ resolve(computeTimezoneOptions());
+ }, 0);
+ }
+ });
+
+ return computePromise;
+}
+
+function findMatchingTimezone(
+ timezone: string | null | undefined,
+ options: TimezoneOption[],
+): string {
+ const targetTimezone = timezone || extendedDayjs.tz.guess();
+ const targetOffsetKey = getOffsetKey(targetTimezone);
+ let fallbackValue: string | undefined;
+
+ for (const option of options) {
+ if (
+ option.offsets === targetOffsetKey &&
+ option.timezoneName === targetTimezone
+ ) {
+ return option.value;
+ }
+ if (!fallbackValue && option.offsets === targetOffsetKey) {
+ fallbackValue = option.value;
+ }
+ }
+
+ return fallbackValue || DEFAULT_TIMEZONE.value;
+}
export default function TimezoneSelector({
onTimezoneChange,
timezone,
- minWidth = MIN_SELECT_WIDTH, // smallest size for current values
+ minWidth = MIN_SELECT_WIDTH,
+ placeholder,
...rest
}: TimezoneSelectorProps) {
- const { TIMEZONE_OPTIONS, TIMEZONE_OPTIONS_SORT_COMPARATOR, validTimezone } =
- useMemo(() => {
- const currentDate = extendedDayjs();
- const JANUARY = extendedDayjs.tz('2021-01-01');
- const JULY = extendedDayjs.tz('2021-07-01');
-
- const getOffsetKey = (name: string) =>
- JANUARY.tz(name).utcOffset().toString() +
- JULY.tz(name).utcOffset().toString();
-
- const getTimezoneName = (name: string) => {
- const offsets = getOffsetKey(name);
- return (
- (isDST(currentDate.tz(name), name)
- ? offsetsToName[offsets as keyof typeof offsetsToName]?.[1]
- : offsetsToName[offsets as keyof typeof offsetsToName]?.[0]) ||
name
- );
- };
-
- // TODO: remove this ts-ignore when typescript is upgraded to 5.1
- // @ts-ignore
- const ALL_ZONES: string[] = Intl.supportedValuesOf('timeZone');
-
- const labels = new Set<string>();
- const TIMEZONE_OPTIONS = ALL_ZONES.map(zone => {
- const label = `GMT ${extendedDayjs
- .tz(currentDate, zone)
- .format('Z')} (${getTimezoneName(zone)})`;
-
- if (labels.has(label)) {
- return null; // Skip duplicates
- }
- labels.add(label);
- return {
- label,
- value: zone,
- offsets: getOffsetKey(zone),
- timezoneName: zone,
- };
- }).filter(Boolean) as {
- label: string;
- value: string;
- offsets: string;
- timezoneName: string;
- }[];
-
- const TIMEZONE_OPTIONS_SORT_COMPARATOR = (
- a: (typeof TIMEZONE_OPTIONS)[number],
- b: (typeof TIMEZONE_OPTIONS)[number],
- ) =>
- extendedDayjs.tz(currentDate, a.timezoneName).utcOffset() -
- extendedDayjs.tz(currentDate, b.timezoneName).utcOffset();
-
- // pre-sort timezone options by time offset
- TIMEZONE_OPTIONS.sort(TIMEZONE_OPTIONS_SORT_COMPARATOR);
-
- const matchTimezoneToOptions = (timezone: string) => {
- const offsetKey = getOffsetKey(timezone);
- let fallbackValue: string | undefined;
-
- for (const option of TIMEZONE_OPTIONS) {
- if (
- option.offsets === offsetKey &&
- option.timezoneName === timezone
- ) {
- return option.value;
- }
- if (!fallbackValue && option.offsets === offsetKey) {
- fallbackValue = option.value;
- }
- }
- return fallbackValue || DEFAULT_TIMEZONE.value;
- };
-
- const validTimezone = matchTimezoneToOptions(
- timezone || extendedDayjs.tz.guess(),
- );
-
- return {
- TIMEZONE_OPTIONS,
- TIMEZONE_OPTIONS_SORT_COMPARATOR,
- validTimezone,
- };
- }, [timezone]);
-
- // force trigger a timezone update if provided `timezone` is not invalid
- useEffect(() => {
- if (validTimezone && timezone !== validTimezone) {
- onTimezoneChange(validTimezone);
+ const [timezoneOptions, setTimezoneOptions] = useState<
+ TimezoneOption[] | null
+ >(cachedTimezoneOptions);
+ const [isLoadingOptions, setIsLoadingOptions] = useState(false);
+
+ const handleOpenChange = useCallback(
+ (isOpen: boolean) => {
+ if (isOpen && !timezoneOptions && !isLoadingOptions) {
+ setIsLoadingOptions(true);
+ getTimezoneOptionsAsync()
+ .then(setTimezoneOptions)
+ .finally(() => setIsLoadingOptions(false));
+ }
+ },
+ [timezoneOptions, isLoadingOptions],
+ );
+
+ const sortComparator = useMemo(() => {
+ if (!timezoneOptions) return undefined;
+ const currentDate = extendedDayjs();
+ return (a: TimezoneOption, b: TimezoneOption) =>
+ extendedDayjs.tz(currentDate, a.timezoneName).utcOffset() -
+ extendedDayjs.tz(currentDate, b.timezoneName).utcOffset();
Review Comment:
**Suggestion:** The comparator in `useMemo` uses
`extendedDayjs.tz(currentDate, ...)` with `currentDate` as a Dayjs instance,
which is inconsistent with the dayjs API used elsewhere and can cause runtime
errors; use `currentDate.tz(...)` instead. [possible bug]
**Severity Level:** Critical 🚨
```suggestion
currentDate.tz(a.timezoneName).utcOffset() -
currentDate.tz(b.timezoneName).utcOffset();
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
This is the same inconsistency as in computeTimezoneOptions: calling
extendedDayjs.tz with a Dayjs instance is inconsistent and risky. Replacing
with currentDate.tz(...) is correct, consistent with other code in the file,
and avoids potential dayjs-timezone API misuse.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx
**Line:** 198:199
**Comment:**
*Possible Bug: The comparator in `useMemo` uses
`extendedDayjs.tz(currentDate, ...)` with `currentDate` as a Dayjs instance,
which is inconsistent with the dayjs API used elsewhere and can cause runtime
errors; use `currentDate.tz(...)` instead.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx:
##########
@@ -37,25 +37,55 @@ afterEach(() => {
});
test('render timezones in correct order for daylight saving time', async () =>
{
+ // Set system time BEFORE loading component to ensure cache uses correct date
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date('2022-07-01'));
+
const TimezoneSelector = await loadComponent('2022-07-01');
const onTimezoneChange = jest.fn();
- render(
+ const { container } = render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="America/Nassau"
/>,
);
+ // Trigger data loading by clicking the select
const searchInput = screen.getByRole('combobox');
await userEvent.click(searchInput);
+ // Run timers to execute queueMicrotask/setTimeout callback
+ jest.runAllTimers();
+
+ // Wait for timezone data to load
+ await waitFor(() => {
+ expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
+ });
+
+ // Verify the selected timezone is displayed correctly (in DST)
+ const selectionItem = container.querySelector('.ant-select-selection-item');
+ expect(selectionItem).toHaveTextContent('GMT -04:00 (Eastern Daylight
Time)');
Review Comment:
**Suggestion:** `container.querySelector(...)` can return null; calling
`toHaveTextContent` on a null value will throw — assert the element exists (or
guard) before checking its text content to avoid a null dereference. [null
pointer]
**Severity Level:** Minor ⚠️
```suggestion
expect(selectionItem).not.toBeNull();
if (selectionItem) {
expect(selectionItem).toHaveTextContent('GMT -04:00 (Eastern Daylight
Time)');
}
```
<details>
<summary><b>Why it matters? ⭐ </b></summary>
Also correct. querySelector can return null and calling toHaveTextContent on
null will throw.
Guarding with not.toBeNull() (or using a get* query) avoids a TypeError and
makes the test failure
clearer if the element is missing.
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx
**Line:** 67:67
**Comment:**
*Null Pointer: `container.querySelector(...)` can return null; calling
`toHaveTextContent` on a null value will throw — assert the element exists (or
guard) before checking its text content to avoid a null dereference.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]