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
24 changes: 0 additions & 24 deletions frontend/e2e/talemu/home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,6 @@ test('Has expected title', async ({ page }) => {
await expect(page).toHaveTitle('Omni - default')
})

test('Download installation media', async ({ page }, testInfo) => {
test.slow()

await page.goto('/')

await page.getByRole('button', { name: 'Download Installation Media' }).click()
await page.getByText('hello-world-service').click()
await page.getByRole('button', { name: 'Download', exact: true }).click()

const [download] = await Promise.all([
page.waitForEvent('download'),
expect(page.getByText('Generating Image')).toBeVisible(),
])

await expect(page.getByRole('heading', { name: 'Home' })).toBeVisible()

const filePath = testInfo.outputPath(download.suggestedFilename())
await download.saveAs(filePath)

const { size } = await stat(filePath)

expect(size).toBeGreaterThan(50 * 1024 * 1024)
})

test('Download machine join config', async ({ page }, testInfo) => {
await page.goto('/')

Expand Down
197 changes: 197 additions & 0 deletions frontend/e2e/talemu/installation_media.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Copyright (c) 2026 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
import { stat } from 'node:fs/promises'

import { faker } from '@faker-js/faker'
import { milliseconds } from 'date-fns'
import { load } from 'js-yaml'

import { expect, test } from '../auth_fixtures'

test.describe.configure({ mode: 'parallel' })

test('Download installation media', async ({ page }, testInfo) => {
test.slow()

await test.step('Go to wizard', async () => {
await page.goto('/')
await page.getByRole('link', { name: 'Download Installation Media' }).click()

const firstStepHeading = page.getByRole('heading', { name: 'Create New Media' })

const isVisible = await firstStepHeading
.waitFor({ timeout: milliseconds({ seconds: 2 }) })
.then(() => true)
.catch(() => false)

// Only click "Create New" if we don't auto redirect (redirects directly to wizard if no configs exist)
if (!isVisible) {
await page.getByRole('link', { name: 'Create New' }).click()
}

await expect(firstStepHeading).toBeVisible()
})

await test.step('Entry step', async () => {
await page.getByRole('radio', { name: 'Bare-metal Machine' }).click()
await expect(page.getByRole('radio', { name: 'Bare-metal Machine' })).toBeChecked()

await page.getByRole('link', { name: 'Next' }).click()
})

await test.step('Talos version step', async () => {
await page.getByRole('combobox', { name: 'Choose Talos Linux Version' }).click()
await page.getByRole('option', { name: '1.12.0' }).click()
await expect(page.getByRole('combobox', { name: 'Choose Talos Linux Version' })).toHaveText(
'1.12.0',
)

await page.getByRole('combobox', { name: 'Join Token' }).click()
await page.getByRole('option', { name: 'initial token' }).click()
await expect(page.getByRole('combobox', { name: 'Join Token' })).toHaveText('initial token')

await page.getByText('Tunnel Omni management').click()
await expect(page.getByRole('checkbox', { name: 'Tunnel Omni management' })).toBeChecked()

await page.getByRole('button', { name: 'new label' }).click()
await page.getByRole('textbox').first().fill('foo:bar')
await page.getByRole('textbox').first().press('Enter')
await expect(page.getByRole('button', { name: 'foo:bar' })).toBeVisible()

await page.getByRole('link', { name: 'Next' }).click()
})

await test.step('Architecture step', async () => {
await page.getByRole('radio', { name: 'arm64' }).click()
await expect(page.getByRole('radio', { name: 'arm64' })).toBeChecked()

await page.getByRole('checkbox', { name: 'SecureBoot' }).click()
await expect(page.getByRole('checkbox', { name: 'SecureBoot' })).toBeChecked()

await page.getByRole('link', { name: 'Next' }).click()
})

await test.step('System extensions step', async () => {
await page.getByPlaceholder('Search').fill('hello')
await page.getByText('siderolabs/hello-world-service').click()
await expect(
page.getByRole('checkbox', { name: 'siderolabs/hello-world-service' }),
).toBeChecked()

await page.getByRole('link', { name: 'Next' }).click()
})

await test.step('Extra args step', async () => {
await page.getByRole('radio', { name: 'Auto' }).click()
await page.locator('.flex.max-h-full').first().click()

await page
.getByRole('textbox', { name: 'Extra kernel command line' })
.fill(`-console console=tty0`)

await page.getByRole('link', { name: 'Next' }).click()
})

const savedPresetName = `e2e-media-${faker.string.alphanumeric(8)}`

let schematicId: string

await test.step('Confirmation step', async () => {
await page.getByRole('button', { name: 'Copy schematic ID' }).click()

schematicId = await page.evaluate(() => navigator.clipboard.readText())
expect(schematicId, 'Expect schematic ID to be valid').toMatch(/[a-zA-Z0-9]{64}/)

await page.getByRole('button', { name: 'Copy schematic YAML' }).click()

const schematicYml = await page.evaluate(() => navigator.clipboard.readText())
const parsedSchematicYml = load(schematicYml)
await testInfo.attach('schematic.yaml', {
body: schematicYml,
contentType: 'application/yaml',
})

expect(parsedSchematicYml, 'Expect YAML to match expected shape').toEqual({
customization: {
extraKernelArgs: [
expect.stringContaining('siderolink.api=grpc://'),
expect.stringContaining('talos.events.sink='),
expect.stringContaining('talos.logging.kernel='),
'-console',
'console=tty0',
],
meta: [{ key: 12, value: expect.any(String) }],
systemExtensions: {
officialExtensions: ['siderolabs/hello-world-service'],
},
},
})

await expect(
page.getByText(
`https://factory.talos.dev/image/${schematicId}/1.12.0/metal-arm64-secureboot.iso`,
),
).toBeVisible()

await expect(
page.getByText(
`https://factory.talos.dev/image/${schematicId}/1.12.0/metal-arm64-secureboot.raw.zst`,
),
).toBeVisible()

await expect(
page.getByText(`https://pxe.factory.talos.dev/${schematicId}/1.12.0/metal-arm64-secureboot`),
).toBeVisible()

await page.getByRole('button', { name: 'Save' }).click()
await page.getByRole('textbox', { name: 'Name:' }).fill(savedPresetName)
await page.getByRole('textbox', { name: 'Name:' }).click()
await page.getByLabel('Save preset').getByRole('button', { name: 'Save' }).click()
await page.getByRole('link', { name: 'Finished' }).click()
})

const presetRow = page.getByRole('row', { name: savedPresetName })

await test.step('Download the image', async () => {
await presetRow.getByLabel('download').click()

const isoRow = page.getByRole('row', { name: 'SecureBoot ISO' })
await isoRow.getByLabel('copy link').click()

const isoLink = await page.evaluate(() => navigator.clipboard.readText())
expect(isoLink).toBe(
`https://factory.talos.dev/image/${schematicId}/1.12.0/metal-arm64-secureboot.iso`,
)

await isoRow.getByLabel('download').click()

const [download] = await Promise.all([
page.waitForEvent('download'),
expect(page.getByText('Generating Image')).toBeVisible(),
])

const filePath = testInfo.outputPath(download.suggestedFilename())
await download.saveAs(filePath)

const { size } = await stat(filePath)

expect(size, 'Expect ISO to be at least 50MB').toBeGreaterThan(50 * 1024 * 1024)

await page.getByRole('button', { name: 'Close', exact: true }).click()
})

await test.step('Delete the image', async () => {
await presetRow.getByLabel('delete').click()

await expect(
page.getByText(`Are you sure you want to delete preset "${savedPresetName}"?`),
).toBeVisible()

await page.getByRole('button', { name: 'Confirm' }).click()

await expect(page.getByText(`Deleted preset ${savedPresetName}`)).toBeVisible()
await expect(presetRow).toBeHidden()
})
})
16 changes: 6 additions & 10 deletions frontend/src/components/SideBar/TSideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
canReadMachines,
setupClusterPermissions,
} from '@/methods/auth'
import { useFeatures, useInstallationMediaEnabled } from '@/methods/features'
import { useFeatures } from '@/methods/features'
import { useIdentity } from '@/methods/identity'
import { useResourceWatch } from '@/methods/useResourceWatch'
import ExposedServiceSideBar from '@/views/cluster/ExposedService/ExposedServiceSideBar.vue'
Expand All @@ -53,7 +53,6 @@ const context = getContext()
const { avatar, fullname, identity } = useIdentity()

const { data: featuresConfig } = useFeatures()
const { value: installationMediaEnabled } = useInstallationMediaEnabled()

const { status: backupStatus } = setupBackupStatus()
const { canSyncKubernetesManifests, canManageClusterFeatures } = setupClusterPermissions(
Expand Down Expand Up @@ -265,17 +264,14 @@ const rootItems = computed(() => {
route: getRoute('JoinTokens', '/machines/jointokens'),
icon: 'key',
},
{
name: 'Installation Media',
route: getRoute('InstallationMedia', '/machines/installation-media'),
icon: 'kube-config',
},
] as SideBarItem[],
} satisfies SideBarItem

if (installationMediaEnabled) {
item.subItems.push({
name: 'Installation Media',
route: getRoute('InstallationMedia', '/machines/installation-media'),
icon: 'kube-config',
})
}

result.push(item)
}

