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(app): Runs History Tables Scrubber #3459

Merged
merged 8 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions weave-js/src/common/types/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const TABLE_FILE_TYPE = {
type: 'file' as const,
wbObjectType: {type: 'table' as const, columnTypes: {}},
};
7 changes: 7 additions & 0 deletions weave-js/src/common/types/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const LIST_RUNS_TYPE = {
type: 'list' as const,
objectType: {
type: 'union' as const,
members: ['none' as const, 'run' as const],
},
};
36 changes: 36 additions & 0 deletions weave-js/src/common/util/table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
isAssignableTo,
list,
listObjectType,
maybe,
Type,
typedDict,
typedDictPropertyTypes,
} from '@wandb/weave/core';

import {TABLE_FILE_TYPE} from '../types/file';

export const getTableKeysFromNodeType = (
inputNodeType: Type,
defaultKey: string | null = null
) => {
if (
inputNodeType != null &&
isAssignableTo(inputNodeType, list(typedDict({})))
) {
const typeMap = typedDictPropertyTypes(listObjectType(inputNodeType));
const tableKeys = Object.keys(typeMap)
.filter(key => {
return isAssignableTo(typeMap[key], maybe(TABLE_FILE_TYPE));
})
.sort();
const value =
tableKeys.length > 0 &&
defaultKey != null &&
tableKeys.indexOf(defaultKey) !== -1
? defaultKey
: tableKeys?.[0] ?? '';
return {tableKeys, value};
}
return {tableKeys: [], value: ''};
};
8 changes: 8 additions & 0 deletions weave-js/src/components/Panel2/PanelExpression/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,14 @@ export function usePanelExpressionState(props: PanelExpressionProps) {
...results,
];
}

// Only paginated tables are supported for run history tables stepper
results = results.filter(r => {
if (r.key.startsWith('run-history-tables-stepper')) {
return r.key === 'run-history-tables-stepper.row.table';
}
return true;
});
return results;
}, [handler, stackIds, weavePlotEnabled]);

Expand Down
2 changes: 2 additions & 0 deletions weave-js/src/components/Panel2/PanelRegistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {weavePythonPanelSpecs} from './PanelRegistryWeavePython';
// converters
import {Spec as RowSpec} from './PanelRow';
import {Spec as RunColorSpec} from './PanelRunColor';
import {Spec as PanelRunHistoryTablesStepperSpec} from './PanelRunHistoryTablesStepper';
import {Spec as RunOverviewSpec} from './PanelRunOverview';
import {Spec as PanelRunsTableSpec} from './PanelRunsTable';
import {Spec as SavedModelSpec} from './PanelSavedModel';
Expand Down Expand Up @@ -237,4 +238,5 @@ export const ConverterSpecs = (): ConverterSpecArray =>
Panel.toConvertSpec(MultiTableSpec2),
Panel.toConvertSpec(ProjectionSpec),
Panel.toConvertSpec(PanelRunsTableSpec),
Panel.toConvertSpec(PanelRunHistoryTablesStepperSpec),
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import SliderInput from '@wandb/weave/common/components/elements/SliderInput';
import {
constFunction,
constNodeUnsafe,
constNumber,
constString,
file,
NodeOrVoidNode,
opDict,
opFileTable,
opIndex,
opMap,
opPick,
opRunHistory,
opTableRows,
voidNode,
} from '@wandb/weave/core';
import React, {useEffect, useState} from 'react';

import {LIST_RUNS_TYPE} from '../../../common/types/run';
import {getTableKeysFromNodeType} from '../../../common/util/table';
import {useNodeValue, useNodeWithServerType} from '../../../react';
import * as ConfigPanel from '../ConfigPanel';
import * as Panel2 from '../panel';
import {PanelComp2} from '../PanelComp';
import {TableSpec} from '../PanelTable/PanelTable';

type PanelRunsHistoryTablesConfigType = {
tableHistoryKey: string;
};

type PanelRunHistoryTablesStepperProps = Panel2.PanelProps<
typeof LIST_RUNS_TYPE,
PanelRunsHistoryTablesConfigType
>;

