diff --git a/server/config/docker.yaml b/server/config/docker.yaml index e661b8ba3f..e475638ec1 100644 --- a/server/config/docker.yaml +++ b/server/config/docker.yaml @@ -71,7 +71,8 @@ codec: includeCredentials: {{ env "TEMPORAL_CODEC_INCLUDE_CREDENTIALS" | default "false" }} defaultErrorMessage: {{ env "TEMPORAL_CODEC_DEFAULT_ERROR_MESSAGE" | default "" }} defaultErrorLink: {{ env "TEMPORAL_CODEC_DEFAULT_ERROR_LINK" | default "" }} - +environment: + name: {{ env "TEMPORAL_ENVIRONMENT_NAME" | default "" }} forwardHeaders: {{- if env "TEMPORAL_FORWARD_HEADERS" }} {{- range env "TEMPORAL_FORWARD_HEADERS" | split "," }} diff --git a/server/server/api/handler.go b/server/server/api/handler.go index 1dc7ac039d..5adf83fccf 100644 --- a/server/server/api/handler.go +++ b/server/server/api/handler.go @@ -57,6 +57,10 @@ type CodecResponse struct { DefaultErrorLink string } +type Environment struct { + Name string +} + type SettingsResponse struct { Auth *Auth BannerText string @@ -65,6 +69,7 @@ type SettingsResponse struct { FeedbackURL string NotifyOnNewVersion bool Codec *CodecResponse + Environment *Environment Version string DisableWriteActions bool WorkflowTerminateDisabled bool @@ -143,6 +148,9 @@ func GetSettings(cfgProvider *config.ConfigProviderWithRefresh) func(echo.Contex DefaultErrorMessage: cfg.Codec.DefaultErrorMessage, DefaultErrorLink: cfg.Codec.DefaultErrorLink, }, + Environment: &Environment{ + Name: cfg.Environment.Name, + }, Version: version.UIVersion, DisableWriteActions: cfg.DisableWriteActions, WorkflowTerminateDisabled: cfg.WorkflowTerminateDisabled, diff --git a/server/server/config/config.go b/server/server/config/config.go index d0cc562b0f..67c18e88ce 100644 --- a/server/server/config/config.go +++ b/server/server/config/config.go @@ -51,6 +51,7 @@ type ( // How often to reload the config RefreshInterval time.Duration `yaml:"refreshInterval"` Codec Codec `yaml:"codec"` + Environment Environment `yaml:"environment"` DisableWriteActions bool `yaml:"disableWriteActions"` // Discrete configuration for Workflow Actions in the UI WorkflowTerminateDisabled bool `yaml:"workflowTerminateDisabled"` @@ -136,6 +137,10 @@ type ( DefaultErrorLink string `yaml:"defaultErrorLink"` } + Environment struct { + Name string `yaml:"name"` + } + Filesystem struct { Path string `yaml:"path"` } diff --git a/src/lib/components/bottom-nav.svelte b/src/lib/components/bottom-nav.svelte index c25cb702ab..8d17ef76ae 100644 --- a/src/lib/components/bottom-nav.svelte +++ b/src/lib/components/bottom-nav.svelte @@ -2,10 +2,11 @@ import { writable } from 'svelte/store'; import { slide } from 'svelte/transition'; + import type { Snippet } from 'svelte'; import { twMerge as merge } from 'tailwind-merge'; import { beforeNavigate } from '$app/navigation'; - import { page } from '$app/stores'; + import { page } from '$app/state'; import Button from '$lib/holocene/button.svelte'; import Icon from '$lib/holocene/icon/icon.svelte'; @@ -14,20 +15,35 @@ import { lastUsedNamespace } from '$lib/stores/namespaces'; import type { NamespaceListItem, NavLinkListItem } from '$lib/types/global'; import { routeForNamespace } from '$lib/utilities/route-for'; - import ziggy from '$lib/vendor/ziggy-full-face.png'; import BottomNavLinks from './bottom-nav-links.svelte'; - import BottomNavNamespaces from './bottom-nav-namespaces.svelte'; import BottomNavSettings from './bottom-nav-settings.svelte'; - export let namespaceList: NamespaceListItem[] | undefined = []; - export let linkList: NavLinkListItem[]; - export let isCloud = false; - export let showNamespacePicker = true; + type Props = { + children: Snippet; + namespacePicker: Snippet<[{ open: boolean; closeMenu: () => void }]>; + avatar: Snippet; + namespaceList?: NamespaceListItem[]; + isCloud: boolean; + linkList: NavLinkListItem[]; + showNamespacePicker?: boolean; + environmentName?: string; + }; - let viewLinks = false; + let { + children, + namespacePicker, + avatar, + namespaceList = [], + isCloud = false, + linkList, + showNamespacePicker = true, + environmentName, + }: Props = $props(); + + let viewLinks = $state(false); let viewNamespaces = writable(false); - let viewSettings = false; + let viewSettings = $state(false); function escapeHandler(e: KeyboardEvent) { if ( @@ -42,10 +58,13 @@ closeMenu(); }); - $: namespace = $page.params.namespace || $lastUsedNamespace; - $: namespaceExists = namespaceList.some( - (namespaceListItem) => namespaceListItem.namespace === namespace, + const namespace = $derived(page.params.namespace || $lastUsedNamespace); + const namespaceExists = $derived( + namespaceList.some( + (namespaceListItem) => namespaceListItem.namespace === namespace, + ), ); + const menuIsOpen = $derived(viewLinks || $viewNamespaces || viewSettings); const onLinksClick = () => { viewLinks = !viewLinks; @@ -71,8 +90,6 @@ viewSettings = false; } - $: menuIsOpen = viewLinks || $viewNamespaces || viewSettings; - const truncateNamespace = (namespace: string) => { if (namespace.length > 16) { return `${namespace.slice(0, 8)}...${namespace.slice(-8)}`; @@ -97,11 +114,9 @@ out:slide={{ duration: 200, delay: 0 }} > - - - + {@render namespacePicker({ open: $viewNamespaces, closeMenu })} - + {@render children?.()} {/if} @@ -111,7 +126,7 @@ 'focus-visible:[&_a]:outline-none focus-visible:[&_a]:ring-2 focus-visible:[&_a]:ring-primary/70 focus-visible:[&_button]:outline-none focus-visible:[&_button]:ring-2 focus-visible:[&_button]:ring-primary/70', isCloud ? 'bg-gradient-to-b from-indigo-600 to-indigo-900 text-off-white focus-visible:[&_a]:ring-success focus-visible:[&_button]:ring-success' - : 'surface-black border-t border-subtle', + : environmentName || 'surface-black border-t border-subtle', )} data-testid="top-nav" aria-label={translate('common.main')} @@ -121,7 +136,7 @@ data-testid="nav-menu-button" class:active-shadow={viewLinks} type="button" - on:click={onLinksClick} + onclick={onLinksClick} > {#if viewLinks} @@ -154,7 +169,7 @@ data-testid="nav-profile-button" class:active-shadow={viewSettings} type="button" - on:click={onSettingsClick} + onclick={onSettingsClick} > {#if viewSettings} @@ -162,13 +177,7 @@
- - {translate('common.user-profile')} - + {@render avatar()}
{/if} @@ -182,4 +191,16 @@ .nav-button { @apply relative select-none p-1 text-center align-middle text-xs font-medium uppercase transition-all; } + + .development { + @apply surface-development border-t border-subtle; + } + + .staging { + @apply surface-staging border-t border-subtle; + } + + .test { + @apply surface-test border-t border-subtle; + } diff --git a/src/lib/components/side-nav.svelte b/src/lib/components/side-nav.svelte index 9adf662c78..820a1892c5 100644 --- a/src/lib/components/side-nav.svelte +++ b/src/lib/components/side-nav.svelte @@ -1,14 +1,30 @@ - + {#each linkList as item} {#if !item?.hidden} {#if item.divider} @@ -25,7 +41,7 @@ /> {/if} {/each} - - - + {#snippet bottom()} + {@render bottomActions?.()} + {/snippet} diff --git a/src/lib/holocene/main-content-container.svelte b/src/lib/holocene/main-content-container.svelte index f70f44802f..39c7729a79 100644 --- a/src/lib/holocene/main-content-container.svelte +++ b/src/lib/holocene/main-content-container.svelte @@ -1,10 +1,21 @@ + +
- + {@render children()}
- + {@render main()}
- + {@render footer()}
diff --git a/src/lib/holocene/navigation/navigation-container.svelte b/src/lib/holocene/navigation/navigation-container.svelte index 2f9e76dcc6..eca15a9c3b 100644 --- a/src/lib/holocene/navigation/navigation-container.svelte +++ b/src/lib/holocene/navigation/navigation-container.svelte @@ -1,6 +1,7 @@ + + diff --git a/src/lib/services/settings-service.ts b/src/lib/services/settings-service.ts index 7f40e3a0c0..33e473f069 100644 --- a/src/lib/services/settings-service.ts +++ b/src/lib/services/settings-service.ts @@ -37,6 +37,9 @@ export const fetchSettings = async (request = fetch): Promise => { }, defaultNamespace: settingsResponse?.DefaultNamespace || 'default', // API returns an empty string if default namespace is not configured disableWriteActions: !!settingsResponse?.DisableWriteActions || false, + buildEnvironment: { + name: settingsResponse?.Environment?.Name || '', + }, workflowTerminateDisabled: !!settingsResponse?.WorkflowTerminateDisabled, workflowCancelDisabled: !!settingsResponse?.WorkflowCancelDisabled, workflowSignalDisabled: !!settingsResponse?.WorkflowSignalDisabled, diff --git a/src/lib/theme/plugin.ts b/src/lib/theme/plugin.ts index 9097c73524..0eee615a6d 100644 --- a/src/lib/theme/plugin.ts +++ b/src/lib/theme/plugin.ts @@ -162,6 +162,18 @@ const temporal = plugin( backgroundColor: css('--color-surface-black'), color: css('--color-text-white'), }, + '.surface-development': { + backgroundColor: css('--color-surface-development'), + color: css('--color-text-white'), + }, + '.surface-test': { + backgroundColor: css('--color-surface-test'), + color: css('--color-text-white'), + }, + '.surface-staging': { + backgroundColor: css('--color-surface-staging'), + color: css('--color-text-white'), + }, }); }, { diff --git a/src/lib/theme/variables.ts b/src/lib/theme/variables.ts index 7ef913c015..43db2191e7 100644 --- a/src/lib/theme/variables.ts +++ b/src/lib/theme/variables.ts @@ -173,6 +173,19 @@ export const variables = { light: 'indigo.100', dark: 'slate.900', }, + // Environment Surfaces + '--color-surface-development': { + light: 'green.700', + dark: 'green.700', + }, + '--color-surface-staging': { + light: 'blue.600', + dark: 'blue.600', + }, + '--color-surface-test': { + light: 'yellow.700', + dark: 'yellow.700', + }, // Border '--color-border-primary': { light: 'space-black', diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 5070d8a159..8eb4e7e0a4 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -286,6 +286,7 @@ export type SettingsResponse = { DefaultErrorMessage?: string; DefaultErrorLink?: string; }; + Environment: { Name: string }; DefaultNamespace: string; DisableWriteActions: boolean; WorkflowTerminateDisabled: boolean; diff --git a/src/lib/utilities/environment-name.ts b/src/lib/utilities/environment-name.ts new file mode 100644 index 0000000000..b64f75d192 --- /dev/null +++ b/src/lib/utilities/environment-name.ts @@ -0,0 +1,6 @@ +export const setValidEnvironmentName = (name: string | undefined): string => { + const env = name.toLowerCase(); + const validEnvs = ['development', 'test', 'staging']; + if (name && name.trim().length > 0 && validEnvs.includes(env)) return env; + return ''; +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index fb0775468a..f2a46c1228 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -4,6 +4,7 @@ import { afterNavigate, goto } from '$app/navigation'; import { page, updated } from '$app/state'; + import BottomNavNamespaces from '$lib/components/bottom-nav-namespaces.svelte'; import BottomNavigation from '$lib/components/bottom-nav.svelte'; import DataEncoderSettings from '$lib/components/data-encoder-settings.svelte'; import NamespacePicker from '$lib/components/namespace-picker.svelte'; @@ -24,6 +25,7 @@ import type { NamespaceListItem, NavLinkListItem } from '$lib/types/global'; import { setCoreContext } from '$lib/utilities/core-context'; import DarkMode from '$lib/utilities/dark-mode'; + import { setValidEnvironmentName } from '$lib/utilities/environment-name'; import { routeForArchivalWorkflows, routeForBatchOperations, @@ -35,6 +37,7 @@ routeForWorkerDeployments, routeForWorkflows, } from '$lib/utilities/route-for'; + import ziggy from '$lib/vendor/ziggy-full-face.png'; import type { DescribeNamespaceResponse as Namespace } from '$types'; @@ -45,6 +48,10 @@ let { children }: Props = $props(); let isCloud = $derived(page.data?.settings?.runtimeEnvironment?.isCloud); + let environmentName = $derived( + setValidEnvironmentName(page.data?.settings?.buildEnvironment?.name), + ); + let activeNamespaceName = $derived( page.params?.namespace ?? $lastUsedNamespace, ); @@ -252,16 +259,17 @@ position={toaster.position} /> @@ -274,22 +282,33 @@ {/snippet} -
- - {@render children()} - -
- - - + {#snippet main()} +
+ + {@render children()} + +
+ {/snippet} + {#snippet footer()} + + + {#snippet namespacePicker({ open })} + + {/snippet} + {#snippet avatar()} + {translate('common.user-profile')} + {/snippet} + + {/snippet}