fix(usage): satisfy Rules of Hooks for plugin extra tabs; date-safe export
- change the breakdown extra-tab contract from a `useVisible()` hook to a plain `isVisible(ctx)` function so the host no longer calls hooks inside a loop (token breakdown-tabs + resource GPU/Storage tabs) - resource export: drop the time portion of the date bucket by slicing the ISO string instead of re-parsing with dayjs (avoids a timezone day-shift)
This commit is contained in:
@@ -237,12 +237,14 @@ const ResourceExportData: React.FC<ResourceExportDataProps> = (props) => {
|
|||||||
|
|
||||||
// Normalize the date bucket to a plain calendar day (drop the ``T00:00:00``
|
// Normalize the date bucket to a plain calendar day (drop the ``T00:00:00``
|
||||||
// the hourly ``metered_usage`` carries) so the export matches the Tokens
|
// the hourly ``metered_usage`` carries) so the export matches the Tokens
|
||||||
// tab's date-only format. The export always requests day granularity.
|
// tab's date-only format. Slice the ISO string rather than re-parsing with
|
||||||
|
// dayjs — a tz-offset timestamp would shift the calendar day on format.
|
||||||
|
// The export always requests day granularity.
|
||||||
const formatRowDates = (
|
const formatRowDates = (
|
||||||
items: ResourceBreakdownItem[]
|
items: ResourceBreakdownItem[]
|
||||||
): ResourceBreakdownItem[] =>
|
): ResourceBreakdownItem[] =>
|
||||||
items.map((i) =>
|
items.map((i) =>
|
||||||
i.date ? { ...i, date: dayjs(i.date).format('YYYY-MM-DD') } : i
|
i.date ? { ...i, date: String(i.date).slice(0, 10) } : i
|
||||||
);
|
);
|
||||||
|
|
||||||
const rows: ResourceBreakdownItem[] = formatRowDates(
|
const rows: ResourceBreakdownItem[] = formatRowDates(
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ type GroupKey = 'gpu_type' | 'instance' | 'user';
|
|||||||
interface ResourceBreakdownExtraTab {
|
interface ResourceBreakdownExtraTab {
|
||||||
key: string;
|
key: string;
|
||||||
labelId: string;
|
labelId: string;
|
||||||
useVisible?: () => boolean;
|
// Plain function (not a hook) — called during render to gate the tab; the
|
||||||
|
// plugin reads any runtime state non-reactively (Rules of Hooks).
|
||||||
|
isVisible?: (ctx: { scope: Scope }) => boolean;
|
||||||
Component: React.ComponentType<{
|
Component: React.ComponentType<{
|
||||||
tab: 'gpu-instances' | 'storage';
|
tab: 'gpu-instances' | 'storage';
|
||||||
dateRange: [dayjs.Dayjs, dayjs.Dayjs];
|
dateRange: [dayjs.Dayjs, dayjs.Dayjs];
|
||||||
@@ -145,15 +147,11 @@ const GpuInstancesTab: React.FC = () => {
|
|||||||
user_groups: userGroups
|
user_groups: userGroups
|
||||||
} = useResourceMeta(scope);
|
} = useResourceMeta(scope);
|
||||||
|
|
||||||
// Enterprise-provided extra bottom sub-tabs (empty in the OSS build). Call
|
// Enterprise-provided extra bottom sub-tabs (empty in the OSS build).
|
||||||
// each descriptor's ``useVisible`` in a stable order — the descriptor list is
|
// Visibility is a plain ``isVisible(ctx)`` function, evaluated at render —
|
||||||
// registered once at plugin init, so its length never changes (rules of
|
// no hooks in a loop.
|
||||||
// hooks).
|
|
||||||
const extraBreakdownTabs: ResourceBreakdownExtraTab[] =
|
const extraBreakdownTabs: ResourceBreakdownExtraTab[] =
|
||||||
getGPUStackPlugin()?.usage?.resourceBreakdownExtraTabs ?? [];
|
getGPUStackPlugin()?.usage?.resourceBreakdownExtraTabs ?? [];
|
||||||
const extraTabVisible = extraBreakdownTabs.map(
|
|
||||||
(t) => t.useVisible?.() ?? true
|
|
||||||
);
|
|
||||||
|
|
||||||
// The daily chart fetches group_by=date here; each bottom table owns its own
|
// The daily chart fetches group_by=date here; each bottom table owns its own
|
||||||
// fetch (group_by=tab key) inside InstancesBreakdownTable. Bumped on any
|
// fetch (group_by=tab key) inside InstancesBreakdownTable. Bumped on any
|
||||||
@@ -528,7 +526,7 @@ const GpuInstancesTab: React.FC = () => {
|
|||||||
// Enterprise Organization breakdown sub-tab(s) — appended after the
|
// Enterprise Organization breakdown sub-tab(s) — appended after the
|
||||||
// built-in tabs; nothing here in the OSS build.
|
// built-in tabs; nothing here in the OSS build.
|
||||||
...extraBreakdownTabs
|
...extraBreakdownTabs
|
||||||
.filter((_, i) => extraTabVisible[i])
|
.filter((t) => (t.isVisible ? t.isVisible({ scope }) : true))
|
||||||
.map((t) => ({
|
.map((t) => ({
|
||||||
key: t.key,
|
key: t.key,
|
||||||
label: intl.formatMessage({ id: t.labelId }),
|
label: intl.formatMessage({ id: t.labelId }),
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ type GroupKey = 'volume' | 'user';
|
|||||||
interface ResourceBreakdownExtraTab {
|
interface ResourceBreakdownExtraTab {
|
||||||
key: string;
|
key: string;
|
||||||
labelId: string;
|
labelId: string;
|
||||||
useVisible?: () => boolean;
|
// Plain function (not a hook) — called during render to gate the tab; the
|
||||||
|
// plugin reads any runtime state non-reactively (Rules of Hooks).
|
||||||
|
isVisible?: (ctx: { scope: Scope }) => boolean;
|
||||||
Component: React.ComponentType<{
|
Component: React.ComponentType<{
|
||||||
tab: 'gpu-instances' | 'storage';
|
tab: 'gpu-instances' | 'storage';
|
||||||
dateRange: [dayjs.Dayjs, dayjs.Dayjs];
|
dateRange: [dayjs.Dayjs, dayjs.Dayjs];
|
||||||
@@ -140,15 +142,11 @@ const StorageTab: React.FC = () => {
|
|||||||
user_groups: userGroups
|
user_groups: userGroups
|
||||||
} = useResourceMeta(scope);
|
} = useResourceMeta(scope);
|
||||||
|
|
||||||
// Enterprise-provided extra bottom sub-tabs (empty in the OSS build). Call
|
// Enterprise-provided extra bottom sub-tabs (empty in the OSS build).
|
||||||
// each descriptor's ``useVisible`` in a stable order — the descriptor list is
|
// Visibility is a plain ``isVisible(ctx)`` function, evaluated at render —
|
||||||
// registered once at plugin init, so its length never changes (rules of
|
// no hooks in a loop.
|
||||||
// hooks).
|
|
||||||
const extraBreakdownTabs: ResourceBreakdownExtraTab[] =
|
const extraBreakdownTabs: ResourceBreakdownExtraTab[] =
|
||||||
getGPUStackPlugin()?.usage?.resourceBreakdownExtraTabs ?? [];
|
getGPUStackPlugin()?.usage?.resourceBreakdownExtraTabs ?? [];
|
||||||
const extraTabVisible = extraBreakdownTabs.map(
|
|
||||||
(t) => t.useVisible?.() ?? true
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bumped on any filter change to snap every mounted table back to page 1;
|
// Bumped on any filter change to snap every mounted table back to page 1;
|
||||||
// each table owns its own page/sort state otherwise.
|
// each table owns its own page/sort state otherwise.
|
||||||
@@ -508,7 +506,7 @@ const StorageTab: React.FC = () => {
|
|||||||
// Enterprise Organization breakdown sub-tab(s) — appended after the
|
// Enterprise Organization breakdown sub-tab(s) — appended after the
|
||||||
// built-in tabs; nothing here in the OSS build.
|
// built-in tabs; nothing here in the OSS build.
|
||||||
...extraBreakdownTabs
|
...extraBreakdownTabs
|
||||||
.filter((_, i) => extraTabVisible[i])
|
.filter((t) => (t.isVisible ? t.isVisible({ scope }) : true))
|
||||||
.map((t) => ({
|
.map((t) => ({
|
||||||
key: t.key,
|
key: t.key,
|
||||||
label: intl.formatMessage({ id: t.labelId }),
|
label: intl.formatMessage({ id: t.labelId }),
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import ModelsTable from '../tables/models-table';
|
|||||||
import UsersTable from '../tables/users-table';
|
import UsersTable from '../tables/users-table';
|
||||||
|
|
||||||
// A breakdown sub-tab contributed by a plugin (e.g. the enterprise
|
// A breakdown sub-tab contributed by a plugin (e.g. the enterprise
|
||||||
// Organization tab). ``useVisible`` is a React hook the host calls
|
// Organization tab). ``isVisible`` is a PLAIN function (not a hook) called
|
||||||
// unconditionally per descriptor (stable array length → hook-safe) so the
|
// during render to gate the tab on the current context — the plugin reads any
|
||||||
// plugin can gate visibility on its own runtime state (e.g. platform-wide
|
// runtime state it needs non-reactively (e.g. the selected-org from storage),
|
||||||
// "All" context).
|
// so the host never calls a hook in a loop (Rules of Hooks).
|
||||||
export interface BreakdownExtraTab {
|
export interface BreakdownExtraTab {
|
||||||
key: string;
|
key: string;
|
||||||
labelId: string;
|
labelId: string;
|
||||||
useVisible?: () => boolean;
|
isVisible?: (ctx: { scope: string }) => boolean;
|
||||||
Component: React.ComponentType<{
|
Component: React.ComponentType<{
|
||||||
filters: BreakdownFilters;
|
filters: BreakdownFilters;
|
||||||
dateRange: { start_date: string; end_date: string };
|
dateRange: { start_date: string; end_date: string };
|
||||||
@@ -39,17 +39,11 @@ const BreakdownTabs: React.FC<{
|
|||||||
|
|
||||||
const extraTabs: BreakdownExtraTab[] =
|
const extraTabs: BreakdownExtraTab[] =
|
||||||
getGPUStackPlugin()?.usage?.breakdownExtraTabs ?? [];
|
getGPUStackPlugin()?.usage?.breakdownExtraTabs ?? [];
|
||||||
// Call each descriptor's visibility hook here (outside useMemo) so React's
|
|
||||||
// rules of hooks hold; the plugin's array is stable, so call order is too.
|
|
||||||
const extraVisible = extraTabs.map((tab) =>
|
|
||||||
tab.useVisible ? tab.useVisible() : true
|
|
||||||
);
|
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const extraItems = extraTabs
|
const extraItems = extraTabs
|
||||||
.map((tab, index) => ({ tab, visible: extraVisible[index] }))
|
.filter((tab) => (tab.isVisible ? tab.isVisible({ scope }) : true))
|
||||||
.filter(({ visible }) => visible)
|
.map((tab) => {
|
||||||
.map(({ tab }) => {
|
|
||||||
const Component = tab.Component;
|
const Component = tab.Component;
|
||||||
return {
|
return {
|
||||||
key: tab.key,
|
key: tab.key,
|
||||||
@@ -122,16 +116,7 @@ const BreakdownTabs: React.FC<{
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.concat(extraItems);
|
.concat(extraItems);
|
||||||
}, [
|
}, [filters, dateRange, pageResetKey, refreshKey, scope, extraTabs, intl]);
|
||||||
filters,
|
|
||||||
dateRange,
|
|
||||||
pageResetKey,
|
|
||||||
refreshKey,
|
|
||||||
scope,
|
|
||||||
extraTabs,
|
|
||||||
extraVisible,
|
|
||||||
intl
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user