Fe2 server management bugfixes (#1787)

* fix(server): inviteList pagination

* Fixes from call with fabians

* more BE bufxies

* reducing server invite precision

* Infinite Scroll fixes. Slight design change to "update available"

* fixed tests

---------

Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
This commit is contained in:
andrewwallacespeckle
2023-09-08 12:55:03 +01:00
committed by GitHub
parent d95d03605e
commit 44bfa6d2c8
10 changed files with 119 additions and 62 deletions
@@ -21,12 +21,18 @@
{{ cta.label }}
</FormButton>
</template>
<template v-else-if="cta?.type === 'text'">
<div class="flex items-center gap-1 text-sm opacity-50">
<CheckCircleIcon class="h-4 w-4" />
{{ cta.label }}
</div>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/outline'
import { ArrowTopRightOnSquareIcon, CheckCircleIcon } from '@heroicons/vue/24/outline'
import { ConcreteComponent } from 'vue'
import { CTA } from '~~/lib/server-management/helpers/types'
@@ -38,6 +44,6 @@ defineProps<{
title: string
value: string
icon: ConcreteComponent
cta?: CTA | undefined
cta?: CTA
}>()
</script>
@@ -1305,6 +1305,7 @@ export type Project = {
/** Return metadata about resources being requested in the viewer */
viewerResources: Array<ViewerResourceGroup>;
visibility: ProjectVisibility;
webhooks?: Maybe<WebhookCollection>;
};
@@ -1355,6 +1356,11 @@ export type ProjectViewerResourcesArgs = {
resourceIdString: Scalars['String'];
};
export type ProjectWebhooksArgs = {
id?: InputMaybe<Scalars['String']>;
};
export type ProjectCollaborator = {
__typename?: 'ProjectCollaborator';
role: Scalars['String'];
@@ -20,9 +20,9 @@ export type InviteItem = NonNullable<
>
export interface CTA {
type: 'button' | 'link'
type: 'button' | 'link' | 'text'
label: string
action: () => MaybeAsync<void>
action?: () => MaybeAsync<void>
}
export interface Button {
@@ -35,11 +35,5 @@ export interface CardInfo {
title: string
value: string
icon: ConcreteComponent
cta?:
| {
type: 'button' | 'link'
label: string
action: () => MaybeAsync<void>
}
| undefined
cta?: CTA
}
@@ -91,7 +91,10 @@
</template>
</ServerManagementTable>
<CommonLoadingBar v-if="loading && !users?.length" loading />
<InfiniteLoading
v-if="users?.length"
:settings="{ identifier: infiniteLoaderId }"
class="-mt-24 -mb-24"
@infinite="infiniteLoad"
@@ -159,7 +162,8 @@ const {
result: extraPagesResult,
fetchMore: fetchMorePages,
variables: resultVariables,
onResult
onResult,
loading
} = useQuery(getUsersQuery, queryVariables)
const oldRole = computed(() => userToModify.value?.role as Optional<ServerRoles>)
@@ -66,6 +66,7 @@ const isLatestVersion = computed(() => {
!latestVersion.value ||
currentVersion.value === latestVersion.value ||
currentVersion.value === 'dev' ||
currentVersion.value?.includes('alpha') ||
currentVersion.value === 'N/A'
)
})
@@ -92,7 +93,10 @@ const serverData = computed((): CardInfo[] => [
label: 'Update is available',
action: openGithubReleasePage
}
: undefined
: {
type: 'text',
label: 'Up-to-date'
}
}
])
const userData = computed((): CardInfo[] => [
@@ -85,7 +85,10 @@
:result-variables="resultVariables"
/>
<CommonLoadingBar v-if="loading && !invites?.length" loading />
<InfiniteLoading
v-if="invites?.length"
:settings="{ identifier: infiniteLoaderId }"
class="py-4"
@infinite="infiniteLoad"
@@ -129,7 +132,8 @@ const {
result: extraPagesResult,
fetchMore: fetchMorePages,
variables: resultVariables,
onResult
onResult,
loading
} = useQuery(getInvitesQuery, () => ({
limit: 50,
query: searchString.value
@@ -89,7 +89,10 @@
</template>
</ServerManagementTable>
<CommonLoadingBar v-if="loading && !projects?.length" loading />
<InfiniteLoading
v-if="projects?.length"
:settings="{ identifier: infiniteLoaderId }"
class="py-4"
@infinite="infiniteLoad"
@@ -133,7 +136,8 @@ const {
result: extraPagesResult,
fetchMore: fetchMorePages,
variables: resultVariables,
onResult
onResult,
loading
} = useQuery(getProjectsQuery, () => ({
limit: 50,
query: searchString.value
@@ -12,6 +12,10 @@ import { expect } from 'chai'
import { ApolloServer } from 'apollo-server-express'
import { ServerInviteRecord } from '@/modules/serverinvites/helpers/types'
import { Optional } from '@/modules/shared/helpers/typeHelper'
import { wait } from '@speckle/shared'
// To ensure that the invites are created in the correct order, we need to wait a bit between each creation
const WAIT_TIMEOUT = 5
function randomEl<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)]
@@ -90,18 +94,18 @@ describe('[Admin users list]', () => {
let remainingSearchQueryInviteCount = SEARCH_QUERY_RESULT_COUNT
// Create Users
await Promise.all(
// count - 1, cause `me` also exists
times(USER_COUNT - 1, (i) =>
createUser({
name: `User #${i} - ${
remainingSearchQueryUserCount-- >= 1 ? SEARCH_QUERY : ''
}`,
email: `speckleuser${i}@gmail.com`,
password: 'sn3aky-1337-b1m'
}).then((id) => userIds.push(id))
)
)
// count - 1, cause `me` also exists
for (let i = 0; i < USER_COUNT - 1; i++) {
const id = await createUser({
name: `User #${i} - ${
remainingSearchQueryUserCount-- >= 1 ? SEARCH_QUERY : ''
}`,
email: `speckleuser${i}@gmail.com`,
password: 'sn3aky-1337-b1m'
})
userIds.push(id)
await wait(WAIT_TIMEOUT)
}
// Create streams
const streamData: { id: string; ownerId: string }[] = []
@@ -117,34 +121,35 @@ describe('[Admin users list]', () => {
)
// Create invites
await Promise.all([
// Server invites
...times(SERVER_INVITE_COUNT, (i) =>
createInviteDirectly(
{
email: `randominvitee${i}.${
remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
}@gmail.com`
},
randomEl(userIds)
)
),
// Stream invites
...times(STREAM_INVITE_COUNT, (i) => {
const { id: streamId, ownerId } = randomEl(streamData)
const email = `streamrandominvitee${i}.${
remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
}@gmail.com`
// Server invites
for (let i = 0; i < SERVER_INVITE_COUNT; i++) {
await createInviteDirectly(
{
email: `randominvitee${i}.${
remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
}@gmail.com`
},
randomEl(userIds)
)
await wait(WAIT_TIMEOUT)
}
return createInviteDirectly(
{
streamId,
email
},
ownerId
)
})
])
// Stream invites
for (let i = 0; i < STREAM_INVITE_COUNT; i++) {
const { id: streamId, ownerId } = randomEl(streamData)
const email = `streamrandominvitee${i}.${
remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
}@gmail.com`
await createInviteDirectly(
{
streamId,
email
},
ownerId
)
await wait(WAIT_TIMEOUT)
}
// Create a few more stream invites to registered users, which should not appear in
// the users list
@@ -0,0 +1,31 @@
import { Knex } from 'knex'
/**
* MIGRATING STREAMS TIMESTAMP FIELDS TO A LOWER PRECISION, CAUSE JS CANT HANDLE
* IT BEING THAT HIGH AND THIS GENERATES BUGS
*/
const TABLE_NAME = 'server_invites'
const TIMESTAMP_COLUMNS = ['createdAt']
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TABLE_NAME, (table) => {
TIMESTAMP_COLUMNS.forEach((col) => {
table
.timestamp(col, { precision: 3, useTz: true })
.defaultTo(knex.fn.now())
.alter()
})
})
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TABLE_NAME, (table) => {
TIMESTAMP_COLUMNS.forEach((col) => {
table
.timestamp(col, { precision: 6, useTz: true })
.defaultTo(knex.fn.now())
.alter()
})
})
}
@@ -13,7 +13,7 @@ const { getStream } = require('@/modules/core/repositories/streams')
/**
* Use this wherever you're retrieving invites, not necessarily where you're writing to them
*/
const getInvitesBaseQuery = () => {
const getInvitesBaseQuery = (sort = 'asc') => {
const q = ServerInvites.knex().select(ServerInvites.cols)
// join just to ensure we don't retrieve invalid invites
@@ -25,7 +25,7 @@ const getInvitesBaseQuery = () => {
w1.whereNull(ServerInvites.col.resourceId).orWhereNotNull(Streams.col.id)
})
q.orderBy(ServerInvites.col.createdAt)
q.orderBy(ServerInvites.col.createdAt, sort)
return q
}
@@ -225,8 +225,8 @@ async function deleteStreamInvite(inviteId) {
.delete()
}
function findServerInvitesBaseQuery(searchQuery) {
const q = getInvitesBaseQuery()
function findServerInvitesBaseQuery(searchQuery, sort) {
const q = getInvitesBaseQuery(sort)
if (searchQuery) {
// TODO: Is this safe from SQL injection?
@@ -272,10 +272,9 @@ async function findServerInvites(searchQuery, limit, offset) {
* @returns {Promise<ServerInviteRecord[]>}
*/
async function queryServerInvites(searchQuery, limit, cursor) {
const q = findServerInvitesBaseQuery(searchQuery)
.limit(limit)
.orderBy(ServerInvites.col.createdAt, 'desc')
if (cursor) q.where(ServerInvites.col.createdAt, '<', cursor)
const q = findServerInvitesBaseQuery(searchQuery, 'desc').limit(limit)
if (cursor) q.where(ServerInvites.col.createdAt, '<', cursor.toISOString())
return await q
}