Skip to content
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
28 changes: 1 addition & 27 deletions spec/openapi.dashboard-api.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 23 additions & 4 deletions src/app/dashboard/[teamSlug]/templates/(tabs)/list/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import { Suspense } from 'react'
import LoadingLayout from '@/features/dashboard/loading-layout'
import {
TEMPLATES_DEFAULT_SORT,
TEMPLATES_PAGE_SIZE,
} from '@/features/dashboard/templates/list/constants'
import TemplatesTable from '@/features/dashboard/templates/list/table'
import { HydrateClient, prefetch, trpc } from '@/trpc/server'

export default async function TemplatesListPage({
params,
}: PageProps<'/dashboard/[teamSlug]/templates/list'>) {
const { teamSlug } = await params

prefetch(
trpc.templates.getTemplates.infiniteQueryOptions({
teamSlug,
limit: TEMPLATES_PAGE_SIZE,
sort: TEMPLATES_DEFAULT_SORT,
})
)

export default function TemplatesListPage() {
return (
<Suspense fallback={<LoadingLayout />}>
<TemplatesTable />
</Suspense>
<HydrateClient>
<Suspense fallback={<LoadingLayout />}>
<TemplatesTable />
</Suspense>
</HydrateClient>
)
}
15 changes: 15 additions & 0 deletions src/core/modules/templates/models.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type {
components as DashboardComponents,
paths as DashboardPaths,
} from '@/contracts/dashboard-api'
import type { components as InfraComponents } from '@/contracts/infra-api'

export type Template = Pick<
Expand All @@ -23,3 +27,14 @@ export type DefaultTemplate = Template & {
isDefault: true
defaultDescription?: string
}

export type TemplatesSort = DashboardComponents['parameters']['templates_sort']

export type ListTeamTemplatesOptions = NonNullable<
DashboardPaths['/templates']['get']['parameters']['query']
>

export interface ListTeamTemplatesResult {
data: Array<Template | DefaultTemplate>
nextCursor: string | null
}
66 changes: 65 additions & 1 deletion src/core/modules/templates/repository.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
MOCK_DEFAULT_TEMPLATES_DATA,
MOCK_TEMPLATES_DATA,
} from '@/configs/mock-data'
import type { DefaultTemplate, Template } from '@/core/modules/templates/models'
import type {
DefaultTemplate,
ListTeamTemplatesOptions,
ListTeamTemplatesResult,
Template,
} from '@/core/modules/templates/models'
import {
type AuthUserEmailResolver,
getAuthUserEmailsById,
Expand All @@ -30,6 +35,9 @@ type TemplatesRepositoryDeps = {

export interface TeamTemplatesRepository {
getTeamTemplates(): Promise<RepoResult<{ templates: Template[] }>>
listTeamTemplates(
options: ListTeamTemplatesOptions
): Promise<RepoResult<ListTeamTemplatesResult>>
deleteTemplate(templateId: string): Promise<RepoResult<{ success: true }>>
updateTemplateVisibility(
templateId: string,
Expand Down Expand Up @@ -87,6 +95,62 @@ export function createTemplatesRepository(
),
})
},
async listTeamTemplates(options) {
if (USE_MOCK_DATA) {
return ok({ data: MOCK_TEMPLATES_DATA, nextCursor: null })
}

const res = await deps.apiClient.GET('/templates', {
params: {
query: options,
},
headers: {
...deps.authHeaders(scope.accessToken, scope.teamId),
},
})

if (!res.response.ok || res.error) {
return err(
repoErrorFromHttp(
res.response.status,
res.error?.message ?? 'Failed to fetch templates',
res.error
)
)
}

if (!res.data?.data?.length) {
return ok({ data: [], nextCursor: res.data?.nextCursor ?? null })
}

const data = res.data.data.map((t): Template | DefaultTemplate => ({
templateID: t.templateID,
buildID: t.buildID,
cpuCount: t.cpuCount,
memoryMB: t.memoryMB,
diskSizeMB: t.diskSizeMB ?? 0,
public: t.public,
aliases: t.aliases,
names: t.names,
createdAt: t.createdAt,
updatedAt: t.updatedAt,
// Email resolution is deferred while the Supabase auth migration is
// in progress; the endpoint returns only the creator id for now.
createdBy: t.createdBy
? { id: t.createdBy.id, email: t.createdBy.email ?? '' }
: null,
lastSpawnedAt: t.lastSpawnedAt ?? null,
spawnCount: t.spawnCount,
buildCount: t.buildCount,
envdVersion: t.envdVersion ?? '',
...(t.isDefault && {
isDefault: true as const,
defaultDescription: t.defaultDescription ?? undefined,
}),
}))

return ok({ data, nextCursor: res.data.nextCursor ?? null })
},
async deleteTemplate(templateId) {
const res = await deps.infraClient.DELETE('/templates/{templateID}', {
params: {
Expand Down
33 changes: 28 additions & 5 deletions src/core/server/api/routers/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,34 @@ const teamTemplatesRepositoryProcedure = protectedTeamProcedure.use(
export const templatesRouter = createTRPCRouter({
// QUERIES

getTemplates: teamTemplatesRepositoryProcedure.query(async ({ ctx }) => {
const result = await ctx.templatesRepository.getTeamTemplates()
if (!result.ok) throwTRPCErrorFromRepoError(result.error)
return result.data
}),
getTemplates: teamTemplatesRepositoryProcedure
.input(
z.object({
cursor: z.string().optional(),
limit: z.number().int().min(1).max(100).default(50),
public: z.boolean().optional(),
search: z.string().optional(),
sort: z
.enum([
'created_at_asc',
'created_at_desc',
'updated_at_asc',
'updated_at_desc',
])
.default('updated_at_desc'),
})
)
.query(async ({ ctx, input }) => {
const result = await ctx.templatesRepository.listTeamTemplates({
cursor: input.cursor,
limit: input.limit,
public: input.public,
search: input.search,
sort: input.sort,
})
if (!result.ok) throwTRPCErrorFromRepoError(result.error)
return result.data
}),

getDefaultTemplatesCached: templatesRepositoryProcedure.query(
async ({ ctx }) => {
Expand Down
14 changes: 0 additions & 14 deletions src/core/shared/contracts/dashboard-api.types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 0 additions & 36 deletions src/features/dashboard/templates/builds/table-cells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,42 +61,6 @@ export function Template({
)
}

export function LoadMoreButton({
isLoading,
onLoadMore,
}: {
isLoading: boolean
onLoadMore: () => void
}) {
if (isLoading) {
return (
<span className="inline-flex items-center gap-1">
Loading
<Loader variant="dots" />
</span>
)
}
return (
<button
onClick={onLoadMore}
className="underline text-fg-secondary hover:text-accent-main-highlight transition-colors"
>
Load more
</button>
)
}

export function BackToTopButton({ onBackToTop }: { onBackToTop: () => void }) {
return (
<button
onClick={onBackToTop}
className="underline text-fg-secondary hover:text-accent-main-highlight transition-colors"
>
Back to top
</button>
)
}

export function Duration({
createdAt,
finishedAt,
Expand Down
3 changes: 1 addition & 2 deletions src/features/dashboard/templates/builds/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
import { useRouteParams } from '@/lib/hooks/use-route-params'
import { cn } from '@/lib/utils/ui'
import { useTRPC } from '@/trpc/client'
import { BackToTopButton, LoadMoreButton } from '@/ui/pagination-buttons'
import { ArrowDownIcon } from '@/ui/primitives/icons'
import { Loader } from '@/ui/primitives/loader'
import {
Expand All @@ -28,10 +29,8 @@ import {
} from '@/ui/primitives/table'
import BuildsEmpty from './empty'
import {
BackToTopButton,
BuildId,
Duration,
LoadMoreButton,
Reason,
StartedAt,
Status,
Expand Down
16 changes: 16 additions & 0 deletions src/features/dashboard/templates/list/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { SortingState } from '@tanstack/react-table'
import type { TemplatesSort } from '@/core/modules/templates/models'

export const TEMPLATES_PAGE_SIZE = 50

export const TEMPLATES_DEFAULT_SORT_COLUMN_ID = 'updatedAt'
export const TEMPLATES_DEFAULT_SORT_BASE = 'updated_at'
export const TEMPLATES_DEFAULT_SORT_DESC = true

export const TEMPLATES_DEFAULT_SORT: TemplatesSort = `${TEMPLATES_DEFAULT_SORT_BASE}_${
TEMPLATES_DEFAULT_SORT_DESC ? 'desc' : 'asc'
}`

export const TEMPLATES_DEFAULT_SORTING: SortingState = [
{ id: TEMPLATES_DEFAULT_SORT_COLUMN_ID, desc: TEMPLATES_DEFAULT_SORT_DESC },
]
Loading
Loading