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
7 changes: 7 additions & 0 deletions .github/workflows/test-web-console-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ jobs:
- name: Verify pipeline-manager is reachable
run: curl -fsSL --retry 5 --retry-delay 2 --retry-connrefused http://localhost:8080/healthz

- name: Run vitest integration tests
if: ${{ vars.CI_DRY_RUN != 'true' }}
run: bun run test-integration
working-directory: js-packages/web-console
env:
FELDERA_API_URL: http://localhost:8080

- name: Run Playwright e2e tests
if: ${{ vars.CI_DRY_RUN != 'true' }}
run: bun run test-e2e
Expand Down
3 changes: 2 additions & 1 deletion js-packages/web-console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@
"test-prepare": "git clone --depth 1 https://github.com/feldera/playwright-snapshots.git || true && mkdir -p playwright-snapshots/e2e playwright-snapshots/component",
"test-update-snapshots": "bun run test -- --update && bun playwright test --update-snapshots",
"test-unit": "vitest",
"test": "bun run test-unit -- --run"
"test-integration": "vitest --run --project integration --project integration-client",
"test": "bun run test-unit -- --run --project client --project server"
},
"trustedDependencies": ["@axa-fr/oidc-client", "sk-oidc-oauth", "svelte-preprocess"],
"type": "module"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import InlineDropdown from '$lib/components/common/InlineDropdown.svelte'
import { Tooltip } from '$lib/components/common/Tooltip.svelte'
import InlineDrawer from '$lib/components/layout/InlineDrawer.svelte'
import { getCaseDependentName } from '$lib/functions/felderaRelation'
import { formatDateTime } from '$lib/functions/format'
import {
type ConnectorError,
Expand Down Expand Up @@ -61,6 +62,10 @@
tagsFilter = filter
})

const strippedConnectorName = $derived(
connectorName.slice(getCaseDependentName(relationName).name.length + 1)
)

