Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Switch between multiple instances #21

Merged
merged 1 commit into from
Dec 3, 2024
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
20 changes: 17 additions & 3 deletions app/app.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<!-- <Login v-if="!credentials" />-->
<div class="relative h-dvh">
<NuxtPage :page-key="route.fullPath" />
<NuxtPage :page-key="pageKey" />
<DebugMemory
v-if="IS_DEV_MODE && config.public.debugMemoryUsage"
class="absolute bottom-0 flex w-full items-center justify-center gap-4 pb-6 text-xs text-gray-600" />
Expand All @@ -16,12 +16,22 @@ import Toaster from '~/components/layout/toasts/Toaster.vue'
import ConfirmationDialog from '~/components/layout/ConfirmationDialog.vue'
import PromisifiedDialogs from '~/components/layout/dialogs/PromisifiedDialogs.vue'
import { safeToRefs } from '~/utils'
import { useConfirmationDialog } from '~/stores'
import { useConfirmationDialog, useCredentials } from '~/stores'
import { toRefs } from 'vue'

const route = useRoute()
const { confirmationDialog } = safeToRefs(useConfirmationDialog())
const IS_DEV_MODE = import.meta.dev
const config = useRuntimeConfig()
const { credentials } = toRefs(useCredentials())
const self: any = reactive({
credentials,
pageKey: computed(() => {
return [route.fullPath, self.credentials?.id].join(':')
}),
})

const { pageKey } = toRefs(self)