const PanelRunHistoryTablesStepperConfig: React.FC<
PanelRunHistoryTablesStepperProps
> = props => {
const firstRun = opIndex({arr: props.input, index: constNumber(0)});
const runHistoryNode = opRunHistory({run: firstRun as any});
const runHistoryRefined = useNodeWithServerType(runHistoryNode);
const {tableKeys, value} = getTableKeysFromNodeType(
runHistoryRefined.result?.type,
props.config?.tableHistoryKey
);
const options = tableKeys.map(key => ({text: key, value: key}));
const updateConfig = props.updateConfig;
const setSummaryKey = React.useCallback(
val => {
updateConfig({tableHistoryKey: val});
},
[updateConfig]
);

return (
<ConfigPanel.ConfigOption label="Table">
<ConfigPanel.ModifiedDropdownConfigField
selection
data-test="compare_method"
scrolling
multiple={false}
options={options}
value={value}
onChange={(e, data) => {
setSummaryKey(data.value as any);
}}
/>
</ConfigPanel.ConfigOption>
);
};

const PanelRunHistoryTablesStepper: React.FC<
PanelRunHistoryTablesStepperProps
> = props => {
const [currentStep, setCurrentStep] = useState(0);
const [currentTableHistoryKey, setCurrentTableHistoryKey] =
useState<any>(null);
const [steps, setSteps] = useState<number[]>([]);
const [tables, setTables] = useState<any[]>([]);

const {input, config} = props;
const firstRun = opIndex({arr: input, index: constNumber(0)});
const runHistoryNode = opRunHistory({run: firstRun as any});
const runHistoryRefined = useNodeWithServerType(runHistoryNode);
const {value} = getTableKeysFromNodeType(
runHistoryRefined.result?.type,
config?.tableHistoryKey
);
const tableWithStepsNode = opMap({
arr: runHistoryRefined.result as any,
mapFn: constFunction({row: runHistoryRefined.result.type}, ({row}) =>
opDict({
_step: opPick({obj: row, key: constString('_step')}),
table: opPick({obj: row, key: constString(value ?? '')}),
} as any)
) as any,
});

const {
result: tablesWithStepsNodeResult,
loading: tablesWithStepsNodeLoading,
} = useNodeValue(tableWithStepsNode, {
skip: !value || currentTableHistoryKey === value,
});

useEffect(() => {
if (tablesWithStepsNodeLoading) {
return;
}

if (tablesWithStepsNodeResult != null) {
const nonNullTables = tablesWithStepsNodeResult.filter(
(row: any) => row.table != null
);
const newSteps = nonNullTables.map((row: any) => row._step);
const newTables = nonNullTables.map((row: any) => row.table);
setSteps(newSteps);
setCurrentStep(newSteps[0]);
setCurrentTableHistoryKey(value);
setTables(newTables);
}
}, [tablesWithStepsNodeResult, tablesWithStepsNodeLoading, value]);

const tableIndex = steps.indexOf(currentStep);
let defaultNode: NodeOrVoidNode = voidNode();
if (tableIndex !== -1 && tables[tableIndex] != null) {
defaultNode = opTableRows({
table: opFileTable({
file: constNodeUnsafe(
file('json', {type: 'table', columnTypes: {}}),
tables[tableIndex]
),
}),
});
}

return (
<>
{defaultNode != null && (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
padding: '2px',
overflowY: 'auto',
}}>
<PanelComp2
input={defaultNode}
inputType={defaultNode.type}
loading={props.loading}
panelSpec={TableSpec}
configMode={false}
config={config}
context={props.context}
updateConfig={props.updateConfig}
updateContext={props.updateContext}
updateInput={props.updateInput}
/>
{steps.length > 0 && (
<div
style={{
padding: '2px',
height: '1.7em',
borderTop: '1px solid #ddd',
backgroundColor: '#f8f8f8',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}>
<SliderInput
min={steps[0]}
max={steps[steps.length - 1]}
minLabel={steps[0].toString()}
maxLabel={steps[steps.length - 1].toString()}
hasInput={true}
value={currentStep}
step={1}
ticks={steps}
onChange={setCurrentStep}
/>
</div>
)}
</div>
)}
</>
);
};

