226 lines
9.3 KiB
Vue
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 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>
|