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
Expand Up @@ -74,6 +74,8 @@ export const REPORTING_REDIRECT_LOCATOR_STORE_KEY = '__REPORTING_REDIRECT_LOCATO

// Management UI route
export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting';
export const REPORTING_MANAGEMENT_SCHEDULES =
'/app/management/insightsAndAlerting/reporting/schedules';

/*
* ILM
Expand Down
2 changes: 2 additions & 0 deletions src/platform/packages/private/kbn-reporting/public/job.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class Job {

public readonly queue_time_ms?: Required<ReportFields>['queue_time_ms'][number];
public readonly execution_time_ms?: Required<ReportFields>['execution_time_ms'][number];
public readonly scheduled_report_id?: ReportSource['scheduled_report_id'];

constructor(report: ReportApiJSON) {
this.id = report.id;
Expand Down Expand Up @@ -117,6 +118,7 @@ export class Job {
this.metrics = report.metrics;
this.queue_time_ms = report.queue_time_ms;
this.execution_time_ms = report.execution_time_ms;
this.scheduled_report_id = report.scheduled_report_id;
}

public isSearch() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,27 @@ describe('ReportingAPIClient', () => {
});
});

describe('getScheduledReportInfo', () => {
beforeEach(() => {
httpClient.get.mockResolvedValueOnce({ data: [{ id: '123', title: 'Scheduled Report 1' }] });
});

it('should send a get request', async () => {
await apiClient.getScheduledReportInfo('123');

expect(httpClient.get).toHaveBeenCalledWith(
expect.stringContaining('/internal/reporting/scheduled/list')
);
});

it('should return a report', async () => {
await expect(apiClient.getScheduledReportInfo('123')).resolves.toEqual({
id: '123',
title: 'Scheduled Report 1',
});
});
});

describe('getError', () => {
it('should get an error message', async () => {
httpClient.get.mockResolvedValueOnce({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import {
buildKibanaPath,
REPORTING_REDIRECT_APP,
} from '@kbn/reporting-common';
import { BaseParams, JobId, ManagementLinkFn, ReportApiJSON } from '@kbn/reporting-common/types';
import {
BaseParams,
JobId,
ManagementLinkFn,
ReportApiJSON,
ScheduledReportApiJSON,
} from '@kbn/reporting-common/types';
import rison from '@kbn/rison';
import moment from 'moment';
import { stringify } from 'query-string';
Expand Down Expand Up @@ -83,7 +89,10 @@ export class ReportingAPIClient implements IReportingAPI {
}

public getKibanaAppHref(job: Job): string {
const searchParams = stringify({ jobId: job.id });
const searchParams = stringify({
jobId: job.id,
...(job.scheduled_report_id ? { scheduledReportId: job.scheduled_report_id } : {}),
});

const path = buildKibanaPath({
basePath: this.http.basePath.serverBasePath,
Expand Down Expand Up @@ -158,6 +167,15 @@ export class ReportingAPIClient implements IReportingAPI {
return new Job(report);
}

public async getScheduledReportInfo(id: string) {
const { data: reportList = [] }: { data: ScheduledReportApiJSON[] } = await this.http.get(
`${INTERNAL_ROUTES.SCHEDULED.LIST}`
);

const report = reportList.find((item) => item.id === id);
return report;
}

public async findForJobIds(jobIds: JobId[]) {
const reports: ReportApiJSON[] = await this.http.fetch(INTERNAL_ROUTES.JOBS.LIST, {
query: { page: 0, ids: jobIds.join(',') },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
* 2.0.
*/

import { Frequency } from '@kbn/rrule';
import { JOB_STATUS } from '@kbn/reporting-common';
import { ReportApiJSON } from '@kbn/reporting-common/types';
import { BaseParamsV2, ReportApiJSON, ScheduledReportApiJSON } from '@kbn/reporting-common/types';
import type { ReportMock } from './types';

const buildMockReport = (baseObj: ReportMock): ReportApiJSON => ({
Expand Down Expand Up @@ -173,3 +174,74 @@ export const mockJobs: ReportApiJSON[] = [
status: JOB_STATUS.COMPLETED,
}),
];