Expand Down
3 changes: 0 additions & 3 deletions frontend/src/components/TModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ const modals: Record<string, Component> = {
maintenanceUpdate: defineAsyncComponent(
() => import('@/views/omni/Modals/MaintenanceUpdate.vue'),
),
downloadInstallationMedia: defineAsyncComponent(
() => import('@/views/omni/Modals/DownloadInstallationMedia.vue'),
),
downloadOmnictlBinaries: defineAsyncComponent(
() => import('@/views/omni/Modals/DownloadOmnictl.vue'),
),
Expand Down
18 changes: 9 additions & 9 deletions frontend/src/components/common/Checkbox/TCheckbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<script setup lang="ts">
import TAnimation from '@/components/common/Animation/TAnimation.vue'
import { CheckboxIndicator, CheckboxRoot } from 'reka-ui'

import TIcon from '@/components/common/Icon/TIcon.vue'

type Props = {
Expand All @@ -21,19 +22,18 @@ const checked = defineModel<boolean>({ default: false })

<template>
<label class="inline-flex cursor-pointer items-center gap-2 has-disabled:cursor-not-allowed">
<input v-model="checked" type="checkbox" :disabled="disabled" class="peer sr-only fixed" />

<div
class="flex size-3.5 items-center justify-center rounded-xs border border-naturals-n7 peer-checked:border-primary-p6 peer-checked:bg-primary-p6 peer-disabled:border-naturals-n5 peer-disabled:bg-naturals-n4"
<CheckboxRoot
v-model="checked"
:disabled
class="flex size-3.5 items-center justify-center rounded-xs border border-naturals-n7 transition-colors data-disabled:border-naturals-n5 data-disabled:bg-naturals-n4 not-data-disabled:data-[state=checked]:border-primary-p6 not-data-disabled:data-[state=checked]:bg-primary-p6"
>
<TAnimation>
<CheckboxIndicator class="transition-opacity data-[state=unchecked]:opacity-0" force-mount>
<TIcon
v-show="checked"
class="size-full fill-current text-primary-p3"
:icon="indeterminate ? 'minus' : 'check'"
/>
</TAnimation>
</div>
</CheckboxIndicator>
</CheckboxRoot>

<span
v-if="label || $slots.default"
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/common/CodeBlock/CodeBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<script setup lang="ts">
import type { ComponentProps } from 'vue-component-type-helpers'

import CopyButton from '@/components/common/CopyButton/CopyButton.vue'

const { code = '' } = defineProps<{ code?: string }>()
interface Props {
code?: string
buttonAttrs?: /* @vue-ignore */ Omit<ComponentProps<typeof CopyButton>, 'text'>
}

const { code = '', buttonAttrs } = defineProps<Props>()
</script>

<template>
<div class="relative rounded border border-naturals-n7 bg-naturals-n2 text-naturals-n14">
<div
class="absolute top-2 right-2 z-10 flex items-center justify-center rounded-md p-1 backdrop-blur"
>
<CopyButton aria-label="Copy" :text="code" />
<CopyButton v-bind="buttonAttrs" :text="code" />
</div>

<div class="p-1">
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/common/CopyButton/CopyButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ included in the LICENSE file.
-->
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import type { ButtonHTMLAttributes } from 'vue'

import TIcon from '@/components/common/Icon/TIcon.vue'

const { text = '' } = defineProps<{ text?: string }>()
interface Props extends /* @vue-ignore */ ButtonHTMLAttributes {
text?: string
}

const { text = '' } = defineProps<Props>()
const { copy, copied } = useClipboard({ copiedDuring: 1000 })
</script>

Expand Down
6 changes: 0 additions & 6 deletions frontend/src/methods/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
// Use of this software is governed by the Business Source License
// included in the LICENSE file.

import { useLocalStorage } from '@vueuse/core'

import { Runtime } from '@/api/common/omni.pb'
import type { Resource } from '@/api/grpc'
import { ResourceService } from '@/api/grpc'
Expand Down Expand Up @@ -47,7 +45,3 @@ const getFeaturesConfig = async (): Promise<Resource<FeaturesConfigSpec>> => {

return cachedFeaturesConfig
}

export function useInstallationMediaEnabled() {
return useLocalStorage('_installation_media_enabled', false)
}
Loading
Loading