Repository navigation
Phase 3: native Radius frontend and native standalone host #416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
ff027ce
feat: add native Radius frontend and standalone host
nicolejms 811d2f0
test: complete native host catalog fixture and cleanup
nicolejms 70abbb9
test: qualify native standalone image rollback
nicolejms 4d94a3a
test: bind rollback qualification to image source tuple
nicolejms 37c2469
docs: record verified FS-16 image tuple and evidence limits
nicolejms 4f9d892
ci: run FS-16 image rollback check on the CI-built candidate
nicolejms a7aed3d
Merge origin/main into Phase 3 branch
nicolejms 1f1629c
test: restore the previous image with its own journeys
nicolejms 62f6f62
build: match public npm bin metadata in yarn.lock
nicolejms 2fdf7ee
test: pull previous image by digest and allow cold page load
nicolejms 8c41fb1
fix: resolve Phase 3 review dependency and cleanup findings
nicolejms d30482c
test: require cleanup regressions in the CI test chain
nicolejms 652c3a8
Merge main into Phase 3 migration
nicolejms ddbc30c
fix: address native frontend review follow-ups
nicolejms 2f871a2
fix(app): annotate frontend features type
Copilot bf80651
fix: align native Backstage package versions
nicolejms e2fcb36
test(app): assert guest sign-in failure state
nicolejms e9f4029
Merge branch 'main' into nicolejms-phase-3-backstage-migration
nicolejms 6e900ad
fix: address native frontend review feedback
nicolejms b2b9fd1
fix: address CI failures
nicolejms cff8992
fix: register catalog API with its module
nicolejms d26f670
fix: stabilize rollback qualification coverage
nicolejms c3a92a1
fix: type catalog route binding
nicolejms b6545cc
test: isolate packed host from auth backend
nicolejms 80c46d5
Merge branch 'main' into nicolejms-phase-3-backstage-migration
nicolejms 0fbbfa9
fix: address new rollback review feedback
nicolejms 4f3512f
fix: type shared test cluster config
nicolejms cb12f23
test: isolate rollback upstream in a dedicated container
nicolejms File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| import { test, expect } from '@playwright/test'; | ||
|
|
||
| test.describe('standalone frontend contracts', () => { | ||
| test('FS-09: guest sign-in failure shows an error without a legacy-token dialog', async ({ | ||
| page, | ||
| }) => { | ||
| const dialogs: string[] = []; | ||
| let guestSignInStatus: number | undefined; | ||
| page.on('dialog', async dialog => { | ||
| dialogs.push(dialog.message()); | ||
| await dialog.dismiss(); | ||
| }); | ||
| page.on('response', response => { | ||
| if (response.url().includes('/api/auth/guest/refresh')) { | ||
| guestSignInStatus = response.status(); | ||
| } | ||
| }); | ||
| await page.route('**/api/auth/guest/refresh*', route => | ||
| route.fulfill({ | ||
| status: 500, | ||
| json: { | ||
| error: { | ||
| name: 'Error', | ||
| message: 'Guest sign-in test failure', | ||
| }, | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| await page.goto('/'); | ||
| const errorPanel = page.getByRole('alert'); | ||
| await expect(errorPanel).toBeVisible({ timeout: 20_000 }); | ||
| await expect( | ||
| errorPanel.getByRole('button', { | ||
| name: /You do not appear to be signed in/, | ||
| }), | ||
| ).toBeVisible(); | ||
| expect(guestSignInStatus).toBe(500); | ||
| await expect(page.getByText('Learn more')).toHaveCount(0); | ||
| expect(dialogs).toEqual([]); | ||
| }); | ||
|
|
||
| test('FS-09/11/12: auto guest, cards, sidebar, settings, themes and native catalog pages', async ({ | ||
| page, | ||
| }) => { | ||
| const errors: string[] = []; | ||
| const dialogs: string[] = []; | ||
| page.on('pageerror', error => errors.push(error.message)); | ||
| page.on('dialog', async dialog => { | ||
| dialogs.push(dialog.message()); | ||
| await dialog.dismiss(); | ||
| }); | ||
| const signIns: number[] = []; | ||
| page.on('response', response => { | ||
| if (response.url().includes('/api/auth/guest/refresh')) | ||
| signIns.push(response.status()); | ||
| }); | ||
| await page.goto('/'); | ||
| await expect(page.getByText('Learn more')).toBeVisible({ timeout: 20_000 }); | ||
| if (process.env.STANDALONE_QUALIFICATION) expect(signIns).toContain(200); | ||
| await expect(page.getByText('Join the community')).toBeVisible(); | ||
| await expect(page.getByText('Get help with Radius')).toBeVisible(); | ||
| await expect( | ||
| page.getByRole('main').getByText('Applications', { exact: true }), | ||
| ).toBeVisible(); | ||
| await expect( | ||
| page.getByRole('main').getByText('Environments', { exact: true }), | ||
| ).toBeVisible(); | ||
| await expect( | ||
| page.getByRole('button', { name: /^Get Started/ }), | ||
| ).toHaveAttribute('href', 'https://docs.radapp.io/getting-started/'); | ||
| const nav = page.getByRole('navigation', { name: 'sidebar nav' }); | ||
| for (const [name, path] of [ | ||
| ['Home', '/'], | ||
| ['Resource Types', '/resource-types'], | ||
| ['Applications', '/applications'], | ||
| ['Environments', '/environments'], | ||
| ['Resources', '/resources'], | ||
| ['Recipes', '/recipes'], | ||
| ]) { | ||
| await expect( | ||
| nav.getByRole('link', { name, exact: true }).filter({ hasText: name }), | ||
| ).toHaveCount(1); | ||
| await expect( | ||
| nav.getByRole('link', { name, exact: true }).filter({ hasText: name }), | ||
| ).toHaveAttribute('href', path); | ||
| } | ||
| await page.goto('/settings'); | ||
| await expect(page.getByText('Appearance')).toBeVisible({ timeout: 15_000 }); | ||
| await page.getByRole('button', { name: 'Dark', exact: true }).click(); | ||
| await expect(page.locator('body')).toHaveAttribute( | ||
| 'data-theme-mode', | ||
| 'dark', | ||
| ); | ||
| await page.reload(); | ||
| await expect(page.locator('body')).toHaveAttribute( | ||
| 'data-theme-mode', | ||
| 'dark', | ||
| ); | ||
| await page.getByRole('button', { name: 'Light', exact: true }).click(); | ||
| await expect(page.locator('body')).toHaveAttribute( | ||
| 'data-theme-mode', | ||
| 'light', | ||
| ); | ||
| await page.getByRole('tab', { name: 'Feature Flags' }).click(); | ||
| await expect( | ||
| page.getByText('radius-catalog', { exact: true }), | ||
| ).toBeVisible(); | ||
| await page.goto('/catalog'); | ||
| await expect( | ||
| page.getByRole('heading', { name: /Catalog/, level: 1 }), | ||
| ).toBeVisible({ timeout: 15_000 }); | ||
| await page.goto('/catalog-graph'); | ||
| await expect( | ||
| page.getByRole('heading', { name: /Catalog Graph/, level: 1 }), | ||
| ).toBeVisible({ timeout: 15_000 }); | ||
| expect(dialogs).toEqual([]); | ||
| expect(errors).toEqual([]); | ||
| }); | ||
|
|
||
| test('FS-12: real backend keeps missing authentication distinct from upstream denial', async ({ | ||
| request, | ||
| }) => { | ||
| test.skip( | ||
| !process.env.STANDALONE_QUALIFICATION, | ||
| 'Requires the controlled real backend', | ||
| ); | ||
| const denied = await request.get('/api/kubernetes/proxy/forbidden'); | ||
| expect(denied.status()).toBe(401); | ||
| const signIn = await request.get('/api/auth/guest/refresh'); | ||
| expect(signIn.status()).toBe(200); | ||
| const { backstageIdentity } = await signIn.json(); | ||
| const forbidden = await request.get('/api/kubernetes/proxy/forbidden', { | ||
| headers: { | ||
| authorization: `Bearer ${backstageIdentity.token}`, | ||
| 'Backstage-Kubernetes-Cluster': 'standalone-test-cluster', | ||
| }, | ||
| }); | ||
| expect(forbidden.status()).toBe(403); | ||
| }); | ||
|
|
||
| test('FS-11: installed catalog entity page and graph resolve the same entity', async ({ | ||
| page, | ||
| }) => { | ||
| const catalogErrors: string[] = []; | ||
| page.on('response', response => { | ||
| if ( | ||
| response.status() >= 400 && | ||
| response.url().includes('/api/catalog/') | ||
| ) { | ||
| catalogErrors.push(`${response.status()} ${response.url()}`); | ||
| } | ||
| }); | ||
| const entity = { | ||
| apiVersion: 'backstage.io/v1alpha1', | ||
| kind: 'Component', | ||
| metadata: { | ||
| name: 'standalone-component', | ||
| namespace: 'default', | ||
| uid: 'standalone-component', | ||
| description: 'Controlled catalog entity for native host qualification', | ||
| }, | ||
| spec: { | ||
| type: 'service', | ||
| lifecycle: 'experimental', | ||
| owner: 'user:default/guest', | ||
| }, | ||
| relations: [], | ||
| }; | ||
| await page.route( | ||
| '**/api/catalog/entities/by-name/component/default/standalone-component', | ||
| route => route.fulfill({ json: entity }), | ||
| ); | ||
| await page.route('**/api/catalog/entities/by-refs', route => | ||
| route.fulfill({ json: { items: [entity] } }), | ||
| ); | ||
| await page.route( | ||
| '**/api/catalog/entities/by-name/component/default/standalone-component/ancestry', | ||
| route => | ||
| route.fulfill({ | ||
| json: { | ||
| rootEntityRef: 'component:default/standalone-component', | ||
| items: [{ entity, parentEntityRefs: [] }], | ||
| }, | ||
| }), | ||
| ); | ||
| await page.goto('/catalog/default/component/standalone-component'); | ||
| await expect( | ||
| page.getByRole('heading', { name: 'standalone-component', exact: true }), | ||
| ).toBeVisible({ timeout: 20_000 }); | ||
| await expect(page.getByText(entity.metadata.description)).toBeVisible({ | ||
| timeout: 15_000, | ||
| }); | ||
| await page.reload(); | ||
| await expect( | ||
| page.getByRole('heading', { name: 'standalone-component', exact: true }), | ||
| ).toBeVisible(); | ||
| await page.goto( | ||
| '/catalog-graph?rootEntityRefs[]=component:default/standalone-component', | ||
| ); | ||
| await expect( | ||
| page.getByRole('heading', { name: /Catalog Graph/, level: 1 }), | ||
| ).toBeVisible({ timeout: 15_000 }); | ||
| await expect( | ||
| page.locator('svg').getByText('standalone-component', { exact: true }), | ||
| ).toBeVisible(); | ||
| expect(catalogErrors).toEqual([]); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.