export const mockScheduledReports: ScheduledReportApiJSON[] = [
{
created_at: '2025-06-10T12:41:45.136Z',
created_by: 'Foo Bar',
enabled: true,
id: 'scheduled-report-1',
jobtype: 'printable_pdf_v2',
last_run: '2025-05-10T12:41:46.959Z',
next_run: '2025-06-16T13:56:07.123Z',
schedule: {
rrule: { freq: Frequency.WEEKLY, tzid: 'UTC', interval: 1 },
},
title: 'Scheduled report 1',
space_id: 'default',
payload: {
browserTimezone: 'UTC',
title: 'test PDF allowed',
layout: {
id: 'preserve_layout',
},
objectType: 'dashboard',
version: '7.14.0',
locatorParams: [
{
id: 'canvas',
version: '7.14.0',
params: {
dashboardId: '7adfa750-4c81-11e8-b3d7-01146121b73d',
preserveSavedFilters: 'true',
timeRange: {
from: 'now-7d',
to: 'now',
},
useHash: 'false',
viewMode: 'view',
},
},
],
isDeprecated: false,
} as BaseParamsV2,
},
{
created_at: '2025-06-16T12:41:45.136Z',
created_by: 'Test abc',
enabled: true,
id: 'scheduled-report-2',
jobtype: 'printable_pdf_v2',
last_run: '2025-06-16T12:41:46.959Z',
next_run: '2025-06-16T13:56:07.123Z',
space_id: 'default',
schedule: {
rrule: { freq: Frequency.DAILY, tzid: 'UTC', interval: 1 },
},
title: 'Scheduled report 2',
},
{
created_at: '2025-06-12T12:41:45.136Z',
created_by: 'New',
enabled: false,
id: 'scheduled-report-3',
jobtype: 'printable_pdf_v2',
last_run: '2025-06-16T12:41:46.959Z',
next_run: '2025-06-16T13:56:07.123Z',
space_id: 'space-a',
schedule: {
rrule: { freq: Frequency.MONTHLY, tzid: 'UTC', interval: 2 },
},
title: 'Scheduled report 3',
},
];
15 changes: 15 additions & 0 deletions x-pack/platform/plugins/private/reporting/public/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export const APP_PATH = '/app/management/insightsAndAlerting/reporting' as const;
export const HOME_PATH = `/`;
export const REPORTING_EXPORTS_PATH = '/exports' as const;
export const REPORTING_SCHEDULES_PATH = '/schedules' as const;
export const EXPORTS_TAB_ID = 'exports' as const;
export const SCHEDULES_TAB_ID = 'schedules' as const;

export type Section = 'exports' | 'schedules';
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ export const IlmPolicyStatusContextProvider: FC<PropsWithChildren<unknown>> = ({

export type UseIlmPolicyStatusReturn = ReturnType<typeof useIlmPolicyStatus>;

export const useIlmPolicyStatus = (): ContextValue => {
export const useIlmPolicyStatus = (isEnabled: boolean): ContextValue => {
const ctx = useContext(IlmPolicyStatusContext);
if (!ctx) {
if (!isEnabled) {
return {
status: undefined,
isLoading: false,
recheckStatus: () => {},
};
}

throw new Error('"useIlmPolicyStatus" can only be used inside of "IlmPolicyStatusContext"');
}
return ctx;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ import { act } from 'react-dom/test-utils';
import { Observable } from 'rxjs';
import { EuiThemeProvider } from '@elastic/eui';

import { ListingProps as Props, ReportListing } from '..';
import { RouteComponentProps } from 'react-router-dom';
import { createLocation, createMemoryHistory } from 'history';
import { ListingProps as Props, ReportingTabs } from '..';
import { mockJobs } from '../../../common/test';
import { IlmPolicyStatusContextProvider } from '../../lib/ilm_policy_status_context';
import { ReportDiagnostic } from '../components';
import { MatchParams } from '../components/reporting_tabs';

export interface TestDependencies {
http: ReturnType<typeof httpServiceMock.createSetupContract>;
Expand Down Expand Up @@ -90,6 +93,21 @@ const license$ = {
},
} as Observable<ILicense>;

const routeProps: RouteComponentProps<MatchParams> = {
history: createMemoryHistory({
initialEntries: ['/exports'],
}),
location: createLocation('/exports'),
match: {
isExact: true,
path: `/exports`,
url: '',
params: {
section: 'exports',
},
},
};

export const createTestBed = registerTestBed(
({
http,
Expand All @@ -107,14 +125,16 @@ export const createTestBed = registerTestBed(
<KibanaContextProvider services={{ http, application, uiSettings, data, share }}>
<InternalApiClientProvider apiClient={reportingAPIClient} http={http}>
<IlmPolicyStatusContextProvider>
<ReportListing
<ReportingTabs
coreStart={coreMock.createStart()}
dataService={data}
shareService={share}
license$={l$}
config={mockConfig}
redirect={jest.fn()}
navigateToUrl={jest.fn()}
urlService={urlService}
toasts={toasts}
apiClient={reportingAPIClient}
{...routeProps}
{...rest}
/>
</IlmPolicyStatusContextProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { HttpSetup } from '@kbn/core/public';
import { INTERNAL_ROUTES } from '@kbn/reporting-common';

export const bulkDisableScheduledReports = async ({
http,
ids = [],
}: {
http: HttpSetup;
ids: string[];
}): Promise<{
scheduled_report_ids: string[];
errors: Array<{ message: string; status?: number; id: string }>;
total: number;
}> => {
return await http.patch(INTERNAL_ROUTES.SCHEDULED.BULK_DISABLE, {
body: JSON.stringify({ ids }),
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { HttpFetchQuery, HttpSetup } from '@kbn/core/public';
import { INTERNAL_ROUTES } from '@kbn/reporting-common';
import { type ScheduledReportApiJSON } from '@kbn/reporting-common/types';

export interface Pagination {
index: number;
size: number;
}

export const getScheduledReportsList = async ({
http,
index,
size,
}: {
http: HttpSetup;
index?: number;
size?: number;
}): Promise<{
page: number;
size: number;
total: number;
data: ScheduledReportApiJSON[];
}> => {
const query: HttpFetchQuery = { page: index, size };

const res = await http.get<{
page: number;
per_page: number;
total: number;
data: ScheduledReportApiJSON[];
}>(INTERNAL_ROUTES.SCHEDULED.LIST, {
query,
});

return {
page: res.page,
size: res.per_page,
total: res.total,
data: res.data,
};
};
Loading