diff --git a/e2e-tests/data/Schema_Example_Derivation.json b/e2e-tests/data/Schema_Example_Derivation.json new file mode 100644 index 0000000000..24c6ab1f57 --- /dev/null +++ b/e2e-tests/data/Schema_Example_Derivation.json @@ -0,0 +1,93 @@ +{ + "event_types": { + "DerivationA": { + "type": "object", + "required": [ + "rules", + "notes", + "should_present" + ], + "properties": { + "rules": { + "type": "string" + }, + "notes": { + "type": "string" + }, + "should_present": { + "type": "boolean" + } + } + }, + "DerivationB": { + "type": "object", + "required": [ + "rules", + "notes", + "should_present" + ], + "properties": { + "rules": { + "type": "string" + }, + "notes": { + "type": "string" + }, + "should_present": { + "type": "boolean" + } + } + }, + "DerivationC": { + "type": "object", + "required": [ + "rules", + "notes", + "should_present" + ], + "properties": { + "rules": { + "type": "string" + }, + "notes": { + "type": "string" + }, + "should_present": { + "type": "boolean" + } + } + }, + "DerivationD": { + "type": "object", + "required": [ + "rules", + "notes", + "should_present" + ], + "properties": { + "rules": { + "type": "string" + }, + "notes": { + "type": "string" + }, + "should_present": { + "type": "boolean" + } + } + } + }, + "source_types": { + "DerivationTest": { + "type": "object", + "required": [ + "exampleAttribute" + ], + "properties": { + "exampleAttribute": { + "type": "string" + } + } + } + } +} diff --git a/e2e-tests/data/Schema_Example_Source.json b/e2e-tests/data/Schema_Example_Source.json new file mode 100644 index 0000000000..7835931784 --- /dev/null +++ b/e2e-tests/data/Schema_Example_Source.json @@ -0,0 +1,46 @@ +{ + "event_types": { + "ExampleEvent": { + "title": "ExampleEvent", + "description": "Schema for the attributes of the ExampleEvent External Event Type.", + "type": "object", + "properties": { + "example1": { + "type": "string" + }, + "example2": { + "type": "number" + }, + "optional": { + "type": "string" + } + }, + "required": [ + "example1", + "example2" + ] + } + }, + "source_types": { + "Example External Source": { + "title": "Example External Source", + "description": "Schema for the attributes of the Example External Source External Source Type.", + "type": "object", + "properties": { + "optional": { + "type": "string" + }, + "version": { + "type": "number" + }, + "spacecraft": { + "type": "string" + } + }, + "required": [ + "version", + "spacecraft" + ] + } + } +} \ No newline at end of file diff --git a/e2e-tests/data/example-external-source.json b/e2e-tests/data/example-external-source.json index ffde8377c3..11ad80a570 100644 --- a/e2e-tests/data/example-external-source.json +++ b/e2e-tests/data/example-external-source.json @@ -7,12 +7,9 @@ "start_time": "2022-001T00:00:00Z", "end_time": "2022-002T00:00:00Z" }, - "metadata": { + "attributes": { "version": 1, - "spacecraft": [ - "sc1", - "sc2" - ] + "spacecraft": "sc1" } }, "events": [ @@ -21,10 +18,10 @@ "event_type": "ExampleEvent", "start_time": "2022-001T12:00:00Z", "duration": "00:00:00", - "properties": { + "attributes": { "example1": "value", "example2": 1 } } ] -} +} \ No newline at end of file diff --git a/e2e-tests/data/example-external-source_no-attr.json b/e2e-tests/data/example-external-source_no-attr.json new file mode 100644 index 0000000000..e08e9762fc --- /dev/null +++ b/e2e-tests/data/example-external-source_no-attr.json @@ -0,0 +1,21 @@ +{ + "source": { + "key": "ExampleExternalSource:example-external-source_no-attr.json", + "source_type": "Empty External Source", + "valid_at": "2024-001T00:00:00Z", + "period": { + "start_time": "2022-001T00:00:00Z", + "end_time": "2022-002T00:00:00Z" + }, + "attributes": {} + }, + "events": [ + { + "key": "EmptyEvent:1/sc/sc1:1:X/1", + "event_type": "EmptyEvent", + "start_time": "2022-001T12:00:00Z", + "duration": "00:00:00", + "attributes": {} + } + ] +} \ No newline at end of file diff --git a/e2e-tests/data/external-event-derivation-1.json b/e2e-tests/data/external-event-derivation-1.json index 9cfe542a59..e9b16123d9 100644 --- a/e2e-tests/data/external-event-derivation-1.json +++ b/e2e-tests/data/external-event-derivation-1.json @@ -1,13 +1,15 @@ { "source": { "key": "external-event-derivation-1.json", - "source_type": "Derivation Test", + "source_type": "DerivationTest", "valid_at": "2022-018T00:00:00Z", "period": { "start_time": "2022-005T00:00:00Z", "end_time": "2022-011T00:00:00Z" }, - "metadata": {} + "attributes": { + "exampleAttribute": "TestString" + } }, "events": [ { @@ -15,13 +17,10 @@ "event_type": "DerivationD", "start_time": "2022-005T23:00:00Z", "duration": "01:10:00", - "properties": { - "rules": [ - 3, - 4 - ], - "notes": "subsumed by test 01, even though end lies outside of 01, also replaced by test 01 by key", - "should_present": false + "attributes": { + "rules": "3, 4", + "notes": "subsumed by test 01, even though end lies outside of 01, also replaced by test 01 by key", + "should_present": false } }, { @@ -29,12 +28,10 @@ "event_type": "DerivationC", "start_time": "2022-009T23:00:00Z", "duration": "02:00:00", - "properties": { - "rules": [ - 3 - ], - "notes": "subsumed by test 02, even though end lies outside of 01, because start time during 01", - "should_present": false + "attributes": { + "rules": "3", + "notes": "subsumed by test 02, even though end lies outside of 01, because start time during 01", + "should_present": false } }, { @@ -42,12 +39,10 @@ "event_type": "DerivationB", "start_time": "2022-010T11:00:00Z", "duration": "01:05:00", - "properties": { - "rules": [ - 1 - ], - "notes": "after everything, subsumed by nothing despite being from oldest file", - "should_present": true + "attributes": { + "rules": "1", + "notes": "after everything, subsumed by nothing despite being from oldest file", + "should_present": true } } ] diff --git a/e2e-tests/data/external-event-derivation-2.json b/e2e-tests/data/external-event-derivation-2.json index 23f1ed8e95..0db0d92553 100644 --- a/e2e-tests/data/external-event-derivation-2.json +++ b/e2e-tests/data/external-event-derivation-2.json @@ -1,13 +1,15 @@ { "source": { "key": "external-event-derivation-2.json", - "source_type": "Derivation Test", + "source_type": "DerivationTest", "valid_at": "2022-019T00:00:00Z", "period": { "start_time": "2022-001T00:00:00Z", "end_time": "2022-007T00:00:00Z" }, - "metadata": {} + "attributes": { + "exampleAttribute": "TestString" + } }, "events": [ { @@ -15,12 +17,10 @@ "event_type": "DerivationA", "start_time": "2022-001T00:00:00Z", "duration": "02:10:00", - "properties": { - "rules": [ - 1 - ], - "notes": "before everything, subsumed by nothing", - "should_present": true + "attributes": { + "rules": "1", + "notes": "before everything, subsumed by nothing", + "should_present": true } }, { @@ -28,12 +28,10 @@ "event_type": "DerivationA", "start_time": "2022-001T12:00:00Z", "duration": "02:10:00", - "properties": { - "rules": [ - 4 - ], - "notes": "overwritten by key in later file, even with type change", - "should_present": false + "attributes": { + "rules": "4", + "notes": "overwritten by key in later file, even with type change", + "should_present": false } }, { @@ -41,12 +39,10 @@ "event_type": "DerivationB", "start_time": "2022-002T23:00:00Z", "duration": "03:00:00", - "properties": { - "rules": [ - 2 - ], - "notes": "starts before next file though occurs during next file, still included", - "should_present": true + "attributes": { + "rules": "2", + "notes": "starts before next file though occurs during next file, still included", + "should_present": true } }, { @@ -54,12 +50,10 @@ "event_type": "DerivationB", "start_time": "2022-005T21:00:00Z", "duration": "03:00:00", - "properties": { - "rules": [ - 3 - ], - "notes": "start subsumed by 02, not included in final result", - "should_present": false + "attributes": { + "rules": "3", + "notes": "start subsumed by 02, not included in final result", + "should_present": false } } ] diff --git a/e2e-tests/data/external-event-derivation-3.json b/e2e-tests/data/external-event-derivation-3.json index 6b11d6cf1c..06a7e5d137 100644 --- a/e2e-tests/data/external-event-derivation-3.json +++ b/e2e-tests/data/external-event-derivation-3.json @@ -1,13 +1,15 @@ { "source": { "key": "external-event-derivation-3.json", - "source_type": "Derivation Test", + "source_type": "DerivationTest", "valid_at": "2022-020T00:00:00Z", "period": { "start_time": "2022-003T00:00:00Z", "end_time": "2022-010T00:00:00Z" }, - "metadata": {} + "attributes": { + "exampleAttribute": "TestString" + } }, "events": [ { @@ -15,12 +17,10 @@ "event_type": "DerivationC", "start_time": "2022-005T23:00:00Z", "duration": "01:10:00", - "properties": { - "rules": [ - 1 - ], - "notes": "not subsumed, optionally change this event to have key 6 and ensure this test fails", - "should_present": true + "attributes": { + "rules": "1", + "notes": "not subsumed, optionally change this event to have key 6 and ensure this test fails", + "should_present": true } }, { @@ -28,12 +28,10 @@ "event_type": "DerivationC", "start_time": "2022-006T12:00:00Z", "duration": "02:00:00", - "properties": { - "rules": [ - 1 - ], - "notes": "not subsumed", - "should_present": true + "attributes": { + "rules": "1", + "notes": "not subsumed", + "should_present": true } }, { @@ -41,12 +39,10 @@ "event_type": "DerivationB", "start_time": "2022-009T11:00:00Z", "duration": "01:05:00", - "properties": { - "rules": [ - 4 - ], - "notes": "replaces 2 in test 01, despite different event type", - "should_present": true + "attributes": { + "rules": "4", + "notes": "replaces 2 in test 01, despite different event type", + "should_present": true } } ] diff --git a/e2e-tests/data/external-event-derivation-4.json b/e2e-tests/data/external-event-derivation-4.json index aada9f5156..50adf4844a 100644 --- a/e2e-tests/data/external-event-derivation-4.json +++ b/e2e-tests/data/external-event-derivation-4.json @@ -1,14 +1,14 @@ { "source": { "key": "external-event-derivation-4.json", - "source_type": "Derivation Test", + "source_type": "DerivationTest", "valid_at": "2022-021T00:00:00Z", "period": { "start_time": "2022-001T12:00:00Z", "end_time": "2022-002T12:00:00Z" }, - "metadata": { - "notes": "events in here aren't too significant. The existence of this ingest is to ensure that slots work correctly in the SQL query." + "attributes": { + "exampleAttribute": "TestString" } }, "events": [ @@ -17,12 +17,10 @@ "event_type": "DerivationC", "start_time": "2022-002T00:00:00Z", "duration": "01:00:00", - "properties": { - "rules": [ - 1 - ], - "notes": "not subsumed", - "should_present": true + "attributes": { + "rules": "1", + "notes": "not subsumed", + "should_present": true } } ] diff --git a/e2e-tests/fixtures/ExternalSources.ts b/e2e-tests/fixtures/ExternalSources.ts index 85140bfe54..8421b6b0f6 100644 --- a/e2e-tests/fixtures/ExternalSources.ts +++ b/e2e-tests/fixtures/ExternalSources.ts @@ -1,9 +1,18 @@ import { expect, type Locator, type Page } from '@playwright/test'; + export class ExternalSources { alertError: Locator; closeButton: Locator; deleteSourceButton: Locator; deleteSourceButtonConfirmation: Locator; + derivationATypeName: string = 'DerivationA'; + derivationATypeSchema: string = 'e2e-tests/data/Schema_DerivationA.json'; + derivationBTypeName: string = 'DerivationB'; + derivationBTypeSchema: string = 'e2e-tests/data/Schema_DerivationB.json'; + derivationCTypeName: string = 'DerivationC'; + derivationCTypeSchema: string = 'e2e-tests/data/Schema_DerivationC.json'; + derivationDTypeName: string = 'DerivationD'; + derivationDTypeSchema: string = 'e2e-tests/data/Schema_DerivationD.json'; derivationTestFile1: string = 'e2e-tests/data/external-event-derivation-1.json'; derivationTestFile2: string = 'e2e-tests/data/external-event-derivation-2.json'; derivationTestFile3: string = 'e2e-tests/data/external-event-derivation-3.json'; @@ -12,22 +21,36 @@ export class ExternalSources { derivationTestFileKey2: string = 'external-event-derivation-2.json'; derivationTestFileKey3: string = 'external-event-derivation-3.json'; derivationTestFileKey4: string = 'external-event-derivation-4.json'; - derivationTestGroupName: string = 'Derivation Test Default'; - derivationTestSourceType: string = 'Derivation Test'; + derivationTestGroupName: string = 'DerivationTest Default'; + derivationTestSourceType: string = 'DerivationTest'; + derivationTestSourceTypeName: string = 'DerivationTest'; + derivationTestTypeSchema: string = 'e2e-tests/data/Schema_Example_Derivation.json'; + derivationTestTypeSchemaExpectedEventTypes: string[] = ['DerivationA', 'DerivationB', 'DerivationC', 'DerivationD']; + derivationTestTypeSchemaExpectedSourceTypes: string[] = ['DerivationTest']; deselectEventButton: Locator; deselectSourceButton: Locator; exampleDerivationGroup: string = 'Example External Source Default'; + exampleEmptyDerivationGroup: string = 'Empty External Source Default'; + exampleEmptyEventType: string = 'EmptyEvent'; + exampleEmptySourceType: string = 'Empty External Source'; exampleEventType: string = 'ExampleEvent'; exampleSourceType: string = 'Example External Source'; + exampleTypeSchema: string = 'e2e-tests/data/Schema_Example_Source.json'; + exampleTypeSchemaExpectedEventTypes: string[] = ['ExampleEvent']; + exampleTypeSchemaExpectedSourceTypes: string[] = ['Example External Source']; externalEventSelectedForm: Locator; externalEventTableHeaderDuration: Locator; externalEventTableHeaderEventType: Locator; externalEventTableRow: Locator; + externalEventTypeName: string = 'ExampleEvent'; externalSourceFileName: string = 'example-external-source.json'; externalSourceFilePath: string = 'e2e-tests/data/example-external-source.json'; externalSourceFilePathMissingField: string = 'e2e-tests/data/example-external-source-missing-field.json'; externalSourceFilePathSyntaxError: string = 'e2e-tests/data/example-external-source-syntax-error.json'; + externalSourceNoAttributeFileName: string = 'example-external-source_no-attr.json'; + externalSourceNoAttributeFilePath: string = 'e2e-tests/data/example-external-source_no-attr.json'; externalSourceSelectedForm: Locator; + externalSourceTypeName: string = 'Example External Source'; externalSourceUpload: Locator; externalSourcesTable: Locator; inputFile: Locator; @@ -48,9 +71,78 @@ export class ExternalSources { await this.closeButton.click(); } + async createTypes(typeSchema: string, expectedSourceTypes: string[], expectedEventTypes: string[]) { + await this.gotoTypeManager(); + + const externalSourceTypeTable = await this.page.locator('.external-source-type-table'); + const externalEventTypeTable = await this.page.locator('.external-event-type-table'); + + await this.page.getByRole('textbox').isVisible(); + await this.page.getByRole('textbox').focus(); + await this.page.getByRole('textbox').setInputFiles(typeSchema); + await this.page.getByRole('textbox').evaluate(e => e.blur()); + + await this.page.getByText('Source & Event Type Attribute Schema Parsed').isVisible(); + + for (const expectedSourceType of expectedSourceTypes) { + await expect(this.page.locator(`li:text("${expectedSourceType}")`)).toBeVisible(); + } + for (const expectedEventType of expectedEventTypes) { + await expect(this.page.locator(`li:text("${expectedEventType}")`)).toBeVisible(); + } + + await this.page.getByLabel('Upload External Source & Event Type(s)').click(); + + for (const expectedSourceType of expectedSourceTypes) { + await expect(externalSourceTypeTable.getByRole('gridcell', { name: expectedSourceType })).toBeVisible(); + } + for (const expectedEventType of expectedEventTypes) { + await expect(externalEventTypeTable.getByRole('gridcell', { name: expectedEventType })).toBeVisible(); + } + } + + async deleteDerivationGroup(derivationGroupName: string) { + const derivationGroupTable = await this.page.locator('.derivation-group-table'); + if (await derivationGroupTable.getByRole('row', { name: derivationGroupName }).isVisible()) { + await derivationGroupTable.getByRole('row', { name: derivationGroupName }).hover(); + await derivationGroupTable + .getByRole('row', { name: derivationGroupName }) + .getByLabel('Delete Derivation Group') + .click(); + await this.page.getByRole('button', { exact: true, name: 'Delete' }).click(); + await expect(derivationGroupTable.getByRole('row', { name: derivationGroupName })).not.toBeVisible(); + } + } + + async deleteExternalEventType(eventTypeName: string) { + const externalEventTypeTable = await this.page.locator('.external-event-type-table'); + if (await externalEventTypeTable.getByRole('row', { name: eventTypeName }).isVisible()) { + await externalEventTypeTable.getByRole('row', { name: eventTypeName }).hover(); + await externalEventTypeTable + .getByRole('row', { name: eventTypeName }) + .getByLabel('Delete External Event Type') + .click(); + await this.page.getByRole('button', { exact: true, name: 'Delete' }).click(); + await expect(externalEventTypeTable.getByRole('row', { name: eventTypeName })).not.toBeVisible(); + } + } + + async deleteExternalSourceType(sourceTypeName: string) { + const externalSourceTypeTable = await this.page.locator('.external-source-type-table'); + if (await externalSourceTypeTable.getByRole('row', { name: sourceTypeName }).isVisible()) { + await externalSourceTypeTable.getByRole('row', { name: sourceTypeName }).hover(); + await externalSourceTypeTable + .getByRole('row', { name: sourceTypeName }) + .getByLabel('Delete External Source Type') + .click(); + await this.page.getByRole('button', { exact: true, name: 'Delete' }).click(); + await expect(externalSourceTypeTable.getByRole('row', { name: sourceTypeName })).not.toBeVisible(); + } + } + async deleteSource(sourceName: string) { // Only delete a source if its visible in the table - if (await this.page.getByRole('gridcell', { name: sourceName }).first().isVisible()) { + if (await this.page.getByRole('gridcell', { name: sourceName }).isVisible()) { await this.selectSource(sourceName); await this.deleteSourceButton.click(); await this.deleteSourceButtonConfirmation.click(); @@ -83,24 +175,30 @@ export class ExternalSources { await this.page.waitForTimeout(250); } + async gotoTypeManager() { + await this.page.goto('/external-sources/types', { waitUntil: 'networkidle' }); + await this.page.waitForTimeout(250); + } + async linkDerivationGroup(derivationGroupName: string, sourceTypeName: string) { // Assumes the Manage Derivation Groups modal is already showing - await this.page.getByRole('row', { name: derivationGroupName }).getByRole('checkbox').click(); + await this.page.getByRole('row', { name: derivationGroupName }).getByRole('checkbox').check(); + await expect(this.page.getByRole('row', { name: derivationGroupName }).getByRole('checkbox')).toBeChecked(); await this.page.getByRole('button', { name: 'Update' }).click(); await this.page.getByRole('button', { name: 'Close' }).click(); await expect(this.page.getByRole('button', { exact: true, name: sourceTypeName })).toBeVisible(); } async selectEvent(eventName: string, sourceName: string = 'example-external-source.json') { - // Assumes the selected source was the test source, and selects the specific event from it - // NOTE: This may not be the case, and should be re-visited when we implement deletion for External Sources! + await this.goto(); await this.selectSource(sourceName); await this.page.getByRole('gridcell', { name: eventName }).click(); } async selectSource(sourceName: string = 'example-external-source.json') { - // Always selects the first source with the example's source type in the table - await this.page.getByRole('gridcell', { name: sourceName }).first().click(); + await this.goto(); + await this.page.getByRole('gridcell', { name: sourceName }).click(); + await expect(this.page.getByText('Selected External Source')).toBeVisible(); } async unlinkDerivationGroup(derivationGroupName: string, sourceTypeName: string) { @@ -140,10 +238,21 @@ export class ExternalSources { async uploadExternalSource( inputFilePath: string = this.externalSourceFilePath, inputFileName: string = this.externalSourceFileName, + validateUpload: boolean = true, ) { + await this.goto(); await this.fillInputFile(inputFilePath); + // Wait for all errors to disappear, assuming stores are just taking time to load + await this.page.getByLabel('please create one before uploading an external source').waitFor({ state: 'hidden' }); + await this.page.getByLabel('Please create it!').waitFor({ state: 'hidden' }); await this.uploadButton.click(); - await expect(this.externalSourcesTable).toBeVisible(); - await expect(this.externalSourcesTable.getByRole('gridcell', { name: inputFileName })).toBeVisible(); + if (validateUpload) { + await expect(this.externalSourcesTable).toBeVisible(); + await expect(this.externalSourcesTable.getByRole('gridcell', { name: inputFileName })).toBeVisible(); + } + } + + async waitForToast(message: string) { + await this.page.waitForSelector(`.toastify:has-text("${message}")`, { timeout: 10000 }); } } diff --git a/e2e-tests/tests/external-sources.test.ts b/e2e-tests/tests/external-sources.test.ts index c0e07b65a9..33455e5c7c 100644 --- a/e2e-tests/tests/external-sources.test.ts +++ b/e2e-tests/tests/external-sources.test.ts @@ -13,6 +13,12 @@ test.beforeAll(async ({ browser }) => { }); test.afterAll(async () => { + await externalSources.goto(); + await externalSources.deleteSource(externalSources.externalSourceFileName); + await externalSources.gotoTypeManager(); + await externalSources.deleteDerivationGroup(externalSources.exampleDerivationGroup); + await externalSources.deleteExternalSourceType(externalSources.exampleSourceType); + await externalSources.deleteExternalEventType(externalSources.exampleEventType); await page.close(); await context.close(); }); @@ -23,17 +29,12 @@ test.beforeEach(async () => { test.describe.serial('External Sources', () => { test('Uploading an external source', async () => { + await externalSources.createTypes( + externalSources.exampleTypeSchema, + externalSources.exampleTypeSchemaExpectedSourceTypes, + externalSources.exampleTypeSchemaExpectedEventTypes, + ); await externalSources.uploadExternalSource(); - await expect( - externalSources.externalSourcesTable.getByRole('gridcell', { - name: 'ExampleExternalSource:example', - }), - ).toBeVisible(); - }); - - test('Upload button should be enabled after entering a filepath', async () => { - await externalSources.fillInputFile(externalSources.externalSourceFilePath); - await expect(externalSources.uploadButton).toBeVisible(); }); test('External event form should be shown when an event is selected', async () => { @@ -41,6 +42,18 @@ test.describe.serial('External Sources', () => { await expect(externalSources.inputFile).not.toBeVisible(); }); + test('Optional argument should be marked in external event form', async () => { + await externalSources.selectEvent('ExampleEvent:1/sc/sc1:1'); + await page.click('text="Attributes"'); + const parameter = page.locator('.parameter').filter({ hasText: 'optional' }).first(); + parameter.hover(); + const parameterInfo = parameter.getByRole('contentinfo'); + await parameterInfo.hover(); + await expect( + page.locator('.parameter-info-values').filter({ hasText: 'Required' }).filter({ hasText: 'false' }), + ).toBeVisible(); + }); + test('External source form should be shown when a source is selected', async () => { await externalSources.selectSource(); await expect(page.locator('.external-source-header-title-value')).toBeVisible(); @@ -64,15 +77,6 @@ test.describe.serial('External Sources', () => { await expect(externalSources.externalSourceSelectedForm).not.toBeVisible(); }); - // TODO: Metadata will be implemented in a future batch of work! - // test('Selected external source should show metadata in a collapsible', async () => { - // await externalSources.selectSource(); - // await externalSources.viewEventSourceMetadata.click(); - // await expect(page.getByText('0', { exact: true })).toBeVisible(); - // await expect(page.getByText('1', { exact: true }).first()).toBeVisible(); - // await expect(page.getByText('version')).toBeVisible(); - // }); - test('Selected external source should show event types in a collapsible', async () => { await externalSources.selectSource(); await externalSources.viewContainedEventTypes.click(); @@ -85,25 +89,81 @@ test.describe.serial('External Sources', () => { await expect(externalSources.externalEventTableHeaderDuration).toBeVisible(); }); - test('Deleting an external source', async () => { + test('Create Empty Source and Event Type', async () => { + await externalSources.uploadExternalSource( + externalSources.externalSourceNoAttributeFilePath, + externalSources.externalSourceNoAttributeFileName, + false, + ); + + await externalSources.gotoTypeManager(); + + const externalSourceTypeTable = await externalSources.page.locator('.external-source-type-table'); + const externalEventTypeTable = await externalSources.page.locator('.external-event-type-table'); + + const sourceType = await externalSourceTypeTable.getByRole('gridcell').filter({ hasText: 'Empty External Source' }); + await sourceType.hover(); + await page.getByRole('button', { name: 'View External Source Type' }).click(); + await expect(page.locator('text="Attribute Schema - Properties"')).toBeVisible(); + const sourceTypeAttributes = await page.locator('text="Attribute Schema - Properties"'); + await sourceTypeAttributes.click(); + await expect(page.locator('.parameter')).toHaveCount(0); + + const eventType = await externalEventTypeTable.getByRole('gridcell').filter({ hasText: 'EmptyEvent' }); + await eventType.hover(); + await page.getByRole('button', { name: 'View External Event Type' }).click(); + await expect(page.locator('text="Attribute Schema - Properties"')).toBeVisible(); + const eventTypeAttributes = await page.locator('text="Attribute Schema - Properties"'); + await eventTypeAttributes.click(); + await expect(page.locator('.parameter')).toHaveCount(0); + + await externalSources.goto(); + }); + + test('Deleting all external sources', async () => { + await expect(externalSources.externalSourcesTable).toBeVisible(); await externalSources.deleteSource(externalSources.externalSourceFileName); - await expect(page.getByText('External Source Deleted')).toBeVisible(); + await externalSources.deleteSource(externalSources.externalSourceNoAttributeFileName); + await expect(page.getByText('External Source Deleted Successfully')).toBeVisible(); await expect(externalSources.inputFile).toBeVisible(); await expect(externalSources.externalEventSelectedForm).not.toBeVisible(); await expect(externalSources.externalSourceSelectedForm).not.toBeVisible(); + await externalSources.gotoTypeManager(); + await externalSources.deleteDerivationGroup(externalSources.exampleDerivationGroup); + await externalSources.deleteDerivationGroup(externalSources.exampleEmptyDerivationGroup); + await externalSources.deleteExternalSourceType(externalSources.exampleSourceType); + await externalSources.deleteExternalSourceType(externalSources.exampleEmptySourceType); + await externalSources.deleteExternalEventType(externalSources.exampleEventType); + await externalSources.deleteExternalEventType(externalSources.exampleEmptyEventType); }); }); test.describe.serial('External Source Error Handling', () => { test('Duplicate keys is handled gracefully', async () => { - await externalSources.uploadExternalSource(); - await externalSources.deselectSourceButton.click(); - await externalSources.uploadExternalSource(); + await externalSources.createTypes( + externalSources.exampleTypeSchema, + externalSources.exampleTypeSchemaExpectedSourceTypes, + externalSources.exampleTypeSchemaExpectedEventTypes, + ); + await externalSources.uploadExternalSource( + externalSources.externalSourceFilePath, + externalSources.externalSourceFileName, + true, + ); + await expect(externalSources.externalSourcesTable).toBeVisible(); + await expect( + externalSources.externalSourcesTable.getByRole('gridcell', { name: externalSources.externalSourceFileName }), + ).toBeVisible(); + await externalSources.uploadExternalSource( + externalSources.externalSourceFilePath, + externalSources.externalSourceFileName, + false, + ); await expect(page.getByLabel('Uniqueness violation.')).toBeVisible(); - await expect(page.getByText('External Source Create Failed')).toBeVisible(); + await externalSources.waitForToast('External Source Create Failed'); await expect(page.getByRole('gridcell', { name: externalSources.externalSourceFileName })).toHaveCount(1); await externalSources.deleteSource(externalSources.externalSourceFileName); - await expect(page.getByText('External Source Deleted')).toBeVisible(); + await expect(page.getByText('External Source Deleted Successfully')).toBeVisible(); }); test("Invalid 'source' field is handled gracefully", async () => { diff --git a/e2e-tests/tests/plan-external-source.test.ts b/e2e-tests/tests/plan-external-source.test.ts index 38333dd970..1cc3b59cd7 100644 --- a/e2e-tests/tests/plan-external-source.test.ts +++ b/e2e-tests/tests/plan-external-source.test.ts @@ -39,6 +39,11 @@ test.beforeAll(async ({ baseURL, browser }) => { await plans.goto(); await plans.createPlan(); await externalSources.goto(); + await externalSources.createTypes( + externalSources.exampleTypeSchema, + externalSources.exampleTypeSchemaExpectedSourceTypes, + externalSources.exampleTypeSchemaExpectedEventTypes, + ); await externalSources.uploadExternalSource(); }); @@ -49,13 +54,23 @@ test.afterAll(async () => { await models.deleteModel(); await externalSources.goto(); - // Cleanup all test files that *may* have been uploaded await externalSources.deleteSource(externalSources.externalSourceFileName); await externalSources.deleteSource(externalSources.derivationTestFileKey1); await externalSources.deleteSource(externalSources.derivationTestFileKey2); await externalSources.deleteSource(externalSources.derivationTestFileKey3); await externalSources.deleteSource(externalSources.derivationTestFileKey4); + await externalSources.gotoTypeManager(); + await externalSources.deleteDerivationGroup(externalSources.exampleDerivationGroup); + await externalSources.deleteDerivationGroup(externalSources.derivationTestGroupName); + await externalSources.deleteExternalSourceType(externalSources.exampleSourceType); + await externalSources.deleteExternalSourceType(externalSources.derivationTestSourceTypeName); + await externalSources.deleteExternalEventType(externalSources.exampleEventType); + await externalSources.deleteExternalEventType(externalSources.derivationATypeName); + await externalSources.deleteExternalEventType(externalSources.derivationBTypeName); + await externalSources.deleteExternalEventType(externalSources.derivationCTypeName); + await externalSources.deleteExternalEventType(externalSources.derivationDTypeName); + await page.close(); await context.close(); }); @@ -141,52 +156,6 @@ test.describe.serial('Plan External Sources', () => { expect(doPixelsExist).toBeTruthy(); }); - test('Cards should be shown when a new external source is uploaded', async () => { - // Upload a test file and link its derivation group to the plan - await externalSources.goto(); - await externalSources.uploadExternalSource( - externalSources.derivationTestFile1, - externalSources.derivationTestFileKey1, - ); - await plan.goto(); - await plan.showPanel(PanelNames.EXTERNAL_SOURCES); - await plan.externalSourceManageButton.click(); - await externalSources.linkDerivationGroup( - externalSources.derivationTestGroupName, - externalSources.derivationTestSourceType, - ); - - // Upload another test - await externalSources.goto(); - await externalSources.uploadExternalSource( - externalSources.derivationTestFile2, - externalSources.derivationTestFileKey2, - ); - - await plan.goto(); - await plan.showPanel(PanelNames.EXTERNAL_SOURCES); - - // Allow stores to load, validate 'new source' card appears - await expect( - page.getByText('New files matching source types and derivation groups in the current plan'), - ).toBeVisible(); - - await page.getByRole('button', { name: 'Dismiss' }).click(); - - await expect( - page.getByText('New files matching source types and derivation groups in the current plan'), - ).not.toBeVisible(); - - await plan.externalSourceManageButton.click(); - await expect( - page.getByRole('row', { name: externalSources.derivationTestGroupName }).getByRole('checkbox'), - ).toBeChecked(); - await externalSources.unlinkDerivationGroup( - externalSources.derivationTestGroupName, - externalSources.derivationTestSourceType, - ); - }); - test('Linked derivation groups should be expandable in panel', async () => { await plan.showPanel(PanelNames.EXTERNAL_SOURCES); // Link derivation group to plan if it isn't already diff --git a/src/components/external-events/ExternalEventForm.svelte b/src/components/external-events/ExternalEventForm.svelte index e8eca5c11c..ac7f334896 100644 --- a/src/components/external-events/ExternalEventForm.svelte +++ b/src/components/external-events/ExternalEventForm.svelte @@ -1,19 +1,56 @@ @@ -65,6 +102,19 @@ /> + +
+ +
+
diff --git a/src/components/external-source/ExternalSourceManager.svelte b/src/components/external-source/ExternalSourceManager.svelte index a805e334f2..f6a49c6e58 100644 --- a/src/components/external-source/ExternalSourceManager.svelte +++ b/src/components/external-source/ExternalSourceManager.svelte @@ -7,12 +7,14 @@ import ExternalEventIcon from '../../assets/external-event-box-with-arrow.svg?component'; import ExternalSourceIcon from '../../assets/external-source-box.svg?component'; import { catchError } from '../../stores/errors'; + import { externalEventTypes } from '../../stores/external-event'; import { createDerivationGroupError, createExternalSourceError, createExternalSourceTypeError, creatingExternalSource, externalSources, + externalSourceTypes, parsingError, planDerivationGroupLinks, } from '../../stores/external-source'; @@ -21,12 +23,15 @@ import { plugins } from '../../stores/plugins'; import type { User } from '../../types/app'; import type { DataGridColumnDef } from '../../types/data-grid'; - import type { ExternalEvent, ExternalEventId } from '../../types/external-event'; + import type { ExternalEvent, ExternalEventId, ExternalEventType } from '../../types/external-event'; import { type ExternalSourceJson, type ExternalSourceSlim, + type ExternalSourceType, type PlanDerivationGroup, } from '../../types/external-source'; + import type { ArgumentsMap, ParametersMap } from '../../types/parameter'; + import type { ValueSchema } from '../../types/schema'; import effects from '../../utilities/effects'; import { getExternalEventRowId, @@ -34,6 +39,11 @@ getExternalSourceSlimRowId, } from '../../utilities/externalEvents'; import { parseJSONStream } from '../../utilities/generic'; + import { + getFormParameters, + translateJsonSchemaArgumentsToValueSchema, + translateJsonSchemaToValueSchema, + } from '../../utilities/parameters'; import { permissionHandler } from '../../utilities/permissionHandler'; import { featurePermissions } from '../../utilities/permissions'; import { formatDate } from '../../utilities/time'; @@ -46,6 +56,7 @@ import DatePickerField from '../form/DatePickerField.svelte'; import Field from '../form/Field.svelte'; import Input from '../form/Input.svelte'; + import Parameters from '../parameters/Parameters.svelte'; import AlertError from '../ui/AlertError.svelte'; import CssGrid from '../ui/CssGrid.svelte'; import CssGridGutter from '../ui/CssGridGutter.svelte'; @@ -55,6 +66,7 @@ import DatePicker from '../ui/DatePicker/DatePicker.svelte'; import Panel from '../ui/Panel.svelte'; import SectionTitle from '../ui/SectionTitle.svelte'; + import ExternalSourceTypeNewCard from './ExternalSourceTypeNewCard.svelte'; export let user: User | null; @@ -131,8 +143,11 @@ // source detail variables let selectedSource: ExternalSourceSlim | null = null; + let selectedSourceAttributes: ArgumentsMap = {}; + let selectedSourceType: ExternalSourceType | undefined = undefined; + let selectedSourceTypeParametersMap: ParametersMap = {}; let selectedSourceId: string | null = null; - let selectedSourceEventTypes: string[] = []; + let selectedSourceEventTypes: ExternalEventType[] = []; // Selected element variables let selectedEvent: ExternalEvent | null = null; @@ -142,7 +157,12 @@ // We want to parse a file selected for upload. let files: FileList | undefined; let file: File | undefined; + let externalSourceFileInput: HTMLInputElement; let parsedExternalSource: ExternalSourceJson | undefined; + let isUploadDisabled: boolean = true; + let uploadDisabledMessage: string | null = null; + let newExternalSourceType: string | null = null; + let newExternalEventTypes: string[] | null = null; // For filtering purposes (modelled after TimelineEditorLayerFilter): let filterExpression: string = ''; @@ -172,6 +192,26 @@ ]); } + $: if (selectedSource !== null) { + // Create an ArgumentsMap for the External Source + selectedSourceAttributes = translateJsonSchemaArgumentsToValueSchema(selectedSource.attributes); + // Create a ParametersMap for the External Source Type + selectedSourceType = $externalSourceTypes.find(sourceType => sourceType.name === selectedSource?.source_type_name); + const selectedSourceTypeAttributesTranslated = translateJsonSchemaToValueSchema( + selectedSourceType?.attribute_schema, + ); + selectedSourceTypeParametersMap = Object.entries(selectedSourceTypeAttributesTranslated).reduce( + (acc: ParametersMap, currentAttribute: [string, ValueSchema], index: number) => { + acc[currentAttribute[0]] = { + order: index, + schema: currentAttribute[1], + }; + return acc; + }, + {} as ParametersMap, + ); + } + $: selectedSourceId = selectedSource ? getExternalSourceRowId({ derivation_group_name: selectedSource.derivation_group_name, key: selectedSource.key }) : null; @@ -302,6 +342,18 @@ return planDerivationGroupLink.derivation_group_name === selectedSource?.derivation_group_name; }); + $: if (parsedExternalSource !== undefined) { + if (doesSourceTypeAndEventTypesExist(parsedExternalSource)) { + isUploadDisabled = false; + uploadDisabledMessage = null; + } else { + isUploadDisabled = true; + } + } else { + isUploadDisabled = true; + uploadDisabledMessage = null; + } + // Permissions $: hasCreatePermission = featurePermissions.externalSource.canCreate(user); @@ -313,6 +365,7 @@ user, ); if (deleteExternalSourceResult !== undefined && deleteExternalSourceResult !== null) { + deselectSource(); selectedSources = null; selectedSource = null; } @@ -321,34 +374,73 @@ async function onFormSubmit(_e: SubmitEvent) { if (parsedExternalSource && file) { - const createExternalSourceResponse: ExternalSourceSlim | undefined = await effects.createExternalSource( - $sourceTypeField.value, - $derivationGroupField.value, - $startTimeDoyField.value, - $endTimeDoyField.value, - parsedExternalSource.events, - parsedExternalSource.source.key, - $validAtDoyField.value, - user, - ); - // Following a successful mutation... - if (createExternalSourceResponse !== undefined) { - // Auto-select the new source - selectedSource = { - ...createExternalSourceResponse, - created_at: new Date().toISOString().replace('Z', '+00:00'), // technically not the exact time it shows up in the database + // Create non-existing types first: + // TODO: cleanup + let newEventTypes: { [x: string]: object } = {}; + let newSourceType: { [x: string]: object } = {}; + + if (newExternalSourceType !== null) { + newSourceType = { + [newExternalSourceType]: { + properties: {}, + required: [], + type: 'object', + }, }; - gridRowSizes = gridRowSizesBottomPanel; } + if (newExternalEventTypes !== null) { + for (let type of newExternalEventTypes) { + newEventTypes[type] = { + properties: {}, + required: [], + type: 'object', + }; + } + } + + const createdTypes = + newExternalSourceType !== null || newExternalEventTypes !== null + ? await effects.createExternalSourceEventTypes(newEventTypes, newSourceType, user) + : true; + + if (createdTypes) { + const requestResponse: + | { createExternalSource: ExternalSourceSlim; upsertDerivationGroup: { name: string } | null } + | undefined = await effects.createExternalSource( + $sourceTypeField.value, + $derivationGroupField.value, + $startTimeDoyField.value, + $endTimeDoyField.value, + parsedExternalSource.events, + parsedExternalSource.source.key, + parsedExternalSource.source.attributes, + $validAtDoyField.value, + user, + ); + // Following a successful mutation... + if (requestResponse !== undefined) { + const { createExternalSource: createExternalSourceResponse } = requestResponse; + // Auto-select the new source + selectedSource = { + ...createExternalSourceResponse, + created_at: new Date().toISOString().replace('Z', '+00:00'), // technically not the exact time it shows up in the database + }; + gridRowSizes = gridRowSizesBottomPanel; + } + } + + // Reset the form behind the source + parsedExternalSource = undefined; + file = undefined; + files = undefined; + externalSourceFileInput.value = ''; + keyField.reset(''); + sourceTypeField.reset(''); + startTimeDoyField.reset(''); + endTimeDoyField.reset(''); + validAtDoyField.reset(''); + derivationGroupField.reset(''); } - // Reset the form behind the source - parsedExternalSource = undefined; - keyField.reset(''); - sourceTypeField.reset(''); - startTimeDoyField.reset(''); - endTimeDoyField.reset(''); - validAtDoyField.reset(''); - derivationGroupField.reset(''); } async function parseExternalSourceFileStream(stream: ReadableStream) { @@ -408,14 +500,6 @@ } } - function onManageGroupsAndTypes() { - effects.manageGroupsAndTypes(user); - } - - function onCreateGroupsOrTypes() { - effects.createGroupsOrTypes(user); - } - function hasDeleteExternalSourcePermissionOnRow(user: User | null, externalSource: ExternalSourceSlim | undefined) { if (externalSource === undefined) { return false; @@ -423,6 +507,50 @@ return featurePermissions.externalSource.canDelete(user, [externalSource]); } } + + function doesSourceTypeAndEventTypesExist(externalSource: ExternalSourceJson) { + // Check that the External Source Type for the source to-be-uploaded exists + const externalSourceType = $externalSourceTypes.find( + sourceType => sourceType.name === externalSource.source.source_type, + ); + if (externalSourceType === undefined && Object.keys(externalSource.source.attributes).length === 0) { + newExternalSourceType = externalSource.source.source_type; + } + + // Check that all the External Event Types for the source to-be-uploaded exist + const newSourceExternalEventTypes: { [event_type: string]: string[] } = {}; + for (const event of externalSource.events) { + const currentKeySet = new Set(Object.keys(event.attributes)); + if (newSourceExternalEventTypes[event.event_type] === undefined) { + newSourceExternalEventTypes[event.event_type] = Object.keys(event.attributes); + } else if (newSourceExternalEventTypes[event.event_type].length === Object.keys(event.attributes).length) { + const attributeInconsistencies = currentKeySet.difference( + new Set(newSourceExternalEventTypes[event.event_type]), + ); + if (attributeInconsistencies.size !== 0) { + uploadDisabledMessage = 'Event attributes are inconsistent across events of type ' + event.event_type + '.'; + return false; + } + } else { + uploadDisabledMessage = 'Event attributes are inconsistent across events of type ' + event.event_type + '.'; + return false; + } + } + + let eventTypes: string[] = []; + for (const entry of Object.entries(newSourceExternalEventTypes)) { + if ( + $externalEventTypes.find(eventTypeFromDB => eventTypeFromDB.name === entry[0]) === undefined && + entry[1].length === 0 // only create new types if they don't have attributes. otherwise, a schema is needed as we do not infer one + ) { + eventTypes.push(entry[0]); + } + } + if (eventTypes.length > 0) { + newExternalEventTypes = eventTypes; + } + return true; + } @@ -528,7 +656,7 @@ {#if selectedSourceEventTypes.length > 0} {#each selectedSourceEventTypes as eventType}
- {eventType} + {eventType.name}
{/each} {:else} @@ -555,7 +683,15 @@
Not used in any plans
{/if} - + +
+ +
+
{/if} - - {#if $externalSources.length} @@ -790,6 +908,27 @@
diff --git a/src/components/external-source/ExternalTypeManager.svelte b/src/components/external-source/ExternalTypeManager.svelte new file mode 100644 index 0000000000..6aa48bf4a0 --- /dev/null +++ b/src/components/external-source/ExternalTypeManager.svelte @@ -0,0 +1,993 @@ + + + + + + {#if selectedDerivationGroup === undefined && selectedExternalSourceType === undefined && selectedExternalEventType === undefined} + + + Upload Type Definition + + +
+
+ + +
+ {#if file !== undefined} + + {#if parsedExternalSourceEventTypeSchema !== undefined} +
+
+ +
+ Source & Event Type Attribute Schema Parsed +
+ {:else} + +
Source & Event Type Attribute Schema Could Not Be Parsed
+ {/if} + {/if} + {#if parsedExternalSourceEventTypeSchema !== undefined} +
+
The following External Source Type(s) will be created
+ {#if parsedExternalSourceEventTypeSchema.source_types} +
    + {#each Object.keys(parsedExternalSourceEventTypeSchema.source_types) as newSourceTypeName} +
  • {newSourceTypeName}
  • + {/each} +
+ {/if} +
The following External Event Type(s) will be created
+ {#if parsedExternalSourceEventTypeSchema.event_types} +
    + {#each Object.keys(parsedExternalSourceEventTypeSchema.event_types) as newEventTypeName} +
  • {newEventTypeName}
  • + {/each} +
+ {/if} +
+ {/if} +
+ {#each uploadResponseErrors as currentError} + + {/each} + +
+
+
+
+ + {:else if selectedDerivationGroup !== undefined} + + + + Sources in '{selectedDerivationGroup.name}' + + + + + {#if selectedDerivationGroupSources.length > 0} + {#each selectedDerivationGroupSources as source} + + + +

+ {selectedDerivationGroup.sources.get(source.key)?.event_counts} events +

+
+
+
Key:
+ {source.key} +
+ +
+
Source Type:
+ {source.source_type_name} +
+ +
+
Start Time:
+ {source.start_time} +
+ +
+
End Time:
+ {source.end_time} +
+ +
+
Valid At:
+ {source.valid_at} +
+ +
+
Created At:
+ {source.created_at} +
+
+ {/each} + {:else} +

No sources in this group.

+ {/if} +
+
+ + {:else if selectedExternalSourceType !== undefined} + + + + '{selectedExternalSourceType.name}' Details + + + + + {#if selectedExternalSourceTypeDerivationGroups.length > 0} + {#each selectedExternalSourceTypeDerivationGroups as associatedDerivationGroup} + + + +

+ {associatedDerivationGroup.derived_event_total} events +

+
+
+
Name:
+ {associatedDerivationGroup.name} +
+ + + {#each associatedDerivationGroup.sources as source} + {source[0]} + {/each} + +
+ {/each} + {:else} +

No sources associated with this External Source Type.

+ {/if} + + {#each Object.entries(selectedExternalSourceType.attribute_schema) as attribute} + {#if attribute[0] !== 'properties'} +
+
{attribute[0]}
+ {#if Array.isArray(attribute[1])} +
    + {#each attribute[1] as attributeValue} +
  • {attributeValue}
  • + {/each} +
+ {:else} +
{attribute[1]}
+ {/if} +
+ {/if} + {/each} +
+ +
+ +
+
+
+
+ + {:else if selectedExternalEventType !== undefined} + + + + '{selectedExternalEventType.name}' Details + + + + + + {#if selectedExternalEventTypeSources.length > 0} + {#each selectedExternalEventTypeSources as associatedSource} +
  • {associatedSource}
  • + {/each} + {:else} + {`No External Sources using ${selectedExternalEventType.name}`} + {/if} +
    + + {#each Object.entries(selectedExternalEventType.attribute_schema) as attribute} + {#if attribute[0] !== 'properties'} +
    +
    {attribute[0]}
    + {#if Array.isArray(attribute[1])} +
      + {#each attribute[1] as attributeValue} +
    • {attributeValue}
    • + {/each} +
    + {:else} +
    {attribute[1]}
    + {/if} +
    + {/if} + {/each} +
    + +
    + +
    +
    +
    +
    + + {/if} +
    + + + Derivation Groups + + + + + +
    + +
    +
    +
    + + + External Source Types + + + + + +
    + +
    +
    +
    + + + External Event Types + + + + + +
    + +
    +
    +
    +
    +
    + + diff --git a/src/components/modals/CreateGroupsOrTypesModal.svelte b/src/components/modals/CreateGroupsOrTypesModal.svelte deleted file mode 100644 index eebb69ba40..0000000000 --- a/src/components/modals/CreateGroupsOrTypesModal.svelte +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - Create Derivation Groups or Types - -
    -
    - - - Derivation Group - External Source Type - External Event Type - - -
    -

    Provide a name and an external source type for the new derivation group.

    -

    - The newly created group will be empty, though you can upload sources into it. -

    -
    -
    - - - -
    -
    - -
    -

    Provide a name for the new external source type.

    -

    - The newly created external source type will be empty, though you can upload sources into it. -

    -
    -
    - - -
    -
    - -
    -

    Provide a name for the new external event type.

    -

    - The newly created external event type will be empty, though you can upload events into it. -

    -
    -
    - - -
    -
    -
    -
    -
    - - - - -
    -
    -
    - - - -
    - - diff --git a/src/components/modals/ManageGroupsAndTypesModal.svelte b/src/components/modals/ManageGroupsAndTypesModal.svelte index 0179f10027..16e9ad4b44 100644 --- a/src/components/modals/ManageGroupsAndTypesModal.svelte +++ b/src/components/modals/ManageGroupsAndTypesModal.svelte @@ -1,20 +1,42 @@ Manage Derivation Groups and Types + {#if selectedDerivationGroup === undefined && selectedExternalSourceType === undefined && selectedExternalEventType === undefined} + + + Upload Type Definition + + +
    +
    + + +
    + {#if file !== undefined} + + {#if parsedExternalSourceEventTypeSchema !== undefined} +
    +
    + +
    + Source & Event Type Attribute Schema Parsed +
    + {:else} + +
    + Source & Event Type Attribute Schema Could Not Be Parsed +
    + {/if} + {/if} + {#if parsedExternalSourceEventTypeSchema !== undefined} +
    +
    The following External Source Type(s) will be created
    + {#each Object.keys(parsedExternalSourceEventTypeSchema.source_types ?? {}) as newSourceTypeName} +
  • {newSourceTypeName}
  • + {/each} +
    The following External Event Type(s) will be created
    + {#each Object.keys(parsedExternalSourceEventTypeSchema.event_types ?? {}) as newEventTypeName} +
  • {newEventTypeName}
  • + {/each} +
    + {/if} +
    + {#each uploadResponseErrors as currentError} + + {/each} + +
    +
    +
    +
    + + {/if}
    - + - Derivation Group - External Source Type - External Event Type + Derivation Group + External Source Type + External Event Type @@ -375,12 +626,21 @@
    {#if selectedDerivationGroup !== undefined} - - + + Sources in '{selectedDerivationGroup.name}' + {#if selectedDerivationGroupSources.length > 0} @@ -429,12 +689,21 @@ {:else if selectedExternalSourceType !== undefined} - - + + - Derivation Groups of Type '{selectedExternalSourceType.name}' + '{selectedExternalSourceType.name}' Details + {#if selectedExternalSourceTypeDerivationGroups.length > 0} @@ -465,6 +734,80 @@ {:else}

    No sources associated with this External Source Type.

    {/if} + + {#each Object.entries(selectedExternalSourceType.attribute_schema) as attribute} + {#if attribute[0] !== 'properties'} +
    +
    {attribute[0]}
    +
    {attribute[1]}
    +
    + {/if} + {/each} +
    + +
    + +
    +
    +
    +
    + {:else if selectedExternalEventType !== undefined} + + + + + '{selectedExternalEventType.name}' Details + + + + + + {#each Object.entries(selectedExternalEventType.attribute_schema) as attribute} + {#if attribute[0] !== 'properties'} +
    +
    {attribute[0]}
    +
    {attribute[1]}
    +
    + {/if} + {/each} +
    + +
    + +
    +
    {/if} @@ -485,6 +828,27 @@
    diff --git a/src/components/parameters/ParameterInfo.svelte b/src/components/parameters/ParameterInfo.svelte index 55baf914eb..208829d9c0 100644 --- a/src/components/parameters/ParameterInfo.svelte +++ b/src/components/parameters/ParameterInfo.svelte @@ -20,10 +20,14 @@ let leaveTimeout: NodeJS.Timeout | null = null; let source: ValueSource; let unit: string | undefined = undefined; + let required: boolean = true; + let externalEvent: boolean = false; $: if (formParameter) { source = formParameter.valueSource; unit = formParameter.schema?.metadata?.unit?.value; + externalEvent = formParameter.externalEvent ?? false; + required = formParameter.required ?? true; } function leaveCallback() { @@ -75,7 +79,7 @@ } -{#if unit || source !== 'none'} +{#if externalEvent || unit || source !== 'none'} {/if} + {#if externalEvent} +
    Required
    +
    {required}
    + {/if} diff --git a/src/components/parameters/ParameterRecStruct.svelte b/src/components/parameters/ParameterRecStruct.svelte index c735b97f75..57749abbd0 100644 --- a/src/components/parameters/ParameterRecStruct.svelte +++ b/src/components/parameters/ParameterRecStruct.svelte @@ -37,7 +37,7 @@ const structKeys = Object.keys(keys).sort(); const subFormParameters = structKeys.map((key, index) => { - const subFormParameter: FormParameter = { + let subFormParameter: FormParameter = { errors: null, key, name: key, @@ -46,7 +46,6 @@ value: value !== null ? value[key] : null, valueSource: formParameter.valueSource, }; - return subFormParameter; }); @@ -73,7 +72,7 @@
    - +