Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8c9aabd
feat(attachments ui): added empty attachments component and modify ta…
cesco-f Dec 1, 2025
a9829ba
feat(attachments): add filters and actions to attachments table
cesco-f Dec 1, 2025
a5f4ee0
feat(attachment detail): added attachment details flyout
cesco-f Dec 1, 2025
8599fcb
feat(modal): add modal when linking/unlinking attachments
cesco-f Dec 1, 2025
92800c6
feat(details flyout): added description, createdAt, updatedAt and str…
cesco-f Dec 2, 2025
f6fd533
feat(toasts): added toasts when linking/unlinking
cesco-f Dec 2, 2025
cd1927f
fix(use async fn): using useAsyncFn for link/unlink operations
cesco-f Dec 2, 2025
eb49da3
feat(tags): added show all option to AttachmentTagsList
cesco-f Dec 2, 2025
ad0a19e
refactor(info panel): info panel is shared in the streams app
cesco-f Dec 2, 2025
0b9c072
Changes from node scripts/lint_ts_projects --fix
kibanamachine Dec 2, 2025
7a3687e
Changes from node scripts/capture_oas_snapshot --include-path /api/…
kibanamachine Dec 2, 2025
772e461
Changes from make api-docs
kibanamachine Dec 2, 2025
dc7b3ec
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Dec 2, 2025
078a590
fix(stream names): stream names one below the other
cesco-f Dec 2, 2025
6136e2d
fix(cr): code review
cesco-f Dec 2, 2025
26f88af
fix(test): fix test
cesco-f Dec 2, 2025
b54d561
Update x-pack/platform/plugins/shared/streams_app/public/components/s…
cesco-f Dec 2, 2025
639dfad
Update x-pack/platform/plugins/shared/streams_app/public/components/s…
cesco-f Dec 2, 2025
be8ec0e
fix(copilot): copilot code review
cesco-f Dec 2, 2025
37643c0
fix(string): fix strings
cesco-f Dec 2, 2025
65b653d
Merge branch 'main' into attachments-ui
cesco-f Dec 2, 2025
916f5db
Merge branch 'main' into attachments-ui
cesco-f Dec 3, 2025
8a9fd5f
fix(strings): use add remove
cesco-f Dec 3, 2025
f814a51
Merge branch 'main' into attachments-ui
cesco-f Dec 3, 2025
bc3481b
fix(confirm modal): confirm modal only when unlinking
cesco-f Dec 3, 2025
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
9 changes: 8 additions & 1 deletion oas_docs/bundle.json
Original file line number Diff line number Diff line change
Expand Up @@ -84388,13 +84388,20 @@
"value": {
"attachments": [
{
"createdAt": "2023-02-23T16:15:47.275Z",
"description": "Dashboard for monitoring production services",
"id": "dashboard-123",
"streamNames": [
"logs.awsfirehose",
"logs.nginx"
],
"tags": [
"monitoring",
"production"
],
"title": "My Dashboard",
"type": "dashboard"
"type": "dashboard",
"updatedAt": "2023-03-24T14:39:17.636Z"
}
]
}
Expand Down
9 changes: 8 additions & 1 deletion oas_docs/bundle.serverless.json
Original file line number Diff line number Diff line change
Expand Up @@ -83467,13 +83467,20 @@
"value": {
"attachments": [
{
"createdAt": "2023-02-23T16:15:47.275Z",
"description": "Dashboard for monitoring production services",
"id": "dashboard-123",
"streamNames": [
"logs.awsfirehose",
"logs.nginx"
],
"tags": [
"monitoring",
"production"
],
"title": "My Dashboard",
"type": "dashboard"
"type": "dashboard",
"updatedAt": "2023-03-24T14:39:17.636Z"
}
]
}
Expand Down
8 changes: 7 additions & 1 deletion oas_docs/output/kibana.serverless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71996,12 +71996,18 @@ paths:
listAttachmentsResponse:
value:
attachments:
- id: dashboard-123
- createdAt: '2023-02-23T16:15:47.275Z'
description: Dashboard for monitoring production services
id: dashboard-123
streamNames:
- logs.awsfirehose
- logs.nginx
tags:
- monitoring
- production
title: My Dashboard
type: dashboard
updatedAt: '2023-03-24T14:39:17.636Z'
description: Successfully retrieved attachments
summary: Get stream attachments
tags:
Expand Down
8 changes: 7 additions & 1 deletion oas_docs/output/kibana.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76365,12 +76365,18 @@ paths:
listAttachmentsResponse:
value:
attachments:
- id: dashboard-123
- createdAt: '2023-02-23T16:15:47.275Z'
description: Dashboard for monitoring production services
id: dashboard-123
streamNames:
- logs.awsfirehose
- logs.nginx
tags:
- monitoring
- production
title: My Dashboard
type: dashboard
updatedAt: '2023-03-24T14:39:17.636Z'
description: Successfully retrieved attachments
summary: Get stream attachments
tags:
Expand Down
1 change: 1 addition & 0 deletions x-pack/platform/plugins/shared/streams/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ dependsOn:
- '@kbn/console-plugin'
- '@kbn/actions-plugin'
- '@kbn/apm-utils'
- '@kbn/alerting-types'
tags:
- plugin
- prod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ import {
ATTACHMENT_TYPES,
type Attachment,
type AttachmentBulkOperation,
type AttachmentData,
type AttachmentDocument,
type AttachmentLink,
type AttachmentType,
} from './types';
import { getAttachmentDocument, getAttachmentLinkUuid, getSoByIds, getSuggestedSo } from './utils';
import {
getAttachmentDocument,
getAttachmentLinkUuid,
getSoByIds,
getSuggestedSo,
processRuleResults,
} from './utils';

