Files
keystone/resources/js/pages/environment-variables/Index.vue
Harry Bayliss 5b977c1f41
Some checks failed
CI / Lint (push) Failing after 22s
CI / Tests (push) Failing after 33s
wowowowowo
2026-05-28 15:15:41 +01:00

226 lines
9.3 KiB
Vue

<script setup lang="ts">
import InputError from "@/components/InputError.vue";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import AppLayout from "@/layouts/AppLayout.vue";
import { Head, Link, router, useForm } from "@inertiajs/vue3";
import { CopyIcon, EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-vue-next";
import { ref } from "vue";
defineProps<{
application: Record<string, any>;
environment: Record<string, any>;
variables: Record<string, any>[];
}>();
const importForm = useForm({
contents: "",
overridable: true,
});
const revealedVariableIds = ref<number[]>([]);
const copiedVariableId = ref<number | null>(null);
const isRevealed = (variable: Record<string, any>): boolean =>
revealedVariableIds.value.includes(variable.id);
const toggleReveal = (variable: Record<string, any>): void => {
revealedVariableIds.value = isRevealed(variable)
? revealedVariableIds.value.filter((id) => id !== variable.id)
: [...revealedVariableIds.value, variable.id];
};
const copyValue = async (variable: Record<string, any>): Promise<void> => {
await navigator.clipboard.writeText(variable.value ?? "");
copiedVariableId.value = variable.id;
window.setTimeout(() => {
if (copiedVariableId.value === variable.id) {
copiedVariableId.value = null;
}
}, 1500);
};
const importVariables = (): void => {
importForm.post(
route("environment-variables.import", {
organisation: route().params.organisation,
application: route().params.application,
environment: route().params.environment,
}),
{
preserveScroll: true,
onSuccess: () => importForm.reset("contents"),
},
);
};
const destroyVariable = (variable: Record<string, any>): void => {
if (!window.confirm(`Delete ${variable.key}?`)) {
return;
}
router.delete(
route("environment-variables.destroy", {
organisation: route().params.organisation,
application: route().params.application,
environment: route().params.environment,
variable: variable.id,
}),
);
};
</script>
<template>
<Head :title="`${environment.name} Variables`" />
<AppLayout
:breadcrumbs="[
{
title: application.name,
href: route('applications.show', {
organisation: $page.props.organisation.id,
application: application.id,
}),
},
{
title: environment.name,
href: route('environments.show', {
organisation: $page.props.organisation.id,
application: application.id,
environment: environment.id,
}),
},
{ title: 'Variables' },
]"
>
<div class="flex h-full flex-1 flex-col gap-4 p-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-3xl font-bold tracking-tight">Environment Variables</h2>
<p class="mt-1 text-sm text-muted-foreground">
User values can be edited. Managed values show their source and lock state.
</p>
</div>
<Button
:as="Link"
:href="
route('environment-variables.create', {
organisation: $page.props.organisation.id,
application: application.id,
environment: environment.id,
})
"
>
<PlusIcon class="size-4" />
Add variable
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>Variables</CardTitle>
<CardDescription>{{ variables.length }} configured values</CardDescription>
</CardHeader>
<CardContent class="grid gap-2">
<div
v-for="variable in variables"
:key="variable.id"
class="flex flex-wrap items-center gap-3 rounded-md border p-3 text-sm"
>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<Link
:href="
route('environment-variables.edit', {
organisation: $page.props.organisation.id,
application: application.id,
environment: environment.id,
variable: variable.id,
})
"
class="font-medium hover:underline"
>
{{ variable.key }}
</Link>
<Badge :variant="variable.source === 'user' ? 'secondary' : 'outline'">
{{ variable.source.replace('_', ' ') }}
</Badge>
<Badge v-if="!variable.overridable" variant="outline">locked</Badge>
<Badge variant="outline">secret</Badge>
</div>
<p class="mt-1 text-muted-foreground">
{{ variable.service_slice?.name ?? "No slice source" }}
</p>
</div>
<code class="max-w-full truncate rounded bg-muted px-2 py-1 text-xs">
{{ isRevealed(variable) ? variable.value : "••••••••" }}
</code>
<Button
size="iconxs"
variant="ghost"
:aria-label="isRevealed(variable) ? `Hide ${variable.key}` : `Reveal ${variable.key}`"
@click="toggleReveal(variable)"
>
<EyeOffIcon v-if="isRevealed(variable)" class="size-3" />
<EyeIcon v-else class="size-3" />
</Button>
<Button
size="iconxs"
variant="ghost"
:aria-label="`Copy ${variable.key}`"
@click="copyValue(variable)"
>
<CopyIcon class="size-3" />
<span class="sr-only">
{{ copiedVariableId === variable.id ? "Copied" : "Copy" }}
</span>
</Button>
<Button size="iconxs" variant="ghost" @click="destroyVariable(variable)">
<Trash2Icon class="size-3" />
</Button>
</div>
<div
v-if="variables.length === 0"
class="rounded-md border border-dashed p-4 text-sm text-muted-foreground"
>
No variables configured.
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Bulk Import</CardTitle>
<CardDescription>Paste KEY=value lines from an .env file.</CardDescription>
</CardHeader>
<CardContent>
<form class="grid gap-3" @submit.prevent="importVariables">
<textarea
v-model="importForm.contents"
class="min-h-40 rounded-md border border-input bg-transparent p-3 font-mono text-sm"
placeholder="APP_ENV=production&#10;APP_DEBUG=false"
required
/>
<InputError :message="importForm.errors.contents" />
<label class="flex items-center gap-2 text-sm">
<input
v-model="importForm.overridable"
type="checkbox"
class="size-4"
/>
Imported values are overridable
</label>
<InputError :message="importForm.errors.overridable" />
<div>
<Button type="submit" :disabled="importForm.processing">
Import variables
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
</AppLayout>
</template>