Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue';
import { Activity, Ban, Check, ChevronDown, Copy, ExternalLink, RotateCcw, ScrollText, X } from '@lucide/vue';
import { RouterLink } from 'vue-router';
import { Activity, Ban, Check, ChevronDown, Copy, ExternalLink, Layers3, RotateCcw, ScrollText, X } from '@lucide/vue';

import StateBadge from '@/components/StateBadge.vue';
import { getJobExecutions, getJobExecutionTelemetryLinks } from '@/api';
Expand All @@ -18,10 +19,12 @@ const props = withDefaults(defineProps<{
telemetryLinks?: JobTelemetryLink[];
pending?: boolean;
showClose?: boolean;
showBatchLink?: boolean;
}>(), {
telemetryLinks: () => [],
pending: false,
showClose: true,
showBatchLink: true,
});

const emit = defineEmits<{
Expand Down Expand Up @@ -215,7 +218,18 @@ function retryButtonLabel(job: JobRecord, pending: boolean): string {
</div>
<div v-if="job.batchHandle">
<dt>Batch</dt>
<dd><code>{{ job.batchHandle }}</code></dd>
<dd>
<RouterLink
v-if="showBatchLink"
class="table-link"
:to="{ name: 'batch-detail', params: { batchHandle: job.batchHandle } }"
:aria-label="`Open batch ${job.batchHandle}`"
>
<Layers3 :size="13" aria-hidden="true" />
{{ job.batchHandle }}
</RouterLink>
<code v-else>{{ job.batchHandle }}</code>
</dd>
</div>
<div>
<dt>Attempt</dt>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ tbody tr:hover {
color: var(--text);
background: transparent;
font: 600 12px "SFMono-Regular", Consolas, monospace;
text-decoration: none;
cursor: pointer;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ async function confirmJobCancel(): Promise<void> {
<JobDetail
v-else-if="jobQuery.data.value"
:job="jobQuery.data.value"
:show-batch-link="false"
:telemetry-links="telemetryLinksQuery.data.value ?? []"
:pending="jobMutations.busyJobHandle.value === selectedJobHandle"
@close="closeGraphJob"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ vi.mock('@/api', async importOriginal => ({
const getJobExecutionsMock = vi.mocked(getJobExecutions);
const getJobExecutionTelemetryLinksMock = vi.mocked(getJobExecutionTelemetryLinks);

function mountJobDetail(props: InstanceType<typeof JobDetail>['$props']) {
const router = createRouter({ history: createMemoryHistory(), routes });
return mount(JobDetail, { props, global: { plugins: [router] } });
}

describe('dashboard components', () => {
beforeEach(() => {
getJobExecutionsMock.mockReset().mockResolvedValue({ items: [], skip: 0, take: 20, hasNext: false });
Expand All @@ -54,6 +59,18 @@ describe('dashboard components', () => {
expect(dashboardStyles).not.toMatch(/\.state-row-label\s*\{[^}]*text-transform:\s*uppercase/s);
});

it('links a batched job to its batch unless the batch is already shown', async () => {
const detail = mountJobDetail({ job: completedJob });
await flushPromises();
const batchLink = detail.get('a[aria-label="Open batch batch-42"]');
expect(batchLink.attributes('href')).toBe('/batches/batch-42');

const inBatch = mountJobDetail({ job: completedJob, showBatchLink: false });
await flushPromises();
expect(inBatch.find('a[aria-label="Open batch batch-42"]').exists()).toBe(false);
expect(inBatch.text()).toContain('batch-42');
});

it('renders job rows, retained executions, and complete payload/context details', async () => {
const attemptTraceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const attemptSpanId = 'bbbbbbbbbbbbbbbb';
Expand Down Expand Up @@ -84,14 +101,12 @@ describe('dashboard components', () => {
getJobExecutionTelemetryLinksMock.mockResolvedValue([
{ label: 'View execution trace', kind: 'Trace', url: `https://telemetry.example/traces/${attemptTraceId}` },
]);
const detail = mount(JobDetail, {
props: {
job: completedJob,
telemetryLinks: [
{ label: 'View trace', kind: 'Trace', url: 'https://telemetry.example/traces/4bf92f' },
{ label: 'View all retry logs', kind: 'Logs', url: 'https://telemetry.example/logs?job=86bf8c31' },
],
},
const detail = mountJobDetail({
job: completedJob,
telemetryLinks: [
{ label: 'View trace', kind: 'Trace', url: 'https://telemetry.example/traces/4bf92f' },
{ label: 'View all retry logs', kind: 'Logs', url: 'https://telemetry.example/logs?job=86bf8c31' },
],
});
await flushPromises();
expect(detail.attributes('aria-label')).toBe('Details for SendGreeting');
Expand Down Expand Up @@ -135,7 +150,7 @@ describe('dashboard components', () => {
hasNext: false,
});

const detail = mount(JobDetail, { props: { job: completedJob } });
const detail = mountJobDetail({ job: completedJob });
await flushPromises();
vi.stubGlobal('navigator', { clipboard: { writeText } });
const copyTrace = detail.get('button[aria-label="Copy trace ID for attempt 3"]');
Expand Down Expand Up @@ -171,7 +186,7 @@ describe('dashboard components', () => {
hasNext: false,
});

const detail = mount(JobDetail, { props: { job: completedJob } });
const detail = mountJobDetail({ job: completedJob });
await flushPromises();
const cards = detail.findAll('details.execution-card');

Expand Down Expand Up @@ -216,7 +231,7 @@ describe('dashboard components', () => {
hasNext: false,
});

const detail = mount(JobDetail, { props: { job: completedJob } });
const detail = mountJobDetail({ job: completedJob });
await flushPromises();
await detail.get('.execution-history > button').trigger('click');
await flushPromises();
Expand Down Expand Up @@ -253,7 +268,7 @@ describe('dashboard components', () => {
});
getJobExecutionTelemetryLinksMock.mockRejectedValue(new Error('telemetry unavailable'));

const detail = mount(JobDetail, { props: { job: completedJob } });
const detail = mountJobDetail({ job: completedJob });
await flushPromises();

expect(detail.text()).toContain(`Attempt ${completedJob.attempt}`);
Expand All @@ -280,7 +295,7 @@ describe('dashboard components', () => {
await cancel.trigger('click');
expect(table.emitted('cancel')?.[0]).toEqual([scheduled]);

const detail = mount(JobDetail, { props: { job: scheduled } });
const detail = mountJobDetail({ job: scheduled });
expect(detail.findAll('button.button-secondary').some(button => button.text().includes('Run now'))).toBe(true);
const cancelDetail = detail.findAll('button.button-secondary').find(button => button.text().includes('Cancel job'));
expect(cancelDetail).toBeDefined();
Expand All @@ -299,11 +314,7 @@ describe('dashboard components', () => {
{ groupId: null, rendersGroup: false },
{ groupId: '', rendersGroup: true },
])('renders job group details according to the nullable contract for $groupId', ({ groupId, rendersGroup }) => {
const detail = mount(JobDetail, {
props: {
job: { ...completedJob, groupId },
},
});
const detail = mountJobDetail({ job: { ...completedJob, groupId } });

expect(detail.findAll('dt').some((term) => term.text() === 'Group')).toBe(rendersGroup);
});
Expand Down
Loading