WIP Environment UI
This commit is contained in:
3
.notes/deploying-an-environment.md
Normal file
3
.notes/deploying-an-environment.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Deploying an environment
|
||||||
|
|
||||||
|
If a server does not have an application container allocated, it will need one before deploying.
|
||||||
@@ -12,7 +12,7 @@ class EnvironmentController extends Controller
|
|||||||
$id = $request->route('environment');
|
$id = $request->route('environment');
|
||||||
|
|
||||||
return inertia('environments/Show', [
|
return inertia('environments/Show', [
|
||||||
'environment' => Environment::findOrFail($id),
|
'environment' => Environment::with('application')->findOrFail($id),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,26 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||||
|
|
||||||
class Environment extends Model
|
class Environment extends Model
|
||||||
{
|
{
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
public function application(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Application::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function slices(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Slice::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function services(): HasManyThrough
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(Service::class, Slice::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Drivers\Driver;
|
|||||||
use App\Enums\ServiceCategory;
|
use App\Enums\ServiceCategory;
|
||||||
use App\Enums\ServiceStatus;
|
use App\Enums\ServiceStatus;
|
||||||
use App\Enums\ServiceType;
|
use App\Enums\ServiceType;
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
@@ -27,6 +28,13 @@ class Service extends Model
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function folderName(): Attribute
|
||||||
|
{
|
||||||
|
return new Attribute(
|
||||||
|
get: fn () => $this->name . '-' . $this->id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function server(): BelongsTo
|
public function server(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Server::class);
|
return $this->belongsTo(Server::class);
|
||||||
|
|||||||
@@ -90,3 +90,13 @@ html {
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pattern-graph-paper {
|
||||||
|
background-color: #ffffff;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23a8a6ac' fill-opacity='0.4'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .pattern-graph-paper {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23454545' fill-opacity='0.4'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ defineProps<{
|
|||||||
<template v-if="index === breadcrumbs.length - 1">
|
<template v-if="index === breadcrumbs.length - 1">
|
||||||
<BreadcrumbPage>{{ item.title }}</BreadcrumbPage>
|
<BreadcrumbPage>{{ item.title }}</BreadcrumbPage>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else-if="item.href">
|
||||||
<BreadcrumbLink as-child>
|
<BreadcrumbLink as-child>
|
||||||
<Link :href="item.href ?? '#'">{{ item.title }}</Link>
|
<Link :href="item.href ?? '#'">{{ item.title }}</Link>
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
</template>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator v-if="index !== breadcrumbs.length - 1" />
|
<BreadcrumbSeparator v-if="index !== breadcrumbs.length - 1" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
58
resources/js/components/environments/ServiceCard.vue
Normal file
58
resources/js/components/environments/ServiceCard.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import ServiceCategory from '@/enums/ServiceCategory';
|
||||||
|
import ServiceStatus from '@/enums/ServiceStatus';
|
||||||
|
import ServiceType from '@/enums/ServiceType';
|
||||||
|
import { DoorOpenIcon } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
icon: {
|
||||||
|
type: [Object, Function],
|
||||||
|
default: () => DoorOpenIcon,
|
||||||
|
},
|
||||||
|
serviceType: {
|
||||||
|
type: String,
|
||||||
|
default: ServiceType.GATEWAY,
|
||||||
|
},
|
||||||
|
serviceCategory: {
|
||||||
|
type: String,
|
||||||
|
default: ServiceCategory.DATABASE
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
default: ServiceStatus.UNKNOWN,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Card class="flex select-none items-center justify-between gap-4 bg-card/30 p-4 backdrop-blur-sm">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<component :is="icon" class="size-4 opacity-50" />
|
||||||
|
<div>
|
||||||
|
<div class="capitalize">{{ serviceCategory }}</div>
|
||||||
|
<div class="text-xs opacity-50">{{ serviceType }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span
|
||||||
|
class="inline-block size-1 rounded-full dark:bg-zinc-500"
|
||||||
|
:class="{
|
||||||
|
'bg-zinc-300 dark:bg-zinc-500': status === ServiceStatus.UNKNOWN || status === ServiceStatus.NOT_INSTALLED,
|
||||||
|
'bg-green-300 dark:bg-green-500': status === ServiceStatus.RUNNING,
|
||||||
|
'bg-red-300 dark:bg-red-500': status === ServiceStatus.STOPPED,
|
||||||
|
'bg-yellow-300 dark:bg-yellow-500': status === ServiceStatus.INSTALLING,
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="text-xs dark:text-zinc-500"
|
||||||
|
:class="{
|
||||||
|
'text-zinc-300 dark:text-zinc-500': status === ServiceStatus.UNKNOWN || status === ServiceStatus.NOT_INSTALLED,
|
||||||
|
'text-green-300 dark:text-green-500': status === ServiceStatus.RUNNING,
|
||||||
|
'text-red-300 dark:text-red-500': status === ServiceStatus.STOPPED,
|
||||||
|
'text-yellow-300 dark:text-yellow-500': status === ServiceStatus.INSTALLING,
|
||||||
|
}"
|
||||||
|
>{{ status.replaceAll('-', ' ') }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import AppLayout from '@/layouts/AppLayout.vue';
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||||||
import { type BreadcrumbItem } from '@/types';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Layers2Icon } from 'lucide-vue-next';
|
||||||
import PlaceholderPattern from '../components/PlaceholderPattern.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
application: {
|
application: {
|
||||||
@@ -15,8 +16,64 @@ const props = defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<Head title="Dashboard" />
|
<Head title="Dashboard" />
|
||||||
|
|
||||||
<AppLayout>
|
<AppLayout
|
||||||
|
:breadcrumbs="[
|
||||||
|
{
|
||||||
|
title: 'Applications',
|
||||||
|
href: route('applications.index', { organisation: $page.props.organisation.id }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: props.application.name,
|
||||||
|
href: route('applications.show', {
|
||||||
|
organisation: $page.props.organisation.id,
|
||||||
|
application: props.application.id,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
<div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
|
<div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<h2 class="text-3xl font-bold tracking-tight">{{ application.name }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<h3 class="text-2xl font-semibold tracking-tight">Environments</h3>
|
||||||
|
<div>
|
||||||
|
<!-- <Button
|
||||||
|
:as="Link"
|
||||||
|
:href="
|
||||||
|
route('environments.create', {
|
||||||
|
organisation: $page.props.organisation.id,
|
||||||
|
server: application.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
<PlusIcon class="size-4" />
|
||||||
|
Add
|
||||||
|
</Button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card v-for="environment in application.environments" :key="environment.id" class="relative">
|
||||||
|
<Link class="absolute inset-0" :href="route('environments.show', {
|
||||||
|
organisation: $page.props.organisation.id,
|
||||||
|
application: application.id,
|
||||||
|
environment: environment.id,
|
||||||
|
})"></Link>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<CardTitle>{{ environment.name }}</CardTitle>
|
||||||
|
<Badge :variant="environment.status === 'active' ? 'success' : 'secondary'">{{ environment.status.replace('-', ' ') }}</Badge>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
<span class="capitalize">{{ environment.type }}</span> {{ environment.version }}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ application }}
|
{{ application }}
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ServiceCard from '@/components/environments/ServiceCard.vue';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import ServiceCategory from '@/enums/ServiceCategory';
|
||||||
|
import ServiceStatus from '@/enums/ServiceStatus';
|
||||||
|
import ServiceType from '@/enums/ServiceType';
|
||||||
import AppLayout from '@/layouts/AppLayout.vue';
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||||||
import { type BreadcrumbItem } from '@/types';
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import PlaceholderPattern from '../components/PlaceholderPattern.vue';
|
import { AppWindowIcon, DatabaseIcon, DatabaseZap, DoorOpenIcon, PlusIcon } from 'lucide-vue-next';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
environment: {
|
environment: {
|
||||||
@@ -15,8 +19,66 @@ const props = defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<Head :title="environment.name" />
|
<Head :title="environment.name" />
|
||||||
|
|
||||||
<AppLayout>
|
<AppLayout
|
||||||
|
:breadcrumbs="[
|
||||||
|
{
|
||||||
|
title: 'Applications',
|
||||||
|
href: route('applications.index', { organisation: $page.props.organisation.id }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: environment.application.name,
|
||||||
|
href: route('applications.show', {
|
||||||
|
organisation: $page.props.organisation.id,
|
||||||
|
application: environment.application.id,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Environments',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: environment.name,
|
||||||
|
href: route('environments.show', {
|
||||||
|
organisation: $page.props.organisation.id,
|
||||||
|
application: environment.application.id,
|
||||||
|
environment: environment.id,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
<div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
|
<div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
|
||||||
|
<Card class="pattern-graph-paper grid grid-cols-3 gap-6 p-6">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Card class="flex select-none items-center gap-2 bg-card/30 p-4 backdrop-blur-sm text-sm group">
|
||||||
|
<PlusIcon class="size-4 opacity-50" />
|
||||||
|
<span class="opacity-50 group-hover:opacity-100 transition">Install a gateway</span>
|
||||||
|
</Card>
|
||||||
|
<!-- <ServiceCard :icon="DoorOpenIcon" :service-type="ServiceType.CADDY" :service-category="ServiceCategory.GATEWAY" :status="ServiceStatus.NOT_INSTALLED" /> -->
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Card class="flex select-none items-center gap-2 bg-card/30 p-4 backdrop-blur-sm text-sm group">
|
||||||
|
<PlusIcon class="size-4 opacity-50" />
|
||||||
|
<span class="opacity-50 group-hover:opacity-100 transition">Install your application on a server</span>
|
||||||
|
</Card>
|
||||||
|
<!-- <ServiceCard
|
||||||
|
:icon="AppWindowIcon"
|
||||||
|
:service-type="ServiceType.FRANKENPHP"
|
||||||
|
:service-category="ServiceCategory.APPLICATION"
|
||||||
|
:status="ServiceStatus.NOT_INSTALLED"
|
||||||
|
/> -->
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<!-- <ServiceCard :icon="DatabaseIcon" :service-type="ServiceType.POSTGRES" :service-category="ServiceCategory.DATABASE" :status="ServiceStatus.NOT_INSTALLED" /> -->
|
||||||
|
<!-- <ServiceCard :icon="DatabaseZap" :service-type="ServiceType.REDIS" :service-category="ServiceCategory.CACHE" :status="ServiceStatus.NOT_INSTALLED" /> -->
|
||||||
|
<Card class="flex select-none items-center gap-2 bg-card/30 p-4 backdrop-blur-sm text-sm group">
|
||||||
|
<PlusIcon class="size-4 opacity-50" />
|
||||||
|
<span class="opacity-50 group-hover:opacity-100 transition">Add a database</span>
|
||||||
|
</Card>
|
||||||
|
<Card class="flex select-none items-center gap-2 bg-card/30 p-4 backdrop-blur-sm text-sm group">
|
||||||
|
<PlusIcon class="size-4 opacity-50" />
|
||||||
|
<span class="opacity-50 group-hover:opacity-100 transition">Add cache</span>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
{{ environment }}
|
{{ environment }}
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user