$effect(() => {
pipelineName
relationName
Expand All @@ -69,11 +74,10 @@
loading = true
status = null

const stripped = connectorName.slice(connectorName.indexOf('.') + 1)
const request =
direction === 'input'
? getInputConnectorStatus(pipelineName, relationName, stripped)
: getOutputConnectorStatus(pipelineName, relationName, stripped)
? getInputConnectorStatus(pipelineName, relationName, strippedConnectorName)
: getOutputConnectorStatus(pipelineName, relationName, strippedConnectorName)

request.then((s) => {
status = s
Expand Down Expand Up @@ -137,7 +141,7 @@
<div class="bg-white-dark flex h-full flex-col gap-2 rounded p-4">
<div class="flex items-start justify-between">
<div>
<div class="font-medium">{connectorName.replace('.', ' · ')}</div>
<div class="font-medium">{relationName} · {strippedConnectorName}</div>
</div>
<button class="fd fd-x text-[20px]" onclick={() => (open = false)} aria-label="Close"
></button>
Expand Down
5 changes: 1 addition & 4 deletions js-packages/web-console/src/lib/functions/felderaRelation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ export const normalizeCaseIndependentName = ({
export const getCaseDependentName = (caseIndependentName: string) => {
const case_sensitive = isCaseSensitive(caseIndependentName)
return {
name: normalizeCaseIndependentName({
name: caseIndependentName.replaceAll('"', ''),
case_sensitive
}),
name: caseIndependentName.replaceAll('"', ''),
case_sensitive
}
}
Expand Down
85 changes: 85 additions & 0 deletions js-packages/web-console/src/lib/services/pipelineManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Integration tests for pipelineManager.ts that require a running Feldera instance.
* Run via the 'integration' vitest project:
*
* bun run test-integration
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import {
getInputConnectorStatus,
getOutputConnectorStatus,
putPipeline
} from '$lib/services/pipelineManager'
import {
cleanupPipeline,
configureTestClient,
startPipelineAndWaitForRunning,
waitForCompilation
} from '$lib/services/testPipelineHelpers'

// Pipeline name is URL-safe by convention, but table/view/connector names are not.
const PIPELINE_NAME = 'test-special-chars-connector-status'

// Names with dots and URL-unsafe characters
const TABLE_NAME = 'my.input.table'
const VIEW_NAME = 'my.output.view'
const INPUT_CONNECTOR_NAME = 'http-input_special-name'
const OUTPUT_CONNECTOR_NAME = 'http-output_special-name'

describe('pipelineManager connector status with special characters', () => {
beforeAll(async () => {
configureTestClient()

// Clean up any leftover pipeline from a previous run
await cleanupPipeline(PIPELINE_NAME)

// SQL program with quoted identifiers containing dots
const programCode = `
CREATE TABLE "${TABLE_NAME}" (id INT NOT NULL, val VARCHAR)
WITH (
'connectors' = '[{
"name": "${INPUT_CONNECTOR_NAME}",
"transport": { "name": "url_input", "config": { "path": "https://feldera.com/test-data.json" } },
"format": { "name": "json", "config": { "update_format": "raw" } }
}]'
);
CREATE VIEW "${VIEW_NAME}"
WITH (
'connectors' = '[{
"name": "${OUTPUT_CONNECTOR_NAME}",
"transport": { "name": "file_output", "config": { "path": "/tmp/feldera-test-output.json" } },
"format": { "name": "json" }
}]'
)
AS SELECT * FROM "${TABLE_NAME}";
`

// Create the pipeline
await putPipeline(PIPELINE_NAME, {
name: PIPELINE_NAME,
description: 'Integration test for special character handling',
program_code: programCode,
runtime_config: {}
})

await waitForCompilation(PIPELINE_NAME, 120_000)
await startPipelineAndWaitForRunning(PIPELINE_NAME, 60_000)
}, 180_000)

afterAll(async () => {
await cleanupPipeline(PIPELINE_NAME)
}, 60_000)

it('getInputConnectorStatus succeeds with dot and URL-unsafe characters in table and connector name', async () => {
const body = await getInputConnectorStatus(PIPELINE_NAME, TABLE_NAME, INPUT_CONNECTOR_NAME)
console.log('body', JSON.stringify(body))
expect(body).toHaveProperty(['metrics', 'num_parse_errors'])
expect(body).toHaveProperty(['metrics', 'num_transport_errors'])
})

it('getOutputConnectorStatus succeeds with dot and URL-unsafe characters in view and connector name', async () => {
const body = await getOutputConnectorStatus(PIPELINE_NAME, VIEW_NAME, OUTPUT_CONNECTOR_NAME)
expect(body).toHaveProperty(['metrics', 'num_encode_errors'])
expect(body).toHaveProperty(['metrics', 'num_transport_errors'])
})
})
14 changes: 7 additions & 7 deletions js-packages/web-console/src/lib/services/pipelineManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ export const getExtendedPipeline = async (
) => {
return mapResponse(
_getPipeline({
path: { pipeline_name: encodeURIComponent(pipeline_name) },
path: { pipeline_name },
...options
}),
toExtendedPipeline,
Expand Down Expand Up @@ -398,7 +398,7 @@ export const putPipeline = async (
await mapResponse(
_putPipeline({
body: newPipeline,
path: { pipeline_name: encodeURIComponent(pipeline_name) },
path: { pipeline_name },
...options
}),
(v) => v
Expand All @@ -412,7 +412,7 @@ export const patchPipeline = async (
) => {
return mapResponse(
_patchPipeline({
path: { pipeline_name: encodeURIComponent(pipeline_name) },
path: { pipeline_name },
body: fromPipeline(pipeline),
...options
}),
Expand All @@ -433,7 +433,7 @@ export const getPipelines = async (options?: FetchOptions): Promise<PipelineThum
export const getPipelineStatus = async (pipeline_name: string, options?: FetchOptions) => {
return mapResponse(
_getPipeline({
path: { pipeline_name: encodeURIComponent(pipeline_name) },
path: { pipeline_name },
query: { selector: 'status' },
...options
}),
Expand All @@ -450,7 +450,7 @@ export const getPipelineStatus = async (pipeline_name: string, options?: FetchOp
export const getPipelineStats = async (pipeline_name: string, options?: FetchOptions) => {
return mapResponse(
_getPipelineStats({
path: { pipeline_name: encodeURIComponent(pipeline_name) },
path: { pipeline_name },
...options
}),
(status) => ({
Expand Down Expand Up @@ -578,7 +578,7 @@ export const getClusterEvent = (eventId: string) =>
const getSamplyProfileStream = (pipelineName: string, latest: boolean) => {
const result = streamingFetch(
getAuthenticatedFetch(),
`${felderaEndpoint}/v0/pipelines/${encodeURIComponent(pipelineName)}/samply_profile${latest ? '?latest=true' : ''}`,
`${felderaEndpoint}/v0/pipelines/${pipelineName}/samply_profile${latest ? '?latest=true' : ''}`,
{
method: 'GET'
}
Expand Down Expand Up @@ -967,5 +967,5 @@ export const getPipelineSupportBundleUrl = (
for (const [key, value] of Object.entries(options)) {
query.append(key, String(value))
}
return `${felderaEndpoint}/v0/pipelines/${encodeURIComponent(pipelineName)}/support_bundle?${query.toString()}`
return `${felderaEndpoint}/v0/pipelines/${pipelineName}/support_bundle?${query.toString()}`
}
Loading
Loading