useHead({
htmlAttrs: {
Expand All @@ -31,7 +41,11 @@ useHead({
class: 'h-full',
},
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} | Meiliweb` : 'Meiliweb'
let appName = 'Meiliweb'
if (self.credentials) {
appName += ` - ${self.credentials.name || self.credentials.baseUri}`
}
return titleChunk ? `${titleChunk} | ${appName}` : appName
},
})
</script>
Expand Down
168 changes: 142 additions & 26 deletions app/components/layout/TopNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,108 @@
<header
:class="[
open ? 'fixed inset-0 z-40 overflow-y-auto' : '',
'bg-white lg:static lg:overflow-y-visible',
'relative z-10 bg-white lg:static lg:overflow-y-visible', // Added z-50 and relative
]">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div
class="relative flex justify-between lg:gap-8 xl:grid xl:grid-cols-12">
<div
class="flex md:absolute md:inset-y-0 md:left-0 lg:static xl:col-span-2">
<div class="flex flex-shrink-0 items-center">
<a href="/" class="flex items-center gap-2">
<img
class="size-16 shrink-0 grow-0"
src="/assets/images/logo.svg"
alt="Meiliweb" />
<span class="flex flex-col">
<span class="text-lg font-semibold">Meiliweb</span>
<span class="-mt-1 text-sm font-light text-gray-600">
{{ credentials.baseUri }} - Meilisearch
{{ version?.pkgVersion }}
</span>
</span>
</a>
<div class="flex items-center gap-2">
<a href="/">
<img
class="size-16 shrink-0 grow-0"
src="~/assets/images/logo.svg"
alt="Meiliweb" />
</a>
<div class="flex flex-col">
<a href="/" class="text-lg font-semibold">Meiliweb</a>
<div class="-mt-1 flex items-center gap-2">
<a href="/" class="text-sm font-light text-gray-600">
{{ credentials!.baseUri }} - Meilisearch
{{ version?.pkgVersion }}
</a>
<Menu as="div" class="relative -mt-1">
<MenuButton
v-tippy="t('actions.switchInstance')"
class="rounded-md text-sm font-medium text-gray-900 hover:bg-gray-50">
<Icon name="heroicons:chevron-down" class="size-4" />
</MenuButton>
<transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95">
<MenuItems
class="absolute right-0 mt-2 w-72 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div v-if="savedInstances.length > 0">
<MenuItem
v-for="instance in savedInstances.filter(
({ id }) => id !== credentials?.id,
)"
:key="instance.baseUri"
v-slot="{ active }">
<span
:class="[
active ? 'bg-gray-50' : '',
'flex w-full items-center justify-between rounded-md px-2 py-1.5 text-xs',
]">
<button
type="button"
class="flex w-full items-center gap-2"
@click="switchInstance(instance.id)">
<Icon
:name="
instance.baseUri === credentials?.baseUri
? 'heroicons:server'
: 'heroicons:server'
"
class="h-5 w-5" />
<span class="flex flex-col items-start">
<span>
{{ instance.name || instance.baseUri }}
</span>
<span
v-if="instance.name"
class="text-xs text-gray-500">
{{ instance.baseUri }}
</span>
</span>
</button>
<button
@click="removeInstance(instance.id)"
class="text-gray-400 hover:text-gray-600">
<Icon
name="heroicons:trash"
class="h-4 w-4" />
</button>
</span>
</MenuItem>
</div>
<div>
<MenuItem v-slot="{ active }">
<NuxtLink
to="/login"
:class="[
active ? 'bg-gray-50' : '',
'flex items-center gap-2 rounded-md px-2 py-1.5 text-xs',
]">
<Icon
name="heroicons:plus-circle"
class="h-5 w-5" />
{{ t('actions.connectToInstance') }}
</NuxtLink>
</MenuItem>
</div>
</MenuItems>
</transition>
</Menu>
</div>
</div>
</div>
</div>
</div>
<div class="min-w-0 flex-1 md:px-8 lg:px-0 xl:col-span-6">
Expand Down Expand Up @@ -111,47 +192,82 @@
</Popover>
</template>

<script setup>
import { useRoute } from '#app'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
<script setup lang="ts">
import { safeToRefs, useConfirmationDialog, useRoute } from '#imports'
import {
Menu,
MenuButton,
MenuItem,
MenuItems,
Popover,
PopoverButton,
PopoverPanel,
} from '@headlessui/vue'
import { MagnifyingGlassIcon } from '@heroicons/vue/20/solid'
import { Bars3Icon, XMarkIcon } from '@heroicons/vue/24/outline'
import { asyncComputed } from '@vueuse/core'
import { computed, reactive } from 'vue'
import { useCredentials } from '~/stores'
import { safeToRefs } from '~/utils'
import { computed, reactive, toRefs } from 'vue'
import GithubButton from '~/components/layout/GithubButton.vue'
import LogoutButton from '~/components/layout/LogoutButton.vue'
import { useCredentials } from '~/stores'

const route = useRoute()
const navigation = reactive([
{
name: 'Indexes',
href: '/indexes',
current: computed(() => route.name.startsWith('indexes')),
current: computed(() => route.name?.startsWith('indexes')),
},
{
name: 'Access Keys',
href: '/keys',
current: computed(() => route.name.startsWith('keys')),
current: computed(() => route.name?.startsWith('keys')),
},
{
name: 'Tasks',
href: '/tasks',
current: computed(() => route.name.startsWith('tasks')),
current: computed(() => route.name?.startsWith('tasks')),
},
{
name: 'Dumps',
href: '/dumps',
current: computed(() => route.name.startsWith('dumps')),
current: computed(() => route.name?.startsWith('dumps')),
},
{
name: 'Snapshots',
href: '/snapshots',
current: computed(() => route.name.startsWith('snapshots')),
current: computed(() => route.name?.startsWith('snapshots')),
},
])
const { credentials } = safeToRefs(useCredentials())

const {
credentials,
records,
switchInstance,
removeInstance: doRemoveInstance,
} = safeToRefs(useCredentials())
const meili = useMeiliClient()
const { confirm } = useConfirmationDialog()
const version = asyncComputed(() => meili.getVersion())
const self: any = reactive({
records,
savedInstances: computed(() => Array.from(self.records.values())),
})

const removeInstance = async (id: string) => {
if (await confirm({ text: t('confirm.removeInstance') })) {
doRemoveInstance(id)
}
}
const { t } = useI18n()
const { savedInstances } = toRefs(self)
</script>

<i18n>
en:
actions:
connectToInstance: Connect to another instance
switchInstance: Switch Instance
confirm:
removeInstance: Are you sure you want to log out from this instance?
</i18n>
50 changes: 43 additions & 7 deletions app/pages/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
<div class="bg-bubbles flex h-dvh flex-col items-center justify-center">
<div
class="-mt-20 w-full max-w-lg space-y-6 rounded-lg border-gray-200 bg-white bg-opacity-90 px-6 py-4 md:w-1/2 md:border md:px-0 md:shadow-lg">
<section class="flex items-center justify-center gap-2">
<NuxtLink to="/indexes" class="flex items-center justify-center gap-2">
<img
class="-ml-10 size-16 shrink-0 grow-0"
src="/assets/images/logo.svg"
src="~/assets/images/logo.svg"
alt="Meiliweb" />
<span class="text-3xl font-semibold">Meiliweb</span>
</section>
</NuxtLink>

<form class="space-y-4 p-4" @submit.prevent="submit(credentials)">
<h1 class="text-lg font-semibold">
Expand All @@ -27,7 +27,8 @@
required
type="url"
class="form-input"
placeholder="http://localhost:7700" />
:placeholder="DEFAULT_BASE_URI"
@keydown.tab="autofillBaseUri()" />
</UniqueId>

<UniqueId as="section" v-slot="{ id }" class="flex flex-col gap-1">
Expand All @@ -38,6 +39,16 @@
class="form-input" />
</UniqueId>

<UniqueId as="section" v-slot="{ id }" class="flex flex-col gap-1">
<label :for="id">{{ t('labels.instanceName') }}</label>
<input
v-model="credentials.name"
type="text"
class="form-input"
:placeholder="suggestedName"
@keydown.tab="autofillName()" />
</UniqueId>

<Button
type="submit"
icon="solar:login-linear"
Expand All @@ -56,15 +67,35 @@ import { useCredentials } from '~/stores'
import { useFormSubmit } from '~/composables'
import Alert from '~/components/layout/Alert.vue'
import Button from '~/components/layout/forms/Button.vue'
import { toRefs } from 'vue'

const { save, factory, auth } = useCredentials()
const { auth, factory } = useCredentials()
const { loading, error, handle } = useFormSubmit()
const credentials = ref(factory())
const self = reactive({
const self: any = reactive({
credentials,
error,
suggestedName: computed(() =>
'' === self.credentials.baseUri ||
self.credentials.baseUri?.indexOf('localhost') > -1
? t('placeholders.localInstance')
: t('placeholders.productionInstance'),
),
})

const DEFAULT_BASE_URI = 'http://localhost:7700'
const autofillBaseUri = () => {
if ('' === self.credentials.baseUri) {
self.credentials.baseUri = DEFAULT_BASE_URI
}
}

const autofillName = () => {
if ('' === self.credentials.name) {
self.credentials.name = self.suggestedName
}
}

const submit = async (credentials: CredentialsRecord) => {
const meili = new Meilisearch({
host: credentials.baseUri,
Expand All @@ -73,7 +104,6 @@ const submit = async (credentials: CredentialsRecord) => {

try {
await handle(() => meili.getVersion(), true)
//save(credentials)
auth(credentials)
navigateTo('/indexes')
} catch (e) {
Expand All @@ -83,6 +113,8 @@ const submit = async (credentials: CredentialsRecord) => {
self.credentials = factory()
}
const { t } = useI18n()
const { suggestedName } = toRefs(self)

useHead({
title: t('title'),
})
Expand All @@ -95,7 +127,11 @@ en:
labels:
instanceUrl: "Instance URL:"
accessToken: "Access Token:"
instanceName: "Instance Name (optional):"
submit: "Connect"
placeholders:
localInstance: "Local instance"
productionInstance: "Production"
</i18n>

<style>
Expand Down
Loading
Loading