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:
committed by
GitHub
parent
d95d03605e
commit
44bfa6d2c8
@@ -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
|
||||
|
||||
+31
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user