diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse2/SmallRef.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse2/SmallRef.tsx index 40555a00f690..6f7075514acb 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse2/SmallRef.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse2/SmallRef.tsx @@ -13,6 +13,7 @@ import {Icon, IconName, IconNames} from '../../../Icon'; import {useWeaveflowRouteContext} from '../Browse3/context'; import {Link} from '../Browse3/pages/common/Links'; import {useWFHooks} from '../Browse3/pages/wfReactInterface/context'; +import {isObjDeleteError} from '../Browse3/pages/wfReactInterface/utilities'; import { ObjectVersionKey, OpVersionKey, @@ -125,6 +126,11 @@ export const SmallRef: FC<{ } const objectVersion = useObjectVersion(objVersionKey); const opVersion = useOpVersion(opVersionKey); + + const isDeleted = + isObjDeleteError(objectVersion?.error) || + isObjDeleteError(opVersion?.error); + const versionIndex = objectVersion.result?.versionIndex ?? opVersion.result?.versionIndex; @@ -177,13 +183,14 @@ export const SmallRef: FC<{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', + textDecoration: isDeleted ? 'line-through' : 'none', }}> {label} )} ); - if (refTypeQuery.loading) { + if (refTypeQuery.loading || isDeleted) { return Item; } if (!isArtifactRef && !isWeaveObjRef) { diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/DatasetVersionPage.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/DatasetVersionPage.tsx index f3aed14e9efd..1fd637d20d44 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/DatasetVersionPage.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/DatasetVersionPage.tsx @@ -13,6 +13,7 @@ import { ScrollableTabContent, SimplePageLayoutWithHeader, } from '../pages/common/SimplePageLayout'; +import {DeleteObjectButtonWithModal} from '../pages/ObjectVersionPage'; import {TabUseDataset} from '../pages/TabUseDataset'; import {useWFHooks} from '../pages/wfReactInterface/context'; import {objectVersionKeyToRefUri} from '../pages/wfReactInterface/utilities'; @@ -21,7 +22,8 @@ import {CustomWeaveTypeProjectContext} from '../typeViews/CustomWeaveTypeDispatc export const DatasetVersionPage: React.FC<{ objectVersion: ObjectVersionSchema; -}> = ({objectVersion}) => { + showDeleteButton?: boolean; +}> = ({objectVersion, showDeleteButton}) => { const {useRootObjectVersions, useRefsData} = useWFHooks(); const entityName = objectVersion.entity; const projectName = objectVersion.project; @@ -73,7 +75,7 @@ export const DatasetVersionPage: React.FC<{ } headerContent={ -
+

Name

Version

{objectVersionIndex}

+ {showDeleteButton && ( +
+ +
+ )}
} diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewer.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewer.tsx index 592ae3eaede7..0bc3bae2e7e4 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewer.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewer.tsx @@ -7,6 +7,7 @@ import { GridRowId, } from '@mui/x-data-grid-pro'; import {Button} from '@wandb/weave/components/Button'; +import {parseRef} from '@wandb/weave/react'; import _ from 'lodash'; import React, { Dispatch, @@ -21,6 +22,7 @@ import React, { import {parseRefMaybe} from '../../../../../../react'; import {LoadingDots} from '../../../../../LoadingDots'; import {Browse2OpDefCode} from '../../../Browse2/Browse2OpDefCode'; +import {objectRefDisplayName} from '../../../Browse2/SmallRef'; import {isWeaveRef} from '../../filters/common'; import {StyledDataGrid} from '../../StyledDataGrid'; import { @@ -161,10 +163,15 @@ export const ObjectViewer = ({ const refValues: RefValues = {}; for (const [r, v] of _.zip(refs, resolvedRefData)) { - if (!r || !v) { + if (!r) { // Shouldn't be possible continue; } + if (!v) { + // Value for ref not found, must be deleted + refValues[r] = deletedRefValuePlaceholder(r); + continue; + } let val = r; if (v == null) { console.error('Error resolving ref', r); @@ -725,3 +732,18 @@ const useTruncatedData = (data: Data) => { return {truncatedData, truncatedStore, setTruncatedData, setTruncatedStore}; }; + +// Placeholder value for deleted refs +const DELETED_REF_KEY = '_weave_deleted_ref'; +const deletedRefValuePlaceholder = ( + ref: string +): {[DELETED_REF_KEY]: string} => { + const parsedRef = parseRef(ref); + const refString = objectRefDisplayName(parsedRef).label; + return {[DELETED_REF_KEY]: refString}; +}; +export const maybeGetDeletedRefValuePlaceholderFromRow = ( + row: any +): string | undefined => { + return row.value?.[DELETED_REF_KEY]; +}; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewerGroupingCell.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewerGroupingCell.tsx index 0681e9845271..a06ce048d6e9 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewerGroupingCell.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewerGroupingCell.tsx @@ -6,6 +6,7 @@ import React, {FC, MouseEvent} from 'react'; import {Button} from '../../../../../Button'; import {Tooltip} from '../../../../../Tooltip'; import {CursorBox} from './CursorBox'; +import {maybeGetDeletedRefValuePlaceholderFromRow} from './ObjectViewer'; const INSET_SPACING = 40; @@ -32,7 +33,9 @@ export const ObjectViewerGroupingCell: FC< event.stopPropagation(); }; + const deletedRef = maybeGetDeletedRefValuePlaceholderFromRow(row); const tooltipContent = row.path.toString(); + const textContent = deletedRef ?? props.value; const box = ( - {props.value} + {textContent} } /> diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CompareEvaluationsPage/ecpTypes.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CompareEvaluationsPage/ecpTypes.ts index df40b28a92df..a0f0eafc9b3e 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CompareEvaluationsPage/ecpTypes.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CompareEvaluationsPage/ecpTypes.ts @@ -25,7 +25,7 @@ export type EvaluationComparisonSummary = { // Models are the Weave Objects used to define the model logic and properties. models: { - [modelRef: string]: ModelObj; + [modelRef: string]: ModelObj | null; }; // ScoreMetrics define the metrics that are associated on each individual prediction diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CompareEvaluationsPage/sections/ComparisonDefinitionSection/EvaluationDefinition.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CompareEvaluationsPage/sections/ComparisonDefinitionSection/EvaluationDefinition.tsx index 0de9421ba1a9..05cce3904519 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CompareEvaluationsPage/sections/ComparisonDefinitionSection/EvaluationDefinition.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CompareEvaluationsPage/sections/ComparisonDefinitionSection/EvaluationDefinition.tsx @@ -1,4 +1,4 @@ -import {Box} from '@material-ui/core'; +import {Box} from '@mui/material'; import React, {useMemo} from 'react'; import { @@ -9,9 +9,10 @@ import { import {hexToRGB} from '../../../../../../../../common/css/utils'; import {parseRef, WeaveObjectRef} from '../../../../../../../../react'; import {Icon, IconNames} from '../../../../../../../Icon'; -import {SmallRef} from '../../../../../Browse2/SmallRef'; +import {objectRefDisplayName, SmallRef} from '../../../../../Browse2/SmallRef'; import {CallLink, ObjectVersionLink} from '../../../common/Links'; import {useWFHooks} from '../../../wfReactInterface/context'; +import {isObjDeleteError} from '../../../wfReactInterface/utilities'; import {ObjectVersionKey} from '../../../wfReactInterface/wfDataModelHooksInterface'; import {EvaluationComparisonState} from '../../ecpState'; @@ -43,11 +44,10 @@ export const EvaluationModelLink: React.FC<{ }> = props => { const {useObjectVersion} = useWFHooks(); const evaluationCall = props.state.summary.evaluationCalls[props.callId]; - const modelObj = props.state.summary.models[evaluationCall.modelRef]; - const objRef = useMemo( - () => parseRef(modelObj.ref) as WeaveObjectRef, - [modelObj.ref] - ); + const objRef = useMemo(() => { + return parseRef(evaluationCall.modelRef) as WeaveObjectRef; + }, [evaluationCall.modelRef]); + const objVersionKey = useMemo(() => { return { scheme: 'weave', @@ -69,10 +69,31 @@ export const EvaluationModelLink: React.FC<{ ]); const objectVersion = useObjectVersion(objVersionKey); + if (isObjDeleteError(objectVersion.error)) { + return ( + + + + {objectRefDisplayName(objRef).label} + + + ); + } + return ( { const model = props.state.summary.models[ref]; - Object.keys(model.properties).forEach(prop => { + Object.keys(model?.properties ?? {}).forEach(prop => { if (!propsRes[prop]) { propsRes[prop] = {}; } - propsRes[prop][ref] = model.properties[prop]; + propsRes[prop][ref] = model?.properties?.[prop]; }); }); @@ -104,7 +104,7 @@ export const ScorecardSection: React.FC<{ if (!propsRes.predict) { propsRes.predict = {}; } - propsRes.predict[ref] = model.predictOpRef; + propsRes.predict[ref] = model?.predictOpRef; }); return propsRes; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ObjectVersionPage.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ObjectVersionPage.tsx index 1b0bb491f1c0..d1a2543a63bc 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ObjectVersionPage.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ObjectVersionPage.tsx @@ -1,18 +1,21 @@ import Box from '@mui/material/Box'; +import {Button} from '@wandb/weave/components/Button'; import {useObjectViewEvent} from '@wandb/weave/integrations/analytics/useViewEvents'; -import React, {useMemo} from 'react'; +import React, {useMemo, useState} from 'react'; import {maybePluralizeWord} from '../../../../../core/util/string'; import {Icon, IconName} from '../../../../Icon'; import {LoadingDots} from '../../../../LoadingDots'; import {Tailwind} from '../../../../Tailwind'; import {Tooltip} from '../../../../Tooltip'; +import {useClosePeek} from '../context'; import {DatasetVersionPage} from '../datasets/DatasetVersionPage'; import {NotFoundPanel} from '../NotFoundPanel'; import {CustomWeaveTypeProjectContext} from '../typeViews/CustomWeaveTypeDispatcher'; import {WeaveCHTableSourceRefContext} from './CallPage/DataTableView'; import {ObjectViewerSection} from './CallPage/ObjectViewerSection'; import {WFHighLevelCallFilter} from './CallsPage/callsTableFilter'; +import {DeleteModal, useShowDeleteButton} from './common/DeleteModal'; import { CallLink, CallsLink, @@ -34,6 +37,7 @@ import {TabUsePrompt} from './TabUsePrompt'; import {KNOWN_BASE_OBJECT_CLASSES} from './wfReactInterface/constants'; import {useWFHooks} from './wfReactInterface/context'; import { + isObjDeleteError, objectVersionKeyToRefUri, refUriToOpVersionKey, } from './wfReactInterface/utilities'; @@ -94,7 +98,10 @@ export const ObjectVersionPage: React.FC<{ path: props.filePath, refExtra: props.refExtra, }); - if (objectVersion.loading) { + if (isObjDeleteError(objectVersion.error)) { + const deletedAtMessage = objectVersion.error?.message ?? 'Object deleted'; + return ; + } else if (objectVersion.loading) { return ; } else if (objectVersion.result == null) { return ; @@ -176,6 +183,8 @@ const ObjectVersionPageInner: React.FC<{ return data.result?.[0] ?? {}; }, [data.loading, data.result]); + const showDeleteButton = useShowDeleteButton(); + const viewerDataAsObject = useMemo(() => { const dataIsPrimitive = typeof viewerData !== 'object' || @@ -199,7 +208,12 @@ const ObjectVersionPageInner: React.FC<{ } if (isDataset) { - return ; + return ( + + ); } return ( @@ -216,7 +230,7 @@ const ObjectVersionPageInner: React.FC<{ } headerContent={ -
+

Name

@@ -257,6 +271,11 @@ const ObjectVersionPageInner: React.FC<{

{refExtra}

)} + {showDeleteButton && ( +
+ +
+ )}
} @@ -623,3 +642,41 @@ const OpVersionCallsLink: React.FC<{ ); }; + +export const DeleteObjectButtonWithModal: React.FC<{ + objVersionSchema: ObjectVersionSchema; + overrideDisplayStr?: string; +}> = ({objVersionSchema, overrideDisplayStr}) => { + const {useObjectDeleteFunc} = useWFHooks(); + const closePeek = useClosePeek(); + const {objectVersionsDelete} = useObjectDeleteFunc(); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + + const deleteStr = + overrideDisplayStr ?? + `${objVersionSchema.objectId}:v${objVersionSchema.versionIndex}`; + + return ( + <> + - ) : undefined; + if (loadingUserInfo) { + return ; + } + + const filteredOnObject = filter.objectName != null; + const hasComparison = filteredOnObject; + const viewer = userInfo ? userInfo.id : null; + const isReadonly = !viewer || !userInfo?.teams.includes(props.entity); + const isAdmin = userInfo?.admin; + const showDeleteButton = filteredOnObject && !isReadonly && isAdmin; return ( + } tabs={[ { label: '', @@ -138,6 +155,51 @@ export const ObjectVersionsPage: React.FC<{ ); }; +const ObjectVersionsPageHeaderExtra: React.FC<{ + entity: string; + project: string; + objectName: string | null; + selectedVersions: string[]; + setSelectedVersions: (selected: string[]) => void; + showDeleteButton?: boolean; + showCompareButton?: boolean; + onCompare: () => void; +}> = ({ + entity, + project, + objectName, + selectedVersions, + setSelectedVersions, + showDeleteButton, + showCompareButton, + onCompare, +}) => { + const compareButton = showCompareButton ? ( + + ) : undefined; + const deleteButton = showDeleteButton ? ( + setSelectedVersions([])} + /> + ) : undefined; + + return ( + +
+ {compareButton} + {deleteButton} +
+
+ ); +}; + export type WFHighLevelObjectVersionFilter = { objectName?: string | null; baseObjectClass?: KnownBaseObjectClassType | null; @@ -568,3 +630,42 @@ const PeerVersionsLink: React.FC<{obj: ObjectVersionSchema}> = props => { /> ); }; + +const DeleteObjectVersionsButtonWithModal: React.FC<{ + entity: string; + project: string; + objectName: string; + objectVersions: string[]; + disabled?: boolean; + onSuccess: () => void; +}> = ({entity, project, objectName, objectVersions, disabled, onSuccess}) => { + const {useObjectDeleteFunc} = useWFHooks(); + const {objectVersionsDelete} = useObjectDeleteFunc(); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + + const numObjects = objectVersions.length; + const versionsStr = maybePluralizeWord(numObjects, 'version', 's'); + const objectDigests = objectVersions.map(v => v.split(':')[1]); + const deleteTitleStr = `${numObjects} ${objectName} ${versionsStr}`; + + return ( + <> + + + + + + ); +}; + +export const useShowDeleteButton = () => { + const viewerInfo = useViewerInfo(); + return viewerInfo.loading ? false : viewerInfo.userInfo?.admin; +}; + +const Dialog = styled(MaterialDialog)` + .MuiDialog-paper { + min-width: 400px; + max-width: min(800px, 90vw); + width: auto !important; + } +`; +Dialog.displayName = 'S.Dialog'; + +const DialogContent = styled(MaterialDialogContent)` + padding: 0 32px !important; +`; +DialogContent.displayName = 'S.DialogContent'; + +const DialogTitle = styled(MaterialDialogTitle)` + padding: 32px 32px 16px 32px !important; + + h2 { + font-weight: 600; + font-size: 24px; + line-height: 30px; + } +`; +DialogTitle.displayName = 'S.DialogTitle'; + +const DialogActions = styled(MaterialDialogActions)<{$align: string}>` + justify-content: ${({$align}) => + $align === 'left' ? 'flex-start' : 'flex-end'} !important; + padding: 32px 32px 32px 32px !important; +`; +DialogActions.displayName = 'S.DialogActions'; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerClient.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerClient.ts index 9b45049ffc7b..a433c0c087cd 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerClient.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerClient.ts @@ -14,6 +14,8 @@ import { TraceCallUpdateReq, TraceObjCreateReq, TraceObjCreateRes, + TraceObjDeleteReq, + TraceObjDeleteRes, TraceRefsReadBatchReq, TraceRefsReadBatchRes, } from './traceServerClientTypes'; @@ -142,6 +144,14 @@ export class TraceServerClient extends CachingTraceServerClient { return res; } + public objectDelete(req: TraceObjDeleteReq): Promise { + const res = super.objectDelete(req).then(r => { + this.onObjectListeners.forEach(listener => listener()); + return r; + }); + return res; + } + public readBatch(req: TraceRefsReadBatchReq): Promise { return this.requestReadBatch(req); } diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerClientTypes.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerClientTypes.ts index 7c89efd44196..6984ac164c33 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerClientTypes.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerClientTypes.ts @@ -252,6 +252,17 @@ export type TraceObjCreateRes = { digest: string; }; +export type TraceObjDeleteReq = { + project_id: string; + object_id: string; + digests: string[]; +}; + +export type TraceObjDeleteRes = { + num_deleted?: number; + detail?: string; +}; + export type TraceRefsReadBatchReq = { refs: string[]; }; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerDirectClient.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerDirectClient.ts index 22d2ec797810..2254b05ebc84 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerDirectClient.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/traceServerDirectClient.ts @@ -42,6 +42,8 @@ import { TraceFileContentReadRes, TraceObjCreateReq, TraceObjCreateRes, + TraceObjDeleteReq, + TraceObjDeleteRes, TraceObjQueryReq, TraceObjQueryRes, TraceObjReadReq, @@ -232,6 +234,13 @@ export class DirectTraceServerClient { return this.makeRequest('/obj/read', req); } + public objectDelete(req: TraceObjDeleteReq): Promise { + return this.makeRequest( + '/obj/delete', + req + ); + } + public readBatch(req: TraceRefsReadBatchReq): Promise { return this.makeRequest( '/refs/read_batch', diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/tsDataModelHooks.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/tsDataModelHooks.ts index 02187b0f65be..c9982cb60ceb 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/tsDataModelHooks.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/tsDataModelHooks.ts @@ -10,7 +10,12 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import * as Types from '../../../../../../core/model/types'; import {useDeepMemo} from '../../../../../../hookUtils'; -import {isWeaveObjectRef, parseRef} from '../../../../../../react'; +import { + isWeaveObjectRef, + parseRef, + refUri, + WeaveObjectRef, +} from '../../../../../../react'; import { callCache, objectVersionCache, @@ -672,12 +677,13 @@ const useFeedback = ( const useOpVersion = ( // Null value skips key: OpVersionKey | null -): Loadable => { +): LoadableWithError => { const getTsClient = useGetTraceServerClientContext(); const loadingRef = useRef(false); const cachedOpVersion = key ? opVersionCache.get(key) : null; const [opVersionRes, setOpVersionRes] = useState(null); + const [error, setError] = useState(null); const deepKey = useDeepMemo(key); useEffect(() => { if (deepKey) { @@ -695,6 +701,13 @@ const useOpVersion = ( .then(res => { loadingRef.current = false; setOpVersionRes(res); + if (res.obj == null) { + setError(new Error(JSON.stringify(res))); + // be conservative and unset the cache when there's an error + if (deepKey) { + opVersionCache.del(deepKey); + } + } }); } }, [deepKey, getTsClient]); @@ -704,18 +717,21 @@ const useOpVersion = ( return { loading: false, result: null, + error, }; } if (cachedOpVersion != null) { return { loading: false, result: cachedOpVersion, + error, }; } if (opVersionRes == null || loadingRef.current) { return { loading: true, result: null, + error, }; } @@ -723,6 +739,7 @@ const useOpVersion = ( return { loading: false, result: null, + error, }; } @@ -739,6 +756,7 @@ const useOpVersion = ( return { loading: true, result: null, + error, }; } @@ -751,14 +769,95 @@ const useOpVersion = ( return { loading: false, result: cacheableResult, + error, + }; + }, [cachedOpVersion, key, opVersionRes, error]); +}; + +const useOpVersions = ( + entity: string, + project: string, + filter: OpVersionFilter, + limit?: number, + metadataOnly?: boolean, + opts?: {skip?: boolean} +): LoadableWithError => { + const getTsClient = useGetTraceServerClientContext(); + const loadingRef = useRef(false); + const [opVersionRes, setOpVersionRes] = useState< + LoadableWithError + >({ + loading: false, + error: null, + result: null, + }); + const deepFilter = useDeepMemo(filter); + + const doFetch = useCallback(() => { + if (opts?.skip) { + return; + } + setOpVersionRes({loading: true, error: null, result: null}); + loadingRef.current = true; + + const req: traceServerTypes.TraceObjQueryReq = { + project_id: projectIdFromParts({entity, project}), + filter: { + object_ids: deepFilter.opIds, + latest_only: deepFilter.latestOnly, + is_op: true, + }, + limit, + metadata_only: metadataOnly, + }; + const onSuccess = (res: traceServerTypes.TraceObjQueryRes) => { + loadingRef.current = false; + setOpVersionRes({ + loading: false, + error: null, + result: res.objs.map(convertTraceServerObjectVersionToOpSchema), + }); + }; + const onError = (e: any) => { + loadingRef.current = false; + console.error(e); + setOpVersionRes({loading: false, error: e, result: null}); }; - }, [cachedOpVersion, key, opVersionRes]); + getTsClient().objsQuery(req).then(onSuccess).catch(onError); + }, [ + deepFilter, + getTsClient, + opts?.skip, + entity, + project, + limit, + metadataOnly, + ]); + + useEffect(() => { + doFetch(); + }, [doFetch]); + + useEffect(() => { + return getTsClient().registerOnObjectListener(doFetch); + }, [getTsClient, doFetch]); + + return useMemo(() => { + if (opts?.skip) { + return {loading: false, error: null, result: null}; + } + if (opVersionRes == null || loadingRef.current) { + return {loading: true, error: null, result: null}; + } + return opVersionRes; + }, [opVersionRes, opts?.skip]); }; +// Helper function to convert trace server object version to op schema const convertTraceServerObjectVersionToOpSchema = ( obj: traceServerTypes.TraceObjSchema ): OpVersionSchema => { - const [entity, project] = obj.project_id.split('/'); + const {entity, project} = projectIdToParts(obj.project_id); return { entity, project, @@ -769,36 +868,6 @@ const convertTraceServerObjectVersionToOpSchema = ( }; }; -const useOpVersions = makeTraceServerEndpointHook< - 'objsQuery', - [string, string, OpVersionFilter, number?, boolean?, {skip?: boolean}?], - OpVersionSchema[] ->( - 'objsQuery', - ( - entity: string, - project: string, - filter: OpVersionFilter, - limit?: number, - metadataOnly?: boolean, - opts?: {skip?: boolean} - ) => ({ - params: { - project_id: projectIdFromParts({entity, project}), - filter: { - object_ids: filter.opIds, - latest_only: filter.latestOnly, - is_op: true, - }, - limit, - metadata_only: metadataOnly, - }, - skip: opts?.skip, - }), - (res): OpVersionSchema[] => - res.objs.map(convertTraceServerObjectVersionToOpSchema) -); - const useFileContent = makeTraceServerEndpointHook< 'fileContent', [string, string, string, {skip?: boolean}?], @@ -823,12 +892,13 @@ const useFileContent = makeTraceServerEndpointHook< const useObjectVersion = ( // Null value skips key: ObjectVersionKey | null -): Loadable => { +): LoadableWithError => { const getTsClient = useGetTraceServerClientContext(); const loadingRef = useRef(false); const cachedObjectVersion = key ? objectVersionCache.get(key) : null; const [objectVersionRes, setObjectVersionRes] = useState(null); + const [error, setError] = useState(null); const deepKey = useDeepMemo(key); useEffect(() => { if (deepKey) { @@ -845,7 +915,18 @@ const useObjectVersion = ( }) .then(res => { loadingRef.current = false; - setObjectVersionRes(res); + if (res.obj == null) { + if ('deleted_at' in res) { + setError(new Error(JSON.stringify(res))); + } else { + setError(new Error('Object not found')); + } + } else { + setObjectVersionRes(res); + } + }) + .catch(err => { + setError(new Error(JSON.stringify(err))); }); } }, [deepKey, getTsClient]); @@ -855,18 +936,21 @@ const useObjectVersion = ( return { loading: false, result: null, + error, }; } if (cachedObjectVersion != null) { return { loading: false, result: cachedObjectVersion, + error, }; } if (objectVersionRes == null || loadingRef.current) { return { loading: true, result: null, + error, }; } @@ -874,6 +958,7 @@ const useObjectVersion = ( return { loading: false, result: null, + error, }; } @@ -889,6 +974,7 @@ const useObjectVersion = ( return { loading: true, result: null, + error, }; } @@ -901,8 +987,9 @@ const useObjectVersion = ( return { loading: false, result: cacheableResult, + error, }; - }, [cachedObjectVersion, key, objectVersionRes]); + }, [cachedObjectVersion, key, objectVersionRes, error]); }; export const convertTraceServerObjectVersionToSchema = < @@ -1012,6 +1099,130 @@ const useRootObjectVersions = ( }, [objectVersionRes, opts?.skip]); }; +const useObjectDeleteFunc = () => { + const getTsClient = useGetTraceServerClientContext(); + + const makeObjectRef = (key: ObjectVersionKey) => { + const ref: WeaveObjectRef = { + scheme: 'weave', + entityName: key.entity, + projectName: key.project, + weaveKind: 'object', + artifactName: key.objectId, + artifactVersion: key.versionHash, + }; + return refUri(ref); + }; + + const makeOpRef = (key: OpVersionKey) => { + const ref: WeaveObjectRef = { + scheme: 'weave', + entityName: key.entity, + projectName: key.project, + weaveKind: 'op', + artifactName: key.opId, + artifactVersion: key.versionHash, + }; + return refUri(ref); + }; + + const updateObjectCaches = useCallback((key: ObjectVersionKey) => { + objectVersionCache.del(key); + const ref = makeObjectRef(key); + refDataCache.del(ref); + }, []); + + const updateOpCaches = useCallback((key: OpVersionKey) => { + opVersionCache.del(key); + const ref = makeOpRef(key); + refDataCache.del(ref); + }, []); + + const objectVersionsDelete = useCallback( + (entity: string, project: string, objectId: string, digests: string[]) => { + digests.forEach(digest => { + updateObjectCaches({ + scheme: 'weave', + weaveKind: 'object', + entity, + project, + objectId, + versionHash: digest, + path: '', + }); + }); + return getTsClient().objectDelete({ + project_id: projectIdFromParts({ + entity, + project, + }), + object_id: objectId, + digests, + }); + }, + [getTsClient, updateObjectCaches] + ); + + const objectDeleteAllVersions = useCallback( + (key: ObjectVersionKey) => { + updateObjectCaches(key); + return getTsClient().objectDelete({ + project_id: projectIdFromParts({ + entity: key.entity, + project: key.project, + }), + object_id: key.objectId, + digests: [], + }); + }, + [getTsClient, updateObjectCaches] + ); + + const opVersionsDelete = useCallback( + (entity: string, project: string, opId: string, digests: string[]) => { + digests.forEach(digest => { + updateOpCaches({ + entity, + project, + opId, + versionHash: digest, + }); + }); + return getTsClient().objectDelete({ + project_id: projectIdFromParts({ + entity, + project, + }), + object_id: opId, + digests, + }); + }, + [getTsClient, updateOpCaches] + ); + + const opDeleteAllVersions = useCallback( + (key: OpVersionKey) => { + updateOpCaches(key); + return getTsClient().objectDelete({ + project_id: projectIdFromParts({ + entity: key.entity, + project: key.project, + }), + object_id: key.opId, + digests: [], + }); + }, + [getTsClient, updateOpCaches] + ); + + return { + objectVersionsDelete, + objectDeleteAllVersions, + opVersionsDelete, + opDeleteAllVersions, + }; +}; + const useRefsReadBatch = makeTraceServerEndpointHook< 'readBatch', [string[], {skip?: boolean}?], @@ -1785,6 +1996,7 @@ export const tsWFDataModelHooks: WFDataModelHooksInterface = { useOpVersion, useOpVersions, useObjectVersion, + useObjectDeleteFunc, useRootObjectVersions, useRefsData, useApplyMutationsToRef, diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/tsDataModelHooksEvaluationComparison.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/tsDataModelHooksEvaluationComparison.ts index c321accd487e..5a918ed46d16 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/tsDataModelHooksEvaluationComparison.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/tsDataModelHooksEvaluationComparison.ts @@ -414,6 +414,9 @@ const fetchEvaluationSummaryData = async ( const ref = modelRefs[objNdx]; const parsed = parseRef(ref) as WeaveObjectRef; const objData = objVal; + if (objData == null) { + return [ref, null]; + } return [ ref, { @@ -497,25 +500,28 @@ const fetchEvaluationComparisonResults = async ( }); // 3.5 Populate the inputs - // We only ned 1 since we are going to effectively do an inner join on the rowDigest + // We only need 1 since we are going to effectively do an inner join on the rowDigest const datasetRef = Object.values(summaryData.evaluations)[0] .datasetRef as string; const datasetObjRes = await traceServerClient.readBatch({refs: [datasetRef]}); - const rowsRef = datasetObjRes.vals[0].rows; - const parsedRowsRef = parseRef(rowsRef) as WeaveObjectRef; - const rowsQuery = await traceServerClient.tableQuery({ - project_id: projectIdFromParts({ - entity: parsedRowsRef.entityName, - project: parsedRowsRef.projectName, - }), - digest: parsedRowsRef.artifactVersion, - }); - rowsQuery.rows.forEach(row => { - result.inputs[row.digest] = { - digest: row.digest, - val: row.val, - }; - }); + // If the dataset has not been deleted, fetch rows + if (datasetObjRes.vals[0] != null) { + const rowsRef = datasetObjRes.vals[0].rows; + const parsedRowsRef = parseRef(rowsRef) as WeaveObjectRef; + const rowsQuery = await traceServerClient.tableQuery({ + project_id: projectIdFromParts({ + entity: parsedRowsRef.entityName, + project: parsedRowsRef.projectName, + }), + digest: parsedRowsRef.artifactVersion, + }); + rowsQuery.rows.forEach(row => { + result.inputs[row.digest] = { + digest: row.digest, + val: row.val, + }; + }); + } // 4. Populate the predictions and scores const evalTraceRes = await evalTraceResProm; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/utilities.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/utilities.ts index 1e27e2c5c00e..b1d3c0849c9a 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/utilities.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/utilities.ts @@ -287,6 +287,17 @@ export const objectVersionNiceString = (ov: ObjectVersionSchema) => { return result; }; +export const isObjDeleteError = (error: Error | null): boolean => { + if (error == null) { + return false; + } + const message = JSON.parse(error.message); + if ('deleted_at' in message) { + return true; + } + return false; +}; + /// Hooks /// export const useParentCall = ( diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/wfDataModelHooksInterface.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/wfDataModelHooksInterface.ts index 34496ffeed04..08fd47101777 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/wfDataModelHooksInterface.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/wfDataModelHooksInterface.ts @@ -220,7 +220,9 @@ export type WFDataModelHooksInterface = { val: any, baseObjectClass?: string ) => Promise; - useOpVersion: (key: OpVersionKey | null) => Loadable; + useOpVersion: ( + key: OpVersionKey | null + ) => LoadableWithError; useOpVersions: ( entity: string, project: string, @@ -231,7 +233,7 @@ export type WFDataModelHooksInterface = { ) => LoadableWithError; useObjectVersion: ( key: ObjectVersionKey | null - ) => Loadable; + ) => LoadableWithError; useTableRowsQuery: ( entity: string, project: string, @@ -256,6 +258,26 @@ export type WFDataModelHooksInterface = { metadataOnly?: boolean, opts?: {skip?: boolean; noAutoRefresh?: boolean} ) => LoadableWithError; + useObjectDeleteFunc: () => { + objectVersionsDelete: ( + entity: string, + project: string, + objectId: string, + digests: string[] + ) => Promise; + objectDeleteAllVersions: ( + key: ObjectVersionKey + ) => Promise; + opVersionsDelete: ( + entity: string, + project: string, + opId: string, + digests: string[] + ) => Promise; + opDeleteAllVersions: ( + key: OpVersionKey + ) => Promise; + }; // `useRefsData` is in beta while we integrate Shawn's new Object DB useRefsData: (refUris: string[], tableQuery?: TableQuery) => Loadable; // `useApplyMutationsToRef` is in beta while we integrate Shawn's new Object DB