From 6299dcd084abaf02d721ad964e29daefabc9ef25 Mon Sep 17 00:00:00 2001
From: Thiago Dallacqua
<104855841+thiagodallacqua-hpe@users.noreply.github.com>
Date: Fri, 12 Jul 2024 15:28:38 -0300
Subject: [PATCH] feat: add basic lineage MDLM link (#9482)
---
.../react/src/components/CheckpointModal.tsx | 11 +
webui/react/src/components/OverviewStats.tsx | 4 +-
.../responses/experiment-details/set-a.json | 285 ++++++++++++++++++
webui/react/src/ioTypes.ts | 2 +
webui/react/src/pages/ModelVersionDetails.tsx | 15 +-
.../pages/TrialDetails/TrialInfoBox.test.tsx | 21 ++
.../src/pages/TrialDetails/TrialInfoBox.tsx | 27 +-
webui/react/src/services/decoder.ts | 1 +
webui/react/src/types.ts | 23 ++
webui/react/src/utils/integrations.test.ts | 32 ++
webui/react/src/utils/integrations.ts | 8 +
11 files changed, 426 insertions(+), 3 deletions(-)
create mode 100644 webui/react/src/utils/integrations.test.ts
create mode 100644 webui/react/src/utils/integrations.ts
diff --git a/webui/react/src/components/CheckpointModal.tsx b/webui/react/src/components/CheckpointModal.tsx
index ff31f1df364..624b420b3ba 100644
--- a/webui/react/src/components/CheckpointModal.tsx
+++ b/webui/react/src/components/CheckpointModal.tsx
@@ -16,6 +16,7 @@ import {
} from 'types';
import { formatDatetime } from 'utils/datetime';
import handleError, { DetError, ErrorType } from 'utils/error';
+import { createPachydermLineageLink } from 'utils/integrations';
import { humanReadableBytes } from 'utils/string';
import { checkpointSize } from 'utils/workload';
@@ -146,6 +147,16 @@ ${checkpoint?.totalBatches}? This action may complete or fail without further no
{ label: 'State', value: },
];
+ if (config.integrations?.pachyderm !== undefined) {
+ const pachydermData = config.integrations.pachyderm;
+ const url = createPachydermLineageLink(pachydermData);
+
+ glossaryContent.splice(1, 0, {
+ label: 'Data Input',
+ value: {pachydermData.dataset.repo},
+ });
+ }
+
if (checkpoint.uuid) glossaryContent.push({ label: 'UUID', value: checkpoint.uuid });
glossaryContent.push({ label: 'Location', value: getStorageLocation(config, checkpoint) });
if (searcherMetric)
diff --git a/webui/react/src/components/OverviewStats.tsx b/webui/react/src/components/OverviewStats.tsx
index a516baddfe1..0ea3642b17e 100644
--- a/webui/react/src/components/OverviewStats.tsx
+++ b/webui/react/src/components/OverviewStats.tsx
@@ -5,10 +5,12 @@ import Row from 'hew/Row';
import { Label, TypographySize } from 'hew/Typography';
import React from 'react';
+import { AnyMouseEvent } from 'utils/routes';
+
interface Props {
children: React.ReactNode;
focused?: boolean;
- onClick?: () => void;
+ onClick?: (e: AnyMouseEvent) => void;
title: string;
}
diff --git a/webui/react/src/fixtures/responses/experiment-details/set-a.json b/webui/react/src/fixtures/responses/experiment-details/set-a.json
index 19aed4419fe..86eda3d0c10 100644
--- a/webui/react/src/fixtures/responses/experiment-details/set-a.json
+++ b/webui/react/src/fixtures/responses/experiment-details/set-a.json
@@ -2523,5 +2523,290 @@
"source_trial_id": null
}
}
+ },
+ {
+ "experiment": {
+ "id": 7230,
+ "description": "",
+ "labels": [],
+ "startTime": "2024-06-26T19:00:17.969118Z",
+ "endTime": "2024-06-26T19:06:30.861340Z",
+ "state": "STATE_COMPLETED",
+ "archived": false,
+ "numTrials": 1,
+ "trialIds": [49301],
+ "displayName": "",
+ "userId": 1262,
+ "username": "thiago.menezes-dallacqua-admin",
+ "resourcePool": "compute-pool",
+ "searcherType": "\"single\"",
+ "searcherMetric": "",
+ "hyperparameters": null,
+ "name": "core-api-stage-2",
+ "notes": "",
+ "jobId": "4f75ae18-b425-4b3c-a9f6-8b6bc69d5403",
+ "forkedFrom": 7129,
+ "progress": 1,
+ "projectId": 2014,
+ "projectName": "test integration 1",
+ "workspaceId": 1816,
+ "workspaceName": "test integration",
+ "parentArchived": false,
+ "config": {
+ "bind_mounts": [],
+ "checkpoint_policy": "best",
+ "checkpoint_storage": {
+ "access_key": null,
+ "bucket": "det-determined-main-us-west-2-573932760021",
+ "endpoint_url": null,
+ "prefix": null,
+ "save_experiment_best": 0,
+ "save_trial_best": 1,
+ "save_trial_latest": 1,
+ "secret_key": null,
+ "type": "s3"
+ },
+ "data": {},
+ "debug": false,
+ "description": null,
+ "entrypoint": "python3 2_checkpoints.py",
+ "environment": {
+ "add_capabilities": [],
+ "drop_capabilities": [],
+ "environment_variables": {
+ "cpu": [],
+ "cuda": [],
+ "rocm": []
+ },
+ "force_pull_image": false,
+ "image": {
+ "cpu": "determinedai/pytorch-ngc-dev:e960eae",
+ "cuda": "determinedai/pytorch-ngc-dev:e960eae",
+ "rocm": "determinedai/environments:rocm-5.0-pytorch-1.10-tf-2.7-rocm-622d512"
+ },
+ "pod_spec": null,
+ "ports": {},
+ "proxy_ports": [],
+ "registry_auth": null
+ },
+ "hyperparameters": {},
+ "integrations": {
+ "pachyderm": {
+ "dataset": {
+ "branch": "master",
+ "commit": "1d2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
+ "project": "test-project",
+ "repo": "test-data",
+ "token": "1234567890abcdef1234567890abcdef"
+ },
+ "pachd": {
+ "host": "localhost",
+ "port": 30650
+ },
+ "proxy": {
+ "host": "localhost",
+ "port": 80,
+ "scheme": "http"
+ }
+ }
+ },
+ "labels": [],
+ "log_policies": [],
+ "max_restarts": 0,
+ "min_checkpoint_period": {
+ "batches": 0
+ },
+ "min_validation_period": {
+ "batches": 0
+ },
+ "name": "core-api-stage-2",
+ "optimizations": {
+ "aggregation_frequency": 1,
+ "auto_tune_tensor_fusion": false,
+ "average_aggregated_gradients": true,
+ "average_training_metrics": true,
+ "grad_updates_size_file": null,
+ "gradient_compression": false,
+ "mixed_precision": "O0",
+ "tensor_fusion_cycle_time": 1,
+ "tensor_fusion_threshold": 64
+ },
+ "pbs": {},
+ "perform_initial_validation": false,
+ "profiling": {
+ "begin_on_batch": 0,
+ "enabled": false,
+ "end_after_batch": null,
+ "sync_timings": true
+ },
+ "project": "test integration 1",
+ "records_per_epoch": 0,
+ "reproducibility": {
+ "experiment_seed": 1718898986
+ },
+ "resources": {
+ "devices": [],
+ "is_single_node": null,
+ "max_slots": null,
+ "native_parallel": false,
+ "priority": null,
+ "resource_pool": "compute-pool",
+ "shm_size": null,
+ "slots_per_trial": 1,
+ "weight": 1
+ },
+ "scheduling_unit": 100,
+ "searcher": {
+ "max_length": 1,
+ "metric": "x",
+ "name": "single",
+ "smaller_is_better": true,
+ "source_checkpoint_uuid": null,
+ "source_trial_id": null
+ },
+ "slurm": {},
+ "workspace": "test integration"
+ },
+ "originalConfig": "environment:\n add_capabilities: []\n drop_capabilities: []\n environment_variables:\n cpu: []\n cuda: []\n rocm: []\n force_pull_image: false\n image:\n cpu: determinedai/pytorch-ngc-dev:e960eae\n cuda: determinedai/pytorch-ngc-dev:e960eae\n rocm: determinedai/environments:rocm-5.0-pytorch-1.10-tf-2.7-rocm-622d512\n pod_spec: null\n ports: {}\n proxy_ports: []\nproject: test integration 1\nworkspace: test integration\nbind_mounts: []\ncheckpoint_policy: best\ncheckpoint_storage:\n access_key: null\n bucket: det-determined-main-us-west-2-573932760021\n endpoint_url: null\n prefix: null\n save_experiment_best: 0\n save_trial_best: 1\n save_trial_latest: 1\n secret_key: null\n type: s3\ndata: {}\ndebug: false\ndescription: null\nentrypoint: python3 2_checkpoints.py\nhyperparameters: {}\nintegrations:\n pachyderm:\n dataset:\n branch: master\n commit: 1d2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b\n project: test-project\n repo: test-data\n token: 1234567890abcdef1234567890abcdef\n pachd:\n host: localhost\n port: 30650\n proxy:\n host: localhost\n port: 80\n scheme: http\nlabels: []\nlog_policies: []\nmax_restarts: 0\nmin_checkpoint_period:\n batches: 0\nmin_validation_period:\n batches: 0\nname: core-api-stage-2\noptimizations:\n aggregation_frequency: 1\n auto_tune_tensor_fusion: false\n average_aggregated_gradients: true\n average_training_metrics: true\n grad_updates_size_file: null\n gradient_compression: false\n mixed_precision: O0\n tensor_fusion_cycle_time: 1\n tensor_fusion_threshold: 64\npbs: {}\nperform_initial_validation: false\nprofiling:\n begin_on_batch: 0\n enabled: false\n end_after_batch: null\n sync_timings: true\nrecords_per_epoch: 0\nreproducibility:\n experiment_seed: 1718898986\nresources:\n devices: []\n is_single_node: null\n max_slots: null\n native_parallel: false\n priority: null\n resource_pool: compute-pool\n shm_size: null\n slots_per_trial: 1\n weight: 1\nscheduling_unit: 100\nsearcher:\n max_length: 1\n metric: x\n name: single\n smaller_is_better: true\n source_checkpoint_uuid: null\n source_trial_id: null\nslurm: {}\n",
+ "projectOwnerId": 1262,
+ "checkpointSize": "79",
+ "checkpointCount": 2,
+ "unmanaged": false,
+ "modelDefinitionSize": 5717,
+ "pachydermIntegration": {
+ "dataset": {
+ "branch": "master",
+ "commit": "1d2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
+ "project": "test-project",
+ "repo": "test-data",
+ "token": "1234567890abcdef1234567890abcdef"
+ },
+ "pachd": {
+ "host": "localhost",
+ "port": 30650
+ },
+ "proxy": {
+ "host": "localhost",
+ "port": 80,
+ "scheme": "http"
+ }
+ }
+ },
+ "jobSummary": null,
+ "config": {
+ "bind_mounts": [],
+ "checkpoint_policy": "best",
+ "checkpoint_storage": {
+ "access_key": null,
+ "bucket": "det-determined-main-us-west-2-573932760021",
+ "endpoint_url": null,
+ "prefix": null,
+ "save_experiment_best": 0,
+ "save_trial_best": 1,
+ "save_trial_latest": 1,
+ "secret_key": null,
+ "type": "s3"
+ },
+ "data": {},
+ "debug": false,
+ "description": null,
+ "entrypoint": "python3 2_checkpoints.py",
+ "environment": {
+ "add_capabilities": [],
+ "drop_capabilities": [],
+ "environment_variables": {
+ "cpu": [],
+ "cuda": [],
+ "rocm": []
+ },
+ "force_pull_image": false,
+ "image": {
+ "cpu": "determinedai/pytorch-ngc-dev:e960eae",
+ "cuda": "determinedai/pytorch-ngc-dev:e960eae",
+ "rocm": "determinedai/environments:rocm-5.0-pytorch-1.10-tf-2.7-rocm-622d512"
+ },
+ "pod_spec": null,
+ "ports": {},
+ "proxy_ports": [],
+ "registry_auth": null
+ },
+ "hyperparameters": {},
+ "integrations": {
+ "pachyderm": {
+ "dataset": {
+ "branch": "master",
+ "commit": "1d2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
+ "project": "test-project",
+ "repo": "test-data",
+ "token": "1234567890abcdef1234567890abcdef"
+ },
+ "pachd": {
+ "host": "localhost",
+ "port": 30650
+ },
+ "proxy": {
+ "host": "localhost",
+ "port": 80,
+ "scheme": "http"
+ }
+ }
+ },
+ "labels": [],
+ "log_policies": [],
+ "max_restarts": 0,
+ "min_checkpoint_period": {
+ "batches": 0
+ },
+ "min_validation_period": {
+ "batches": 0
+ },
+ "name": "core-api-stage-2",
+ "optimizations": {
+ "aggregation_frequency": 1,
+ "auto_tune_tensor_fusion": false,
+ "average_aggregated_gradients": true,
+ "average_training_metrics": true,
+ "grad_updates_size_file": null,
+ "gradient_compression": false,
+ "mixed_precision": "O0",
+ "tensor_fusion_cycle_time": 1,
+ "tensor_fusion_threshold": 64
+ },
+ "pbs": {},
+ "perform_initial_validation": false,
+ "profiling": {
+ "begin_on_batch": 0,
+ "enabled": false,
+ "end_after_batch": null,
+ "sync_timings": true
+ },
+ "project": "test integration 1",
+ "records_per_epoch": 0,
+ "reproducibility": {
+ "experiment_seed": 1718898986
+ },
+ "resources": {
+ "devices": [],
+ "is_single_node": null,
+ "max_slots": null,
+ "native_parallel": false,
+ "priority": null,
+ "resource_pool": "compute-pool",
+ "shm_size": null,
+ "slots_per_trial": 1,
+ "weight": 1
+ },
+ "scheduling_unit": 100,
+ "searcher": {
+ "max_length": 1,
+ "metric": "x",
+ "name": "single",
+ "smaller_is_better": true,
+ "source_checkpoint_uuid": null,
+ "source_trial_id": null
+ },
+ "slurm": {},
+ "workspace": "test integration"
+ }
}
]
diff --git a/webui/react/src/ioTypes.ts b/webui/react/src/ioTypes.ts
index 2da81f0f9f7..e67f6b2f371 100644
--- a/webui/react/src/ioTypes.ts
+++ b/webui/react/src/ioTypes.ts
@@ -7,6 +7,7 @@ import {
CheckpointStorageType,
ExperimentSearcherName,
HyperparameterType,
+ Integration,
LogLevel,
Primitive,
RunState,
@@ -215,6 +216,7 @@ export const ioExperimentConfig = io.type({
checkpoint_storage: optional(ioCheckpointStorage),
description: optional(io.string),
hyperparameters: ioHyperparameters,
+ integrations: optional(Integration),
labels: optional(io.array(io.string)),
max_restarts: io.number,
name: io.string,
diff --git a/webui/react/src/pages/ModelVersionDetails.tsx b/webui/react/src/pages/ModelVersionDetails.tsx
index 373529d06a0..9e77c6d4d24 100644
--- a/webui/react/src/pages/ModelVersionDetails.tsx
+++ b/webui/react/src/pages/ModelVersionDetails.tsx
@@ -23,6 +23,7 @@ import { getModelVersion, patchModelVersion } from 'services/api';
import workspaceStore from 'stores/workspaces';
import { Metadata, ModelVersion, Note, ValueOf } from 'types';
import handleError, { ErrorType } from 'utils/error';
+import { createPachydermLineageLink } from 'utils/integrations';
import { isAborted, isNotFound } from 'utils/service';
import { humanReadableBytes } from 'utils/string';
import { checkpointSize } from 'utils/workload';
@@ -186,7 +187,8 @@ const ModelVersionDetails: React.FC = () => {
.sort((a, b) => checkpointResources[a] - checkpointResources[b])
.map((key) => ({ name: key, size: humanReadableBytes(checkpointResources[key]) }));
const hasExperiment = !!modelVersion.checkpoint.experimentId;
- return [
+ const pachydermData = modelVersion.checkpoint.experimentConfig?.integrations?.pachyderm;
+ const infoElements = [
{
label: 'Source',
value: hasExperiment ? (
@@ -227,6 +229,17 @@ const ModelVersionDetails: React.FC = () => {
value: resources.map((resource) => renderResource(resource.name, resource.size)),
},
];
+
+ if (pachydermData !== undefined) {
+ const url = createPachydermLineageLink(pachydermData);
+
+ infoElements.splice(1, 0, {
+ label: 'Data Input',
+ value: {pachydermData?.dataset.repo},
+ });
+ }
+
+ return infoElements;
}, [modelVersion?.checkpoint]);
const validationMetrics = useMemo(() => {
diff --git a/webui/react/src/pages/TrialDetails/TrialInfoBox.test.tsx b/webui/react/src/pages/TrialDetails/TrialInfoBox.test.tsx
index 9b6684b0046..c95e522995b 100644
--- a/webui/react/src/pages/TrialDetails/TrialInfoBox.test.tsx
+++ b/webui/react/src/pages/TrialDetails/TrialInfoBox.test.tsx
@@ -6,6 +6,7 @@ import {} from 'stores/cluster';
import { ThemeProvider } from 'components/ThemeProvider';
import { ExperimentBase, TrialDetails } from 'types';
+import { mockIntegrationData } from 'utils/integrations.test';
import TrialInfoBox from './TrialInfoBox';
@@ -230,4 +231,24 @@ describe('Trial Info Box', () => {
expect(await screen.findByText('Forever')).toBeVisible();
});
});
+
+ describe('Lineage card', () => {
+ it('should show Data input card with tge lineage link when pachyderm integration data is present', async () => {
+ const mockExperimentWith = Object.assign(
+ { ...mockExperiment },
+ {
+ config: { ...mockExperiment.config, integrations: { pachyderm: mockIntegrationData } },
+ },
+ );
+
+ setup(mockTrial1, mockExperimentWith);
+ expect(await screen.findByText('Data input')).toBeVisible();
+ expect(await screen.findByText(mockIntegrationData.dataset.repo)).toBeVisible();
+ });
+
+ it('should not show Data input card when pachyderm integration is missing', () => {
+ setup(mockTrial1, mockExperiment);
+ expect(screen.queryByText('Data input')).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/webui/react/src/pages/TrialDetails/TrialInfoBox.tsx b/webui/react/src/pages/TrialDetails/TrialInfoBox.tsx
index bb7d72a1c00..cb10cae97f9 100644
--- a/webui/react/src/pages/TrialDetails/TrialInfoBox.tsx
+++ b/webui/react/src/pages/TrialDetails/TrialInfoBox.tsx
@@ -11,11 +11,13 @@ import Section from 'components/Section';
import TimeAgo from 'components/TimeAgo';
import { useCheckpointFlow } from 'hooks/useCheckpointFlow';
import { NodeElement } from 'pages/ResourcePool/Topology';
-import { paths } from 'routes/utils';
+import { handlePath, paths } from 'routes/utils';
import { getTaskAcceleratorData } from 'services/api';
import { V1AcceleratorData } from 'services/api-ts-sdk/api';
import { CheckpointWorkloadExtended, ExperimentBase, TrialDetails } from 'types';
import handleError from 'utils/error';
+import { createPachydermLineageLink } from 'utils/integrations';
+import { AnyMouseEvent } from 'utils/routes';
import { humanReadableBytes, pluralizer } from 'utils/string';
import css from './TrialInfoBox.module.scss';
@@ -151,6 +153,28 @@ const TrialInfoBox: React.FC = ({ trial, experiment }: Props) => {
}, [acceleratorData]);
const allocationModal = useModal(allocationModalComponent);
+ const lineageComponent = useMemo(() => {
+ const {
+ config: { integrations },
+ } = experiment;
+
+ if (integrations?.pachyderm !== undefined) {
+ const url = createPachydermLineageLink(integrations.pachyderm);
+ const handleClickDataInput = (e: AnyMouseEvent) => {
+ handlePath(e, {
+ path: url,
+ });
+ };
+
+ return (
+
+ {integrations.pachyderm.dataset.repo}
+
+ );
+ }
+
+ return null;
+ }, [experiment]);
return (
@@ -186,6 +210,7 @@ const TrialInfoBox: React.FC = ({ trial, experiment }: Props) => {
) : null}
{{logRetentionDays}}
+ {lineageComponent}
diff --git a/webui/react/src/services/decoder.ts b/webui/react/src/services/decoder.ts
index 748fcd537cc..9b16ec27578 100644
--- a/webui/react/src/services/decoder.ts
+++ b/webui/react/src/services/decoder.ts
@@ -373,6 +373,7 @@ export const ioToExperimentConfig = (
: undefined,
description: io.description || undefined,
hyperparameters: ioToHyperparametereters(io.hyperparameters),
+ integrations: io.integrations || undefined,
labels: io.labels || undefined,
maxRestarts: io.max_restarts,
name: io.name,
diff --git a/webui/react/src/types.ts b/webui/react/src/types.ts
index 3d2467667c1..edf64d4efde 100644
--- a/webui/react/src/types.ts
+++ b/webui/react/src/types.ts
@@ -367,6 +367,28 @@ export type HyperparameterType = t.TypeOf;
export type Hyperparameters = {
[keys: string]: Hyperparameters | HyperparameterBase;
};
+
+const PachydermIntegrationData = t.type({
+ dataset: t.type({
+ branch: t.string,
+ commit: t.string,
+ project: t.string,
+ repo: t.string,
+ token: t.string,
+ }),
+ pachd: t.type({
+ host: t.string,
+ port: t.number,
+ }),
+ proxy: t.type({
+ host: t.string,
+ port: t.number,
+ scheme: t.string,
+ }),
+});
+export const Integration = t.partial({ pachyderm: PachydermIntegrationData });
+export type IntegrationType = t.TypeOf;
+export type PachydermIntegrationDataType = t.TypeOf;
const Hyperparameters: t.RecursiveType> = t.recursion(
'Hyperparameters',
() => t.record(t.string, t.union([Hyperparameters, HyperparameterBase])),
@@ -451,6 +473,7 @@ export const ExperimentConfig = t.intersection([
t.partial({
checkpointStorage: CheckpointStorage,
description: t.string,
+ integrations: Integration,
labels: t.array(t.string),
profiling: t.type({
enabled: t.boolean,
diff --git a/webui/react/src/utils/integrations.test.ts b/webui/react/src/utils/integrations.test.ts
new file mode 100644
index 00000000000..38f3a0d29d5
--- /dev/null
+++ b/webui/react/src/utils/integrations.test.ts
@@ -0,0 +1,32 @@
+import { PachydermIntegrationDataType } from 'types';
+
+import { createPachydermLineageLink } from './integrations';
+
+export const mockIntegrationData: PachydermIntegrationDataType = {
+ dataset: {
+ branch: 'test_branch',
+ commit: 'commit_example_123',
+ project: 'test-project',
+ repo: 'test-data',
+ token: 'token_example_123',
+ },
+ pachd: {
+ host: 'test_host',
+ port: 123456,
+ },
+ proxy: {
+ host: 'test_host',
+ port: 12,
+ scheme: 'http',
+ },
+};
+const expectedResult = `${mockIntegrationData.proxy.scheme}://${mockIntegrationData.proxy.host}:${mockIntegrationData.proxy.port}/lineage/${mockIntegrationData.dataset.project}/repos/${mockIntegrationData.dataset.repo}/commit/${mockIntegrationData.dataset.commit}/?branchId=${mockIntegrationData.dataset.branch}`;
+
+describe('Integrations', () => {
+ describe('createPachydermLineageLink', () => {
+ it('should return the link when passed pachyderm integration data', () => {
+ const result = createPachydermLineageLink(mockIntegrationData);
+ expect(result).toBe(expectedResult);
+ });
+ });
+});
diff --git a/webui/react/src/utils/integrations.ts b/webui/react/src/utils/integrations.ts
new file mode 100644
index 00000000000..d3c4881f659
--- /dev/null
+++ b/webui/react/src/utils/integrations.ts
@@ -0,0 +1,8 @@
+import { PachydermIntegrationDataType } from 'types';
+
+export const createPachydermLineageLink = (
+ pachydermIntegrationData: PachydermIntegrationDataType,
+): string => {
+ const { dataset, proxy } = pachydermIntegrationData;
+ return `${proxy.scheme}://${proxy.host}:${proxy.port}/lineage/${dataset.project}/repos/${dataset.repo}/commit/${dataset.commit}/?branchId=${dataset.branch}`;
+};