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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ src/routes/
auth/ # sign-in / callback / sign-out (unauthenticated)
(auth)/ # layout group — redirects to /auth/signin if no token
(project)/ # layout group — requires ?project= param
audit-log/ deployment/ disk/ domain/ dropbox/ email/
audit-log/ deployment/ disk/ domain/ dropbox/ email/ env-group/
pull-secret/ registry/ role/ route/
service-account/ workload-identity/
billing/
Expand Down
1 change: 1 addition & 0 deletions src/routes/(auth)/(project)/audit-log/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
{ value: 'deployment', label: 'Deployment' },
{ value: 'disk', label: 'Disk' },
{ value: 'domain', label: 'Domain' },
{ value: 'envGroup', label: 'Env Group' },
{ value: 'pullSecret', label: 'Pull Secret' },
{ value: 'role', label: 'Role' },
{ value: 'serviceAccount', label: 'Service Account' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,30 @@
</div>
</div>

<h6><strong>Env Groups</strong></h6>
<div class="nm-table-container">
<table class="nm-table is-variant-compact" style="--table-data-border-color: none">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{#each deployment.envGroups || [] as name (name)}
<tr>
<td>
<a class="nm-link" href={`/env-group/create?project=${deployment.project}&name=${name}`}>
{name}
</a>
</td>
</tr>
{:else}
<NoDataRow span={1} />
{/each}
</tbody>
</table>
</div>

<h6><strong>Environment Variables</strong></h6>
<div class="nm-table-container">
<table class="nm-table is-variant-compact" style="--table-data-border-color: none">
Expand Down
108 changes: 107 additions & 1 deletion src/routes/(auth)/(project)/deployment/deploy/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
const permission = $state({
pullSecrets: true,
workloadIdentities: true,
disks: true
disks: true,
envGroups: true
})

/** @type {Api.PullSecret[]} */
Expand All @@ -29,6 +30,9 @@
/** @type {Api.Disk[]} */
let disks = $state([])

/** @type {Api.EnvGroup[]} */
let envGroups = $state([])

const form = $state({
location: deployment?.location || '',
name: '',
Expand Down Expand Up @@ -61,6 +65,8 @@
},
/** @type {{ k: string, v: string }[]} */
env: [],
/** @type {string[]} */
envGroups: [],
/** @type {{ k: string, v: string }[]} */
mountData: [],
/** @type {Api.SidecarForm[]} */
Expand Down Expand Up @@ -91,6 +97,7 @@
form.maxReplicas = deployment.maxReplicas
form.resources = deployment.resources
form.env = Object.entries(deployment.env || {}).map(([k, v]) => ({ k, v }))
form.envGroups = [...(deployment.envGroups || [])]
form.mountData = Object.entries(deployment.mountData || {}).map(([k, v]) => ({ k, v }))
form.sidecars = (deployment.sidecars || []).map((s) => {
if (s.cloudSqlProxy) {
Expand Down Expand Up @@ -178,6 +185,19 @@
disks = resp.result.items ?? []
}

async function fetchEnvGroups () {
const resp = await api.invoke('envGroup.list', { project }, fetch)
if (!resp.ok) {
if (resp.error?.forbidden) {
permission.envGroups = false
return
}
modal.error({ error: resp.error })
return
}
envGroups = resp.result.items ?? []
}

async function changeLocation () {
pullSecrets = []
workloadIdentities = []
Expand Down Expand Up @@ -208,6 +228,34 @@
.join('\n')
}

let envGroupInput = $state('')

/**
* @param {string} name
*/
function addEnvGroup (name) {
const n = name.trim()
if (!n || form.envGroups.includes(n)) return
form.envGroups = [...form.envGroups, n]
}

function selectEnvGroupChanged (e) {
addEnvGroup(e.target.value)
e.target.value = ''
}

function addEnvGroupFromInput () {
addEnvGroup(envGroupInput)
envGroupInput = ''
}

/**
* @param {string} name
*/
function removeEnvGroup (name) {
form.envGroups = form.envGroups.filter((g) => g !== name)
}

function convertSidecars () {
return form.sidecars
.filter((s) => s.type)
Expand Down Expand Up @@ -283,6 +331,7 @@
onMount(() => {
changeLocation()
parseEnvValue()
fetchEnvGroups()
})
</script>

Expand Down Expand Up @@ -666,6 +715,63 @@
<hr>
<br>

<h6><strong>Env Groups</strong></h6>
<small class="helper">
Env groups are project-scoped sets of environment variables that are merged into the deployment.
Variables defined here take precedence over those defined in env groups.
</small>
{#if permission.envGroups}
<div class="nm-field _dp-f">
<div class="nm-select">
{#key envGroups}
<select onchange={selectEnvGroupChanged}>
<option value="" disabled selected>Select Env Group</option>
{#each envGroups.filter((g) => !form.envGroups.includes(g.name)) as it (it.name)}
<option value={it.name}>{it.name}</option>
{/each}
</select>
{/key}
</div>
</div>
{:else}
<div class="nm-field _dp-f _g-5">
<div class="nm-input _f-1">
<input placeholder="Env Group Name" bind:value={envGroupInput}>
</div>
<button class="nm-button" type="button" onclick={addEnvGroupFromInput}>Add</button>
</div>
<p class="_fs-1">* You don't have permission to list env groups</p>
{/if}
<div class="nm-table-container">
<table class="nm-table is-variant-compact">
<thead>
<tr>
<th>Name</th>
<th class="is-collapse is-align-right"></th>
</tr>
</thead>
<tbody>
{#each form.envGroups as name (name)}
<tr>
<td>{name}</td>
<td>
<button class="icon-button" type="button" aria-label="Remove env group"
onclick={() => removeEnvGroup(name)}>
<i class="fa-solid fa-trash-alt"></i>
</button>
</td>
</tr>
{:else}
<tr>
<td colspan="2" class="_tta-c _co-co-50">No env groups</td>
</tr>
{/each}
</tbody>
</table>
</div>

<hr>

<h6><strong>Environment Variables</strong></h6>
<div>
<div class="nm-table-container">
Expand Down
6 changes: 6 additions & 0 deletions src/routes/(auth)/(project)/env-group/+layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function load () {
return {
menu: 'env-group',
overrideRedirect: '/env-group'
}
}
12 changes: 12 additions & 0 deletions src/routes/(auth)/(project)/env-group/+page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import api from '$lib/api'

export async function load ({ parent, fetch }) {
const { project } = await parent()

/** @type {Api.Response<Api.List<Api.EnvGroup>>} */
const res = await api.invoke('envGroup.list', { project }, fetch)
return {
envGroups: res.result?.items ?? [],
error: res.error
}
}
60 changes: 60 additions & 0 deletions src/routes/(auth)/(project)/env-group/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script>
import NoDataRow from '$lib/components/NoDataRow.svelte'
import * as format from '$lib/format'
import ErrorRow from '$lib/components/ErrorRow.svelte'

const { data } = $props()

const project = $derived(data.project)
const envGroups = $derived(data.envGroups)
const error = $derived(data.error)
</script>

<h6>Env Groups</h6>
<br>
<div class="nm-panel is-level-300">
<div class="_dp-f _jtfct-spbtw _alit-ct">
<div class="lo-grid-span-horizontal _g-4 _mgl-at">
<a class="nm-button" href="/env-group/create?project={project}">
Create
</a>
</div>
</div>

<div class="nm-table-container _mgt-6">
<table class="nm-table is-variant-compact">
<thead>
<tr>
<th>Name</th>
<th>Variables</th>
<th>Created at</th>
<th>Created by</th>
<th class="is-collapse is-align-right"></th>
</tr>
</thead>
<tbody>
{#each envGroups as it (it.name)}
<tr>
<td>
<a class="nm-link" href="/env-group/create?project={project}&name={it.name}">
<strong>{it.name}</strong>
</a>
</td>
<td>{Object.keys(it.env ?? {}).length}</td>
<td>{format.datetime(it.createdAt)}</td>
<td>{it.createdBy}</td>
<td>
<a href="/env-group/create?project={project}&name={it.name}" aria-label="Edit">
<div class="icon-button">
<i class="fa-solid fa-pen"></i>
</div>
</a>
</td>
</tr>
{/each}
<NoDataRow span={5} list={envGroups} />
<ErrorRow span={5} {error} />
</tbody>
</table>
</div>
</div>
24 changes: 24 additions & 0 deletions src/routes/(auth)/(project)/env-group/create/+page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { redirect, error } from '@sveltejs/kit'
import api from '$lib/api'

export async function load ({ url, parent, fetch }) {
const { project } = await parent()
const name = url.searchParams.get('name')

let envGroup = null
if (name) {
/** @type {Api.Response<Api.EnvGroup>} */
const res = await api.invoke('envGroup.get', { project, name }, fetch)
if (!res.ok) {
if (res.error?.notFound) redirect(302, `/env-group?project=${project}`)
error(500, res.error?.message)
}
if (!res.result) redirect(302, `/env-group?project=${project}`)
envGroup = res.result
}

return {
menu: 'env-group',
envGroup
}
}
Loading