Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
10 changes: 10 additions & 0 deletions src/platform/packages/private/kbn-reporting/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
LENS_APP_LOCATOR,
VISUALIZE_APP_LOCATOR,
} from '@kbn/deeplinks-analytics';
import { LicenseType } from '@kbn/licensing-plugin/common/types';

export const ALLOWED_JOB_CONTENT_TYPES = [
'application/json',
Expand Down Expand Up @@ -40,6 +41,13 @@ export const LICENSE_TYPE_CLOUD_STANDARD = 'standard' as const;
export const LICENSE_TYPE_GOLD = 'gold' as const;
export const LICENSE_TYPE_PLATINUM = 'platinum' as const;
export const LICENSE_TYPE_ENTERPRISE = 'enterprise' as const;
export const SCHEDULED_REPORT_VALID_LICENSES: LicenseType[] = [
LICENSE_TYPE_TRIAL,
LICENSE_TYPE_CLOUD_STANDARD,
LICENSE_TYPE_GOLD,
LICENSE_TYPE_PLATINUM,
LICENSE_TYPE_ENTERPRISE,
];

/*
* Notifications
Expand All @@ -66,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
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
"@kbn/i18n",
"@kbn/task-manager-plugin",
"@kbn/deeplinks-analytics",
"@kbn/licensing-plugin",
]
}
23 changes: 22 additions & 1 deletion src/platform/packages/private/kbn-reporting/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
LayoutParams,
PerformanceMetrics as ScreenshotMetrics,
} from '@kbn/screenshotting-plugin/common';
import type { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server';
import type { ConcreteTaskInstance, RruleSchedule } from '@kbn/task-manager-plugin/server';
import { JOB_STATUS } from './constants';
import type { LocatorParams } from './url';

Expand Down Expand Up @@ -211,3 +211,24 @@ export interface LicenseCheckResults {
showLinks: boolean;
message: string;
}

export interface ScheduledReportApiJSON {
id: string;
created_at: string;
created_by: string;
enabled: boolean;
jobtype: string;
last_run: string | undefined;
next_run: string | undefined;
notification?: {
email?: {
to?: string[];
cc?: string[];
bcc?: string[];
};
};
payload?: ReportApiJSON['payload'];
schedule: RruleSchedule;
space_id: string;

@tsullivan tsullivan Jun 23, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should space_id be optional?

title: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';

export type { ClientConfigType } from './types';
export { Job } from './job';
export * from './job_completion_notifications';
Expand All @@ -15,7 +17,7 @@ export { useCheckIlmPolicyStatus } from './hooks';
export { ReportingAPIClient } from './reporting_api_client';
export { checkLicense } from './license_check';

import type { CoreSetup, CoreStart } from '@kbn/core/public';
import type { CoreSetup, CoreStart, NotificationsStart } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { useKibana as _useKibana } from '@kbn/kibana-react-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
Expand All @@ -26,10 +28,13 @@ import type { SharePluginStart } from '@kbn/share-plugin/public';
export interface KibanaContext {
http: CoreSetup['http'];
application: CoreStart['application'];
settings: CoreStart['settings'];
uiSettings: CoreStart['uiSettings'];
docLinks: CoreStart['docLinks'];
data: DataPublicPluginStart;
share: SharePluginStart;
actions: ActionsPublicPluginSetup;
notifications: NotificationsStart;
}

export const useKibana = () => _useKibana<KibanaContext>();
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' }] });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Because we filter out the reports that do not match the ID, let's add another item here.

});

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have a get by ID API?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No we don't, right @ymao1 ?

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 @@ -15,10 +15,42 @@ import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { FormattedMessage, InjectedIntl } from '@kbn/i18n-react';
import { ShareContext, type ExportShare } from '@kbn/share-plugin/public';
import { LocatorParams } from '@kbn/reporting-common/types';
import { ReportParamsGetter, ReportParamsGetterOptions } from '../../types';
import { getSearchCsvJobParams, CsvSearchModeParams } from '../shared/get_search_csv_job_params';
import type { ExportModalShareOpts } from '.';
import { checkLicense } from '../..';

export const getCsvReportParams: ReportParamsGetter<
ReportParamsGetterOptions & { forShareUrl?: boolean },
CsvSearchModeParams
> = ({ sharingData, forShareUrl = false }) => {
const getSearchSource = sharingData.getSearchSource as ({
addGlobalTimeFilter,
absoluteTime,
}: {
addGlobalTimeFilter?: boolean;
absoluteTime?: boolean;
}) => SerializedSearchSourceFields;

if (sharingData.isTextBased) {
// csv v2 uses locator params
return {
isEsqlMode: true,
locatorParams: sharingData.locatorParams as LocatorParams[],
};
}

// csv v1 uses search source and columns
return {
isEsqlMode: false,
columns: sharingData.columns as string[] | undefined,
searchSource: getSearchSource({
addGlobalTimeFilter: true,
absoluteTime: !forShareUrl,
}),
};
};

export const reportingCsvExportProvider = ({
apiClient,
startServices$,
Expand All @@ -27,33 +59,8 @@ export const reportingCsvExportProvider = ({
objectType,
sharingData,
}: ShareContext): ReturnType<ExportShare['config']> => {
const getSearchSource = sharingData.getSearchSource as ({
addGlobalTimeFilter,
absoluteTime,
}: {
addGlobalTimeFilter?: boolean;
absoluteTime?: boolean;
}) => SerializedSearchSourceFields;

const getSearchModeParams = (forShareUrl?: boolean): CsvSearchModeParams => {
if (sharingData.isTextBased) {
// csv v2 uses locator params
return {
isEsqlMode: true,
locatorParams: sharingData.locatorParams as LocatorParams[],
};
}

// csv v1 uses search source and columns
return {
isEsqlMode: false,
columns: sharingData.columns as string[] | undefined,
searchSource: getSearchSource({
addGlobalTimeFilter: true,
absoluteTime: !forShareUrl,
}),
};
};
const getSearchModeParams = (forShareUrl?: boolean): CsvSearchModeParams =>
getCsvReportParams({ sharingData, forShareUrl });

const generateReportingJobCSV = ({ intl }: { intl: InjectedIntl }) => {
const { reportType, decoratedJobParams } = getSearchCsvJobParams({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,69 @@ import { ShareContext } from '@kbn/share-plugin/public';
import React from 'react';
import { firstValueFrom } from 'rxjs';
import { ExportGenerationOpts, ExportShare } from '@kbn/share-plugin/public/types';
import { ReportParamsGetter, ReportParamsGetterOptions } from '../../types';
import { ExportModalShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.';
import { checkLicense } from '../../license_check';

const getJobParams = (opts: JobParamsProviderOptions, type: 'pngV2' | 'printablePdfV2') => () => {
const {
objectType,
sharingData: { title, locatorParams },
optimizedForPrinting,
} = opts;

const getBaseParams = (objectType: string) => {
const el = document.querySelector('[data-shared-items-container]');
const { height, width } = el ? el.getBoundingClientRect() : { height: 768, width: 1024 };
const dimensions = { height, width };
const layoutId = optimizedForPrinting ? ('print' as const) : ('preserve_layout' as const);
const layout = { id: layoutId, dimensions };
const baseParams = { objectType, layout, title };
return {
objectType,
layout: {
id: 'preserve_layout' as 'preserve_layout' | 'print',
dimensions,
},
};
};

interface PngPdfReportBaseParams {
layout: { dimensions: { height: number; width: number }; id: 'preserve_layout' | 'print' };
objectType: string;
locatorParams: any;
}

export const getPngReportParams: ReportParamsGetter<
ReportParamsGetterOptions,
PngPdfReportBaseParams
> = ({ sharingData }): PngPdfReportBaseParams => {
return {
...getBaseParams('pngV2'),
locatorParams: sharingData.locatorParams,
};
};

if (type === 'printablePdfV2') {
// multi locator for PDF V2
return { ...baseParams, locatorParams: [locatorParams] };
export const getPdfReportParams: ReportParamsGetter<
ReportParamsGetterOptions & { optimizedForPrinting?: boolean },
PngPdfReportBaseParams
> = ({ sharingData, optimizedForPrinting = false }) => {
const params = {
...getBaseParams('printablePdfV2'),
locatorParams: [sharingData.locatorParams],
};
if (optimizedForPrinting) {
params.layout.id = 'print';
}
// single locator for PNG V2
return { ...baseParams, locatorParams };
return params;
};

const getJobParams = (opts: JobParamsProviderOptions, type: 'pngV2' | 'printablePdfV2') => () => {
const { objectType, sharingData, optimizedForPrinting } = opts;
let baseParams: PngPdfReportBaseParams;
if (type === 'pngV2') {
baseParams = getPngReportParams({ sharingData });
} else {
baseParams = getPdfReportParams({
sharingData,
optimizedForPrinting,
});
}
return {
...baseParams,
objectType,
title: sharingData.title,
};
};

export const reportingPDFExportProvider = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@
"@kbn/home-plugin",
"@kbn/management-plugin",
"@kbn/ui-actions-plugin",
"@kbn/actions-plugin",
]
}
10 changes: 10 additions & 0 deletions src/platform/packages/private/kbn-reporting/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,13 @@ export interface ClientConfigType {
};
statefulSettings: { enabled: boolean };
}

export interface ReportParamsGetterOptions {
objectType?: string;
sharingData: any;
}

export type ReportParamsGetter<
O extends ReportParamsGetterOptions = ReportParamsGetterOptions,
T = unknown
> = (options: O) => T;
Loading