Skip to content

fix(breadcrumbs): link executions to the workflow that ran - #921

Open
1fanwang wants to merge 4 commits into
flyteorg:masterfrom
1fanwang:fix-breadcrumb-uses-executed-workflow
Open

1fanwang wants to merge 4 commits into
flyteorg:masterfrom
1fanwang:fix-breadcrumb-uses-executed-workflow

Conversation

@1fanwang

@1fanwang 1fanwang commented Jun 21, 2026 •

Copy link
Copy Markdown
Contributor

Read then delete this section

Release version rules

TL;DR

An execution's workflow breadcrumb can use the launch plan's project and domain instead of the workflow's. The detail link, version menu and full-version-list link now use the workflow that ran. Direct task executions keep their task names and versions.

Type

  • Bug Fix
  • Feature
  • Plugin

Are all requirements met?

  • Code completed
  • Smoke tested
  • Unit tests added
  • Code documentation added
  • Any pending items have an associated Issue

Complete description

The detail link and version menu use the workflow identifier in the execution closure. The version request uses that workflow's project and domain too. Direct task executions still use the task identifier.

Testing Done

I ran the breadcrumb functions against a local HTTP server returning protobuf execution and version-list responses. The product cache, HTTP client, protobuf decoder and version-request functions were unchanged. The execution, launch plan and workflow have different identities.

Scenario Command Observed result
Workflow and direct-task executions NODE_ENV=test node breadcrumb_repro.cjs The name, detail link, versions and view-all link use the executed entity's identity.

With Node 16.19.0 and the checkout's installed dependencies, save the source below as breadcrumb_repro.cjs in the repository root and run:

NODE_ENV=test node breadcrumb_repro.cjs
{"execution":"wf-execution","name":"hello_world_wf","selfLink":"/projects/workflow-project/domains/workflow-domain/workflows/hello_world_wf","versions":[{"title":"v1","url":"/projects/workflow-project/domains/workflow-domain/workflow/hello_world_wf/version/v1","createdAt":"11/19/2024 7:06:40 AM UTC","active":true}],"viewAll":"/projects/workflow-project/domains/workflow-domain/workflows/hello_world_wf"}
{"execution":"task-execution","name":"my-actual-task","selfLink":"/projects/execution-project/domains/execution-domain/tasks/my-actual-task","versions":[{"title":"v1","url":"/projects/execution-project/domains/execution-domain/task/my-actual-task/version/v1","createdAt":"11/19/2024 7:06:40 AM UTC","active":true}],"viewAll":"/projects/execution-project/domains/execution-domain/tasks/my-actual-task"}
{"requests":["/api/v1/executions/execution-project/execution-domain/wf-execution","/api/v1/workflows/workflow-project/workflow-domain/hello_world_wf?filters=eq%28workflow.name%2Chello_world_wf%29%2Beq%28workflow.project%2Cworkflow-project%29%2Beq%28workflow.domain%2Cworkflow-domain%29&limit=25&sort_by.direction=DESCENDING&sort_by.key=execution_created_at","/api/v1/executions/execution-project/execution-domain/task-execution","/api/v1/tasks/execution-project/execution-domain/my-actual-task?filters=eq%28task.name%2Cmy-actual-task%29%2Beq%28task.project%2Cexecution-project%29%2Beq%28task.domain%2Cexecution-domain%29&limit=25&sort_by.direction=DESCENDING&sort_by.key=execution_created_at"]}
Reproducer source: breadcrumb_repro.cjs
const assert = require('node:assert/strict');
const http = require('node:http');
const { createRequire } = require('node:module');

