diff --git a/web/package-lock.json b/web/package-lock.json
index 937bd4de77..f7f8895370 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -5142,16 +5142,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/longest-streak": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
- "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/loupe": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz",
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 4599a966c7..a0d170b2a4 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -3,7 +3,7 @@ import { LinuxConfigProvider } from "./contexts/LinuxConfigContext";
import { KubernetesConfigProvider } from "./contexts/KubernetesConfigContext";
import { SettingsProvider } from "./contexts/SettingsContext";
import { WizardProvider } from "./contexts/WizardModeContext";
-import { BrandingProvider } from "./contexts/BrandingContext";
+import { InitialStateProvider } from "./contexts/InitialStateContext";
import { AuthProvider } from "./contexts/AuthContext";
import ConnectionMonitor from "./components/common/ConnectionMonitor";
import InstallWizard from "./components/wizard/InstallWizard";
@@ -13,12 +13,12 @@ import { getQueryClient } from "./query-client";
function App() {
const queryClient = getQueryClient();
return (
-
-
-
-
-
-
+
+
+
+
+
+
@@ -30,18 +30,17 @@ function App() {
}
/>
-
} />
-
-
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/web/src/components/common/Logo.tsx b/web/src/components/common/Logo.tsx
index a25ca1099c..4b4cf8519d 100644
--- a/web/src/components/common/Logo.tsx
+++ b/web/src/components/common/Logo.tsx
@@ -1,9 +1,9 @@
import React from 'react';
-import { useBranding } from '../../contexts/BrandingContext';
+import { useInitialState } from '../../contexts/InitialStateContext';
export const AppIcon: React.FC<{ className?: string }> = ({ className = 'w-6 h-6' }) => {
- const { icon } = useBranding();
+ const { icon } = useInitialState();
if (!icon) {
return
;
}
diff --git a/web/src/components/wizard/completion/KubernetesCompletionStep.tsx b/web/src/components/wizard/completion/KubernetesCompletionStep.tsx
index 094e46a660..f4ae8563f2 100644
--- a/web/src/components/wizard/completion/KubernetesCompletionStep.tsx
+++ b/web/src/components/wizard/completion/KubernetesCompletionStep.tsx
@@ -1,15 +1,15 @@
-import React, {useState} from "react";
+import React, { useState } from "react";
import Card from "../../common/Card";
import Button from "../../common/Button";
import { useKubernetesConfig } from "../../../contexts/KubernetesConfigContext";
-import { useBranding } from "../../../contexts/BrandingContext";
+import { useInitialState } from "../../../contexts/InitialStateContext";
import { useSettings } from "../../../contexts/SettingsContext";
import { CheckCircle, ClipboardCheck, Copy, Terminal } from "lucide-react";
const KubernetesCompletionStep: React.FC = () => {
const [copied, setCopied] = useState(false);
const { config } = useKubernetesConfig();
- const { title } = useBranding();
+ const { title } = useInitialState();
const { settings } = useSettings();
const themeColor = settings.themeColor;
diff --git a/web/src/components/wizard/completion/LinuxCompletionStep.tsx b/web/src/components/wizard/completion/LinuxCompletionStep.tsx
index 74e90eb66d..a097433d3c 100644
--- a/web/src/components/wizard/completion/LinuxCompletionStep.tsx
+++ b/web/src/components/wizard/completion/LinuxCompletionStep.tsx
@@ -2,13 +2,13 @@ import React from "react";
import Card from "../../common/Card";
import Button from "../../common/Button";
import { useLinuxConfig } from "../../../contexts/LinuxConfigContext";
-import { useBranding } from "../../../contexts/BrandingContext";
+import { useInitialState } from "../../../contexts/InitialStateContext";
import { useSettings } from "../../../contexts/SettingsContext";
import { CheckCircle, ExternalLink } from "lucide-react";
const LinuxCompletionStep: React.FC = () => {
const { config } = useLinuxConfig();
- const { title } = useBranding();
+ const { title } = useInitialState();
const { settings } = useSettings();
const themeColor = settings.themeColor;
diff --git a/web/src/components/wizard/setup/LinuxSetupStep.tsx b/web/src/components/wizard/setup/LinuxSetupStep.tsx
index 21c003ce78..4ac67c545b 100644
--- a/web/src/components/wizard/setup/LinuxSetupStep.tsx
+++ b/web/src/components/wizard/setup/LinuxSetupStep.tsx
@@ -3,7 +3,7 @@ import Input from "../../common/Input";
import Select from "../../common/Select";
import Button from "../../common/Button";
import Card from "../../common/Card";
-import { useBranding } from "../../../contexts/BrandingContext";
+import { useInitialState } from "../../../contexts/InitialStateContext";
import { useLinuxConfig } from "../../../contexts/LinuxConfigContext";
import { useWizard } from "../../../contexts/WizardModeContext";
import { useQuery, useMutation } from "@tanstack/react-query";
@@ -19,17 +19,17 @@ import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
* - Error formatting: formatErrorMessage("adminConsolePort invalid") -> "Admin Console Port invalid"
*/
const fieldNames = {
- adminConsolePort: "Admin Console Port",
- dataDirectory: "Data Directory",
- localArtifactMirrorPort: "Local Artifact Mirror Port",
- httpProxy: "HTTP Proxy",
- httpsProxy: "HTTPS Proxy",
- noProxy: "Proxy Bypass List",
- networkInterface: "Network Interface",
- podCidr: "Pod CIDR",
- serviceCidr: "Service CIDR",
- globalCidr: "Reserved Network Range (CIDR)",
- cidr: "CIDR",
+ adminConsolePort: "Admin Console Port",
+ dataDirectory: "Data Directory",
+ localArtifactMirrorPort: "Local Artifact Mirror Port",
+ httpProxy: "HTTP Proxy",
+ httpsProxy: "HTTPS Proxy",
+ noProxy: "Proxy Bypass List",
+ networkInterface: "Network Interface",
+ podCidr: "Pod CIDR",
+ serviceCidr: "Service CIDR",
+ globalCidr: "Reserved Network Range (CIDR)",
+ cidr: "CIDR",
}
interface LinuxSetupStepProps {
@@ -49,7 +49,7 @@ interface ConfigError extends Error {
const LinuxSetupStep: React.FC = ({ onNext, onBack }) => {
const { config, updateConfig } = useLinuxConfig();
const { text } = useWizard();
- const { title } = useBranding();
+ const { title } = useInitialState();
const [showAdvanced, setShowAdvanced] = useState(false);
const [error, setError] = useState(null);
const { token } = useAuth();
@@ -269,9 +269,9 @@ const LinuxSetupStep: React.FC = ({ onNext, onBack }) => {
options={[
...(availableNetworkInterfaces.length > 0
? availableNetworkInterfaces.map((iface: string) => ({
- value: iface,
- label: iface,
- }))
+ value: iface,
+ label: iface,
+ }))
: []),
]}
helpText={`Network interface to use for ${title}`}
@@ -296,7 +296,7 @@ const LinuxSetupStep: React.FC = ({ onNext, onBack }) => {
{error && (
- {submitError?.errors && submitError.errors.length > 0
+ {submitError?.errors && submitError.errors.length > 0
? "Please fix the errors in the form above before proceeding."
: error
}
@@ -326,13 +326,13 @@ const LinuxSetupStep: React.FC
= ({ onNext, onBack }) => {
* @returns The formatted error message with replaced field names
*/
export function formatErrorMessage(message: string) {
- let finalMsg = message
- for (const [field, fieldName] of Object.entries(fieldNames)) {
- // Case-insensitive regex that matches whole words only
- // Example: "podCidr", "PodCidr", "PODCIDR" all become "Pod CIDR"
- finalMsg = finalMsg.replace(new RegExp(`\\b${field}\\b`, 'gi'), fieldName)
- }
- return finalMsg
+ let finalMsg = message
+ for (const [field, fieldName] of Object.entries(fieldNames)) {
+ // Case-insensitive regex that matches whole words only
+ // Example: "podCidr", "PodCidr", "PODCIDR" all become "Pod CIDR"
+ finalMsg = finalMsg.replace(new RegExp(`\\b${field}\\b`, 'gi'), fieldName)
+ }
+ return finalMsg
}
export default LinuxSetupStep;
diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx
index 27d84e10c2..abccaaab42 100644
--- a/web/src/contexts/AuthContext.tsx
+++ b/web/src/contexts/AuthContext.tsx
@@ -1,5 +1,6 @@
import React, { createContext, useContext, useState, useEffect } from "react";
import { handleUnauthorized } from "../utils/auth";
+import { useInitialState } from "./InitialStateContext";
interface AuthContextType {
token: string | null;
@@ -32,17 +33,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
setTokenState(newToken);
};
+ // Get the installation target from initial state
+ const { installTarget } = useInitialState()
// Check token validity on mount and when token changes
useEffect(() => {
if (token) {
- // Get the installation target from initial state
- const initialState = window.__INITIAL_STATE__ || {};
- const target = initialState.installTarget;
-
// Make a request to any authenticated endpoint to check token validity
// Use the correct target-specific endpoint based on installation target
- fetch(`/api/${target}/install/installation/config`, {
+ fetch(`/api/${installTarget}/install/installation/config`, {
headers: {
Authorization: `Bearer ${token}`,
},
diff --git a/web/src/contexts/BrandingContext.tsx b/web/src/contexts/BrandingContext.tsx
deleted file mode 100644
index 859e5ae30a..0000000000
--- a/web/src/contexts/BrandingContext.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, { createContext, useContext } from "react";
-
-interface Branding {
- title: string;
- icon?: string;
-}
-
-export const BrandingContext = createContext({ title: "My App" });
-
-export const useBranding = () => {
- const context = useContext(BrandingContext);
- if (!context) {
- throw new Error("useBranding must be used within a BrandingProvider");
- }
- return context;
-};
-
-export const BrandingProvider: React.FC<{ children: React.ReactNode }> = ({
- children,
-}) => {
- // __INITIAL_STATE__ is a global variable that can be set by the server-side rendering process
- // as a way to pass initial data to the client.
- const initialState = window.__INITIAL_STATE__ || {};
-
- const branding = {
- title: initialState.title || "My App",
- icon: initialState.icon,
- };
-
- return (
-
- {children}
-
- );
-};
diff --git a/web/src/contexts/InitialStateContext.tsx b/web/src/contexts/InitialStateContext.tsx
new file mode 100644
index 0000000000..e1efa29fda
--- /dev/null
+++ b/web/src/contexts/InitialStateContext.tsx
@@ -0,0 +1,40 @@
+import React, { createContext, useContext } from "react";
+import { InitialState } from "../types";
+import { InstallationTarget, isInstallationTarget } from "../types/installation-target";
+
+export const InitialStateContext = createContext({ title: "My App", installTarget: "linux" });
+
+export const useInitialState = () => {
+ const context = useContext(InitialStateContext);
+ if (!context) {
+ throw new Error("useInitialState must be used within a InitialStateProvider");
+ }
+ return context;
+};
+
+function parseInstallationTarget(target: string): InstallationTarget {
+ if (isInstallationTarget(target)) {
+ return target;
+ }
+ throw new Error(`Invalid installation target: ${target}`);
+}
+
+export const InitialStateProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ // __INITIAL_STATE__ is a global variable that can be set by the server-side rendering process
+ // as a way to pass initial data to the client.
+ const initialState = window.__INITIAL_STATE__ || {};
+
+ const state = {
+ title: initialState.title || "My App",
+ icon: initialState.icon,
+ installTarget: parseInstallationTarget(initialState.installTarget || "linux"), // default to "linux" if not provided
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/web/src/contexts/WizardModeContext.tsx b/web/src/contexts/WizardModeContext.tsx
index 35fbdab8ea..4d33e9e06c 100644
--- a/web/src/contexts/WizardModeContext.tsx
+++ b/web/src/contexts/WizardModeContext.tsx
@@ -1,8 +1,8 @@
import React, { createContext, useContext } from "react";
-import { useBranding } from "./BrandingContext";
+import { useInitialState } from "./InitialStateContext";
+import { InstallationTarget } from "../types/installation-target";
export type WizardMode = "install" | "upgrade";
-export type WizardTarget = "linux" | "kubernetes";
interface WizardText {
title: string;
@@ -59,7 +59,7 @@ const getTextVariations = (isLinux: boolean, title: string): Record(undefined);
export const WizardProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- // __INITIAL_STATE__ is a global variable that can be set by the server-side rendering process
- // as a way to pass initial data to the client.
- const initialState = window.__INITIAL_STATE__ || {};
- const target: WizardTarget = initialState.installTarget as WizardTarget;
+ const { title, installTarget } = useInitialState();
const mode = "install"; // TODO: get mode from initial state
-
- const { title } = useBranding();
- const isLinux = target === "linux";
+ const isLinux = installTarget === "linux";
const text = getTextVariations(isLinux, title)[mode];
- return {children};
+ return {children};
};
export const useWizard = (): WizardModeContextType => {
diff --git a/web/src/contexts/tests/InitialStateContext.test.tsx b/web/src/contexts/tests/InitialStateContext.test.tsx
new file mode 100644
index 0000000000..a39e0b5fea
--- /dev/null
+++ b/web/src/contexts/tests/InitialStateContext.test.tsx
@@ -0,0 +1,258 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { InitialStateContext, useInitialState, InitialStateProvider } from "../InitialStateContext";
+import { InstallationTarget } from "../../types/installation-target";
+
+type CustomWindow = typeof window & {
+ __INITIAL_STATE__?: unknown;
+}
+
+describe("InitialStateContext", () => {
+ const originalWindow = global.window;
+
+ beforeEach(() => {
+ // Reset window mock before each test
+ global.window = {
+ ...originalWindow,
+ __INITIAL_STATE__: undefined,
+ } as CustomWindow;
+ });
+
+ afterEach(() => {
+ // Restore original window
+ global.window = originalWindow;
+ vi.clearAllMocks();
+ });
+
+ describe("useInitialState hook", () => {
+ it("returns context value when used within provider", () => {
+ const mockContext = {
+ title: "Test App",
+ icon: "test-icon.png",
+ installTarget: "linux" as InstallationTarget,
+ };
+
+ const TestComponent = () => {
+ const state = useInitialState();
+ return {state.title}
;
+ };
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("Test App")).toBeInTheDocument();
+ });
+ });
+
+ describe("parseInstallationTarget function", () => {
+ it("linux is a valid installation target", () => {
+
+ const TestComponent = () => {
+ const state = useInitialState();
+ return {state.installTarget}
;
+ };
+
+ // Set up window.__INITIAL_STATE__ with valid target
+ global.window.__INITIAL_STATE__ = {
+ installTarget: "linux",
+ };
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("linux")).toBeInTheDocument();
+ });
+
+ it("kubernetes is a valid installation target", () => {
+
+ const TestComponent = () => {
+ const state = useInitialState();
+ return {state.installTarget}
;
+ };
+
+ // Set up window.__INITIAL_STATE__ with valid target
+ global.window.__INITIAL_STATE__ = {
+ installTarget: "kubernetes",
+ };
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("kubernetes")).toBeInTheDocument();
+ });
+
+ it("throws error for invalid installation target", () => {
+
+ // Set up window.__INITIAL_STATE__ with invalid target
+ global.window.__INITIAL_STATE__ = {
+ installTarget: "invalid-target",
+ };
+
+ // Mock console.error to prevent error output in tests
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
+
+ expect(() => {
+ render(
+
+ Test
+
+ );
+ }).toThrow("Invalid installation target: invalid-target");
+
+ consoleSpy.mockRestore();
+ });
+ });
+
+ describe("InitialStateProvider", () => {
+ it("provides default values when window.__INITIAL_STATE__ is not available", () => {
+
+ const TestComponent = () => {
+ const state = useInitialState();
+ return (
+
+ {state.title}
+ {state.icon || "no-icon"}
+ {state.installTarget}
+
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId("title")).toHaveTextContent("My App");
+ expect(screen.getByTestId("icon")).toHaveTextContent("no-icon");
+ expect(screen.getByTestId("target")).toHaveTextContent("linux");
+ });
+
+ it("uses values from window.__INITIAL_STATE__ when available", () => {
+
+ global.window.__INITIAL_STATE__ = {
+ title: "Custom App Title",
+ icon: "custom-icon.png",
+ installTarget: "kubernetes",
+ };
+
+ const TestComponent = () => {
+ const state = useInitialState();
+ return (
+
+ {state.title}
+ {state.icon}
+ {state.installTarget}
+
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId("title")).toHaveTextContent("Custom App Title");
+ expect(screen.getByTestId("icon")).toHaveTextContent("custom-icon.png");
+ expect(screen.getByTestId("target")).toHaveTextContent("kubernetes");
+ });
+
+ it("falls back to defaults for missing properties", () => {
+
+ global.window.__INITIAL_STATE__ = {
+ title: "Partial Config",
+ // icon and installTarget are missing
+ };
+
+ const TestComponent = () => {
+ const state = useInitialState();
+ return (
+
+ {state.title}
+ {state.icon || "no-icon"}
+ {state.installTarget}
+
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId("title")).toHaveTextContent("Partial Config");
+ expect(screen.getByTestId("icon")).toHaveTextContent("no-icon");
+ expect(screen.getByTestId("target")).toHaveTextContent("linux");
+ });
+
+ it("handles empty window.__INITIAL_STATE__ object", () => {
+
+ global.window.__INITIAL_STATE__ = {};
+
+ const TestComponent = () => {
+ const state = useInitialState();
+ return (
+
+ {state.title}
+ {state.icon || "no-icon"}
+ {state.installTarget}
+
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId("title")).toHaveTextContent("My App");
+ expect(screen.getByTestId("icon")).toHaveTextContent("no-icon");
+ expect(screen.getByTestId("target")).toHaveTextContent("linux");
+ });
+
+ it("renders children correctly", () => {
+
+ render(
+
+ Child Component
+
+ );
+
+ expect(screen.getByTestId("child")).toHaveTextContent("Child Component");
+ });
+ });
+
+ describe("InitialStateContext default value", () => {
+ it("has correct default values", () => {
+ const TestComponent = () => {
+ return (
+
+ {(value) => (
+
+ {value.title}
+ {value.installTarget}
+
+ )}
+
+ );
+ };
+
+ render();
+
+ expect(screen.getByTestId("title")).toHaveTextContent("My App");
+ expect(screen.getByTestId("target")).toHaveTextContent("linux");
+ });
+ });
+});
diff --git a/web/src/global.d.ts b/web/src/global.d.ts
index e67927c531..65b7616a82 100644
--- a/web/src/global.d.ts
+++ b/web/src/global.d.ts
@@ -1,15 +1,15 @@
// src/global.d.ts
export { };
+// Initial state is how the server can pass initial data to the client.
+interface InitialState {
+ icon?: string;
+ title?: string;
+ installTarget?: string;
+}
+
declare global {
interface Window {
__INITIAL_STATE__?: InitialState;
}
-
- // Initial state is how the server can pass initial data to the client.
- interface InitialState {
- icon?: string;
- title?: string;
- installTarget?: string;
- }
}
diff --git a/web/src/test/setup.tsx b/web/src/test/setup.tsx
index 05b705eee3..ee616551c3 100644
--- a/web/src/test/setup.tsx
+++ b/web/src/test/setup.tsx
@@ -3,13 +3,16 @@ import { QueryClientProvider } from "@tanstack/react-query";
import { render, RenderOptions } from "@testing-library/react";
import { createMemoryRouter, RouterProvider, RouteObject } from "react-router-dom";
import { vi } from "vitest";
+import { JSX } from "react/jsx-runtime";
+import { InitialState } from "../types";
+import { InstallationTarget } from "../types/installation-target";
import { createQueryClient } from "../query-client";
import { LinuxConfigContext, LinuxConfig } from "../contexts/LinuxConfigContext";
import { KubernetesConfigContext, KubernetesConfig } from "../contexts/KubernetesConfigContext";
import { SettingsContext, Settings } from "../contexts/SettingsContext";
-import { WizardContext, WizardMode, WizardTarget } from "../contexts/WizardModeContext";
-import { BrandingContext } from "../contexts/BrandingContext";
+import { WizardContext, WizardMode } from "../contexts/WizardModeContext";
+import { InitialStateContext } from "../contexts/InitialStateContext";
import { AuthContext } from "../contexts/AuthContext";
// Mock localStorage for tests
@@ -30,10 +33,7 @@ interface MockProviderProps {
children: React.ReactNode;
queryClient: ReturnType;
contexts: {
- brandingContext: {
- title: string;
- icon?: string;
- };
+ initialStateContext: InitialState
linuxConfigContext: {
config: LinuxConfig;
updateConfig: (newConfig: Partial) => void;
@@ -49,7 +49,7 @@ interface MockProviderProps {
updateSettings: (newSettings: Partial) => void;
};
wizardModeContext: {
- target: WizardTarget;
+ target: InstallationTarget;
mode: WizardMode;
text: {
title: string;
@@ -87,19 +87,19 @@ const MockProvider = ({ children, queryClient, contexts }: MockProviderProps) =>
}, [contexts.authContext.token]);
return (
-
-
-
-
-
-
+
+
+
+
+
+
{children}
-
-
-
-
-
-
+
+
+
+
+
+
);
};
@@ -111,7 +111,7 @@ interface RenderWithProvidersOptions extends RenderOptions {
routePath?: string;
authenticated?: boolean;
authToken?: string;
- target?: WizardTarget;
+ target?: InstallationTarget;
};
wrapper?: React.ComponentType<{ children: React.ReactNode }>;
}
@@ -121,7 +121,7 @@ export const renderWithProviders = (
options: RenderWithProvidersOptions = {},
) => {
const defaultContextValues: MockProviderProps["contexts"] = {
- brandingContext: { title: "My App" },
+ initialStateContext: { title: "My App", installTarget: options.wrapperProps?.target || "linux" },
linuxConfigContext: {
config: {
adminConsolePort: 8800,
@@ -174,7 +174,7 @@ export const renderWithProviders = (
};
const mergedContextValues: MockProviderProps["contexts"] = {
- brandingContext: { ...defaultContextValues.brandingContext, ...options.wrapperProps?.contextValues?.brandingContext },
+ initialStateContext: { ...defaultContextValues.initialStateContext, ...options.wrapperProps?.contextValues?.initialStateContext },
linuxConfigContext: { ...defaultContextValues.linuxConfigContext, ...options.wrapperProps?.contextValues?.linuxConfigContext },
kubernetesConfigContext: { ...defaultContextValues.kubernetesConfigContext, ...options.wrapperProps?.contextValues?.kubernetesConfigContext },
settingsContext: { ...defaultContextValues.settingsContext, ...options.wrapperProps?.contextValues?.settingsContext },
diff --git a/web/src/types/index.ts b/web/src/types/index.ts
index 8262d1cc43..79d3da7a46 100644
--- a/web/src/types/index.ts
+++ b/web/src/types/index.ts
@@ -1,3 +1,11 @@
+import { InstallationTarget } from './installation-target';
+
+export interface InitialState {
+ title: string;
+ icon?: string;
+ installTarget: InstallationTarget;
+}
+
export interface InfraStatusResponse {
components: InfraComponent[];
status: InfraStatus;
diff --git a/web/src/types/installation-target.ts b/web/src/types/installation-target.ts
new file mode 100644
index 0000000000..e8acab3a23
--- /dev/null
+++ b/web/src/types/installation-target.ts
@@ -0,0 +1,7 @@
+export const installationTargets = ['linux', 'kubernetes'] as const;
+
+export type InstallationTarget = typeof installationTargets[number];
+
+export function isInstallationTarget(value: string): value is InstallationTarget {
+ return (installationTargets as readonly string[]).includes(value);
+}