diff --git a/.changeset/source-section-field.md b/.changeset/source-section-field.md new file mode 100644 index 0000000000..e37cc5c64d --- /dev/null +++ b/.changeset/source-section-field.md @@ -0,0 +1,12 @@ +--- +"@hyperdx/common-utils": patch +"@hyperdx/api": patch +"@hyperdx/app": patch +--- + +feat: add an optional Section field to data sources + +Sources can now carry an optional free-text Section label, set from the source +settings form. The value is persisted and returned by GET /api/v2/sources, so +external API consumers can read it. This lays the groundwork for grouping and +searching sources by section in the source selector. diff --git a/packages/api/openapi.json b/packages/api/openapi.json index 9bcad76117..a2b81197ce 100644 --- a/packages/api/openapi.json +++ b/packages/api/openapi.json @@ -2951,6 +2951,12 @@ "description": "Display name for the source.", "example": "Logs" }, + "section": { + "type": "string", + "maxLength": 256, + "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", + "example": "Billing" + }, "kind": { "type": "string", "enum": [ @@ -3139,6 +3145,12 @@ "description": "Display name for the source.", "example": "Traces" }, + "section": { + "type": "string", + "maxLength": 256, + "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", + "example": "Billing" + }, "kind": { "type": "string", "enum": [ @@ -3354,6 +3366,12 @@ "description": "Display name for the source.", "example": "Metrics" }, + "section": { + "type": "string", + "maxLength": 256, + "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", + "example": "Billing" + }, "kind": { "type": "string", "enum": [ @@ -3422,6 +3440,12 @@ "description": "Display name for the source.", "example": "Sessions" }, + "section": { + "type": "string", + "maxLength": 256, + "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", + "example": "Billing" + }, "kind": { "type": "string", "enum": [ diff --git a/packages/api/src/models/source.ts b/packages/api/src/models/source.ts index b7e7d55d33..7623af007b 100644 --- a/packages/api/src/models/source.ts +++ b/packages/api/src/models/source.ts @@ -91,6 +91,7 @@ const sourceBaseSchema = new Schema( ref: 'Connection', }, name: String, + section: String, disabled: { type: Boolean, default: false, diff --git a/packages/api/src/routers/external-api/__tests__/sources.test.ts b/packages/api/src/routers/external-api/__tests__/sources.test.ts index 91a7296ff0..dfe3d59494 100644 --- a/packages/api/src/routers/external-api/__tests__/sources.test.ts +++ b/packages/api/src/routers/external-api/__tests__/sources.test.ts @@ -533,6 +533,54 @@ describe('External API v2 Sources', () => { expect(response.body.data).toHaveLength(1); expect(response.body.data[0].id).toBe(validSource._id.toString()); }); + + describe('section field', () => { + const SECTION = 'Control Plane Prod'; + + it('returns the section on a source that has one', async () => { + const logSource = await LogSource.create({ + kind: SourceKind.Log, + team: team._id, + name: 'Sectioned Log Source', + section: SECTION, + from: { + databaseName: DEFAULT_DATABASE, + tableName: DEFAULT_LOGS_TABLE, + }, + timestampValueExpression: 'Timestamp', + defaultTableSelectExpression: '*', + connection: connection._id, + }); + + const response = await authRequest('get', BASE_URL).expect(200); + + expect(response.body.data).toHaveLength(1); + expect(response.body.data[0]).toMatchObject({ + id: logSource._id.toString(), + section: SECTION, + }); + }); + + it('omits the section on a source that has none', async () => { + await LogSource.create({ + kind: SourceKind.Log, + team: team._id, + name: 'Unsectioned Log Source', + from: { + databaseName: DEFAULT_DATABASE, + tableName: DEFAULT_LOGS_TABLE, + }, + timestampValueExpression: 'Timestamp', + defaultTableSelectExpression: '*', + connection: connection._id, + }); + + const response = await authRequest('get', BASE_URL).expect(200); + + expect(response.body.data).toHaveLength(1); + expect(response.body.data[0]).not.toHaveProperty('section'); + }); + }); }); describe('backward compatibility with legacy flat-model documents', () => { diff --git a/packages/api/src/routers/external-api/v2/sources.ts b/packages/api/src/routers/external-api/v2/sources.ts index 0903dccf0e..106c9a4c76 100644 --- a/packages/api/src/routers/external-api/v2/sources.ts +++ b/packages/api/src/routers/external-api/v2/sources.ts @@ -274,6 +274,11 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Display name for the source. * example: Logs + * section: + * type: string + * maxLength: 256 + * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. + * example: Billing * kind: * type: string * enum: [log] @@ -421,6 +426,11 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Display name for the source. * example: Traces + * section: + * type: string + * maxLength: 256 + * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. + * example: Billing * kind: * type: string * enum: [trace] @@ -589,6 +599,11 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Display name for the source. * example: Metrics + * section: + * type: string + * maxLength: 256 + * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. + * example: Billing * kind: * type: string * enum: [metric] @@ -641,6 +656,11 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Display name for the source. * example: Sessions + * section: + * type: string + * maxLength: 256 + * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. + * example: Billing * kind: * type: string * enum: [session] diff --git a/packages/app/src/components/Sources/SourceForm.tsx b/packages/app/src/components/Sources/SourceForm.tsx index 6c4043afb1..308b81fd6a 100644 --- a/packages/app/src/components/Sources/SourceForm.tsx +++ b/packages/app/src/components/Sources/SourceForm.tsx @@ -2447,6 +2447,14 @@ export function TableSourceForm({ rules={{ required: 'Name is required' }} /> + + +