From a98595852bcd1b906d8a0a8a3175a67c1c961b5f Mon Sep 17 00:00:00 2001 From: HelaKaraa Date: Wed, 7 Aug 2024 11:42:56 +0200 Subject: [PATCH 1/2] fix#715: invalidate query subscription --- .../backoffice/apis/TeamApiSubscriptions.tsx | 597 ++++++++++-------- 1 file changed, 346 insertions(+), 251 deletions(-) diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx index 576881cc8..36cc2e644 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx @@ -1,15 +1,23 @@ -import {getApolloContext} from "@apollo/client"; -import {format, type} from "@maif/react-forms"; -import {createColumnHelper} from '@tanstack/react-table'; -import {useContext, useEffect, useRef, useState} from 'react'; -import {toast} from 'sonner'; +import { getApolloContext } from '@apollo/client'; +import { format, type } from '@maif/react-forms'; +import { createColumnHelper } from '@tanstack/react-table'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { toast } from 'sonner'; -import {ModalContext} from '../../../contexts'; -import {CustomSubscriptionData} from '../../../contexts/modals/SubscriptionMetadataModal'; -import {I18nContext} from '../../../contexts'; +import { ModalContext } from '../../../contexts'; +import { CustomSubscriptionData } from '../../../contexts/modals/SubscriptionMetadataModal'; +import { I18nContext } from '../../../contexts'; import * as Services from '../../../services'; -import {IApi, isError, IState, ISubscriptionCustomization, ITeamSimple, IUsagePlan, ResponseError} from "../../../types"; -import {SwitchButton, Table, TableRef} from '../../inputs'; +import { + IApi, + isError, + IState, + ISubscriptionCustomization, + ITeamSimple, + IUsagePlan, + ResponseError, +} from '../../../types'; +import { SwitchButton, Table, TableRef } from '../../inputs'; import { api as API, BeautifulTitle, @@ -20,132 +28,161 @@ import { Option, Spinner, } from '../../utils'; -import {useQuery, useQueryClient, useMutation} from "@tanstack/react-query"; +import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'; type TeamApiSubscriptionsProps = { - api: IApi, - currentTeam: ITeamSimple -} + api: IApi; + currentTeam: ITeamSimple; +}; type SubscriptionsFilter = { - metadata: Array<{ key: string, value: string }>, - tags: Array, - clientIds: Array -} + metadata: Array<{ key: string; value: string }>; + tags: Array; + clientIds: Array; +}; type LimitedPlan = { - _id: string - customName?: string - type: string - -} + _id: string; + customName?: string; + type: string; +}; interface IApiSubscriptionGql extends ISubscriptionCustomization { - _id: string + _id: string; apiKey: { - clientName: string - clientId: string - clientSecret: string - } - plan: LimitedPlan + clientName: string; + clientId: string; + clientSecret: string; + }; + plan: LimitedPlan; team: { - _id: string - name: string - type: string - } - createdAt: string + _id: string; + name: string; + type: string; + }; + createdAt: string; api: { - _id: string - } - customName: string - enabled: boolean - customMetadata?: JSON - adminCustomName?: string - customMaxPerSecond?: number - customMaxPerDay?: number - customMaxPerMonth?: number - customReadOnly?: boolean - tags: Array - metadata?: JSON + _id: string; + }; + customName: string; + enabled: boolean; + customMetadata?: JSON; + adminCustomName?: string; + customMaxPerSecond?: number; + customMaxPerDay?: number; + customMaxPerMonth?: number; + customReadOnly?: boolean; + tags: Array; + metadata?: JSON; parent: { - _id: string - adminCustomName: string + _id: string; + adminCustomName: string; api: { - _id: string - name: string - } + _id: string; + name: string; + }; plan: { - _id: string - customName: string - type: string - } - } + _id: string; + customName: string; + type: string; + }; + }; } interface IApiSubscriptionGqlWithUsage extends IApiSubscriptionGql { - lastUsage?: number + lastUsage?: number; } -export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsProps) => { - +export const TeamApiSubscriptions = ({ + api, + currentTeam, +}: TeamApiSubscriptionsProps) => { const { client } = useContext(getApolloContext()); const queryClient = useQueryClient(); - const [filters, setFilters] = useState() - const tableRef = useRef() + const [filters, setFilters] = useState(); + const tableRef = useRef(); const { translate, language, Translation } = useContext(I18nContext); - const { confirm, openFormModal, openSubMetadataModal, } = useContext(ModalContext); + const { confirm, openFormModal, openSubMetadataModal } = + useContext(ModalContext); - const plansQuery = useQuery({ queryKey: ['plans'], queryFn: () => Services.getAllPlanOfApi(api.team, api._id, api.currentVersion) }) + const plansQuery = useQuery({ + queryKey: ['plans'], + queryFn: () => + Services.getAllPlanOfApi(api.team, api._id, api.currentVersion), + }); const subscriptionsQuery = useQuery({ queryKey: ['subscriptions'], - queryFn: () => client!.query<{ apiApiSubscriptions: Array; }>({ - query: Services.graphql.getApiSubscriptions, - fetchPolicy: "no-cache", - variables: { - apiId: api._id, - teamId: currentTeam._id, - version: api.currentVersion - } - }).then(({ data: { apiApiSubscriptions } }) => { - if (!filters || (!filters.tags.length && !Object.keys(filters.metadata).length && !filters.clientIds.length)) { - return apiApiSubscriptions - } else { - const filterByMetadata = (subscription: IApiSubscriptionGql) => { - const meta = { ...(subscription.metadata || {}), ...(subscription.customMetadata || {}) }; - - return !Object.keys(meta) || (!filters.metadata.length || filters.metadata.every(item => { - const value = meta[item.key] - return value && value.includes(item.value) - })) - } + queryFn: () => + client! + .query<{ apiApiSubscriptions: Array }>({ + query: Services.graphql.getApiSubscriptions, + fetchPolicy: 'no-cache', + variables: { + apiId: api._id, + teamId: currentTeam._id, + version: api.currentVersion, + }, + }) + .then(({ data: { apiApiSubscriptions } }) => { + if ( + !filters || + (!filters.tags.length && + !Object.keys(filters.metadata).length && + !filters.clientIds.length) + ) { + return apiApiSubscriptions; + } else { + const filterByMetadata = (subscription: IApiSubscriptionGql) => { + const meta = { + ...(subscription.metadata || {}), + ...(subscription.customMetadata || {}), + }; - const filterByTags = (subscription: IApiSubscriptionGql) => { - return filters.tags.every(tag => subscription.tags.includes(tag)) - } + return ( + !Object.keys(meta) || + !filters.metadata.length || + filters.metadata.every((item) => { + const value = meta[item.key]; + return value && value.includes(item.value); + }) + ); + }; - const filterByClientIds = (subscription: IApiSubscriptionGql) => { - return filters.clientIds.includes(subscription.apiKey.clientId) - } + const filterByTags = (subscription: IApiSubscriptionGql) => { + return filters.tags.every((tag) => + subscription.tags.includes(tag) + ); + }; + const filterByClientIds = (subscription: IApiSubscriptionGql) => { + return filters.clientIds.includes(subscription.apiKey.clientId); + }; - return apiApiSubscriptions - .filter(filterByMetadata) - .filter(filterByTags) - .filter(filterByClientIds) - } - }) - .then((apiApiSubscriptions) => Services.getSubscriptionsLastUsages(api.team, subscriptionsQuery.data?.map(s => s._id) || []) - .then(lastUsages => { - if (isError(lastUsages)) { - return subscriptionsQuery.data as IApiSubscriptionGqlWithUsage[] - } else { - return (apiApiSubscriptions ?? []) - .map(s => ({ - ...s, - lastUsage: lastUsages.find(u => u.subscription === s._id)?.date - } as IApiSubscriptionGqlWithUsage)); + return apiApiSubscriptions + .filter(filterByMetadata) + .filter(filterByTags) + .filter(filterByClientIds); } - })) - }) + }) + .then((apiApiSubscriptions) => + Services.getSubscriptionsLastUsages( + api.team, + subscriptionsQuery.data?.map((s) => s._id) || [] + ).then((lastUsages) => { + if (isError(lastUsages)) { + return subscriptionsQuery.data as IApiSubscriptionGqlWithUsage[]; + } else { + return (apiApiSubscriptions ?? []).map( + (s) => + ({ + ...s, + lastUsage: lastUsages.find((u) => u.subscription === s._id) + ?.date, + }) as IApiSubscriptionGqlWithUsage + ); + } + }) + ), + }); useEffect(() => { document.title = `${currentTeam.name} - ${translate('Subscriptions')}`; @@ -153,74 +190,88 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP useEffect(() => { if (api && subscriptionsQuery.data) { - tableRef.current?.update() + tableRef.current?.update(); } - }, [api, subscriptionsQuery.data]) + }, [api, subscriptionsQuery.data]); useEffect(() => { - tableRef.current?.update() - }, [filters]) + tableRef.current?.update(); + }, [filters]); - const columnHelper = createColumnHelper() + const columnHelper = createColumnHelper(); const columns = (usagePlans) => [ - columnHelper.accessor(row => row.adminCustomName || row.apiKey.clientName, { - id: 'adminCustomName', - header: translate('Name'), - meta: { style: { textAlign: 'left' } }, - filterFn: (row, _, value) => { - const sub = row.original - const displayed: string = sub.team._id === currentTeam._id ? sub.customName || sub.apiKey.clientName : sub.apiKey.clientName + columnHelper.accessor( + (row) => row.adminCustomName || row.apiKey.clientName, + { + id: 'adminCustomName', + header: translate('Name'), + meta: { style: { textAlign: 'left' } }, + filterFn: (row, _, value) => { + const sub = row.original; + const displayed: string = + sub.team._id === currentTeam._id + ? sub.customName || sub.apiKey.clientName + : sub.apiKey.clientName; - return displayed.toLocaleLowerCase().includes(value.toLocaleLowerCase()) - }, - sortingFn: 'basic', - cell: (info) => { - const sub = info.row.original; - if (sub.parent) { - const title = `
+ return displayed + .toLocaleLowerCase() + .includes(value.toLocaleLowerCase()); + }, + sortingFn: 'basic', + cell: (info) => { + const sub = info.row.original; + if (sub.parent) { + const title = `
${translate('aggregated.apikey.badge.title')}
  • ${translate('Api')}: ${sub.parent.api.name}
  • ${translate('Plan')}: ${sub.parent.plan.customName}
  • ${translate('aggregated.apikey.badge.apikey.name')}: ${sub.parent.adminCustomName}
-
` - return ( -
- {info.getValue()} - - -
A
-
-
- ); - } - return
{info.getValue()}
; +
`; + return ( +
+ {info.getValue()} + +
A
+
+
+ ); + } + return
{info.getValue()}
; + }, } - }), + ), columnHelper.accessor('plan', { header: translate('Plan'), meta: { style: { textAlign: 'left' } }, - cell: (info) => Option(usagePlans.find((pp) => pp._id === info.getValue()._id)) - .map((p: IUsagePlan) => p.customName || formatPlanType(p, translate)) - .getOrNull(), + cell: (info) => + Option(usagePlans.find((pp) => pp._id === info.getValue()._id)) + .map((p: IUsagePlan) => p.customName || formatPlanType(p, translate)) + .getOrNull(), filterFn: (row, columnId, value) => { - const displayed: string = Option(usagePlans.find((pp) => pp._id === row.original.plan._id)) + const displayed: string = Option( + usagePlans.find((pp) => pp._id === row.original.plan._id) + ) .map((p: IUsagePlan) => p.customName || formatPlanType(p, translate)) - .getOrElse("") + .getOrElse(''); - return displayed.toLocaleLowerCase().includes(value.toLocaleLowerCase()) - } + return displayed + .toLocaleLowerCase() + .includes(value.toLocaleLowerCase()); + }, }), columnHelper.accessor('team', { header: translate('Team'), meta: { style: { textAlign: 'left' } }, cell: (info) => info.getValue().name, filterFn: (row, columnId, value) => { - const displayed: string = row.original.team.name + const displayed: string = row.original.team.name; - return displayed.toLocaleLowerCase().includes(value.toLocaleLowerCase()) - } + return displayed + .toLocaleLowerCase() + .includes(value.toLocaleLowerCase()); + }, }), columnHelper.accessor('enabled', { header: translate('Enabled'), @@ -231,9 +282,19 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP const sub = info.row.original; return ( Services.archiveSubscriptionByOwner(currentTeam._id, sub._id, !sub.enabled) - .then(() => tableRef.current?.update())} - checked={sub.enabled} />); + onSwitch={() => + Services.archiveSubscriptionByOwner( + currentTeam._id, + sub._id, + !sub.enabled + ).then(() => { + tableRef.current?.update(); + queryClient.invalidateQueries({ queryKey: ['subscriptions'] }); + }) + } + checked={sub.enabled} + /> + ); }, }), columnHelper.accessor('createdAt', { @@ -241,11 +302,11 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP header: translate('Created at'), meta: { style: { textAlign: 'left' } }, cell: (info) => { - const date = info.getValue() + const date = info.getValue(); if (!!date) { - return formatDate(date, language) + return formatDate(date, language); } - return translate('N/A') + return translate('N/A'); }, }), columnHelper.accessor('lastUsage', { @@ -253,11 +314,11 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP header: translate('apisubscription.lastUsage.label'), meta: { style: { textAlign: 'left' } }, cell: (info) => { - const date = info.getValue() + const date = info.getValue(); if (!!date) { - return formatDate(date, language) + return formatDate(date, language); } - return translate('N/A') + return translate('N/A'); }, }), columnHelper.display({ @@ -265,50 +326,70 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP meta: { style: { textAlign: 'center', width: '120px' } }, cell: (info) => { const sub = info.row.original; - return (
- - - - - - - - - -
); + return ( +
+ + + + + + + + + +
+ ); }, }), - ] + ]; - const updateMeta = (sub: IApiSubscriptionGql) => openSubMetadataModal({ - save: (updates: CustomSubscriptionData) => { - Services.updateSubscription(currentTeam, { ...sub, ...updates }) - .then(() => { - queryClient.invalidateQueries({ queryKey: ['subscriptions'] }) - }); - }, - api: sub.api._id, - plan: sub.plan._id, - team: sub.team, - subscription: sub, - creationMode: false, - value: (plansQuery.data as Array) - .find(p => sub.plan._id === p._id)! - }); + const updateMeta = (sub: IApiSubscriptionGql) => + openSubMetadataModal({ + save: (updates: CustomSubscriptionData) => { + Services.updateSubscription(currentTeam, { ...sub, ...updates }).then( + () => { + queryClient.invalidateQueries({ queryKey: ['subscriptions'] }); + } + ); + }, + api: sub.api._id, + plan: sub.plan._id, + team: sub.team, + subscription: sub, + creationMode: false, + value: (plansQuery.data as Array).find( + (p) => sub.plan._id === p._id + )!, + }); const regenerateApiKeySecret = useMutation({ mutationFn: (sub: IApiSubscriptionGql) => - Services.regenerateApiKeySecret(currentTeam._id, sub._id), + Services.regenerateApiKeySecret(currentTeam._id, sub._id), onSuccess: () => { - toast.success(translate("secret.refresh.success")); + toast.success(translate('secret.refresh.success')); tableRef.current?.update(); - queryClient.invalidateQueries({queryKey: ["subscriptions"]}); + queryClient.invalidateQueries({ queryKey: ['subscriptions'] }); }, onError: (e: ResponseError) => { toast.error(translate(e.error)); @@ -316,31 +397,32 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP }); const regenerateSecret = (sub: IApiSubscriptionGql) => { - - const plan = sub.plan + const plan = sub.plan; confirm({ message: translate({ key: 'secret.refresh.confirm', - replacements: [sub.team.name, plan.customName ? plan.customName : plan.type] + replacements: [ + sub.team.name, + plan.customName ? plan.customName : plan.type, + ], }), okLabel: translate('Yes'), cancelLabel: translate('No'), - }) - .then((ok) => { - if (ok) { - regenerateApiKeySecret.mutate(sub) - } - }); + }).then((ok) => { + if (ok) { + regenerateApiKeySecret.mutate(sub); + } + }); }; const deleteApiSubscription = useMutation({ mutationFn: (sub: IApiSubscriptionGql) => - Services.deleteApiSubscription(sub.team._id, sub._id), + Services.deleteApiSubscription(sub.team._id, sub._id), onSuccess: () => { - toast.success(translate("api.delete.subscription.deleted")); + toast.success(translate('api.delete.subscription.deleted')); tableRef.current?.update(); - queryClient.invalidateQueries({queryKey: ["subscriptions"]}); + queryClient.invalidateQueries({ queryKey: ['subscriptions'] }); }, onError: (e: ResponseError) => { toast.error(translate(e.error)); @@ -348,16 +430,16 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP }); const deleteSubscription = (sub: IApiSubscriptionGql) => { confirm({ - title: translate("api.delete.subscription.form.title"), + title: translate('api.delete.subscription.form.title'), message: translate({ - key: "api.delete.subscription.message", + key: 'api.delete.subscription.message', replacements: [ sub.team.name, sub.plan.customName ? sub.plan.customName : sub.plan.type, ], }), - okLabel: translate("Yes"), - cancelLabel: translate("No"), + okLabel: translate('Yes'), + cancelLabel: translate('No'), }).then((ok) => { if (ok) { deleteApiSubscription.mutate(sub); @@ -366,58 +448,71 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP }; if (plansQuery.isLoading) { - return () + return ; } else if (plansQuery.data && !isError(plansQuery.data)) { const usagePlans = plansQuery.data; - const options = usagePlans.flatMap(plan => { + const options = usagePlans.flatMap((plan) => { return [ - ...(plan.otoroshiTarget?.apikeyCustomization.customMetadata.map(({ key }) => key) || []), - ...Object.keys(plan.otoroshiTarget?.apikeyCustomization.metadata || {}) - ] + ...(plan.otoroshiTarget?.apikeyCustomization.customMetadata.map( + ({ key }) => key + ) || []), + ...Object.keys(plan.otoroshiTarget?.apikeyCustomization.metadata || {}), + ]; }); return (
-
- + array: true, + label: translate('Filter Client Ids'), + }, + }, + title: translate('Filter data'), + value: filters, + }) + } + > + {' '} + {translate('Filter')}{' '} + {!!filters && ( -
setFilters(undefined)}> +
setFilters(undefined)} + > clear filter
@@ -429,9 +524,9 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP columns={columns(usagePlans)} fetchItems={() => { if (subscriptionsQuery.isLoading || subscriptionsQuery.error) { - return [] + return []; } else { - return subscriptionsQuery.data ?? [] + return subscriptionsQuery.data ?? []; } }} ref={tableRef} @@ -441,6 +536,6 @@ export const TeamApiSubscriptions = ({ api, currentTeam }: TeamApiSubscriptionsP ); } else { - return
error while fetching usage plan
+ return
error while fetching usage plan
; } }; From 6fe6b47491040040caff0c5a29ebb733fa321e5b Mon Sep 17 00:00:00 2001 From: Quentin AUBERT Date: Thu, 8 Aug 2024 12:23:05 +0200 Subject: [PATCH 2/2] Update Table component to have a default sort & setup default sort for TeamApiSubscriptions --- .../src/components/backoffice/apis/TeamApiSubscriptions.tsx | 2 +- daikoku/javascript/src/components/inputs/Table.tsx | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx index 36cc2e644..a235994d5 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx @@ -520,7 +520,7 @@ export const TeamApiSubscriptions = ({
{ if (subscriptionsQuery.isLoading || subscriptionsQuery.error) { diff --git a/daikoku/javascript/src/components/inputs/Table.tsx b/daikoku/javascript/src/components/inputs/Table.tsx index aefd4dfd9..3a97297eb 100644 --- a/daikoku/javascript/src/components/inputs/Table.tsx +++ b/daikoku/javascript/src/components/inputs/Table.tsx @@ -68,6 +68,7 @@ const TableComponent = (props: TableProps, ref: React.Ref< const { translate } = useContext(I18nContext); + console.debug({ id: props.columns[0].id!, desc: false, col: props.columns[0] }) const table = useReactTable({ data: items, columns: props.columns, @@ -82,6 +83,11 @@ const TableComponent = (props: TableProps, ref: React.Ref< getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), getFacetedMinMaxValues: getFacetedMinMaxValues(), + initialState: { + sorting: [ + {id: props.defaultSort || "", desc: !!props.defaultSortDesc} + ] + } });