const load = createRequire(`${process.cwd()}/package.json`);
global.fetch = load('cross-fetch');
load('@babel/register')({
  babelrc: false,
  configFile: false,
  only: [/react-syntax-highlighter\/dist\/esm\//],
  presets: [[load.resolve('@babel/preset-env'), { targets: { node: 'current' } }]],
});
load('ts-node').register({
  transpileOnly: true,
  skipProject: true,
  compilerOptions: {
    module: 'CommonJS',
    moduleResolution: 'node',
    target: 'ES2020',
    jsx: 'react',
    esModuleInterop: true,
  },
});
const paths = load('tsconfig-paths');
const config = paths.loadConfig(process.cwd());
assert.equal(config.resultType, 'success');
paths.register({ baseUrl: config.absoluteBaseUrl, paths: config.paths });

async function main() {
  const { JSDOM } = load('jsdom');
  const dom = new JSDOM('<!doctype html><html><body></body></html>', {
    url: 'http://localhost/',
    pretendToBeVisual: true,
  });
  global.window = dom.window;
  global.document = dom.window.document;
  global.localStorage = dom.window.localStorage;
  Object.defineProperty(global, 'navigator', { value: dom.window.navigator, configurable: true });
  global.HTMLElement = dom.window.HTMLElement;
  global.getComputedStyle = dom.window.getComputedStyle;
  global.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
  global.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
  const Admin = load('@clients/common/flyteidl/admin').default;
  const Core = load('@clients/common/flyteidl/core').default;
  const workflow = {
    resourceType: Core.ResourceType.WORKFLOW,
    project: 'workflow-project',
    domain: 'workflow-domain',
    name: 'hello_world_wf',
    version: 'v1',
  };
  const task = {
    resourceType: Core.ResourceType.TASK,
    project: 'execution-project',
    domain: 'execution-domain',
    name: 'my-actual-task',
    version: 'v1',
  };
  const requests = [];
  const server = http.createServer((request, response) => {
    const url = new URL(request.url, 'http://localhost');
    requests.push(url.pathname + url.search);
    let message;
    if (url.pathname.includes('/executions/')) {
      const isTask = url.pathname.endsWith('/task-execution');
      message = Admin.Execution.encode({
        id: { project: 'execution-project', domain: 'execution-domain', name: isTask ? 'task-execution' : 'wf-execution' },
        spec: {
          launchPlan: isTask ? task : {
            resourceType: Core.ResourceType.LAUNCH_PLAN,
            project: 'launch-plan-project',
            domain: 'launch-plan-domain',
            name: 'hello_world_lp',
            version: 'lp-v1',
          },
        },
        closure: { workflowId: isTask ? { ...task, resourceType: Core.ResourceType.WORKFLOW, name: 'task-wrapper' } : workflow },
      });
    } else if (url.pathname.includes('/workflows/')) {
      message = Admin.WorkflowList.encode({ workflows: [{ id: workflow, closure: { createdAt: { seconds: 1732000000 } } }] });
    } else if (url.pathname.includes('/tasks/')) {
      message = Admin.TaskList.encode({ tasks: [{ id: task, closure: { createdAt: { seconds: 1732000000 } } }] });
    } else {
      response.writeHead(404);
      response.end();
      return;
    }
    response.writeHead(200, { 'content-type': 'application/octet-stream' });
    response.end(Buffer.from(message.finish()));
  });
  await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
  process.env.ADMIN_API = `http://127.0.0.1:${server.address().port}`;
  try {
    const api = load('@clients/oss-console/components/Breadcrumbs/async/executionContext');
    const breadcrumb = { projectId: 'execution-project', domainId: 'execution-domain' };
    for (const [execution, expected] of [['wf-execution', workflow], ['task-execution', task]]) {
      const location = { pathname: `/projects/execution-project/domains/execution-domain/executions/${execution}` };
      const name = await api.executonTaskWorkFlowNameAsyncValue(location, breadcrumb);
      const selfLink = await api.executonTaskWorkFlowNameAsyncSelfLink(location, breadcrumb);
      const versions = await api.executionTaskWorkflowVersions(location, breadcrumb);
      const viewAll = await api.executionTaskWorkflowViewAll(location, breadcrumb);
      console.log(JSON.stringify({ execution, name, selfLink, versions, viewAll }));
      assert.equal(name, expected.name);
      assert(selfLink.includes(`/projects/${expected.project}/domains/${expected.domain}/`));
      assert.equal(versions[0].title, 'v1');
      assert(versions[0].url.includes(expected.name));
      assert.equal(versions[0].active, true);
      assert.equal(viewAll, selfLink);
    }
    console.log(JSON.stringify({ requests }));
    assert(requests.some((url) => url.startsWith('/api/v1/workflows/workflow-project/workflow-domain')));
    assert(requests.some((url) => url.startsWith('/api/v1/tasks/execution-project/execution-domain')));
  } finally {
    await new Promise((resolve) => server.close(resolve));
    dom.window.close();
  }
}
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The new version-menu case failed because the request used the browsing project and domain:

-   "domain": "workflow-domain",
+   "domain": "browsing-domain",
-   "project": "workflow-project",
+   "project": "workflow-domain",

The HTTP server is a local substitute for FlyteAdmin. I did not run a full browser or live-backend smoke test. The regression suite above passed; ESLint and Prettier checks passed too.

  • Local code review completed

Tracking Issue

N/A

Follow-up issue

N/A

@1fanwang 1fanwang changed the title fix(breadcrumbs): link to the executed workflow, not the launch plan fix(breadcrumbs): link executions to the workflow that ran Aug 20, 2026
@1fanwang
1fanwang force-pushed the fix-breadcrumb-uses-executed-workflow branch from 2f51a4a to 240301a Compare August 24, 2026 10:06
The execution breadcrumb read its self-link, name, version, project, and domain from the launch-plan identifier. When a launch plan name differed from its workflow name, the breadcrumb pointed at a workflow page that did not exist. Read the identity from the executed workflow and relabel the breadcrumb as Workflow Name.

Signed-off-by: 1fanwang <1fannnw@gmail.com>
Identical project and domain values let the self-link test pass even when it read the launch-plan identifier. Separate every identity so the route assertion covers project and domain as well as the workflow name.

Signed-off-by: 1fanwang <1fannnw@gmail.com>
@1fanwang
1fanwang force-pushed the fix-breadcrumb-uses-executed-workflow branch from 240301a to c8ac8f0 Compare August 24, 2026 10:28
Signed-off-by: 1fanwang <1fannnw@gmail.com>
@1fanwang

Copy link
Copy Markdown
Contributor Author

@Sovietaced could you review when you have time?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant