Skip to content

Commit

Permalink
feat: add & list server regions (#3423)
Browse files Browse the repository at this point in the history
* WIP create modal

* babababa

* create dialog looks ok

* FE largely there

* workss

* cleanup

* fixed up test plumbing to avoid deadlocks and simplify GQL calls

* test fix

* added all tests

* CI fix
  • Loading branch information
fabis94 authored Oct 31, 2024
1 parent c793bb1 commit 5df716b
Show file tree
Hide file tree
Showing 45 changed files with 1,248 additions and 101 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,7 @@ minio-data/
postgres-data/
redis-data/

.tshy-build
.tshy-build

# Server
multiregion.json
65 changes: 65 additions & 0 deletions packages/frontend-2/components/settings/server/Regions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<section>
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
<SettingsSectionHeader
title="Regions"
text="Manage the regions available for customizing data residency"
/>
<div class="flex flex-col space-y-6">
<div class="flex flex-row-reverse">
<div v-tippy="disabledMessage">
<FormButton
:disabled="!canCreateRegion"
@click="isAddEditDialogOpen = true"
>
Create
</FormButton>
</div>
</div>
<SettingsServerRegionsTable :items="tableItems" />
</div>
</div>
<SettingsServerRegionsAddEditDialog
v-model:open="isAddEditDialogOpen"
:available-region-keys="availableKeys"
/>
</section>
</template>
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import { graphql } from '~~/lib/common/generated/gql'
const isAddEditDialogOpen = ref(false)
const query = graphql(`
query SettingsServerRegions {
serverInfo {
multiRegion {
regions {
id
...SettingsServerRegionsTable_ServerRegionItem
}
availableKeys
}
}
}
`)
const pageFetchPolicy = usePageQueryStandardFetchPolicy()
const { result } = useQuery(query, undefined, () => ({
fetchPolicy: pageFetchPolicy.value
}))
const tableItems = computed(() => result.value?.serverInfo?.multiRegion?.regions)
const availableKeys = computed(
() => result.value?.serverInfo?.multiRegion?.availableKeys || []
)
const canCreateRegion = computed(() => availableKeys.value.length > 0)
const disabledMessage = computed(() => {
if (canCreateRegion.value) return undefined
if (!availableKeys.value.length) return 'No available region keys'
return undefined
})
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<LayoutDialog
v-model:open="open"
max-width="sm"
:buttons="dialogButtons"
hide-closer
prevent-close-on-click-outside
:on-submit="onSubmit"
>
<template #header>Create a new region</template>
<div class="flex flex-col gap-y-4 mb-2">
<FormTextInput
name="name"
label="Region name"
placeholder="Name"
color="foundation"
:rules="[isRequired, isStringOfLength({ maxLength: 64 })]"
auto-focus
autocomplete="off"
show-required
show-label
help="Human readable name for the region."
/>
<FormTextArea
name="description"
label="Region description"
placeholder="Description"
color="foundation"
size="lg"
show-label
show-optional
:rules="[isStringOfLength({ maxLength: 65536 })]"
/>
<SettingsServerRegionsKeySelect
show-label
name="key"
:items="availableRegionKeys"
label="Region key"
:rules="[isRequired]"
show-required
help="These keys come from the server multi region configuration file."
/>
</div>
</LayoutDialog>
</template>
<script lang="ts" setup>
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'
import type { LayoutDialogButton } from '@speckle/ui-components'
import { graphql } from '~/lib/common/generated/gql'
import type { SettingsServerRegionsAddEditDialog_ServerRegionItemFragment } from '~/lib/common/generated/gql/graphql'
import { useForm } from 'vee-validate'
import { useCreateRegion } from '~/lib/multiregion/composables/management'
import { useMutationLoading } from '@vue/apollo-composable'
graphql(`
fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {
id
name
description
key
}
`)
type ServerRegionItem = SettingsServerRegionsAddEditDialog_ServerRegionItemFragment
type DialogModel = Omit<ServerRegionItem, 'id'>
defineProps<{
availableRegionKeys: string[]
}>()
const open = defineModel<boolean>('open', { required: true })
const model = defineModel<DialogModel>()
const { handleSubmit, setValues } = useForm<DialogModel>()
const createRegion = useCreateRegion()
const loading = useMutationLoading()
const dialogButtons = computed((): LayoutDialogButton[] => {
return [
{
text: 'Cancel',
props: { color: 'outline' },
onClick: () => (open.value = false)
},
{
text: 'Create',
props: {
submit: true,
disabled: loading.value
},
onClick: noop
}
]
})
const isEditMode = computed(() => !!model.value)
const onSubmit = handleSubmit(async (values) => {
if (isEditMode.value) return // TODO:
const res = await createRegion({
input: values
})
if (res?.id) {
open.value = false
}
})
watch(
model,
(newVal, oldVal) => {
if (newVal && newVal !== oldVal) {
setValues(newVal)
}
},
{ immediate: true }
)
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<FormSelectBase
v-bind="props"
v-model="selectedValue"
:name="name || 'regions-key'"
:allow-unset="false"
mount-menu-on-body
>
<template #option="{ item }">
<div class="flex items-center">
<span class="truncate">{{ item }}</span>
</div>
</template>
<template #nothing-selected>
{{ multiple ? 'Select region keys' : 'Select a region key' }}
</template>
<template #something-selected="{ value }">
<template v-if="isArray(value)">
{{ value.join(', ') }}
</template>
<template v-else>
{{ value }}
</template>
</template>
</FormSelectBase>
</template>
<script setup lang="ts">
import { isArray } from 'lodash-es'
import type { RuleExpression } from 'vee-validate'
import { useFormSelectChildInternals } from '~/lib/form/composables/select'
type ValueType = string | string[] | undefined
const emit = defineEmits<{
(e: 'update:modelValue', v: ValueType): void
}>()
const props = defineProps<{
modelValue?: ValueType
label: string
items: string[]
multiple?: boolean
name?: string
showOptional?: boolean
showRequired?: boolean
showLabel?: boolean
labelId?: string
buttonId?: string
help?: string
rules?: RuleExpression<string | string[] | undefined>
}>()
const { selectedValue } = useFormSelectChildInternals<string>({
props: toRefs(props),
emit
})
</script>
77 changes: 77 additions & 0 deletions packages/frontend-2/components/settings/server/regions/Table.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<template>
<div>
<LayoutTable
:items="items"
:columns="[
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
{ id: 'key', header: 'Key', classes: 'col-span-2 truncate' },
{ id: 'description', header: 'Description', classes: 'col-span-6 truncate' },
{ id: 'actions', header: '', classes: 'col-span-1' }
]"
empty-message="No regions defined"
>
<template #name="{ item }">
{{ item.name }}
</template>
<template #key="{ item }">
<span class="text-foreground-2">{{ item.key }}</span>
</template>
<template #description="{ item }">
<span class="text-foreground-2">{{ item.description }}</span>
</template>
<template #actions="{ item }">
<template v-if="true">
<!-- Hiding actions for now -->
&nbsp;
</template>
<LayoutMenu
v-else
v-model:open="showActionsMenu[item.id]"
:items="actionItems"
mount-menu-on-body
:menu-position="HorizontalDirection.Left"
>
<FormButton
:color="showActionsMenu[item.id] ? 'outline' : 'subtle'"
hide-text
:icon-right="showActionsMenu[item.id] ? XMarkIcon : EllipsisHorizontalIcon"
@click.stop="toggleMenu(item.id)"
/>
</LayoutMenu>
</template>
</LayoutTable>
</div>
</template>
<script setup lang="ts">
import type { LayoutMenuItem } from '@speckle/ui-components'
import { HorizontalDirection } from '~~/lib/common/composables/window'
import { EllipsisHorizontalIcon, XMarkIcon } from '@heroicons/vue/24/outline'
import { graphql } from '~/lib/common/generated/gql'
import type { SettingsServerRegionsTable_ServerRegionItemFragment } from '~/lib/common/generated/gql/graphql'
graphql(`
fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {
id
name
key
description
}
`)
enum ActionTypes {
Edit = 'edit'
}
defineProps<{
items: SettingsServerRegionsTable_ServerRegionItemFragment[] | undefined
}>()
const showActionsMenu = ref<Record<string, boolean>>({})
const actionItems: LayoutMenuItem[][] = [
[{ title: 'Edit region', id: ActionTypes.Edit }]
]
const toggleMenu = (itemId: string) => {
showActionsMenu.value[itemId] = !showActionsMenu.value[itemId]
}
</script>
20 changes: 20 additions & 0 deletions packages/frontend-2/lib/common/generated/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ const documents = {
"\n fragment SettingsDialog_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n slug\n role\n name\n }\n": types.SettingsDialog_WorkspaceFragmentDoc,
"\n fragment SettingsDialog_User on User {\n id\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n": types.SettingsDialog_UserFragmentDoc,
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsServerProjects_ProjectCollectionFragmentDoc,
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": types.SettingsServerRegionsDocument,
"\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n": types.SettingsServerRegionsAddEditDialog_ServerRegionItemFragmentDoc,
"\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n": types.SettingsServerRegionsTable_ServerRegionItemFragmentDoc,
"\n fragment SettingsSharedProjects_Project on Project {\n id\n name\n visibility\n createdAt\n updatedAt\n models {\n totalCount\n }\n versions {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n }\n": types.SettingsSharedProjects_ProjectFragmentDoc,
"\n fragment SettingsUserEmails_User on User {\n id\n emails {\n ...SettingsUserEmailCards_UserEmail\n }\n }\n": types.SettingsUserEmails_UserFragmentDoc,
"\n fragment SettingsUserNotifications_User on User {\n id\n notificationPreferences\n }\n": types.SettingsUserNotifications_UserFragmentDoc,
Expand Down Expand Up @@ -185,6 +188,7 @@ const documents = {
"\n query GendoAIRenders($versionId: String!, $projectId: String!) {\n project(id: $projectId) {\n id\n version(id: $versionId) {\n id\n gendoAIRenders {\n totalCount\n items {\n id\n createdAt\n updatedAt\n status\n gendoGenerationId\n prompt\n camera\n }\n }\n }\n }\n }\n": types.GendoAiRendersDocument,
"\n subscription ProjectVersionGendoAIRenderCreated($id: String!, $versionId: String!) {\n projectVersionGendoAIRenderCreated(id: $id, versionId: $versionId) {\n id\n createdAt\n updatedAt\n status\n gendoGenerationId\n prompt\n camera\n }\n }\n": types.ProjectVersionGendoAiRenderCreatedDocument,
"\n subscription ProjectVersionGendoAIRenderUpdated($id: String!, $versionId: String!) {\n projectVersionGendoAIRenderUpdated(id: $id, versionId: $versionId) {\n id\n projectId\n modelId\n versionId\n createdAt\n updatedAt\n gendoGenerationId\n status\n prompt\n camera\n responseImage\n }\n }\n": types.ProjectVersionGendoAiRenderUpdatedDocument,
"\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": types.CreateNewRegionDocument,
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": types.ProjectPageTeamInternals_ProjectFragmentDoc,
"\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n": types.ProjectPageTeamInternals_WorkspaceFragmentDoc,
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": types.ProjectDashboardItemNoModelsFragmentDoc,
Expand Down Expand Up @@ -706,6 +710,18 @@ export function graphql(source: "\n fragment SettingsDialog_User on User {\n
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n"): (typeof documents)["\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n"): (typeof documents)["\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n"): (typeof documents)["\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n"): (typeof documents)["\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down Expand Up @@ -1042,6 +1058,10 @@ export function graphql(source: "\n subscription ProjectVersionGendoAIRenderCre
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n subscription ProjectVersionGendoAIRenderUpdated($id: String!, $versionId: String!) {\n projectVersionGendoAIRenderUpdated(id: $id, versionId: $versionId) {\n id\n projectId\n modelId\n versionId\n createdAt\n updatedAt\n gendoGenerationId\n status\n prompt\n camera\n responseImage\n }\n }\n"): (typeof documents)["\n subscription ProjectVersionGendoAIRenderUpdated($id: String!, $versionId: String!) {\n projectVersionGendoAIRenderUpdated(id: $id, versionId: $versionId) {\n id\n projectId\n modelId\n versionId\n createdAt\n updatedAt\n gendoGenerationId\n status\n prompt\n camera\n responseImage\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading

0 comments on commit 5df716b

Please sign in to comment.