/**
* Client for managing attachments linked to streams.
Expand All @@ -44,7 +51,7 @@ export class AttachmentClient {

private getAttachmentEntitiesMap: Record<
AttachmentType,
(ids: string[]) => Promise<Attachment[]>
(ids: string[]) => Promise<AttachmentData[]>
> = {
dashboard: async (ids) =>
getSoByIds({ soClient: this.clients.soClient, attachmentType: 'dashboard', ids }),
Expand All @@ -54,13 +61,7 @@ export class AttachmentClient {
ids,
});

return rules.map((rule) => ({
id: rule.id,
redirectId: rule.id,
title: rule.name,
tags: rule.tags,
type: 'rule',
}));
return processRuleResults(rules);
} catch (error) {
if (error.message === 'No rules found for bulk get') {
return [];
Expand All @@ -73,7 +74,7 @@ export class AttachmentClient {

private getSuggestedEntitiesMap: Record<
AttachmentType,
(options: { query: string; tags?: string[]; perPage: number }) => Promise<Attachment[]>
(options: { query: string; tags?: string[]; perPage: number }) => Promise<AttachmentData[]>
> = {
dashboard: async ({ query, tags, perPage }) =>
getSuggestedSo({
Expand All @@ -96,13 +97,7 @@ export class AttachmentClient {
},
});

return data.map((rule) => ({
id: rule.id,
redirectId: rule.id,
title: rule.name,
tags: rule.tags,
type: 'rule',
}));
return processRuleResults(data);
},
slo: async ({ query, tags, perPage }) =>
getSuggestedSo({
Expand Down Expand Up @@ -164,14 +159,14 @@ export class AttachmentClient {
* client methods (bulk operations when available).
*
* @param attachmentLinks - Array of attachment links to fetch
* @returns A promise that resolves with an array of full attachment details
* @returns A promise that resolves with an array of full attachment details (without stream names)
*/
private async fetchAttachments(attachmentLinks: AttachmentLink[]): Promise<Attachment[]> {
private async fetchAttachments(attachmentLinks: AttachmentLink[]): Promise<AttachmentData[]> {
// Group attachment links by type using lodash groupBy
const attachmentLinksByType = groupBy(attachmentLinks, 'type');

// Fetch attachments for each type and flatten results
const attachments: Attachment[] = (
const attachments: AttachmentData[] = (
await Promise.all(
Object.entries(attachmentLinksByType).map(async ([type, links]) => {
const ids = links.map((link) => link.id);
Expand Down Expand Up @@ -537,13 +532,23 @@ export class AttachmentClient {
},
});

// Convert attachment documents to attachment links
const attachmentLinks: AttachmentLink[] = attachmentsResponse.hits.hits.map(({ _source }) => ({
id: _source[ATTACHMENT_ID],
type: _source[ATTACHMENT_TYPE],
}));
// Extract attachment links and stream names in a single pass
const streamNamesById = new Map<string, string[]>();
const attachmentLinks: AttachmentLink[] = attachmentsResponse.hits.hits.map(({ _source }) => {
streamNamesById.set(_source[ATTACHMENT_ID], _source[STREAM_NAMES]);
return {
id: _source[ATTACHMENT_ID],
type: _source[ATTACHMENT_TYPE],
};
});

return this.fetchAttachments(attachmentLinks);
const attachments = await this.fetchAttachments(attachmentLinks);

// Enrich attachments with stream names
return attachments.map((attachment) => ({
...attachment,
streamNames: streamNamesById.get(attachment.id) ?? [],
}));
}

/**
Expand Down Expand Up @@ -593,8 +598,49 @@ export class AttachmentClient {
);

const results = await Promise.all(suggestionsPromises);
const attachments = results.flat();

// Get stream names for all suggested attachments
const streamNamesById = await this.getStreamNamesForAttachments(attachments.map((a) => a.id));

// Enrich attachments with stream names (empty array if not linked to any stream)
return attachments.map((attachment) => ({
...attachment,
streamNames: streamNamesById.get(attachment.id) ?? [],
}));
}

/**
* Retrieves the stream names for a list of attachment IDs.
*
* Queries the attachments storage to find which streams each attachment is linked to.
*
* @param attachmentIds - Array of attachment IDs to look up
* @returns A Map where keys are attachment IDs and values are arrays of stream names
*/
private async getStreamNamesForAttachments(
attachmentIds: string[]
): Promise<Map<string, string[]>> {
if (attachmentIds.length === 0) {
return new Map();
}

const response = await this.clients.storageClient.search({
size: attachmentIds.length,
track_total_hits: false,
query: {
bool: {
filter: [{ terms: { [ATTACHMENT_ID]: attachmentIds } }],
},
},
});

const streamNamesById = new Map<string, string[]>();
for (const hit of response.hits.hits) {
streamNamesById.set(hit._source[ATTACHMENT_ID], hit._source[STREAM_NAMES]);
}

return results.flat();
return streamNamesById;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ export interface AttachmentLink {
type: AttachmentType;
}

export interface Attachment extends AttachmentLink {
export interface AttachmentData extends AttachmentLink {
/**
* The display title of the attachment.
*/
title: string;
/**
* List of tag IDs associated with the attachment.
*/
tags: string[];
/**
* The identifier used for navigation to the attachment's detail page.
Expand All @@ -33,6 +39,25 @@ export interface Attachment extends AttachmentLink {
* own ID, which is required for proper navigation.
*/
redirectId: string;
/**
* Optional description of the attachment.
*/
description?: string;
/**
* The date and time the attachment was created.
*/
createdAt?: string;
/**
* The date and time the attachment was last updated.
*/
updatedAt?: string;
}

export interface Attachment extends AttachmentData {
/**
* The names of streams this attachment is linked to.
*/
streamNames: string[];
}

export interface AttachmentDocument {
Expand All @@ -53,9 +78,11 @@ export type AttachmentBulkOperation = AttachmentBulkIndexOperation | AttachmentB

export interface DashboardSOAttributes {
title: string;
description?: string;
}

export interface SloSOAttributes {
name: string;
id: string;
description?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import type {
SavedObjectsClientContract,
SavedObjectsFindOptions,
} from '@kbn/core/server';
import type { SanitizedRule } from '@kbn/alerting-types';
import type {
AttachmentLink,
AttachmentDocument,
AttachmentType,
Attachment,
AttachmentData,
DashboardSOAttributes,
SloSOAttributes,
} from './types';
Expand Down Expand Up @@ -44,7 +45,7 @@ export const getAttachmentDocument = (attachment: {

const processDashboardResults = (
savedObjects: Array<SavedObject<DashboardSOAttributes>>
): Attachment[] => {
): AttachmentData[] => {
return savedObjects
.filter((savedObject) => !savedObject.error)
.map((savedObject) => ({
Expand All @@ -53,10 +54,13 @@ const processDashboardResults = (
type: 'dashboard',
title: savedObject.attributes.title,
tags: savedObject.references.filter((ref) => ref.type === 'tag').map((ref) => ref.id),
description: savedObject.attributes.description,
createdAt: savedObject.created_at,
updatedAt: savedObject.updated_at,
}));
};

const processSloResults = (savedObjects: Array<SavedObject<SloSOAttributes>>): Attachment[] => {
const processSloResults = (savedObjects: Array<SavedObject<SloSOAttributes>>): AttachmentData[] => {
return savedObjects
.filter((savedObject) => !savedObject.error)
.map((savedObject) => ({
Expand All @@ -65,9 +69,24 @@ const processSloResults = (savedObjects: Array<SavedObject<SloSOAttributes>>): A
type: 'slo',
title: savedObject.attributes.name,
tags: savedObject.references.filter((ref) => ref.type === 'tag').map((ref) => ref.id),
description: savedObject.attributes.description,
createdAt: savedObject.created_at,
updatedAt: savedObject.updated_at,
}));
};

export const processRuleResults = (rules: SanitizedRule[]): AttachmentData[] => {
return rules.map((rule) => ({
id: rule.id,
redirectId: rule.id,
type: 'rule',
title: rule.name,
tags: rule.tags,
createdAt: rule.createdAt.toISOString(),
updatedAt: rule.updatedAt.toISOString(),
}));
};

/**
* Fetches saved objects by IDs for dashboards and SLOs only.
* Rules use the rule client instead.
Expand All @@ -80,7 +99,7 @@ export const getSoByIds = async ({
soClient: SavedObjectsClientContract;
attachmentType: Extract<AttachmentType, 'dashboard' | 'slo'>;
ids: string[];
}): Promise<Attachment[]> => {
}): Promise<AttachmentData[]> => {
if (attachmentType === 'dashboard') {
const result = await soClient.bulkGet<DashboardSOAttributes>(
ids.map((id) => ({ id, type: attachmentType }))
Expand Down Expand Up @@ -112,7 +131,7 @@ export const getSuggestedSo = async ({
query: string;
tags?: string[];
perPage: number;
}): Promise<Attachment[]> => {
}): Promise<AttachmentData[]> => {
const searchOptions: SavedObjectsFindOptions = {
type: attachmentType,
search: query,
Expand Down
Loading