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 @@
-
-
-
+ {@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}
/>
-
-
+
+ {#snippet bottomActions()}
+
+ {/snippet}
@@ -274,22 +282,33 @@
{/snippet}
-
-
- {@render children()}
-
-
-
-
-
+ {#snippet main()}
+
+
+ {@render children()}
+
+
+ {/snippet}
+ {#snippet footer()}
+
+
+ {#snippet namespacePicker({ open })}
+
+ {/snippet}
+ {#snippet avatar()}
+
+ {/snippet}
+
+ {/snippet}