export const Spec: Panel2.PanelSpec<PanelRunsHistoryTablesConfigType> = {
id: 'run-history-tables-stepper',
displayName: 'Run History Tables Stepper',
Component: PanelRunHistoryTablesStepper,
ConfigComponent: PanelRunHistoryTablesStepperConfig,
inputType: LIST_RUNS_TYPE,
outputType: () => ({
type: 'list' as const,
objectType: {
type: 'list' as const,
objectType: {type: 'typedDict' as const, propertyTypes: {}},
},
}),
};

Panel2.registerPanelFunction(
Spec.id,
Spec.inputType,
Spec.equivalentTransform!
);
57 changes: 10 additions & 47 deletions weave-js/src/components/Panel2/PanelRunsTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,77 +3,37 @@ import {
constString,
isAssignableTo,
list,
listObjectType,
maybe,
opDropNa,
opPick,
opRunSummary,
Type,
typedDict,
typedDictPropertyTypes,
} from '@wandb/weave/core';
import React from 'react';

import {LIST_RUNS_TYPE} from '../../../common/types/run';
import {getTableKeysFromNodeType} from '../../../common/util/table';
import {useNodeWithServerType} from '../../../react';
import * as ConfigPanel from '../ConfigPanel';
import * as Panel2 from '../panel';
import {normalizeTableLike} from '../PanelTable/tableType';

const CUSTOM_TABLE_TYPE = {
type: 'file' as const,
wbObjectType: {type: 'table' as const, columnTypes: {}},
};

const inputType = {
type: 'list' as const,
objectType: {
type: 'union' as const,
members: ['none' as const, 'run' as const],
},
};

type PanelRunsTableConfigType = {summaryKey: string};

type PanelRunsTableProps = Panel2.PanelProps<
typeof inputType,
typeof LIST_RUNS_TYPE,
PanelRunsTableConfigType
>;

const PanelRunsTable: React.FC<PanelRunsTableProps> = props => {
throw new Error('PanelRunsTable: Cannot be rendered directly');
};

const getKeysFromInputType = (
inputNodeType?: Type,
config?: PanelRunsTableConfigType
) => {
if (
inputNodeType != null &&
isAssignableTo(inputNodeType, list(typedDict({})))
) {
const typeMap = typedDictPropertyTypes(listObjectType(inputNodeType));
const tableKeys = Object.keys(typeMap)
.filter(key => {
return isAssignableTo(typeMap[key], maybe(CUSTOM_TABLE_TYPE));
})
.sort();
const value =
tableKeys.length > 0 &&
config?.summaryKey != null &&
tableKeys.indexOf(config?.summaryKey) !== -1
? config.summaryKey
: tableKeys?.[0] ?? '';
return {tableKeys, value};
}
return {tableKeys: [], value: ''};
};

const PanelRunsTableConfig: React.FC<PanelRunsTableProps> = props => {
const runSummaryNode = opRunSummary({run: props.input});
const runSummaryRefined = useNodeWithServerType(runSummaryNode);
const {tableKeys, value} = getKeysFromInputType(
const {tableKeys, value} = getTableKeysFromNodeType(
runSummaryRefined.result?.type,
props.config
props.config?.summaryKey
);
const options = tableKeys.map(key => ({text: key, value: key}));
const updateConfig = props.updateConfig;
Expand Down Expand Up @@ -106,7 +66,7 @@ export const Spec: Panel2.PanelSpec = {
displayName: 'Run Tables',
Component: PanelRunsTable,
ConfigComponent: PanelRunsTableConfig,
inputType,
inputType: LIST_RUNS_TYPE,
outputType: () => ({
type: 'list' as const,
objectType: {
Expand All @@ -120,7 +80,10 @@ export const Spec: Panel2.PanelSpec = {
const defaultNode = constNodeUnsafe(expectedReturnType, []);
const runSummaryNode = opRunSummary({run: inputNode as any});
const runSummaryRefined = await refineType(runSummaryNode);
const {value} = getKeysFromInputType(runSummaryRefined.type, config);
const {value} = getTableKeysFromNodeType(
runSummaryRefined.type,
config?.summaryKey
);

const runTableNode = opPick({
obj: runSummaryNode,
Expand Down
Loading