diff --git a/apps/api/src/routes/v1/templates/list.ts b/apps/api/src/routes/v1/templates/list.ts index 4f853b3a5..2bd7ab4e3 100644 --- a/apps/api/src/routes/v1/templates/list.ts +++ b/apps/api/src/routes/v1/templates/list.ts @@ -11,6 +11,7 @@ const responseSchema = z.array( name: z.string(), logoUrl: z.string().nullable(), summary: z.string(), + deploy: z.string(), }) ) }) @@ -24,7 +25,9 @@ const route = createRoute({ description: "Returns a list of deployment templates grouped by categories", content: { "application/json": { - schema: responseSchema + schema: z.object({ + data: responseSchema + }) } } } @@ -38,5 +41,5 @@ export default new OpenAPIHono().openapi(route, async c => { const response = filteredTemplatesPerCategory.success ? filteredTemplatesPerCategory.data : templatesPerCategory; - return c.json(response); + return c.json({ data: response }); }); diff --git a/apps/deploy-web/src/components/new-deployment/TemplateList.tsx b/apps/deploy-web/src/components/new-deployment/TemplateList.tsx index 78d5c670d..26bbb262a 100644 --- a/apps/deploy-web/src/components/new-deployment/TemplateList.tsx +++ b/apps/deploy-web/src/components/new-deployment/TemplateList.tsx @@ -10,13 +10,14 @@ import { useRouter } from "next/navigation"; import { CI_CD_TEMPLATE_ID } from "@src/config/remote-deploy.config"; import { useTemplates } from "@src/context/TemplatesProvider"; import sdlStore from "@src/store/sdlStore"; -import { ApiTemplate, TemplateCreation } from "@src/types"; +import { TemplateCreation } from "@src/types"; import { RouteStep } from "@src/types/route-steps.type"; import { helloWorldTemplate } from "@src/utils/templates"; import { domainName, NewDeploymentParams, UrlService } from "@src/utils/urlUtils"; import { CustomNextSeo } from "../shared/CustomNextSeo"; import { TemplateBox } from "../templates/TemplateBox"; import { DeployOptionBox } from "./DeployOptionBox"; +import { TemplateOutputSummaryWithCategory } from "@src/queries/useTemplateQuery"; const previewTemplateIds = [ "akash-network-awesome-akash-Llama-3.1-8B", @@ -42,7 +43,7 @@ type Props = { export const TemplateList: React.FunctionComponent = ({ onChangeGitProvider, onTemplateSelected, setEditedManifest }) => { const { templates } = useTemplates(); const router = useRouter(); - const [previewTemplates, setPreviewTemplates] = useState([]); + const [previewTemplates, setPreviewTemplates] = useState([]); const [, setSdlEditMode] = useAtom(sdlStore.selectedSdlEditMode); const handleGithubTemplate = async () => { @@ -52,8 +53,8 @@ export const TemplateList: React.FunctionComponent = ({ onChangeGitProvid useEffect(() => { if (templates) { - const _previewTemplates = previewTemplateIds.map(x => templates.find(y => x === y.id)).filter(x => !!x); - setPreviewTemplates(_previewTemplates as ApiTemplate[]); + const _previewTemplates = templates.filter(template => previewTemplateIds.includes(template.id)); + setPreviewTemplates(_previewTemplates); } }, [templates]); diff --git a/apps/deploy-web/src/components/sdl/ImageSelect.tsx b/apps/deploy-web/src/components/sdl/ImageSelect.tsx index 03636baa2..bf06a5cdd 100644 --- a/apps/deploy-web/src/components/sdl/ImageSelect.tsx +++ b/apps/deploy-web/src/components/sdl/ImageSelect.tsx @@ -1,6 +1,4 @@ "use client"; -import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; -import { Control, Controller } from "react-hook-form"; import { buttonVariants, CustomTooltip } from "@akashnetwork/ui/components"; import { cn } from "@akashnetwork/ui/utils"; import ClickAwayListener from "@mui/material/ClickAwayListener"; @@ -11,22 +9,25 @@ import TextField from "@mui/material/TextField"; import { InfoCircle, OpenNewWindow } from "iconoir-react"; import Image from "next/image"; import Link from "next/link"; +import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { Control, Controller } from "react-hook-form"; import { useGpuTemplates } from "@src/hooks/useGpuTemplates"; -import { ApiTemplate, RentGpusFormValuesType, SdlBuilderFormValuesType, ServiceType } from "@src/types"; +import { TemplateOutputSummaryWithCategory } from "@src/queries/useTemplateQuery"; +import { RentGpusFormValuesType, SdlBuilderFormValuesType, ServiceType } from "@src/types"; type Props = { children?: ReactNode; control: Control; currentService: ServiceType; - onSelectTemplate: (template: ApiTemplate) => void; + onSelectTemplate: (template: TemplateOutputSummaryWithCategory) => void; }; export const ImageSelect: React.FunctionComponent = ({ control, currentService, onSelectTemplate }) => { const muiTheme = useMuiTheme(); const { gpuTemplates } = useGpuTemplates(); - const [hoveredTemplate, setHoveredTemplate] = useState(null); - const [selectedTemplate, setSelectedTemplate] = useState(null); + const [hoveredTemplate, setHoveredTemplate] = useState(null); + const [selectedTemplate, setSelectedTemplate] = useState(null); const [popperWidth, setPopperWidth] = useState(null); const eleRefs = useRef(null); const textFieldRef = useRef(null); @@ -97,7 +98,7 @@ export const ImageSelect: React.FunctionComponent = ({ control, currentSe } }; - const _onSelectTemplate = (template: ApiTemplate) => { + const _onSelectTemplate = (template: TemplateOutputSummaryWithCategory) => { setAnchorEl(null); onSelectTemplate(template); diff --git a/apps/deploy-web/src/components/sdl/RentGpusForm.tsx b/apps/deploy-web/src/components/sdl/RentGpusForm.tsx index a16bdc7a0..15171fbc2 100644 --- a/apps/deploy-web/src/components/sdl/RentGpusForm.tsx +++ b/apps/deploy-web/src/components/sdl/RentGpusForm.tsx @@ -1,6 +1,4 @@ "use client"; -import { useEffect, useRef, useState } from "react"; -import { useForm } from "react-hook-form"; import { certificateManager } from "@akashnetwork/akashjs/build/certificates/certificate-manager"; import { Alert, Button, Form, Spinner } from "@akashnetwork/ui/components"; import { EncodeObject } from "@cosmjs/proto-signing"; @@ -9,6 +7,8 @@ import { Rocket } from "iconoir-react"; import { useAtom } from "jotai"; import { useRouter, useSearchParams } from "next/navigation"; import { event } from "nextjs-google-analytics"; +import { useEffect, useRef, useState } from "react"; +import { useForm } from "react-hook-form"; import { browserEnvConfig } from "@src/config/browser-env.config"; import { useCertificate } from "@src/context/CertificateProvider"; @@ -20,8 +20,9 @@ import { useManagedWalletDenom } from "@src/hooks/useManagedWalletDenom"; import { useWhen } from "@src/hooks/useWhen"; import { useGpuModels } from "@src/queries/useGpuQuery"; import { useDepositParams } from "@src/queries/useSettings"; +import { TemplateOutputSummaryWithCategory } from "@src/queries/useTemplateQuery"; import sdlStore from "@src/store/sdlStore"; -import { ApiTemplate, ProfileGpuModelType, RentGpusFormValuesSchema, RentGpusFormValuesType, ServiceType } from "@src/types"; +import { ProfileGpuModelType, RentGpusFormValuesSchema, RentGpusFormValuesType, ServiceType } from "@src/types"; import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { DepositParams } from "@src/types/deployment"; import { ProviderAttributeSchemaDetailValue } from "@src/types/providerAttributes"; @@ -170,7 +171,7 @@ export const RentGpusForm: React.FunctionComponent = () => { } }; - const onSelectTemplate = (template: ApiTemplate) => { + const onSelectTemplate = (template: TemplateOutputSummaryWithCategory) => { const result = createAndValidateSdl(template?.deploy); if (!result) return; diff --git a/apps/deploy-web/src/components/templates/MobileTemplatesFilter.tsx b/apps/deploy-web/src/components/templates/MobileTemplatesFilter.tsx index 9d25de9ca..1cd5136ea 100644 --- a/apps/deploy-web/src/components/templates/MobileTemplatesFilter.tsx +++ b/apps/deploy-web/src/components/templates/MobileTemplatesFilter.tsx @@ -5,14 +5,14 @@ import { cn } from "@akashnetwork/ui/utils"; import Drawer from "@mui/material/Drawer"; import { Xmark } from "iconoir-react"; -import { ApiTemplate } from "@src/types"; +import { EnhancedTemplateCategory, TemplateOutputSummaryWithCategory } from "@src/queries/useTemplateQuery"; type Props = { children?: ReactNode; isOpen: boolean; handleDrawerToggle: () => void; - categories: Array<{ title: string; templates: Array }>; - templates: Array; + categories: EnhancedTemplateCategory[]; + templates: TemplateOutputSummaryWithCategory[]; selectedCategoryTitle: string | null; onCategoryClick: (categoryTitle: string | null) => void; }; diff --git a/apps/deploy-web/src/components/templates/TemplateBox.tsx b/apps/deploy-web/src/components/templates/TemplateBox.tsx index a3f84b057..603d1de43 100644 --- a/apps/deploy-web/src/components/templates/TemplateBox.tsx +++ b/apps/deploy-web/src/components/templates/TemplateBox.tsx @@ -4,12 +4,12 @@ import { cn } from "@akashnetwork/ui/utils"; import { MediaImage } from "iconoir-react"; import Link from "next/link"; -import { ApiTemplate } from "@src/types"; +import { TemplateOutputSummaryWithCategory } from "@src/queries/useTemplateQuery"; import { getShortText } from "@src/utils/stringUtils"; import { UrlService } from "@src/utils/urlUtils"; type Props = { - template: ApiTemplate; + template: TemplateOutputSummaryWithCategory; linkHref?: string; children?: React.ReactNode; }; @@ -23,7 +23,7 @@ export const TemplateBox: React.FunctionComponent = ({ template, linkHref
- + diff --git a/apps/deploy-web/src/components/templates/TemplateGallery.tsx b/apps/deploy-web/src/components/templates/TemplateGallery.tsx index 0c9451cf9..576664e5d 100644 --- a/apps/deploy-web/src/components/templates/TemplateGallery.tsx +++ b/apps/deploy-web/src/components/templates/TemplateGallery.tsx @@ -1,15 +1,15 @@ "use client"; -import { useEffect, useState } from "react"; -import { MdSearchOff } from "react-icons/md"; import { Button, buttonVariants, Spinner } from "@akashnetwork/ui/components"; import { cn } from "@akashnetwork/ui/utils"; import IconButton from "@mui/material/IconButton"; import TextField from "@mui/material/TextField"; import { FilterList, Xmark } from "iconoir-react"; import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { MdSearchOff } from "react-icons/md"; import { LinkTo } from "@src/components/shared/LinkTo"; -import { ApiTemplate } from "@src/types"; +import { TemplateOutputSummaryWithCategory } from "@src/queries/useTemplateQuery"; import { domainName, UrlService } from "@src/utils/urlUtils"; import { useTemplates } from "../../context/TemplatesProvider"; import Layout from "../layout/Layout"; @@ -23,7 +23,7 @@ let timeoutId: NodeJS.Timeout | null = null; export const TemplateGallery: React.FunctionComponent = () => { const [selectedCategoryTitle, setSelectedCategoryTitle] = useState(null); const [searchTerms, setSearchTerms] = useState(""); - const [shownTemplates, setShownTemplates] = useState([]); + const [shownTemplates, setShownTemplates] = useState([]); const { isLoading: isLoadingTemplates, categories, templates } = useTemplates(); const router = useRouter(); const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false); @@ -43,9 +43,9 @@ export const TemplateGallery: React.FunctionComponent = () => { }, []); useEffect(() => { - const queryCategory = searchParams?.get("category") as string; - const querySearch = searchParams?.get("search") as string; - let _templates: ApiTemplate[] = []; + const queryCategory = searchParams?.get("category"); + const querySearch = searchParams?.get("search"); + let _templates: TemplateOutputSummaryWithCategory[] = []; if (queryCategory) { const selectedCategory = categories.find(x => x.title === queryCategory); @@ -55,8 +55,9 @@ export const TemplateGallery: React.FunctionComponent = () => { } if (querySearch) { + // TODO: use minisearch instead https://lucaong.github.io/minisearch/ const searchTermsSplit = querySearch?.split(" ").map(x => x.toLowerCase()); - _templates = templates.filter(x => searchTermsSplit.some(s => x.name?.toLowerCase().includes(s) || x.readme?.toLowerCase().includes(s))); + _templates = templates.filter(x => searchTermsSplit.some(s => x.name?.toLowerCase().includes(s) || x.summary?.toLowerCase().includes(s))); } setShownTemplates(_templates); diff --git a/apps/deploy-web/src/context/TemplatesProvider/TemplatesProviderContext.tsx b/apps/deploy-web/src/context/TemplatesProvider/TemplatesProviderContext.tsx index cfe92b793..18de0968b 100644 --- a/apps/deploy-web/src/context/TemplatesProvider/TemplatesProviderContext.tsx +++ b/apps/deploy-web/src/context/TemplatesProvider/TemplatesProviderContext.tsx @@ -1,28 +1,22 @@ "use client"; import React from "react"; -import { useTemplates as useTemplatesQuery } from "@src/queries/useTemplateQuery"; -import { ApiTemplate } from "@src/types"; +import { EnhancedTemplateCategory, TemplateOutputSummaryWithCategory, useTemplates as useTemplatesQuery } from "@src/queries/useTemplateQuery"; type ContextType = { isLoading: boolean; - categories: Array<{ title: string; templates: Array }>; - templates: Array; - getTemplateById: (id: string) => ApiTemplate; + categories: EnhancedTemplateCategory[]; + templates: TemplateOutputSummaryWithCategory[]; }; const TemplatesProviderContext = React.createContext({} as ContextType); export const TemplatesProvider = ({ children }) => { const { data, isFetching: isLoading } = useTemplatesQuery(); - const categories = data ? data.categories : []; - const templates = data ? data.templates : []; + const categories = data?.categories || []; + const templates = data?.templates || []; - function getTemplateById(id: string) { - return categories.flatMap(x => x.templates).find(x => x.id === id); - } - - return {children}; + return {children}; }; export const useTemplates = () => { diff --git a/apps/deploy-web/src/queries/useTemplateQuery.tsx b/apps/deploy-web/src/queries/useTemplateQuery.tsx index e8a24cd68..4d1ee0e3b 100644 --- a/apps/deploy-web/src/queries/useTemplateQuery.tsx +++ b/apps/deploy-web/src/queries/useTemplateQuery.tsx @@ -1,14 +1,15 @@ -import { QueryKey, useMutation, useQuery, useQueryClient, UseQueryOptions } from "react-query"; import { Snackbar } from "@akashnetwork/ui/components"; import axios from "axios"; import { useRouter } from "next/navigation"; import { useSnackbar } from "notistack"; +import { QueryKey, useMutation, useQuery, useQueryClient, UseQueryOptions, UseQueryResult } from "react-query"; import { useCustomUser } from "@src/hooks/useCustomUser"; +import { services } from "@src/services/http/http-browser.service"; import { ITemplate } from "@src/types"; -import { ApiUrlService } from "@src/utils/apiUtils"; import { UrlService } from "@src/utils/urlUtils"; import { QueryKeys } from "./queryKeys"; +import { TemplateCategory, TemplateOutputSummary } from "@akashnetwork/http-sdk/src/template/template-http.service"; async function getUserTemplates(username: string): Promise { const response = await axios.get(`/api/proxy/user/templates/${username}`); @@ -106,22 +107,40 @@ export function useRemoveFavoriteTemplate(id: string) { } async function getTemplates() { - const response = await axios.get(ApiUrlService.templates()); + const response = await services.template.findGroupedByCategory(); if (!response.data) { return { categories: [], templates: [] }; } - const categories = response.data.filter(x => (x.templates || []).length > 0); - categories.forEach(c => { - c.templates.forEach(t => (t.category = c.title)); + const categories = response.data.filter(x => !!x.templates?.length); + const modifiedCategories = categories.map(category => { + const templatesWithCategory = category.templates.map(template => ({ + ...template, + category: category.title, + })); + + return { ...category, templates: templatesWithCategory }; }); - const templates = categories.flatMap(x => x.templates); + const templates = modifiedCategories.flatMap(category => category.templates); + + return { categories: modifiedCategories, templates }; +} + +export interface EnhancedTemplateCategory extends Omit { + templates: TemplateOutputSummaryWithCategory[]; +} + +export interface TemplateOutputSummaryWithCategory extends TemplateOutputSummary { + category: TemplateCategory['title'] +} - return { categories, templates }; +export interface CategoriesAndTemplates { + categories: EnhancedTemplateCategory[]; + templates: TemplateOutputSummaryWithCategory[]; } -export function useTemplates(options = {}) { +export function useTemplates(options = {}): UseQueryResult { return useQuery(QueryKeys.getTemplatesKey(), () => getTemplates(), { ...options, refetchInterval: 60000 * 2, // Refetch templates every 2 minutes diff --git a/packages/http-sdk/src/api-http/api-http.service.ts b/packages/http-sdk/src/api-http/api-http.service.ts index f17475dea..0984f6771 100644 --- a/packages/http-sdk/src/api-http/api-http.service.ts +++ b/packages/http-sdk/src/api-http/api-http.service.ts @@ -11,15 +11,15 @@ export class ApiHttpService extends HttpService { super(config); } - post>, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise { + post>, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise { return super.post(url, data, config); } - get>, D = any>(url: string, config?: AxiosRequestConfig): Promise { + get>, D = any>(url: string, config?: AxiosRequestConfig): Promise { return super.get(url, config); } - protected extractApiData(response: ApiOutput>): AxiosResponse["data"] { - return this.extractData(response.data); + protected extractApiData(response: AxiosResponse>): ApiOutput["data"] { + return this.extractData(response).data; } } diff --git a/packages/http-sdk/src/template/template-http.service.ts b/packages/http-sdk/src/template/template-http.service.ts index 6f938f32d..cbe13f979 100644 --- a/packages/http-sdk/src/template/template-http.service.ts +++ b/packages/http-sdk/src/template/template-http.service.ts @@ -1,6 +1,6 @@ import type { AxiosRequestConfig } from "axios"; -import { ApiHttpService } from "../api-http/api-http.service"; +import { ApiHttpService, ApiOutput } from "../api-http/api-http.service"; export interface TemplateOutput { id: string; @@ -18,12 +18,32 @@ export interface TemplateOutput { }; } +export interface TemplateOutputSummary { + id: string; + name: string; + logoUrl?: string | null; + summary: string; + deploy: string; +} + +export interface TemplateCategory { + title: string; + templates: TemplateOutputSummary[]; +} + export class TemplateHttpService extends ApiHttpService { constructor(config?: Pick) { super(config); } - async findById(id: string) { - return await this.extractApiData(await this.get(`/v1/templates/${id}`)); + async findById(id: string): Promise { + const response = await this.get(`/v1/templates/${id}`); + return this.extractApiData(response); + } + + async findGroupedByCategory(): Promise> { + const response = await this.get('/v1/templates-list'); + return response.data; } } +