Skip to content

Commit

Permalink
refactor(template): replaces /v1/templates with /v1/templates-list
Browse files Browse the repository at this point in the history
refs #477
  • Loading branch information
stalniy committed Jan 14, 2025
1 parent 68eebdd commit c2c7d59
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 59 deletions.
7 changes: 5 additions & 2 deletions apps/api/src/routes/v1/templates/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const responseSchema = z.array(
name: z.string(),
logoUrl: z.string().nullable(),
summary: z.string(),
deploy: z.string(),
})
)
})
Expand All @@ -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
})
}
}
}
Expand All @@ -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 });
});
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -42,7 +43,7 @@ type Props = {
export const TemplateList: React.FunctionComponent<Props> = ({ onChangeGitProvider, onTemplateSelected, setEditedManifest }) => {
const { templates } = useTemplates();
const router = useRouter();
const [previewTemplates, setPreviewTemplates] = useState<ApiTemplate[]>([]);
const [previewTemplates, setPreviewTemplates] = useState<TemplateOutputSummaryWithCategory[]>([]);
const [, setSdlEditMode] = useAtom(sdlStore.selectedSdlEditMode);

const handleGithubTemplate = async () => {
Expand All @@ -52,8 +53,8 @@ export const TemplateList: React.FunctionComponent<Props> = ({ 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]);

Expand Down
15 changes: 8 additions & 7 deletions apps/deploy-web/src/components/sdl/ImageSelect.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<SdlBuilderFormValuesType | RentGpusFormValuesType, any>;
currentService: ServiceType;
onSelectTemplate: (template: ApiTemplate) => void;
onSelectTemplate: (template: TemplateOutputSummaryWithCategory) => void;
};

export const ImageSelect: React.FunctionComponent<Props> = ({ control, currentService, onSelectTemplate }) => {
const muiTheme = useMuiTheme();
const { gpuTemplates } = useGpuTemplates();
const [hoveredTemplate, setHoveredTemplate] = useState<ApiTemplate | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<ApiTemplate | null>(null);
const [hoveredTemplate, setHoveredTemplate] = useState<TemplateOutputSummaryWithCategory | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<TemplateOutputSummaryWithCategory | null>(null);
const [popperWidth, setPopperWidth] = useState<number | null>(null);
const eleRefs = useRef(null);
const textFieldRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -97,7 +98,7 @@ export const ImageSelect: React.FunctionComponent<Props> = ({ control, currentSe
}
};

const _onSelectTemplate = (template: ApiTemplate) => {
const _onSelectTemplate = (template: TemplateOutputSummaryWithCategory) => {
setAnchorEl(null);

onSelectTemplate(template);
Expand Down
9 changes: 5 additions & 4 deletions apps/deploy-web/src/components/sdl/RentGpusForm.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiTemplate> }>;
templates: Array<ApiTemplate>;
categories: EnhancedTemplateCategory[];
templates: TemplateOutputSummaryWithCategory[];
selectedCategoryTitle: string | null;
onCategoryClick: (categoryTitle: string | null) => void;
};
Expand Down
6 changes: 3 additions & 3 deletions apps/deploy-web/src/components/templates/TemplateBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -23,7 +23,7 @@ export const TemplateBox: React.FunctionComponent<Props> = ({ template, linkHref
<CardHeader>
<div className="flex items-center">
<Avatar className="h-10 w-10">
<AvatarImage src={template.logoUrl} alt={template.name} className="object-contain" />
<AvatarImage src={template.logoUrl || undefined} alt={template.name} className="object-contain" />
<AvatarFallback>
<MediaImage />
</AvatarFallback>
Expand Down
17 changes: 9 additions & 8 deletions apps/deploy-web/src/components/templates/TemplateGallery.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -23,7 +23,7 @@ let timeoutId: NodeJS.Timeout | null = null;
export const TemplateGallery: React.FunctionComponent = () => {
const [selectedCategoryTitle, setSelectedCategoryTitle] = useState<string | null>(null);
const [searchTerms, setSearchTerms] = useState("");
const [shownTemplates, setShownTemplates] = useState<ApiTemplate[]>([]);
const [shownTemplates, setShownTemplates] = useState<TemplateOutputSummaryWithCategory[]>([]);
const { isLoading: isLoadingTemplates, categories, templates } = useTemplates();
const router = useRouter();
const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ApiTemplate> }>;
templates: Array<ApiTemplate>;
getTemplateById: (id: string) => ApiTemplate;
categories: EnhancedTemplateCategory[];
templates: TemplateOutputSummaryWithCategory[];
};

const TemplatesProviderContext = React.createContext<ContextType>({} 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 <TemplatesProviderContext.Provider value={{ isLoading, categories, templates, getTemplateById }}>{children}</TemplatesProviderContext.Provider>;
return <TemplatesProviderContext.Provider value={{ isLoading, categories, templates }}>{children}</TemplatesProviderContext.Provider>;
};

export const useTemplates = () => {
Expand Down
37 changes: 28 additions & 9 deletions apps/deploy-web/src/queries/useTemplateQuery.tsx
Original file line number Diff line number Diff line change
@@ -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<ITemplate[]> {
const response = await axios.get(`/api/proxy/user/templates/${username}`);
Expand Down Expand Up @@ -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<TemplateCategory, 'templates'> {
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<CategoriesAndTemplates> {
return useQuery(QueryKeys.getTemplatesKey(), () => getTemplates(), {
...options,
refetchInterval: 60000 * 2, // Refetch templates every 2 minutes
Expand Down
8 changes: 4 additions & 4 deletions packages/http-sdk/src/api-http/api-http.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ export class ApiHttpService extends HttpService {
super(config);
}

post<T = any, R = ApiOutput<AxiosResponse<T>>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R> {
post<T = any, R = AxiosResponse<ApiOutput<T>>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R> {
return super.post(url, data, config);
}

get<T = any, R = ApiOutput<AxiosResponse<T>>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R> {
get<T = any, R = AxiosResponse<ApiOutput<T>>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R> {
return super.get(url, config);
}

protected extractApiData<T = unknown>(response: ApiOutput<AxiosResponse<T>>): AxiosResponse<T>["data"] {
return this.extractData(response.data);
protected extractApiData<T = unknown>(response: AxiosResponse<ApiOutput<T>>): ApiOutput<T>["data"] {
return this.extractData(response).data;
}
}
Loading

0 comments on commit c2c7d59

Please sign in to comment.