diff --git a/packages/api-v4/src/marketplace/marketplace.ts b/packages/api-v4/src/marketplace/marketplace.ts index e253226a592..845326f7807 100644 --- a/packages/api-v4/src/marketplace/marketplace.ts +++ b/packages/api-v4/src/marketplace/marketplace.ts @@ -29,7 +29,7 @@ export const getMarketplaceProducts = (params?: Params, filters?: Filter) => export const getMarketplaceProduct = (productId: number) => Request( setURL( - `${BETA_API_ROOT}/marketplace/products/${encodeURIComponent(productId)}`, + `${BETA_API_ROOT}/marketplace/products/${encodeURIComponent(productId)}/details`, ), setMethod('GET'), ); diff --git a/packages/api-v4/src/marketplace/types.ts b/packages/api-v4/src/marketplace/types.ts index 430f4dc395d..179b7591c0e 100644 --- a/packages/api-v4/src/marketplace/types.ts +++ b/packages/api-v4/src/marketplace/types.ts @@ -1,42 +1,59 @@ export interface MarketplaceProductDetail { - documentation: string; - overview: { + documentation?: string; + overview?: { description: string; }; - pricing: string; - support: string; + pricing?: string; + support?: string; } export interface MarketplaceProduct { category_ids: number[]; + created_at: string; + created_by: string; details?: MarketplaceProductDetail; id: number; info_banner?: string; + logo_url: string; name: string; partner_id: number; product_tags?: string[]; short_description: string; - title_tag?: string; + tile_tag?: string; type_id: number; + updated_at?: string; + updated_by?: string; } export interface MarketplaceCategory { - category: string; + created_at: string; + created_by: string; id: number; - product_count: number; + name: string; + products_count: number; + updated_at?: string; + updated_by?: string; } export interface MarketplaceType { + created_at: string; + created_by: string; id: number; name: string; - product_count: number; + products_count: number; + updated_at?: string; + updated_by?: string; } export interface MarketplacePartner { + created_at: string; + created_by: string; id: number; + logo_url_dark_mode: string; logo_url_light_mode: string; - logo_url_night_mode?: string; name: string; + updated_at?: string; + updated_by?: string; url: string; } diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index c0d19f90ace..ebf8fa0d757 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -651,7 +651,7 @@ const marketplace = [ const marketplaceProduct = marketplaceProductFactory.buildList(10); return HttpResponse.json(makeResourcePage([...marketplaceProduct])); }), - http.get('*/v4beta/marketplace/products/:productId', () => { + http.get('*/v4beta/marketplace/products/:productId/details', () => { const marketplaceProductDetail = marketplaceProductFactory.build({ details: { overview: { diff --git a/packages/queries/.changeset/pr-13255-upcoming-features-1767845238509.md b/packages/queries/.changeset/pr-13255-upcoming-features-1767845238509.md new file mode 100644 index 00000000000..31ea725d372 --- /dev/null +++ b/packages/queries/.changeset/pr-13255-upcoming-features-1767845238509.md @@ -0,0 +1,5 @@ +--- +"@linode/queries": Upcoming Features +--- + +Add API queries for MarketplaceV2 ([#13255](https://github.com/linode/manager/pull/13255)) diff --git a/packages/queries/src/index.ts b/packages/queries/src/index.ts index 041af378f50..016e4c60cfb 100644 --- a/packages/queries/src/index.ts +++ b/packages/queries/src/index.ts @@ -11,6 +11,7 @@ export * from './iam'; export * from './images'; export * from './linodes'; export * from './locks'; +export * from './marketplace'; export * from './netloadbalancers'; export * from './networking'; export * from './networktransfer'; diff --git a/packages/queries/src/marketplace/index.ts b/packages/queries/src/marketplace/index.ts new file mode 100644 index 00000000000..ca0b44a0997 --- /dev/null +++ b/packages/queries/src/marketplace/index.ts @@ -0,0 +1,3 @@ +export * from './keys'; +export * from './marketplace'; +export * from './requests'; diff --git a/packages/queries/src/marketplace/keys.ts b/packages/queries/src/marketplace/keys.ts new file mode 100644 index 00000000000..600086c7197 --- /dev/null +++ b/packages/queries/src/marketplace/keys.ts @@ -0,0 +1,108 @@ +import { + getMarketplaceCategories, + getMarketplacePartners, + getMarketplaceProduct, + getMarketplaceProducts, + getMarketplaceTypes, +} from '@linode/api-v4'; +import { createQueryKeys } from '@lukemorales/query-key-factory'; + +import { + getAllMarketplaceCategories, + getAllMarketplacePartners, + getAllMarketplaceProducts, + getAllMarketplaceTypes, +} from './requests'; + +import type { Filter, Params } from '@linode/api-v4'; + +export const marketplaceQueries = createQueryKeys('marketplace', { + product: (productId: number) => ({ + queryFn: () => getMarketplaceProduct(productId), + queryKey: [productId], + }), + products: { + contextQueries: { + all: (params: Params = {}, filter: Filter = {}) => ({ + queryFn: () => getAllMarketplaceProducts(params, filter), + queryKey: [params, filter], + }), + infinite: (filter: Filter = {}) => ({ + queryFn: ({ pageParam }) => + getMarketplaceProducts( + { page: pageParam as number, page_size: 25 }, + filter, + ), + queryKey: [filter], + }), + paginated: (params: Params = {}, filter: Filter = {}) => ({ + queryFn: () => getMarketplaceProducts(params, filter), + queryKey: [params, filter], + }), + }, + queryKey: null, + }, + categories: { + contextQueries: { + all: (params: Params = {}, filter: Filter = {}) => ({ + queryFn: () => getAllMarketplaceCategories(params, filter), + queryKey: [params, filter], + }), + infinite: (filter: Filter = {}) => ({ + queryFn: ({ pageParam }) => + getMarketplaceCategories( + { page: pageParam as number, page_size: 25 }, + filter, + ), + queryKey: [filter], + }), + paginated: (params: Params = {}, filter: Filter = {}) => ({ + queryFn: () => getMarketplaceCategories(params, filter), + queryKey: [params, filter], + }), + }, + queryKey: null, + }, + types: { + contextQueries: { + all: (params: Params = {}, filter: Filter = {}) => ({ + queryFn: () => getAllMarketplaceTypes(params, filter), + queryKey: [params, filter], + }), + infinite: (filter: Filter = {}) => ({ + queryFn: ({ pageParam }) => + getMarketplaceTypes( + { page: pageParam as number, page_size: 25 }, + filter, + ), + queryKey: [filter], + }), + paginated: (params: Params = {}, filter: Filter = {}) => ({ + queryFn: () => getMarketplaceTypes(params, filter), + queryKey: [params, filter], + }), + }, + queryKey: null, + }, + partners: { + contextQueries: { + all: (params: Params = {}, filter: Filter = {}) => ({ + queryFn: () => getAllMarketplacePartners(params, filter), + queryKey: [params, filter], + }), + infinite: (filter: Filter = {}) => ({ + queryFn: ({ pageParam }) => + getMarketplacePartners( + { page: pageParam as number, page_size: 25 }, + filter, + ), + queryKey: [filter], + }), + paginated: (params: Params = {}, filter: Filter = {}) => ({ + queryFn: () => getMarketplacePartners(params, filter), + queryKey: [params, filter], + }), + }, + queryKey: null, + }, +}); diff --git a/packages/queries/src/marketplace/marketplace.ts b/packages/queries/src/marketplace/marketplace.ts new file mode 100644 index 00000000000..feb6b7b3a4d --- /dev/null +++ b/packages/queries/src/marketplace/marketplace.ts @@ -0,0 +1,199 @@ +import { createPartnerReferral } from '@linode/api-v4'; +import { + keepPreviousData, + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; + +import { accountQueries } from '../account'; +import { marketplaceQueries } from './keys'; + +import type { + APIError, + Filter, + MarketplaceCategory, + MarketplacePartner, + MarketplacePartnerReferralPayload, + MarketplaceProduct, + MarketplaceType, + Params, + ResourcePage, +} from '@linode/api-v4'; + +export const useMarketplaceProductsQuery = ( + params: Params, + filter: Filter, + enabled: boolean = true, +) => + useQuery, APIError[]>({ + ...marketplaceQueries.products._ctx.paginated(params, filter), + enabled, + placeholderData: keepPreviousData, + }); + +export const useAllMarketplaceProductsQuery = ( + params: Params = {}, + filter: Filter = {}, + enabled: boolean = true, +) => + useQuery({ + ...marketplaceQueries.products._ctx.all(params, filter), + enabled, + }); + +export const useInfiniteMarketplaceProductsQuery = ( + filter: Filter, + enabled: boolean, +) => + useInfiniteQuery, APIError[]>({ + ...marketplaceQueries.products._ctx.infinite(filter), + enabled, + getNextPageParam: ({ page, pages }) => { + if (page === pages) { + return undefined; + } + return page + 1; + }, + initialPageParam: 1, + retry: false, + }); + +export const useMarketplaceProductQuery = ( + productId: number, + enabled: boolean = true, +) => + useQuery({ + ...marketplaceQueries.product(productId), + enabled, + }); + +export const useMarketplaceCategoriesQuery = ( + params: Params, + filter: Filter, + enabled: boolean = true, +) => + useQuery, APIError[]>({ + ...marketplaceQueries.categories._ctx.paginated(params, filter), + enabled, + placeholderData: keepPreviousData, + }); + +export const useAllMarketplaceCategoriesQuery = ( + params: Params = {}, + filter: Filter = {}, + enabled: boolean = true, +) => + useQuery({ + ...marketplaceQueries.categories._ctx.all(params, filter), + enabled, + }); + +export const useInfiniteMarketplaceCategoriesQuery = ( + filter: Filter, + enabled: boolean, +) => + useInfiniteQuery, APIError[]>({ + ...marketplaceQueries.categories._ctx.infinite(filter), + enabled, + getNextPageParam: ({ page, pages }) => { + if (page === pages) { + return undefined; + } + return page + 1; + }, + initialPageParam: 1, + retry: false, + }); + +export const useMarketplaceTypesQuery = ( + params: Params, + filter: Filter, + enabled: boolean = true, +) => + useQuery, APIError[]>({ + ...marketplaceQueries.types._ctx.paginated(params, filter), + enabled, + placeholderData: keepPreviousData, + }); + +export const useAllMarketplaceTypesQuery = ( + params: Params = {}, + filter: Filter = {}, + enabled: boolean = true, +) => + useQuery({ + ...marketplaceQueries.types._ctx.all(params, filter), + enabled, + }); + +export const useInfiniteMarketplaceTypesQuery = ( + filter: Filter, + enabled: boolean, +) => + useInfiniteQuery, APIError[]>({ + ...marketplaceQueries.types._ctx.infinite(filter), + enabled, + getNextPageParam: ({ page, pages }) => { + if (page === pages) { + return undefined; + } + return page + 1; + }, + initialPageParam: 1, + retry: false, + }); + +export const useMarketplacePartnersQuery = ( + params: Params, + filter: Filter, + enabled: boolean = true, +) => + useQuery, APIError[]>({ + ...marketplaceQueries.partners._ctx.paginated(params, filter), + enabled, + placeholderData: keepPreviousData, + }); + +export const useAllMarketplacePartnersQuery = ( + params: Params = {}, + filter: Filter = {}, + enabled: boolean = true, +) => + useQuery({ + ...marketplaceQueries.partners._ctx.all(params, filter), + enabled, + }); + +export const useInfiniteMarketplacePartnersQuery = ( + filter: Filter, + enabled: boolean, +) => + useInfiniteQuery, APIError[]>({ + ...marketplaceQueries.partners._ctx.infinite(filter), + enabled, + getNextPageParam: ({ page, pages }) => { + if (page === pages) { + return undefined; + } + return page + 1; + }, + initialPageParam: 1, + retry: false, + }); + +export const useCreatePartnerReferralMutation = () => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[], MarketplacePartnerReferralPayload>({ + mutationFn: createPartnerReferral, + onSuccess: () => { + setTimeout(() => { + // Refetch notifications after 1.5 seconds. The API needs some time to process. + queryClient.invalidateQueries({ + queryKey: accountQueries.notifications.queryKey, + }); + }, 1500); + }, + }); +}; diff --git a/packages/queries/src/marketplace/requests.ts b/packages/queries/src/marketplace/requests.ts new file mode 100644 index 00000000000..5ae6b21d0d2 --- /dev/null +++ b/packages/queries/src/marketplace/requests.ts @@ -0,0 +1,60 @@ +import { + getMarketplaceCategories, + getMarketplacePartners, + getMarketplaceProducts, + getMarketplaceTypes, +} from '@linode/api-v4'; +import { getAll } from '@linode/utilities'; + +import type { + Filter, + MarketplaceCategory, + MarketplacePartner, + MarketplaceProduct, + MarketplaceType, + Params, +} from '@linode/api-v4'; + +export const getAllMarketplaceProducts = ( + passedParams: Params = {}, + passedFilter: Filter = {}, +) => + getAll((params, filter) => + getMarketplaceProducts( + { ...params, ...passedParams }, + { ...filter, ...passedFilter }, + ), + )().then((data) => data.data); + +export const getAllMarketplaceCategories = ( + passedParams: Params = {}, + passedFilter: Filter = {}, +) => + getAll((params, filter) => + getMarketplaceCategories( + { ...params, ...passedParams }, + { ...filter, ...passedFilter }, + ), + )().then((data) => data.data); + +export const getAllMarketplaceTypes = ( + passedParams: Params = {}, + passedFilter: Filter = {}, +) => + getAll((params, filter) => + getMarketplaceTypes( + { ...params, ...passedParams }, + { ...filter, ...passedFilter }, + ), + )().then((data) => data.data); + +export const getAllMarketplacePartners = ( + passedParams: Params = {}, + passedFilter: Filter = {}, +) => + getAll((params, filter) => + getMarketplacePartners( + { ...params, ...passedParams }, + { ...filter, ...passedFilter }, + ), + )().then((data) => data.data); diff --git a/packages/utilities/src/factories/marketplace.ts b/packages/utilities/src/factories/marketplace.ts index b9f8adae80c..215c2df1534 100644 --- a/packages/utilities/src/factories/marketplace.ts +++ b/packages/utilities/src/factories/marketplace.ts @@ -9,37 +9,46 @@ import type { export const marketplaceProductFactory = Factory.Sync.makeFactory({ + category_ids: [1, 2], + created_at: '2024-01-01T00:00:00', + created_by: 'user1', id: Factory.each((id) => id), + logo_url: 'https://www.example.com/logo.png', name: Factory.each((id) => `marketplace-product-${id}`), partner_id: Factory.each((id) => id), - type_id: Factory.each((id) => id), - category_ids: [1, 2], + product_tags: ['tag1', 'tag2'], short_description: 'This is a short description of the marketplace product.', - title_tag: 'Marketplace Product Title Tag', - product_tags: ['tag1', 'tag2'], + tile_tag: '60 days free trial', + type_id: Factory.each((id) => id), }); export const marketplaceCategoryFactory = Factory.Sync.makeFactory({ + name: Factory.each((id) => `marketplace-category-${id}`), + created_at: '2024-01-01T00:00:00', + created_by: 'user1', id: Factory.each((id) => id), - category: Factory.each((id) => `marketplace-category-${id}`), - product_count: Factory.each((id) => id * 10), + products_count: Factory.each((id) => id * 10), }); export const marketplaceTypeFactory = Factory.Sync.makeFactory( { + created_at: '2024-01-01T00:00:00', + created_by: 'user1', id: Factory.each((id) => id), + products_count: Factory.each((id) => id * 5), name: Factory.each((id) => `marketplace-type-${id}`), - product_count: Factory.each((id) => id * 5), }, ); export const marketplacePartnersFactory = Factory.Sync.makeFactory({ + created_at: '2024-01-01T00:00:00', + created_by: 'user1', id: Factory.each((id) => id), + logo_url_dark_mode: 'https://www.example.com/logo-dark-mode.png', + logo_url_light_mode: 'https://www.example.com/logo-light-mode.png', name: Factory.each((id) => `marketplace-partner-${id}`), url: 'https://www.example.com', - logo_url_light_mode: 'https://www.example.com/logo-light-mode.png', - logo_url_night_mode: 'https://www.example.com/logo-night-mode.png', });