diff --git a/daikoku/app/controllers/AdminApiController.scala b/daikoku/app/controllers/AdminApiController.scala index 2abd49911..2a85859b2 100644 --- a/daikoku/app/controllers/AdminApiController.scala +++ b/daikoku/app/controllers/AdminApiController.scala @@ -322,7 +322,7 @@ class StateAdminApiController( } def reset() = - DaikokuApiAction.async { _ => + DaikokuApiAction.async { ctx => (for { _ <- EitherT.cond[Future][AppError, Unit]( env.config.isDev || env.config.mode == DaikokuMode.Test, @@ -330,7 +330,7 @@ class StateAdminApiController( AppError.SecurityError("Action not avalaible") ) _ <- EitherT.liftF[Future, AppError, Unit](env.dataStore.clear()) - _ <- EitherT.liftF[Future, AppError, Done](env.initDatastore()) + _ <- EitherT.liftF[Future, AppError, Done](env.initDatastore(ctx.request.getQueryString("path"))) } yield Ok(Json.obj("done" -> true))) .leftMap(_.render()) .merge diff --git a/daikoku/app/controllers/AppError.scala b/daikoku/app/controllers/AppError.scala index 79753328a..11d46e2c6 100644 --- a/daikoku/app/controllers/AppError.scala +++ b/daikoku/app/controllers/AppError.scala @@ -52,6 +52,7 @@ object AppError { case object SubscriptionAggregationTeamConflict extends AppError case object SubscriptionAggregationOtoroshiConflict extends AppError case object SubscriptionAggregationDisabled extends AppError + case object EnvironmentSubscriptionAggregationDisabled extends AppError case object MissingParentSubscription extends AppError case object TranslationNotFound extends AppError case object Unauthorized extends AppError @@ -106,6 +107,7 @@ object AppError { case SubscriptionNotFound => NotFound(toJson(error)) case SubscriptionParentExisted => Conflict(toJson(error)) case SubscriptionAggregationDisabled => BadRequest(toJson(error)) + case EnvironmentSubscriptionAggregationDisabled => BadRequest(toJson(error)) case SubscriptionAggregationTeamConflict => Conflict(toJson(error)) case SubscriptionAggregationOtoroshiConflict => Conflict(toJson(error)) case MissingParentSubscription => NotFound(toJson(error)) @@ -115,7 +117,7 @@ object AppError { case NameAlreadyExists => Conflict(toJson(error)) case ThirdPartyPaymentSettingsNotFound => NotFound(toJson(error)) case SecurityError(security) => - play.api.mvc.Results.Unauthorized(toJson(error)) + play.api.mvc.Results.Forbidden(toJson(error)) case TeamAlreadyVerified => Conflict(toJson(error)) case UnexpectedError => BadRequest(toJson(error)) case InternalServerError(message) => @@ -167,6 +169,8 @@ object AppError { "The subscription already has a subscription parent - it cannot be extended any further" case SubscriptionAggregationDisabled => "Aggregation of api keys is disabled on plan or on tenant" + case EnvironmentSubscriptionAggregationDisabled => + "Aggregation of api keys is disabled on plan or on tenant for environment mode" case SubscriptionAggregationTeamConflict => "The new subscription has another team of the parent subscription" case SubscriptionAggregationOtoroshiConflict => diff --git a/daikoku/app/controllers/admin-api-openapi.json b/daikoku/app/controllers/admin-api-openapi.json index 1dfac1d21..25baf46b9 100644 --- a/daikoku/app/controllers/admin-api-openapi.json +++ b/daikoku/app/controllers/admin-api-openapi.json @@ -142,6 +142,10 @@ "type": "boolean", "nullable": true }, + "environmentAggregationApiKeysSecurity": { + "type": "boolean", + "nullable": true + }, "robotTxt": { "type": "string", "nullable": true @@ -2486,7 +2490,12 @@ "aggregationApiKeysSecurity": { "type": "boolean", "nullable": false + }, + "environmentAggregationApiKeysSecurity": { + "type": "boolean", + "nullable": false } + }, "required": [ "_id", @@ -2581,6 +2590,10 @@ "type": "boolean", "nullable": true }, + "environmentAggregationApiKeysSecurity": { + "type": "boolean", + "nullable": true + }, "swagger": { "nullable": true, "$ref": "#/components/schemas/SwaggerAccess" @@ -2703,6 +2716,10 @@ "type": "boolean", "nullable": false }, + "environmentAggregationApiKeysSecurity": { + "type": "boolean", + "nullable": false + }, "swagger": { "nullable": true, "$ref": "#/components/schemas/SwaggerAccess" @@ -2835,6 +2852,10 @@ "type": "boolean", "nullable": false }, + "environmentAggregationApiKeysSecurity": { + "type": "boolean", + "nullable": false + }, "swagger": { "nullable": true, "$ref": "#/components/schemas/SwaggerAccess" @@ -2973,6 +2994,10 @@ "type": "boolean", "nullable": false }, + "environmentAggregationApiKeysSecurity": { + "type": "boolean", + "nullable": false + }, "swagger": { "nullable": true, "$ref": "#/components/schemas/SwaggerAccess" @@ -3108,6 +3133,10 @@ "type": "boolean", "nullable": false }, + "environmentAggregationApiKeysSecurity": { + "type": "boolean", + "nullable": false + }, "swagger": { "nullable": true, "$ref": "#/components/schemas/SwaggerAccess" diff --git a/daikoku/app/domain/SchemaDefinition.scala b/daikoku/app/domain/SchemaDefinition.scala index c23495066..64f73e029 100644 --- a/daikoku/app/domain/SchemaDefinition.scala +++ b/daikoku/app/domain/SchemaDefinition.scala @@ -245,6 +245,10 @@ object SchemaDefinition { OptionType(BooleanType), resolve = _.value.aggregationApiKeysSecurity ), + Field( + "environmentAggregationApiKeysSecurity", + OptionType(BooleanType), + resolve = _.value.environmentAggregationApiKeysSecurity), Field( "display", OptionType(StringType), diff --git a/daikoku/app/domain/json.scala b/daikoku/app/domain/json.scala index 98afebe58..e632c15b9 100644 --- a/daikoku/app/domain/json.scala +++ b/daikoku/app/domain/json.scala @@ -1007,7 +1007,7 @@ object json { otoroshiTarget = (json \ "otoroshiTarget").asOpt(OtoroshiTargetFormat), aggregationApiKeysSecurity = - (json \ "aggregationApiKeysSecurity").asOpt[Boolean] + (json \ "aggregationApiKeysSecurity").asOpt[Boolean], ) ) } recover { @@ -2202,6 +2202,8 @@ object json { tenantMode = (json \ "tenantMode").asOpt(TenantModeFormat), aggregationApiKeysSecurity = (json \ "aggregationApiKeysSecurity") .asOpt[Boolean], + environmentAggregationApiKeysSecurity = (json \ "environmentAggregationApiKeysSecurity") + .asOpt[Boolean], robotTxt = (json \ "robotTxt").asOpt[String], thirdPartyPaymentSettings = (json \ "thirdPartyPaymentSettings") .asOpt(SeqThirdPartyPaymentSettingsFormat) @@ -2279,6 +2281,10 @@ object json { .map(JsBoolean) .getOrElse(JsBoolean(false)) .as[JsValue], + "environmentAggregationApiKeysSecurity" -> o.environmentAggregationApiKeysSecurity + .map(JsBoolean) + .getOrElse(JsBoolean(false)) + .as[JsValue], "robotTxt" -> o.robotTxt .map(JsString.apply) .getOrElse(JsNull) diff --git a/daikoku/app/domain/tenantEntities.scala b/daikoku/app/domain/tenantEntities.scala index bda1581cb..33dad90d9 100644 --- a/daikoku/app/domain/tenantEntities.scala +++ b/daikoku/app/domain/tenantEntities.scala @@ -388,6 +388,7 @@ case class Tenant( defaultMessage: Option[String] = None, tenantMode: Option[TenantMode] = None, aggregationApiKeysSecurity: Option[Boolean] = None, + environmentAggregationApiKeysSecurity: Option[Boolean] = None, robotTxt: Option[String] = None, thirdPartyPaymentSettings: Seq[ThirdPartyPaymentSettings] = Seq.empty, display: TenantDisplay = TenantDisplay.Default, @@ -455,6 +456,10 @@ case class Tenant( .map(JsBoolean) .getOrElse(JsBoolean(false)) .as[JsValue], + "environmentAggregationApiKeysSecurity" -> environmentAggregationApiKeysSecurity + .map(JsBoolean) + .getOrElse(JsBoolean(false)) + .as[JsValue], "display" -> display.name, "environments" -> JsArray(environments.map(JsString.apply).toSeq), "loginProvider" -> authProvider.name, diff --git a/daikoku/app/env/env.scala b/daikoku/app/env/env.scala index c7d3635cd..76a9e3b01 100644 --- a/daikoku/app/env/env.scala +++ b/daikoku/app/env/env.scala @@ -306,7 +306,7 @@ sealed trait Env { def getDaikokuUrl(tenant: Tenant, path: String): String - def initDatastore()(implicit ec: ExecutionContext): Future[Done] + def initDatastore(path: Option[String]= None)(implicit ec: ExecutionContext): Future[Done] } class DaikokuEnv( @@ -376,14 +376,14 @@ class DaikokuEnv( } } - override def initDatastore()(implicit ec: ExecutionContext): Future[Done] = { + override def initDatastore(path: Option[String]=None)(implicit ec: ExecutionContext): Future[Done] = { def run(isEmpty: Boolean): Future[Unit] = { if (isEmpty) { (dataStore match { case store: PostgresDataStore => store.checkDatabase() case _ => FastFuture.successful(None) }).map { _ => - config.init.data.from match { + path.orElse(config.init.data.from) match { case Some(path) if path.startsWith("http://") || path .startsWith("https://") => diff --git a/daikoku/app/utils/ApiService.scala b/daikoku/app/utils/ApiService.scala index ff3fd391d..2d88f3803 100644 --- a/daikoku/app/utils/ApiService.scala +++ b/daikoku/app/utils/ApiService.scala @@ -2328,6 +2328,14 @@ class ApiService( .findById(subscription.plan), AppError.PlanNotFound ) + _ <- EitherT.cond[Future][AppError, Unit]( + tenant.display != TenantDisplay.Environment || (tenant.environmentAggregationApiKeysSecurity match { + case Some(true) => plan.customName == parentPlan.customName + case _ => true + }), + (), + AppError.SecurityError(s"Environment Subscription Aggregation security is enabled, a subscription cannot be extended by another environment") + ) _ <- EitherT.cond[Future][AppError, Unit]( parentPlan.otoroshiTarget .map(_.otoroshiSettings) == plan.otoroshiTarget diff --git a/daikoku/javascript/src/components/adminbackoffice/otoroshi/InititializeFromOtoroshi.tsx b/daikoku/javascript/src/components/adminbackoffice/otoroshi/InititializeFromOtoroshi.tsx index d95c5e592..6e4ced022 100644 --- a/daikoku/javascript/src/components/adminbackoffice/otoroshi/InititializeFromOtoroshi.tsx +++ b/daikoku/javascript/src/components/adminbackoffice/otoroshi/InititializeFromOtoroshi.tsx @@ -184,7 +184,6 @@ export const InitializeFromOtoroshi = () => { const orderedApikeys = orderBy(state.context.apikeys, ['clientName']); const filterApikeys = (entity: { label: string, prefix: string, value: string }) => { - console.debug({ entity, orderedApikeys }) return orderedApikeys.filter((apikey) => (apikey.authorizedEntities || '').includes(`${entity.prefix}${entity.value}`)); }; diff --git a/daikoku/javascript/src/components/adminbackoffice/tenants/TenantEdit.tsx b/daikoku/javascript/src/components/adminbackoffice/tenants/TenantEdit.tsx index ed5aac232..348cdf28d 100644 --- a/daikoku/javascript/src/components/adminbackoffice/tenants/TenantEdit.tsx +++ b/daikoku/javascript/src/components/adminbackoffice/tenants/TenantEdit.tsx @@ -1,68 +1,103 @@ import { useContext } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; -import { Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom'; +import { + Route, + Routes, + useLocation, + useNavigate, + useParams, +} from 'react-router-dom'; import { useDaikokuBackOffice, useTenantBackOffice } from '../../../contexts'; import { I18nContext } from '../../../contexts/i18n-context'; import * as Services from '../../../services'; import { ITenantFull } from '../../../types/tenant'; import { Spinner } from '../../utils/Spinner'; -import { AuditForm, AuthenticationForm, BucketForm, CustomizationForm, GeneralForm, MailForm } from './forms'; +import { + AuditForm, + AuthenticationForm, + BucketForm, + CustomizationForm, + GeneralForm, + MailForm, +} from './forms'; import { SecurityForm } from './forms/SecurityForm'; import { ThirdPartyPaymentForm } from './forms/ThirdPartyPaymentForm'; import { ResponseError, isError } from '../../../types'; import { DisplayForm } from './forms/DisplayForm'; -export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: string, fromDaikokuAdmin?: boolean }) => { - const { translate } = useContext(I18nContext) +export const TenantEditComponent = ({ + tenantId, + fromDaikokuAdmin, +}: { + tenantId: string; + fromDaikokuAdmin?: boolean; +}) => { + const { translate } = useContext(I18nContext); const navigate = useNavigate(); const { state } = useLocation(); - const queryClient = useQueryClient() + const queryClient = useQueryClient(); const { isLoading, data } = useQuery({ queryKey: ['full-tenant'], queryFn: () => Services.oneTenant(tenantId), - enabled: !state - }) + enabled: !state, + }); const updateTenant = useMutation({ - mutationFn: (tenant: ITenantFull) => Services.saveTenant(tenant).then(r => isError(r) ? Promise.reject(r) : r), + mutationFn: (tenant: ITenantFull) => + Services.saveTenant(tenant).then((r) => + isError(r) ? Promise.reject(r) : r + ), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["full-tenant"] }); - queryClient.invalidateQueries({ queryKey: ["context"] }); - toast.success(translate('Tenant updated successfully')) + queryClient.invalidateQueries({ queryKey: ['full-tenant'] }); + queryClient.invalidateQueries({ queryKey: ['context'] }); + toast.success(translate('Tenant updated successfully')); }, onError: (e: ResponseError) => { - toast.error(translate(e.error)) + toast.error(translate(e.error)); //todo: reset forms - } + }, }); const createTenant = useMutation({ mutationFn: (tenant: ITenantFull) => Services.createTenant(tenant), onSuccess: (createdTenant) => { - navigate(`/settings/tenants/${createdTenant._humanReadableId}/general`) - queryClient.invalidateQueries({ queryKey: ['tenant'] }) - toast.success(translate({ key: 'tenant.created.success', replacements:[`${createdTenant.name}`]})) - + navigate(`/settings/tenants/${createdTenant._humanReadableId}/general`); + queryClient.invalidateQueries({ queryKey: ['tenant'] }); + toast.success( + translate({ + key: 'tenant.created.success', + replacements: [`${createdTenant.name}`], + }) + ); + }, + onError: () => { + toast.error(translate('Error')); }, - onError: () => { toast.error(translate('Error')) } }); if (isLoading && !state) { - return ( - - ) + return ; } else if ((!!state && state.newTenant) || (data && !isError(data))) { - const tenant: ITenantFull = state?.newTenant || data + const tenant: ITenantFull = state?.newTenant || data; return ( - {fromDaikokuAdmin &&

{tenant.name} - {translate('General')}

} - + {fromDaikokuAdmin && ( +

+ {tenant.name} - {translate('General')} +

+ )} + } /> @@ -70,7 +105,11 @@ export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: path="/customization" element={ <> - {fromDaikokuAdmin &&

{tenant.name} - {translate('Customization')}

} + {fromDaikokuAdmin && ( +

+ {tenant.name} - {translate('Customization')} +

+ )} } @@ -79,7 +118,11 @@ export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: path="/audit" element={ <> - {fromDaikokuAdmin &&

{tenant.name} - {translate('Audit')}

} + {fromDaikokuAdmin && ( +

+ {tenant.name} - {translate('Audit')} +

+ )} } @@ -88,7 +131,11 @@ export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: path="/mail" element={ <> - {fromDaikokuAdmin &&

{tenant.name} - {translate('Mail')}

} + {fromDaikokuAdmin && ( +

+ {tenant.name} - {translate('Mail')} +

+ )} } @@ -97,7 +144,11 @@ export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: path="/authentication" element={ <> - {fromDaikokuAdmin &&

{tenant.name} - {translate('Authentication')}

} + {fromDaikokuAdmin && ( +

+ {tenant.name} - {translate('Authentication')} +

+ )} } @@ -106,7 +157,11 @@ export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: path="/bucket" element={ <> - {fromDaikokuAdmin &&

{tenant.name} - {translate('Bucket')}

} + {fromDaikokuAdmin && ( +

+ {tenant.name} - {translate('Bucket')} +

+ )} } @@ -115,8 +170,15 @@ export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: path="/payment" element={ <> - {fromDaikokuAdmin &&

{tenant.name} - {translate('Security')}

} - + {fromDaikokuAdmin && ( +

+ {tenant.name} - {translate('Security')} +

+ )} + } /> @@ -124,7 +186,11 @@ export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: path="/security" element={ <> - {fromDaikokuAdmin &&

{tenant.name} - {translate('Third-Party payment')}

} + {fromDaikokuAdmin && ( +

+ {tenant.name} - {translate('Third-Party payment')} +

+ )} } @@ -133,34 +199,33 @@ export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: path="/display-mode" element={ <> - {fromDaikokuAdmin &&

{tenant.name} - {translate('DisplayMode')}

} + {fromDaikokuAdmin && ( +

+ {tenant.name} - {translate('DisplayMode')} +

+ )} } />
- ) + ); } else { - return
Error while fetching tenant
+ return
Error while fetching tenant
; } -} +}; -export const TenantEdit = ({ }) => { +export const TenantEdit = ({}) => { const { tenant } = useTenantBackOffice(); - return ( - - ) -} + return ; +}; -export const TenantEditForAdmin = ({ }) => { +export const TenantEditForAdmin = ({}) => { const { tenantId } = useParams(); const { state } = useLocation(); useDaikokuBackOffice({ creation: state?.newTenant }); - return ( - - ) -} - + return ; +}; diff --git a/daikoku/javascript/src/components/adminbackoffice/tenants/forms/SecurityForm.tsx b/daikoku/javascript/src/components/adminbackoffice/tenants/forms/SecurityForm.tsx index 37d589e3d..63e69d329 100644 --- a/daikoku/javascript/src/components/adminbackoffice/tenants/forms/SecurityForm.tsx +++ b/daikoku/javascript/src/components/adminbackoffice/tenants/forms/SecurityForm.tsx @@ -2,19 +2,21 @@ import { useContext } from 'react'; import { Form, Schema, type } from '@maif/react-forms'; import { UseMutationResult } from '@tanstack/react-query'; - import { I18nContext } from '../../../../contexts'; -import { ITenantFull } from '../../../../types'; +import { Display, ITenantFull } from '../../../../types'; import { ModalContext } from '../../../../contexts'; -export const SecurityForm = (props: { tenant?: ITenantFull, updateTenant: UseMutationResult }) => { +export const SecurityForm = (props: { + tenant?: ITenantFull; + updateTenant: UseMutationResult; +}) => { const { translate } = useContext(I18nContext); const { alert } = useContext(ModalContext); const schema: Schema = { isPrivate: { type: type.bool, - label: translate('Private tenant') + label: translate('Private tenant'), }, creationSecurity: { type: type.bool, @@ -30,11 +32,20 @@ export const SecurityForm = (props: { tenant?: ITenantFull, updateTenant: UseMut type: type.bool, label: translate('aggregation api keys security'), onChange: (value) => { - const security = (value as { value: any }).value + const security = (value as { value: any }).value; if (security) { - alert({ message: translate('aggregation.api_key.security.notification') }); + alert({ + message: translate('aggregation.api_key.security.notification'), + }); } - } + }, + }, + environmentAggregationApiKeysSecurity: { + type: type.bool, + label: translate('aggregation api keys security for environment mode'), + help: translate('aggregation.environment.api_key.security.notification'), + deps: ['aggregationApiKeysSecurity'], + visible: ({ rawValues }) => rawValues.aggregationApiKeysSecurity && rawValues.display === Display.environment }, apiReferenceHideForGuest: { type: type.bool, @@ -46,20 +57,21 @@ export const SecurityForm = (props: { tenant?: ITenantFull, updateTenant: UseMut array: true, label: translate('CMS Redirections Domains'), help: translate('cms.redirections.domains'), - } - } + }, + }; return (
props.updateTenant.mutateAsync(updatedTenant)} + onSubmit={(updatedTenant) => + props.updateTenant.mutateAsync(updatedTenant) + } value={props.tenant} options={{ actions: { - submit: { label: translate('Save') } - } + submit: { label: translate('Save') }, + }, }} /> - ) - -} \ No newline at end of file + ); +}; diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiGroup.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiGroup.tsx index 65c6b2480..fc4e559cd 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApiGroup.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiGroup.tsx @@ -48,7 +48,6 @@ export const TeamApiGroup = () => { const save = (group: IApi) => { if (creation) { - console.debug({ group }) return Services.createTeamApi(currentTeam!._id, group) .then((createdGroup) => { if (createdGroup.error) { diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiPricings.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiPricings.tsx index df8321dd3..2bd779096 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApiPricings.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiPricings.tsx @@ -1,5 +1,12 @@ import { UniqueIdentifier } from '@dnd-kit/core'; -import { CodeInput, constraints, Form, format, Schema, type } from '@maif/react-forms'; +import { + CodeInput, + constraints, + Form, + format, + Schema, + type, +} from '@maif/react-forms'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import classNames from 'classnames'; import cloneDeep from 'lodash/cloneDeep'; @@ -23,16 +30,43 @@ import { GlobalContext } from '../../../contexts/globalContext'; import * as Services from '../../../services'; import { currencies } from '../../../services/currencies'; import { ITeamSimple } from '../../../types'; -import { IApi, IDocumentation, isError, isValidationStepEmail, isValidationStepHttpRequest, isValidationStepPayment, isValidationStepTeamAdmin, IUsagePlan, IValidationStep, IValidationStepEmail, IValidationStepHttpRequest, IValidationStepTeamAdmin, IValidationStepType, UsagePlanVisibility } from '../../../types/api'; -import { IOtoroshiSettings, ITenant, ITenantFull, IThirdPartyPaymentSettings } from '../../../types/tenant'; import { - BeautifulTitle, formatPlanType, IMultistepsformStep, - MultiStepForm, Option, + IApi, + IDocumentation, + isError, + isValidationStepEmail, + isValidationStepHttpRequest, + isValidationStepPayment, + isValidationStepTeamAdmin, + IUsagePlan, + IValidationStep, + IValidationStepEmail, + IValidationStepHttpRequest, + IValidationStepTeamAdmin, + IValidationStepType, + UsagePlanVisibility, +} from '../../../types/api'; +import { + IOtoroshiSettings, + ITenant, + ITenantFull, + IThirdPartyPaymentSettings, +} from '../../../types/tenant'; +import { + BeautifulTitle, + formatPlanType, + IMultistepsformStep, + MultiStepForm, + Option, renderPricing, - Spinner + Spinner, } from '../../utils'; import { addArrayIf, insertArrayIndex } from '../../utils/array'; -import { FixedItem, SortableItem, SortableList } from '../../utils/dnd/SortableList'; +import { + FixedItem, + SortableItem, + SortableList, +} from '../../utils/dnd/SortableList'; import { Help } from '../apikeys'; import { TeamApiDocumentation } from './TeamApiDocumentation'; import { TeamApiSwagger } from './TeamApiSwagger'; @@ -41,28 +75,31 @@ import { TeamApiTesting } from './TeamApiTesting'; const SUBSCRIPTION_PLAN_TYPES = { FreeWithoutQuotas: { defaultName: 'Free plan', - defaultDescription: 'Free plan with unlimited number of calls per day and per month', + defaultDescription: + 'Free plan with unlimited number of calls per day and per month', }, FreeWithQuotas: { defaultName: 'Free plan with quotas', - defaultDescription: 'Free plan with limited number of calls per day and per month', + defaultDescription: + 'Free plan with limited number of calls per day and per month', }, QuotasWithLimits: { defaultName: 'Quotas with limits', - defaultDescription: 'Priced plan with limited number of calls per day and per month', + defaultDescription: + 'Priced plan with limited number of calls per day and per month', }, QuotasWithoutLimits: { defaultName: 'Quotas with Pay per use', - defaultDescription: 'Priced plan with unlimited number of calls per day and per month', + defaultDescription: + 'Priced plan with unlimited number of calls per day and per month', + }, + PayPerUse: { + defaultName: 'Pay per use', + defaultDescription: 'Plan priced on usage', }, - PayPerUse: { defaultName: 'Pay per use', defaultDescription: 'Plan priced on usage' }, }; -const OtoroshiEntitiesSelector = ({ - rawValues, - onChange, - translate -}: any) => { +const OtoroshiEntitiesSelector = ({ rawValues, onChange, translate }: any) => { const [loading, setLoading] = useState(true); const [groups, setGroups] = useState>([]); const [services, setServices] = useState>([]); @@ -90,29 +127,35 @@ const OtoroshiEntitiesSelector = ({ Services.getOtoroshiRoutesAsTeamAdmin( params.teamId, rawValues.otoroshiTarget.otoroshiSettings - ) + ), ]) .then(([groups, services, routes]) => { if (!groups.error) - setGroups(groups.map((g: any) => ({ - label: g.name, - value: g.id, - type: 'group' - }))); + setGroups( + groups.map((g: any) => ({ + label: g.name, + value: g.id, + type: 'group', + })) + ); else setGroups([]); if (!services.error) - setServices(services.map((g: any) => ({ - label: g.name, - value: g.id, - type: 'service' - }))); + setServices( + services.map((g: any) => ({ + label: g.name, + value: g.id, + type: 'service', + })) + ); else setServices([]); if (!routes.error) - setRoutes(routes.map((g: any) => ({ - label: g.name, - value: g.id, - type: 'route' - }))); + setRoutes( + routes.map((g: any) => ({ + label: g.name, + value: g.id, + type: 'route', + })) + ); else setRoutes([]); }) .catch(() => { @@ -131,12 +174,28 @@ const OtoroshiEntitiesSelector = ({ }, [services, groups, routes]); useEffect(() => { - if (!!groups && !!services && !!routes && !!rawValues.otoroshiTarget.authorizedEntities) { - setValue([ - ...rawValues.otoroshiTarget.authorizedEntities.groups.map((authGroup: any) => (groups as any).find((g: any) => g.value === authGroup)), - ...(rawValues.otoroshiTarget.authorizedEntities.services || []).map((authService: any) => (services as any).find((g: any) => g.value === authService)), - ...(rawValues.otoroshiTarget.authorizedEntities.routes || []).map((authRoute: any) => (routes as any).find((g: any) => g.value === authRoute)) - ].filter((f) => f)); + if ( + !!groups && + !!services && + !!routes && + !!rawValues.otoroshiTarget.authorizedEntities + ) { + setValue( + [ + ...rawValues.otoroshiTarget.authorizedEntities.groups.map( + (authGroup: any) => + (groups as any).find((g: any) => g.value === authGroup) + ), + ...(rawValues.otoroshiTarget.authorizedEntities.services || []).map( + (authService: any) => + (services as any).find((g: any) => g.value === authService) + ), + ...(rawValues.otoroshiTarget.authorizedEntities.routes || []).map( + (authRoute: any) => + (routes as any).find((g: any) => g.value === authRoute) + ), + ].filter((f) => f) + ); } }, [rawValues, groups, services, routes]); @@ -151,26 +210,41 @@ const OtoroshiEntitiesSelector = ({ case 'group': return { ...acc, - groups: [...acc.groups, groups.find((g: any) => g.value === entitie.value).value], + groups: [ + ...acc.groups, + groups.find((g: any) => g.value === entitie.value).value, + ], }; case 'service': return { ...acc, - services: [...acc.services, services.find((s: any) => s.value === entitie.value).value], + services: [ + ...acc.services, + services.find((s: any) => s.value === entitie.value).value, + ], }; case 'route': return { ...acc, - routes: [...acc.routes, routes.find((s: any) => s.value === entitie.value).value], + routes: [ + ...acc.routes, + routes.find((s: any) => s.value === entitie.value).value, + ], }; } }, { groups: [], services: [], routes: [] } ); setValue([ - ...value.groups.map((authGroup: any) => groups.find((g: any) => g.value === authGroup)), - ...value.services.map((authService: any) => services.find((g: any) => g.value === authService)), - ...value.routes.map((authRoute: any) => routes.find((g: any) => g.value === authRoute)), + ...value.groups.map((authGroup: any) => + groups.find((g: any) => g.value === authGroup) + ), + ...value.services.map((authService: any) => + services.find((g: any) => g.value === authService) + ), + ...value.routes.map((authRoute: any) => + routes.find((g: any) => g.value === authRoute) + ), ]); onChange(value); } @@ -179,7 +253,7 @@ const OtoroshiEntitiesSelector = ({ const groupedOptions = [ { label: 'Service groups', options: groups }, { label: 'Services', options: services }, - { label: 'Routes', options: routes } + { label: 'Routes', options: routes }, ]; const formatGroupLabel = (data) => ( @@ -187,76 +261,101 @@ const OtoroshiEntitiesSelector = ({ {data.label} {data.options.length} - ) - - return (
- } + formatGroupLabel={formatGroupLabel} + options={groupedOptions} + value={value} + onChange={onValueChange} + classNamePrefix="reactSelect" + className="reactSelect" + /> +
+
+ + + Services Groups + + + {!!value && + value + .filter((x: any) => x.type === 'group') + .map((g: any, idx: any) => ( + + {g.label} + + ))} +
+
+ + Services + + {!!value && + value + .filter((x: any) => x.type === 'service') + .map((g: any, idx: any) => ( + + {g.label} + + ))} +
+
+ + Routes + + {!!value && + value + .filter((x: any) => x.type === 'route') + .map((g: any, idx: any) => ( + + {g.label} + + ))} +
- ); + ); }; const CustomMetadataInput = (props: { - value?: Array<{ key: string, possibleValues: Array }>; + value?: Array<{ key: string; possibleValues: Array }>; onChange?: (param: any) => void; setValue?: (key: string, data: any) => void; - translate: (key: string) => string + translate: (key: string) => string; }) => { const { alert } = useContext(ModalContext); const changeValue = (possibleValues: any, key: string) => { - const oldValue = Option(props.value?.find((x) => x.key === key)).getOrElse({ key: '', possibleValues: [] }); - const newValues = [...(props.value || []).filter((x) => x.key !== key), { ...oldValue, key, possibleValues }]; + const oldValue = Option(props.value?.find((x) => x.key === key)).getOrElse({ + key: '', + possibleValues: [], + }); + const newValues = [ + ...(props.value || []).filter((x) => x.key !== key), + { ...oldValue, key, possibleValues }, + ]; props.onChange && props.onChange(newValues); }; - const changeKey = (e: React.ChangeEvent, oldName: string) => { + const changeKey = ( + e: React.ChangeEvent, + oldName: string + ) => { if (e && e.preventDefault) e.preventDefault(); - const oldValue = Option(props.value?.find((x) => x.key === oldName)).getOrElse({ key: '', possibleValues: [] }); + const oldValue = Option( + props.value?.find((x) => x.key === oldName) + ).getOrElse({ key: '', possibleValues: [] }); const newValues = [ ...(props.value || []).filter((x) => x.key !== oldName), { ...oldValue, key: e.target.value }, @@ -268,7 +367,10 @@ const CustomMetadataInput = (props: { if (e && e.preventDefault) e.preventDefault(); if (!props.value || props.value.length === 0) { props.onChange && props.onChange([{ key: '', possibleValues: [] }]); - alert({ message: props.translate('custom.metadata.process.change.to.manual'), title: props.translate('Information') }) + alert({ + message: props.translate('custom.metadata.process.change.to.manual'), + title: props.translate('Information'), + }); } }; @@ -282,23 +384,25 @@ const CustomMetadataInput = (props: { const remove = (e: React.MouseEvent, key: string) => { if (e && e.preventDefault) e.preventDefault(); - props.onChange && props.onChange((props.value || []).filter((x: any) => x.key !== key)); + props.onChange && + props.onChange((props.value || []).filter((x: any) => x.key !== key)); }; return (
{!props.value?.length && (
-
)} - {(props.value || []).map(({ - key, - possibleValues - }, idx) => ( + {(props.value || []).map(({ key, possibleValues }, idx) => (
({ label: value, - value + value, }))} className="input-select reactSelect flex-grow-1" classNamePrefix="reactSelect" @@ -347,15 +451,15 @@ const CustomMetadataInput = (props: { }; type CardProps = { - plan: IUsagePlan - isDefault: boolean - makeItDefault?: () => void - toggleVisibility?: () => void - deletePlan: () => void - editPlan?: () => void - duplicatePlan?: () => void - creation?: boolean -} + plan: IUsagePlan; + isDefault: boolean; + makeItDefault?: () => void; + toggleVisibility?: () => void; + deletePlan: () => void; + editPlan?: () => void; + duplicatePlan?: () => void; + creation?: boolean; +}; const Card = ({ plan, isDefault, @@ -364,60 +468,81 @@ const Card = ({ deletePlan, editPlan, duplicatePlan, - creation + creation, }: CardProps) => { const { translate, Translation } = useContext(I18nContext); const { confirm } = useContext(ModalContext); const { tenant } = useContext(GlobalContext); - const pricing = renderPricing(plan, translate) + const pricing = renderPricing(plan, translate); const deleteWithConfirm = () => { - confirm({ message: translate('delete.plan.confirm') }) - .then((ok) => { - if (ok) { - deletePlan() - } - }); + confirm({ message: translate('delete.plan.confirm') }).then((ok) => { + if (ok) { + deletePlan(); + } + }); }; - const noOtoroshi = !plan.otoroshiTarget || !plan.otoroshiTarget.authorizedEntities || + const noOtoroshi = + !plan.otoroshiTarget || + !plan.otoroshiTarget.authorizedEntities || (!plan.otoroshiTarget.authorizedEntities.groups.length && !plan.otoroshiTarget.authorizedEntities.services.length && - !plan.otoroshiTarget.authorizedEntities.routes.length) + !plan.otoroshiTarget.authorizedEntities.routes.length); return ( -
+
{noOtoroshi && ( - + + + )} +
- - - )} -
+ }} + > {plan.visibility === PRIVATE && ( - + )} {isDefault && ( - + )}
{!creation && (
{!isDefault && plan.visibility !== PRIVATE && ( - - {tenant.display === 'environment' ? translate('pricing.default.env.btn.label') : translate('Make default plan')} + + {tenant.display === 'environment' + ? translate('pricing.default.env.btn.label') + : translate('Make default plan')} )} {!isDefault && ( - + {plan.visibility === PUBLIC && ( - Make it private + + Make it private + )} {plan.visibility === PRIVATE && ( - Make it public + + Make it public + )} )}
- - {tenant.display === 'environment' ? translate('pricing.clone.env.btn.label') : translate('Duplicate plan')} + + {tenant.display === 'environment' + ? translate('pricing.clone.env.btn.label') + : translate('Duplicate plan')} - {tenant.display === 'environment' ? translate('pricing.edit.env.btn.label') : translate('Edit plan')} + {tenant.display === 'environment' + ? translate('pricing.edit.env.btn.label') + : translate('Edit plan')}
- {tenant.display === 'environment' ? translate('pricing.delete.env.btn.label') : translate('Delete plan')} + {tenant.display === 'environment' + ? translate('pricing.delete.env.btn.label') + : translate('Delete plan')}
)} -
+
{plan.customName || formatPlanType(plan, translate)}
@@ -468,7 +617,9 @@ const Card = ({

- {!plan.maxPerSecond && !plan.maxPerMonth && translate('plan.limits.unlimited')} + {!plan.maxPerSecond && + !plan.maxPerMonth && + translate('plan.limits.unlimited')} {!!plan.maxPerSecond && !!plan.maxPerMonth && (
@@ -476,7 +627,8 @@ const Card = ({ i18nkey="plan.limits" replacements={[plan.maxPerSecond, plan.maxPerMonth]} > - Limits: {plan.maxPerSecond} req./sec, {plan.maxPerMonth} req./month + Limits: {plan.maxPerSecond} req./sec, {plan.maxPerMonth}{' '} + req./month
@@ -497,89 +649,157 @@ const PUBLIC: UsagePlanVisibility = 'Public'; const PRIVATE: UsagePlanVisibility = 'Private'; type Props = { - currentTeam: ITeamSimple - api: IApi - team: ITeamSimple - tenant: ITenant - reload: () => Promise - setDefaultPlan: (plan: IUsagePlan) => void - creation: boolean - expertMode: boolean - injectSubMenu: (x: JSX.Element | null) => void - openApiSelectModal?: () => void - setHeader: (t?: string) => void -} -type Tab = 'settings' | 'security' | 'payment' | 'subscription-process' | 'swagger' | 'documentation' | 'testing' + currentTeam: ITeamSimple; + api: IApi; + team: ITeamSimple; + tenant: ITenant; + reload: () => Promise; + setDefaultPlan: (plan: IUsagePlan) => void; + creation: boolean; + expertMode: boolean; + injectSubMenu: (x: JSX.Element | null) => void; + openApiSelectModal?: () => void; + setHeader: (t?: string) => void; +}; +type Tab = + | 'settings' + | 'security' + | 'payment' + | 'subscription-process' + | 'swagger' + | 'documentation' + | 'testing'; export const TeamApiPricings = (props: Props) => { - const possibleMode = { list: 'LIST', creation: 'CREATION', edition: 'EDITION' }; + const possibleMode = { + list: 'LIST', + creation: 'CREATION', + edition: 'EDITION', + }; const [planForEdition, setPlanForEdition] = useState(); const [mode, setMode] = useState('LIST'); const [creation, setCreation] = useState(false); - const [selectedTab, setSelectedTab] = useState('settings') + const [selectedTab, setSelectedTab] = useState('settings'); const { translate } = useContext(I18nContext); const { openApiSelectModal, confirm } = useContext(ModalContext); const { tenant } = useContext(GlobalContext); - const queryClient = useQueryClient() - const queryFullTenant = useQuery({ queryKey: ['full-tenant'], queryFn: () => Services.oneTenant(props.tenant._id) }) - const queryPlans = useQuery({ queryKey: ['plans'], queryFn: () => Services.getAllPlanOfApi(props.api.team, props.api._id, props.api.currentVersion) }) + const queryClient = useQueryClient(); + const queryFullTenant = useQuery({ + queryKey: ['full-tenant'], + queryFn: () => Services.oneTenant(props.tenant._id), + }); + const queryPlans = useQuery({ + queryKey: ['plans'], + queryFn: () => + Services.getAllPlanOfApi( + props.api.team, + props.api._id, + props.api.currentVersion + ), + }); useEffect(() => { return () => { props.injectSubMenu(null); - props.setHeader(undefined) + props.setHeader(undefined); }; }, []); useEffect(() => { if (mode !== possibleMode.list) { - props.injectSubMenu(
- setSelectedTab('settings')}>{translate('Settings')} - {mode === possibleMode.edition && planForEdition && paidPlans.includes(planForEdition.type) && ( - setSelectedTab('payment')}>{translate('Payment')} - )} - {mode === possibleMode.edition && ( - setSelectedTab('subscription-process')}>{translate('Process')} - )} - {mode === possibleMode.edition && ( - setSelectedTab('security')}>{translate('Security')} - )} - {mode === possibleMode.edition && tenant.display === 'environment' && ( - setSelectedTab('swagger')}>{translate('Swagger')} - )} - {mode === possibleMode.edition && tenant.display === 'environment' && ( - + { - const swaggerExist = !!planForEdition?.swagger?.content || !!planForEdition?.swagger?.url - if (swaggerExist) { - setSelectedTab('testing') - } - }}> - {translate('Testing')} + onClick={() => setSelectedTab('settings')} + > + {translate('Settings')} - )} - {mode === possibleMode.edition && tenant.display === 'environment' && ( - setSelectedTab('documentation')}>{translate('Documentation')} - )} -
) + {mode === possibleMode.edition && + planForEdition && + paidPlans.includes(planForEdition.type) && ( + setSelectedTab('payment')} + > + {translate('Payment')} + + )} + {mode === possibleMode.edition && ( + setSelectedTab('subscription-process')} + > + {translate('Process')} + + )} + {mode === possibleMode.edition && ( + setSelectedTab('security')} + > + {translate('Security')} + + )} + {mode === possibleMode.edition && + tenant.display === 'environment' && ( + setSelectedTab('swagger')} + > + {translate('Swagger')} + + )} + {mode === possibleMode.edition && + tenant.display === 'environment' && ( + { + const swaggerExist = + !!planForEdition?.swagger?.content || + !!planForEdition?.swagger?.url; + if (swaggerExist) { + setSelectedTab('testing'); + } + }} + > + {translate('Testing')} + + )} + {mode === possibleMode.edition && + tenant.display === 'environment' && ( + setSelectedTab('documentation')} + > + {translate('Documentation')} + + )} +
+ ); } else { - props.injectSubMenu(null) + props.injectSubMenu(null); } - - }, [mode, selectedTab, planForEdition]) + }, [mode, selectedTab, planForEdition]); useEffect(() => { if (mode === possibleMode.creation) { @@ -589,13 +809,15 @@ export const TeamApiPricings = (props: Props) => { }, [props.api]); useEffect(() => { - if (queryPlans.data && !isError(queryPlans.data) && mode === possibleMode.edition) { - const plan = queryPlans.data.find(p => p._id === planForEdition?._id) - setPlanForEdition(plan) + if ( + queryPlans.data && + !isError(queryPlans.data) && + mode === possibleMode.edition + ) { + const plan = queryPlans.data.find((p) => p._id === planForEdition?._id); + setPlanForEdition(plan); } - - }, [queryPlans.data]) - + }, [queryPlans.data]); const pathes = { type: type.object, @@ -624,7 +846,10 @@ export const TeamApiPricings = (props: Props) => { label: translate('http.path'), defaultValue: '/', constraints: [ - constraints.matches(/^\/([^\s]\w*)*$/, translate('constraint.match.path')), + constraints.matches( + /^\/([^\s]\w*)*$/, + translate('constraint.match.path') + ), ], }, }, @@ -708,31 +933,33 @@ export const TeamApiPricings = (props: Props) => { } }; - - const deletePlan = (plan: IUsagePlan) => { - Services.deletePlan(props.team._id, props.api._id, props.api.currentVersion, plan) + Services.deletePlan( + props.team._id, + props.api._id, + props.api.currentVersion, + plan + ) .then(() => props.reload()) .then(() => { - queryClient.invalidateQueries({ queryKey: ['plans'] }) - toast.success(translate('plan.deletion.successful')) - }) + queryClient.invalidateQueries({ queryKey: ['plans'] }); + toast.success(translate('plan.deletion.successful')); + }); }; const createNewPlan = () => { - Services.fetchNewPlan('FreeWithQuotas') - .then(newPlan => { - setPlanForEdition(newPlan); - setMode(possibleMode.creation); - setSelectedTab('settings') - setCreation(true); - }) + Services.fetchNewPlan('FreeWithQuotas').then((newPlan) => { + setPlanForEdition(newPlan); + setMode(possibleMode.creation); + setSelectedTab('settings'); + setCreation(true); + }); }; const editPlan = (plan: IUsagePlan) => { setCreation(false); setPlanForEdition(plan); setMode(possibleMode.edition); - props.setHeader(plan.customName || plan.type) + props.setHeader(plan.customName || plan.type); }; const makePlanDefault = (plan: IUsagePlan) => { @@ -749,41 +976,53 @@ export const TeamApiPricings = (props: Props) => { }; const savePlan = (plan: IUsagePlan) => { - const service = creation ? Services.createPlan : Services.updatePlan - return service(props.team._id, props.api._id, props.api.currentVersion, plan) - .then(response => { - if (isError(response)) { - toast.error(translate(response.error)) - } else { - toast.success(creation ? translate('plan.creation.successful') : translate('plan.update.successful')) - setPlanForEdition(response) - setCreation(false) - props.reload() - queryClient.invalidateQueries({ queryKey: ['plans'] }) - } - }) + const service = creation ? Services.createPlan : Services.updatePlan; + return service( + props.team._id, + props.api._id, + props.api.currentVersion, + plan + ).then((response) => { + if (isError(response)) { + toast.error(translate(response.error)); + } else { + toast.success( + creation + ? translate('plan.creation.successful') + : translate('plan.update.successful') + ); + setPlanForEdition(response); + setCreation(false); + props.reload(); + queryClient.invalidateQueries({ queryKey: ['plans'] }); + } + }); }; const setupPayment = (plan: IUsagePlan) => { //FIXME: beware of update --> display a message to explain what user is doing !! - return Services.setupPayment(props.team._id, props.api._id, props.api.currentVersion, plan) - .then(response => { - if (isError(response)) { - toast.error(translate(response.error)) - } else { - toast.success(translate('plan.payment.setup.successful')) - setPlanForEdition(response) - props.reload() - } - }) - } + return Services.setupPayment( + props.team._id, + props.api._id, + props.api.currentVersion, + plan + ).then((response) => { + if (isError(response)) { + toast.error(translate(response.error)); + } else { + toast.success(translate('plan.payment.setup.successful')); + setPlanForEdition(response); + props.reload(); + } + }); + }; const clonePlanAndEdit = (plan: IUsagePlan) => { const clone: IUsagePlan = { ...cloneDeep(plan), _id: nanoid(32), customName: `${plan.customName} (copy)`, - paymentSettings: undefined + paymentSettings: undefined, }; setPlanForEdition(clone); setMode(possibleMode.creation); @@ -811,7 +1050,7 @@ export const TeamApiPricings = (props: Props) => { setPlanForEdition(undefined); setMode(possibleMode.list); props.injectSubMenu(null); - props.setHeader(undefined) + props.setHeader(undefined); setCreation(false); }; @@ -823,15 +1062,15 @@ export const TeamApiPricings = (props: Props) => { 'PayPerUse', ]; - const paidPlans = [ - 'QuotasWithLimits', - 'QuotasWithoutLimits', - 'PayPerUse', - ]; + const paidPlans = ['QuotasWithLimits', 'QuotasWithoutLimits', 'PayPerUse']; const customNameSchemaPart = (plans: Array, api: IApi) => { if (tenant.display === 'environment' && api.visibility !== 'AdminOnly') { - const availablePlans = tenant.environments.filter(e => plans.filter(p => p._id !== planForEdition?._id).every(p => p.customName !== e)) + const availablePlans = tenant.environments.filter((e) => + plans + .filter((p) => p._id !== planForEdition?._id) + .every((p) => p.customName !== e) + ); return { customName: { @@ -841,23 +1080,29 @@ export const TeamApiPricings = (props: Props) => { placeholder: translate('Plan name'), options: availablePlans, constraints: [ - constraints.oneOf(tenant.environments, translate('constraints.plan.custom-name.one-of.environment')), - constraints.required(translate('constraints.required.value')) - ] - } - } + constraints.oneOf( + tenant.environments, + translate('constraints.plan.custom-name.one-of.environment') + ), + constraints.required(translate('constraints.required.value')), + ], + }, + }; } else { return { customName: { type: type.string, label: translate('Name'), placeholder: translate('Plan name'), - } - } + }, + }; } - } + }; - const steps = (plans: IUsagePlan[], api: IApi): Array> => ([ + const steps = ( + plans: IUsagePlan[], + api: IApi + ): Array> => [ { id: 'info', label: 'Informations', @@ -867,30 +1112,39 @@ export const TeamApiPricings = (props: Props) => { format: format.select, label: translate('Type'), onAfterChange: ({ rawValues, setValue, value, reset }: any) => { + Services.fetchNewPlan(value).then((newPlan) => { + const isDescIsDefault = Object.values(SUBSCRIPTION_PLAN_TYPES) + .map(({ defaultDescription }) => defaultDescription) + .some( + (d) => + !rawValues.customDescription || + d === rawValues.customDescription + ); + let customDescription = rawValues.customDescription; + if (isDescIsDefault) { + const planType = SUBSCRIPTION_PLAN_TYPES[value]; + customDescription = planType.defaultDescription; + } - Services.fetchNewPlan(value) - .then((newPlan => { - const isDescIsDefault = Object.values(SUBSCRIPTION_PLAN_TYPES) - .map(({ defaultDescription }) => defaultDescription) - .some((d) => !rawValues.customDescription || d === rawValues.customDescription); - let customDescription = rawValues.customDescription; - if (isDescIsDefault) { - const planType = SUBSCRIPTION_PLAN_TYPES[value] - customDescription = planType.defaultDescription; - } - - reset({ ...newPlan, ...rawValues, type: value, customDescription }) - })) - + reset({ + ...newPlan, + ...rawValues, + type: value, + customDescription, + }); + }); }, options: planTypes, transformer: (value: any) => ({ label: translate(value), - value + value, }), constraints: [ constraints.required(translate('constraints.required.type')), - constraints.oneOf(planTypes, translate('constraints.oneof.plan.type')), + constraints.oneOf( + planTypes, + translate('constraints.oneof.plan.type') + ), ], }, ...customNameSchemaPart(plans, props.api), @@ -915,27 +1169,36 @@ export const TeamApiPricings = (props: Props) => { otoroshiSettings: { type: type.string, format: format.select, - disabled: !creation && !!planForEdition?.otoroshiTarget?.otoroshiSettings, + disabled: + !creation && !!planForEdition?.otoroshiTarget?.otoroshiSettings, label: translate('Otoroshi instances'), - optionsFrom: Services.allSimpleOtoroshis(props.tenant._id, props.currentTeam) - .then(r => {console.log({r}); return r}) - .then(r => isError(r) ? [] : r), + optionsFrom: Services.allSimpleOtoroshis( + props.tenant._id, + props.currentTeam + ) + .then((r) => { + console.log({ r }); + return r; + }) + .then((r) => (isError(r) ? [] : r)), transformer: (s: IOtoroshiSettings) => ({ label: s.url, - value: s._id + value: s._id, }), }, authorizedEntities: { type: type.object, - visible: ({ rawValues }) => !!rawValues.otoroshiTarget.otoroshiSettings, + visible: ({ rawValues }) => + !!rawValues.otoroshiTarget.otoroshiSettings, deps: ['otoroshiTarget.otoroshiSettings'], - render: (props) => OtoroshiEntitiesSelector({ ...props, translate }), + render: (props) => + OtoroshiEntitiesSelector({ ...props, translate }), label: translate('Authorized entities'), placeholder: translate('Authorized.entities.placeholder'), help: translate('authorized.entities.help'), }, }, - } + }, }, flow: ['otoroshiTarget', 'subscriptionProcess'], }, @@ -966,12 +1229,12 @@ export const TeamApiPricings = (props: Props) => { label: ({ rawValues }) => { if (rawValues.aggregationApiKeysSecurity) { return `${translate('Read only apikey')} (${translate('disabled.due.to.aggregation.security')})`; - } - else { + } else { return translate('Apikey with clientId only'); } }, - disabled: ({ rawValues }) => !!rawValues.aggregationApiKeysSecurity, + disabled: ({ rawValues }) => + !!rawValues.aggregationApiKeysSecurity, onChange: ({ setValue, value }) => { if (value) { setValue('aggregationApiKeysSecurity', false); @@ -983,12 +1246,12 @@ export const TeamApiPricings = (props: Props) => { label: ({ rawValues }) => { if (rawValues.aggregationApiKeysSecurity) { return `${translate('Read only apikey')} (${translate('disabled.due.to.aggregation.security')})`; - } - else { + } else { return translate('Read only apikey'); } }, - disabled: ({ rawValues }) => !!rawValues.aggregationApiKeysSecurity, + disabled: ({ rawValues }) => + !!rawValues.aggregationApiKeysSecurity, onChange: ({ setValue, value }) => { if (value) { setValue('aggregationApiKeysSecurity', false); @@ -1009,7 +1272,9 @@ export const TeamApiPricings = (props: Props) => { array: true, label: translate('Custom Apikey metadata'), defaultValue: [], - render: (props) => , + render: (props) => ( + + ), help: translate('custom.metadata.help'), }, tags: { @@ -1017,13 +1282,15 @@ export const TeamApiPricings = (props: Props) => { array: true, label: translate('Apikey tags'), constraints: [ - constraints.required(translate('constraints.required.value')) - ] + constraints.required( + translate('constraints.required.value') + ), + ], }, restrictions: { type: type.object, format: format.form, - label: "Restrictions", + label: 'Restrictions', schema: { enabled: { type: type.bool, @@ -1031,27 +1298,43 @@ export const TeamApiPricings = (props: Props) => { }, allowLast: { type: type.bool, - visible: ({ rawValues }) => !!rawValues.otoroshiTarget.apikeyCustomization.restrictions.enabled, - deps: ['otoroshiTarget.apikeyCustomization.restrictions.enabled'], + visible: ({ rawValues }) => + !!rawValues.otoroshiTarget.apikeyCustomization + .restrictions.enabled, + deps: [ + 'otoroshiTarget.apikeyCustomization.restrictions.enabled', + ], label: translate('Allow at last'), help: translate('allow.least.help'), }, allowed: { label: translate('Allowed pathes'), - visible: ({ rawValues }) => rawValues.otoroshiTarget.apikeyCustomization.restrictions.enabled, - deps: ['otoroshiTarget.apikeyCustomization.restrictions.enabled'], + visible: ({ rawValues }) => + rawValues.otoroshiTarget.apikeyCustomization + .restrictions.enabled, + deps: [ + 'otoroshiTarget.apikeyCustomization.restrictions.enabled', + ], ...pathes, }, forbidden: { label: translate('Forbidden pathes'), - visible: ({ rawValues }) => rawValues.otoroshiTarget.apikeyCustomization.restrictions.enabled, - deps: ['otoroshiTarget.apikeyCustomization.restrictions.enabled'], + visible: ({ rawValues }) => + rawValues.otoroshiTarget.apikeyCustomization + .restrictions.enabled, + deps: [ + 'otoroshiTarget.apikeyCustomization.restrictions.enabled', + ], ...pathes, }, notFound: { label: translate('Not found pathes'), - visible: ({ rawValues }) => rawValues.otoroshiTarget.apikeyCustomization.restrictions.enabled, - deps: ['otoroshiTarget.apikeyCustomization.restrictions.enabled'], + visible: ({ rawValues }) => + rawValues.otoroshiTarget.apikeyCustomization + .restrictions.enabled, + deps: [ + 'otoroshiTarget.apikeyCustomization.restrictions.enabled', + ], ...pathes, }, }, @@ -1065,7 +1348,8 @@ export const TeamApiPricings = (props: Props) => { { id: 'quotas', label: translate('Quotas'), - disabled: (plan) => plan.type === 'FreeWithoutQuotas' || plan.type === 'PayPerUse', + disabled: (plan) => + plan.type === 'FreeWithoutQuotas' || plan.type === 'PayPerUse', schema: { maxPerSecond: { type: type.number, @@ -1108,7 +1392,7 @@ export const TeamApiPricings = (props: Props) => { }, }, }, - ]); + ]; const billingSchema = { paymentSettings: { @@ -1121,19 +1405,32 @@ export const TeamApiPricings = (props: Props) => { format: format.select, label: translate('Type'), help: 'If no type is selected, use Daikoku APIs to get billing informations', - options: queryFullTenant.data ? (queryFullTenant.data as ITenantFull).thirdPartyPaymentSettings : [], - transformer: (s: IThirdPartyPaymentSettings) => ({ label: s.name, value: s._id }), + options: queryFullTenant.data + ? (queryFullTenant.data as ITenantFull).thirdPartyPaymentSettings + : [], + transformer: (s: IThirdPartyPaymentSettings) => ({ + label: s.name, + value: s._id, + }), props: { isClearable: true }, onChange: ({ rawValues, setValue, value }) => { - const settings = queryFullTenant.data ? (queryFullTenant.data as ITenantFull).thirdPartyPaymentSettings : [] - setValue('paymentSettings.type', settings.find(s => value === s._id)?.type); - } - } - } + const settings = queryFullTenant.data + ? (queryFullTenant.data as ITenantFull).thirdPartyPaymentSettings + : []; + setValue( + 'paymentSettings.type', + settings.find((s) => value === s._id)?.type + ); + }, + }, + }, }, costPerMonth: { type: type.number, - label: ({ rawValues }) => translate(`Cost per ${rawValues?.billingDuration?.unit.toLocaleLowerCase()}`), + label: ({ rawValues }) => + translate( + `Cost per ${rawValues?.billingDuration?.unit.toLocaleLowerCase()}` + ), placeholder: translate('Cost per billing period'), constraints: [constraints.positive(translate('constraints.positive'))], }, @@ -1182,7 +1479,9 @@ export const TeamApiPricings = (props: Props) => { constraints: [ constraints.positive(translate('constraints.positive')), constraints.integer(translate('constraints.integer')), - constraints.required(translate('constraints.required.billing.period')), + constraints.required( + translate('constraints.required.billing.period') + ), ], }, unit: { @@ -1197,7 +1496,10 @@ export const TeamApiPricings = (props: Props) => { ], constraints: [ constraints.required('constraints.required.billing.period'), - constraints.oneOf(['Hour', 'Day', 'Month', 'Year'], translate('constraints.oneof.period')), + constraints.oneOf( + ['Hour', 'Day', 'Month', 'Year'], + translate('constraints.oneof.period') + ), ], }, }, @@ -1218,7 +1520,11 @@ export const TeamApiPricings = (props: Props) => { }, constraints: [ constraints.integer(translate('constraints.integer')), - constraints.test('positive', translate('constraints.positive'), (v) => v >= 0), + constraints.test( + 'positive', + translate('constraints.positive'), + (v) => v >= 0 + ), ], }, unit: { @@ -1233,13 +1539,16 @@ export const TeamApiPricings = (props: Props) => { { label: translate('Years'), value: 'Year' }, ], constraints: [ - constraints.oneOf(['Hour', 'Day', 'Month', 'Year'], translate('constraints.oneof.period')), + constraints.oneOf( + ['Hour', 'Day', 'Month', 'Year'], + translate('constraints.oneof.period') + ), // constraints.when('trialPeriod.value', (value) => value > 0, [constraints.oneOf(['Hour', 'Day', 'Month', 'Year'], translate('constraints.oneof.period'))]) //FIXME ], }, }, }, - } + }; const securitySchema: Schema = { otoroshiTarget: { @@ -1253,7 +1562,7 @@ export const TeamApiPricings = (props: Props) => { props: { trueLabel: translate('Enabled'), falseLabel: translate('Disabled'), - } + }, }, aggregationApiKeysSecurity: { type: type.bool, @@ -1263,18 +1572,22 @@ export const TeamApiPricings = (props: Props) => { help: translate('aggregation_apikeys.security.help'), onChange: ({ value, setValue }: any) => { if (value) - confirm({ message: translate('aggregation.api_key.security.notification') }) - .then((ok) => { - if (ok) { - setValue('otoroshiTarget.apikeyCustomization.readOnly', false); - setValue('otoroshiTarget.apikeyCustomization.clientIdOnly', false); - } - }); + confirm({ + message: translate('aggregation.api_key.security.notification'), + }).then((ok) => { + if (ok) { + setValue('otoroshiTarget.apikeyCustomization.readOnly', false); + setValue( + 'otoroshiTarget.apikeyCustomization.clientIdOnly', + false + ); + } + }); }, props: { trueLabel: translate('Enabled'), falseLabel: translate('Disabled'), - } + }, }, allowMultipleKeys: { type: type.bool, @@ -1283,7 +1596,7 @@ export const TeamApiPricings = (props: Props) => { props: { trueLabel: translate('Enabled'), falseLabel: translate('Disabled'), - } + }, }, integrationProcess: { type: type.string, @@ -1303,9 +1616,9 @@ export const TeamApiPricings = (props: Props) => { format: format.buttonsSelect, label: () => translate('Visibility'), options: [ - { label: translate('Public'), value: 'Public', }, + { label: translate('Public'), value: 'Public' }, { label: translate('Private'), value: 'Private' }, - ] + ], }, authorizedTeams: { type: type.string, @@ -1317,129 +1630,176 @@ export const TeamApiPricings = (props: Props) => { optionsFrom: '/api/me/teams', transformer: (t: any) => ({ label: t.name, - value: t._id + value: t._id, }), }, - } + }; - const availablePlans = queryPlans.data && !isError(queryPlans.data) && tenant.environments.filter(e => (queryPlans.data as Array).every(p => p.customName !== e)) - return (
-
- {planForEdition && mode !== possibleMode.list && } -
-
- {!planForEdition && } - {!planForEdition && !!props.api.parent && ()} -
- {planForEdition && mode !== possibleMode.list && (
-
- {queryPlans.data && selectedTab === 'settings' && - value={planForEdition} - steps={steps(queryPlans.data as Array, props.api)} - initial="info" - creation={creation} - save={savePlan} - currentTeam={props.currentTeam} - labels={{ - previous: translate('Previous'), - skip: translate('Skip'), - next: translate('Next'), - save: translate('Save'), - }} />} - {queryPlans.data && selectedTab === 'payment' && ( - setupPayment(plan)} - value={planForEdition} - /> - )} - {queryPlans.data && selectedTab === 'security' && ( - - )} - {queryPlans.data && selectedTab === 'subscription-process' && ( - - )} - {queryPlans.data && selectedTab === 'swagger' && ( - - )} - {queryPlans.data && selectedTab === 'testing' && ( - - )} - {queryPlans.data && selectedTab === 'documentation' && ( - savePlan({ ...planForEdition, documentation })} - reloadState={() => queryClient.invalidateQueries({ queryKey: ['plans'] })} - plans={queryPlans.data as Array} - /> - )} -
-
)} - {mode === possibleMode.list && ( -
- {queryPlans.isLoading && } - {queryPlans.data && !isError(queryPlans.data) && ( - queryPlans.data - .sort((a, b) => (a.customName || a.type).localeCompare(b.customName || b.type)) - .map((plan) =>
- makePlanDefault(plan)} - toggleVisibility={() => toggleVisibility(plan)} - deletePlan={() => deletePlan(plan)} - editPlan={() => editPlan(plan)} - duplicatePlan={() => clonePlanAndEdit(plan)} /> -
) + const availablePlans = + queryPlans.data && + !isError(queryPlans.data) && + tenant.environments.filter((e) => + (queryPlans.data as Array).every((p) => p.customName !== e) + ); + return ( +
+
+ {planForEdition && mode !== possibleMode.list && ( + + )} +
+
+ {!planForEdition && ( + )} - {queryPlans.isError && ( -
Error while fetching usage plan
+ {!planForEdition && !!props.api.parent && ( + )}
- )} + {planForEdition && mode !== possibleMode.list && ( +
+
+ {queryPlans.data && selectedTab === 'settings' && ( + + value={planForEdition} + steps={steps( + queryPlans.data as Array, + props.api + )} + initial="info" + creation={creation} + save={savePlan} + currentTeam={props.currentTeam} + labels={{ + previous: translate('Previous'), + skip: translate('Skip'), + next: translate('Next'), + save: translate('Save'), + }} + /> + )} + {queryPlans.data && selectedTab === 'payment' && ( + setupPayment(plan)} + value={planForEdition} + /> + )} + {queryPlans.data && selectedTab === 'security' && ( + + )} + {queryPlans.data && selectedTab === 'subscription-process' && ( + + )} + {queryPlans.data && selectedTab === 'swagger' && ( + + )} + {queryPlans.data && selectedTab === 'testing' && ( + + )} + {queryPlans.data && selectedTab === 'documentation' && ( + + savePlan({ ...planForEdition, documentation }) + } + reloadState={() => + queryClient.invalidateQueries({ queryKey: ['plans'] }) + } + plans={queryPlans.data as Array} + /> + )} +
+
+ )} + {mode === possibleMode.list && ( +
+ {queryPlans.isLoading && } + {queryPlans.data && + !isError(queryPlans.data) && + queryPlans.data + .sort((a, b) => + (a.customName || a.type).localeCompare( + b.customName || b.type + ) + ) + .map((plan) => ( +
+ makePlanDefault(plan)} + toggleVisibility={() => toggleVisibility(plan)} + deletePlan={() => deletePlan(plan)} + editPlan={() => editPlan(plan)} + duplicatePlan={() => clonePlanAndEdit(plan)} + /> +
+ ))} + {queryPlans.isError &&
Error while fetching usage plan
} +
+ )} +
-
); + ); }; - - type SubProcessProps = { - savePlan: (plan: IUsagePlan) => Promise, - value: IUsagePlan, - team: ITeamSimple, - tenant: ITenantFull -} + savePlan: (plan: IUsagePlan) => Promise; + value: IUsagePlan; + team: ITeamSimple; + tenant: ITenantFull; +}; -type EmailOption = { option: 'all' | 'oneOf' } +type EmailOption = { option: 'all' | 'oneOf' }; const SubscriptionProcessEditor = (props: SubProcessProps) => { const { translate } = useContext(I18nContext); const { openCustomModal, openFormModal, close } = useContext(ModalContext); - const editProcess = (name: IValidationStepType, index: number) => { //todo: use the index !! switch (name) { @@ -1449,10 +1809,10 @@ const SubscriptionProcessEditor = (props: SubProcessProps) => { schema: { title: { type: type.string, - defaultValue: "Email", + defaultValue: 'Email', constraints: [ - constraints.required(translate('constraints.required.value')) - ] + constraints.required(translate('constraints.required.value')), + ], }, emails: { type: type.string, @@ -1460,48 +1820,80 @@ const SubscriptionProcessEditor = (props: SubProcessProps) => { array: true, constraints: [ constraints.required(translate('constraints.required.value')), - constraints.email(translate('constraints.matches.email')) - ] + constraints.email(translate('constraints.matches.email')), + ], }, option: { type: type.string, format: format.buttonsSelect, - options: ["all", 'oneOf'], + options: ['all', 'oneOf'], defaultValue: 'oneOf', visible: ({ rawValues }) => { - return rawValues.emails && rawValues.emails.length > 1 - } + return rawValues.emails && rawValues.emails.length > 1; + }, }, message: { type: type.string, - format: format.text + format: format.text, }, }, onSubmit: (data: IValidationStepEmail & EmailOption) => { if (data.option === 'oneOf') { - const step: IValidationStepEmail = { type: 'email', emails: data.emails, message: data.message, id: nanoid(32), title: data.title } - props.savePlan({ ...props.value, subscriptionProcess: insertArrayIndex({ ...step, id: nanoid(32) }, props.value.subscriptionProcess, index) }) + const step: IValidationStepEmail = { + type: 'email', + emails: data.emails, + message: data.message, + id: nanoid(32), + title: data.title, + }; + props.savePlan({ + ...props.value, + subscriptionProcess: insertArrayIndex( + { ...step, id: nanoid(32) }, + props.value.subscriptionProcess, + index + ), + }); } else { - const steps: Array = data.emails.map(email => ({ type: 'email', emails: [email], message: data.message, id: nanoid(32), title: data.title })) - const subscriptionProcess = steps.reduce((process, step) => insertArrayIndex(step, process, index), props.value.subscriptionProcess) - props.savePlan({ ...props.value, subscriptionProcess }) - + const steps: Array = data.emails.map( + (email) => ({ + type: 'email', + emails: [email], + message: data.message, + id: nanoid(32), + title: data.title, + }) + ); + const subscriptionProcess = steps.reduce( + (process, step) => insertArrayIndex(step, process, index), + props.value.subscriptionProcess + ); + props.savePlan({ ...props.value, subscriptionProcess }); } }, - actionLabel: translate('Create') - }) + actionLabel: translate('Create'), + }); case 'teamAdmin': { const step: IValidationStepTeamAdmin = { type: 'teamAdmin', team: props.team._id, id: nanoid(32), title: 'Admin', - schema: { motivation: { type: type.string, format: format.text, constraints: [{ type: 'required' }] } }, - formatter: '[[motivation]]' - } - return props.savePlan({ ...props.value, subscriptionProcess: [step, ...props.value.subscriptionProcess] }) - .then(() => close()) - + schema: { + motivation: { + type: type.string, + format: format.text, + constraints: [{ type: 'required' }], + }, + }, + formatter: '[[motivation]]', + }; + return props + .savePlan({ + ...props.value, + subscriptionProcess: [step, ...props.value.subscriptionProcess], + }) + .then(() => close()); } case 'httpRequest': { const step: IValidationStepHttpRequest = { @@ -1509,42 +1901,45 @@ const SubscriptionProcessEditor = (props: SubProcessProps) => { id: nanoid(32), title: 'Admin', url: 'https://changeit.io', - headers: {} - } + headers: {}, + }; return openFormModal({ title: translate('subscription.process.add.httpRequest.step.title'), schema: { title: { type: type.string, - defaultValue: "HttpRequest", + defaultValue: 'HttpRequest', constraints: [ - constraints.required(translate('constraints.required.value')) - ] + constraints.required(translate('constraints.required.value')), + ], }, url: { type: type.string, constraints: [ constraints.required(translate('constraints.required.value')), - constraints.url(translate('constraints.matches.url')) - ] + constraints.url(translate('constraints.matches.url')), + ], }, Headers: { type: type.object, - defaultValue: {} + defaultValue: {}, }, }, value: step, onSubmit: (data: IValidationStepHttpRequest) => { - const subscriptionProcess = insertArrayIndex(data, props.value.subscriptionProcess, index) - props.savePlan({ ...props.value, subscriptionProcess }) - + const subscriptionProcess = insertArrayIndex( + data, + props.value.subscriptionProcess, + index + ); + props.savePlan({ ...props.value, subscriptionProcess }); }, - actionLabel: translate('Create') - }) + actionLabel: translate('Create'), + }); } } - } + }; const editMailStep = (value: IValidationStepEmail) => { return openFormModal({ @@ -1556,24 +1951,24 @@ const SubscriptionProcessEditor = (props: SubProcessProps) => { }, message: { type: type.string, - format: format.text - } + format: format.text, + }, }, onSubmit: (data: IValidationStepEmail) => { props.savePlan({ ...props.value, - subscriptionProcess: props.value.subscriptionProcess.map(p => { + subscriptionProcess: props.value.subscriptionProcess.map((p) => { if (p.id === data.id) { - return data + return data; } - return p - }) - }) + return p; + }), + }); }, actionLabel: translate('Update'), - value - }) - } + value, + }); + }; const editHttpRequestStep = (value: IValidationStepHttpRequest) => { return openFormModal({ @@ -1582,15 +1977,15 @@ const SubscriptionProcessEditor = (props: SubProcessProps) => { title: { type: type.string, constraints: [ - constraints.required(translate('constraints.required.value')) - ] + constraints.required(translate('constraints.required.value')), + ], }, url: { type: type.string, constraints: [ constraints.required(translate('constraints.required.value')), - constraints.url(translate('constraints.matches.url')) - ] + constraints.url(translate('constraints.matches.url')), + ], }, Headers: { type: type.object, @@ -1599,27 +1994,39 @@ const SubscriptionProcessEditor = (props: SubProcessProps) => { onSubmit: (data: IValidationStepHttpRequest) => { props.savePlan({ ...props.value, - subscriptionProcess: props.value.subscriptionProcess.map(p => { + subscriptionProcess: props.value.subscriptionProcess.map((p) => { if (p.id === data.id) { - return data + return data; } - return p - }) - }) + return p; + }), + }); }, actionLabel: translate('Update'), - value - }) - } + value, + }); + }; //todo const addProcess = (index: number) => { - const alreadyStepAdmin = props.value.subscriptionProcess.some(isValidationStepTeamAdmin) - - const options = addArrayIf(!alreadyStepAdmin, [ - { value: 'email', label: translate('subscription.process.email') }, - { value: 'httpRequest', label: translate('subscription.process.httpRequest') } - ], { value: 'teamAdmin', label: translate('subscription.process.team.admin') }) + const alreadyStepAdmin = props.value.subscriptionProcess.some( + isValidationStepTeamAdmin + ); + + const options = addArrayIf( + !alreadyStepAdmin, + [ + { value: 'email', label: translate('subscription.process.email') }, + { + value: 'httpRequest', + label: translate('subscription.process.httpRequest'), + }, + ], + { + value: 'teamAdmin', + label: translate('subscription.process.team.admin'), + } + ); openFormModal({ title: translate('subscription.process.creation.title'), @@ -1628,37 +2035,49 @@ const SubscriptionProcessEditor = (props: SubProcessProps) => { type: type.string, format: format.buttonsSelect, label: translate('subscription.process.type.selection'), - options - } + options, + }, }, onSubmit: (data: IValidationStep) => editProcess(data.type, index), actionLabel: translate('Create'), - noClose: true - }) - } + noClose: true, + }); + }; const deleteStep = (deletedStepId: UniqueIdentifier) => { - const subscriptionProcess = props.value.subscriptionProcess.filter(step => step.id !== deletedStepId) - props.savePlan({ ...props.value, subscriptionProcess }) - } + const subscriptionProcess = props.value.subscriptionProcess.filter( + (step) => step.id !== deletedStepId + ); + props.savePlan({ ...props.value, subscriptionProcess }); + }; if (!props.value.subscriptionProcess.length) { return ( -
+
{translate('api.pricings.no.step.explanation')}
-
- ) + ); } return ( -
- +
+ props.savePlan({ ...props.value, subscriptionProcess })} + onChange={(subscriptionProcess) => + props.savePlan({ ...props.value, subscriptionProcess }) + } className="flex-grow-1" renderItem={(item, idx) => { if (isValidationStepPayment(item)) { @@ -1667,63 +2086,117 @@ const SubscriptionProcessEditor = (props: SubProcessProps) => { + tenant={props.tenant} + /> - ) + ); } else { return ( <> - {isValidationStepEmail(item) ? : <>} - {isValidationStepHttpRequest(item) ? : <>} - {isValidationStepTeamAdmin(item) ? +
+ {isValidationStepEmail(item) ? ( : <>} - -
} - id={item.id}> + + ) : ( + <> + )} + {isValidationStepHttpRequest(item) ? ( + + ) : ( + <> + )} + {isValidationStepTeamAdmin(item) ? ( + + ) : ( + <> + )} + +
+ } + id={item.id} + > + tenant={props.tenant} + /> - + - ) + ); } }} />
- ) -} + ); +}; type MotivationFormProps = { - saveMotivation: (m: { schema: object, formatter: string }) => void - value: IValidationStepTeamAdmin -} + saveMotivation: (m: { schema: object; formatter: string }) => void; + value: IValidationStepTeamAdmin; +}; const MotivationForm = (props: MotivationFormProps) => { - const [schema, setSchema] = useState(props.value.schema || '{}') - const [realSchema, setRealSchema] = useState(props.value.schema || {}) - const [formatter, setFormatter] = useState(props.value.formatter || '') - const [value, setValue] = useState({}) - const [example, setExample] = useState('') + const [schema, setSchema] = useState( + props.value.schema || '{}' + ); + const [realSchema, setRealSchema] = useState(props.value.schema || {}); + const [formatter, setFormatter] = useState(props.value.formatter || ''); + const [value, setValue] = useState({}); + const [example, setExample] = useState(''); const { translate } = useContext(I18nContext); const { close } = useContext(ModalContext); @@ -1731,38 +2204,38 @@ const MotivationForm = (props: MotivationFormProps) => { const childRef = useRef(); const codeInputRef = useRef(); - useEffect(() => { //@ts-ignore + useEffect(() => { + //@ts-ignore if (codeInputRef.current.hasFocus) { let maybeFormattedSchema = schema; try { - maybeFormattedSchema = typeof schema === 'object' ? schema : JSON.parse(schema); - } catch (_) { } + maybeFormattedSchema = + typeof schema === 'object' ? schema : JSON.parse(schema); + } catch (_) {} - setRealSchema(maybeFormattedSchema || {}) + setRealSchema(maybeFormattedSchema || {}); } }, [schema]); useEffect(() => { - const regexp = /\[\[(.+?)\]\]/g - const matches = formatter.match(regexp) + const regexp = /\[\[(.+?)\]\]/g; + const matches = formatter.match(regexp); const result = matches?.reduce((acc, match) => { - const key = match.replace('[[', '').replace(']]', '') - return acc.replace(match, value[key] || match) - }, formatter) - - - setExample(result || formatter) - }, [value, formatter]) + const key = match.replace('[[', '').replace(']]', ''); + return acc.replace(match, value[key] || match); + }, formatter); + setExample(result || formatter); + }, [value, formatter]); return ( <>
-
+
{translate('motivation.form.setting.title')}
-
+
{ setSchema(e); }} value={ - typeof schema === "object" + typeof schema === 'object' ? JSON.stringify(schema, null, 2) : schema } setRef={(ref) => (codeInputRef.current = ref)} />
-
+
{
-
{/* @ts-ignore */} +
+ {/* @ts-ignore */}
{translate('motivation.form.preview.title')}
{translate('motivation.form.sample.help')} @@ -1801,9 +2275,9 @@ const MotivationForm = (props: MotivationFormProps) => { options={{ actions: { submit: { - label: translate("motivation.form.sample.button.label") - } - } + label: translate('motivation.form.sample.button.label'), + }, + }, }} />
@@ -1816,96 +2290,129 @@ const MotivationForm = (props: MotivationFormProps) => {
- +
- ) -} + ); +}; type ValidationStepProps = { - step: IValidationStep, - tenant: ITenantFull, - update?: () => void, - index: number -} + step: IValidationStep; + tenant: ITenantFull; + update?: () => void; + index: number; +}; const ValidationStep = (props: ValidationStepProps) => { - const step = props.step + const step = props.step; if (isValidationStepPayment(step)) { - const thirdPartyPaymentSettings = props.tenant.thirdPartyPaymentSettings.find(setting => setting._id == step.thirdPartyPaymentSettingsId) + const thirdPartyPaymentSettings = + props.tenant.thirdPartyPaymentSettings.find( + (setting) => setting._id == step.thirdPartyPaymentSettingsId + ); return ( -
- {String(props.index).padStart(2, '0')} - {step.title} - +
+ + {String(props.index).padStart(2, '0')} + + {step.title} + + +
{thirdPartyPaymentSettings?.name} {thirdPartyPaymentSettings?.type}
- ) + ); } else if (isValidationStepEmail(step)) { return ( -
- {String(props.index).padStart(2, '0')} - {step.title} - +
+ + {String(props.index).padStart(2, '0')} + + {step.title} + + +
{step.emails[0]} - {step.emails.length > 1 && {` + ${step.emails.length - 1}`}} + {step.emails.length > 1 && ( + {` + ${step.emails.length - 1}`} + )}
- ) + ); } else if (isValidationStepTeamAdmin(step)) { return ( -
- {String(props.index).padStart(2, '0')} - {step.title} - +
+ + {String(props.index).padStart(2, '0')} + + {step.title} + + +
- ) + ); } else if (isValidationStepHttpRequest(step)) { return ( -
- {String(props.index).padStart(2, '0')} - {step.title} - +
+ + {String(props.index).padStart(2, '0')} + + {step.title} + + +
- ) + ); } else { - return <> + return <>; } -} +}; type TeamApiPricingDocumentationProps = { - planForEdition: IUsagePlan - team: ITeamSimple - api: IApi - reloadState: () => Promise - onSave: (d: IDocumentation) => Promise - plans: Array -} -const TeamApiPricingDocumentation = (props: TeamApiPricingDocumentationProps) => { + planForEdition: IUsagePlan; + team: ITeamSimple; + api: IApi; + reloadState: () => Promise; + onSave: (d: IDocumentation) => Promise; + plans: Array; +}; +const TeamApiPricingDocumentation = ( + props: TeamApiPricingDocumentationProps +) => { const { openApiDocumentationSelectModal } = useContext(ModalContext); const { translate } = useContext(I18nContext); - const createPlanDoc = () => { - Services.fetchNewApiDoc() - .then(props.onSave) - } - + Services.fetchNewApiDoc().then(props.onSave); + }; if (!props.planForEdition.documentation) { return (
-
{translate('documentation.not.setted.message')}
- +
+ {translate('documentation.not.setted.message')} +
+
- ) + ); } else { return ( creationInProgress={true} reloadState={props.reloadState} onSave={props.onSave} - importAuthorized={props.plans.filter(p => p._id !== props.planForEdition._id).some(p => p.documentation?.pages.length)} - importPage={() => openApiDocumentationSelectModal({ - api: props.api, - teamId: props.team._id, - onClose: () => { - toast.success(translate('doc.page.import.successfull')); - props.reloadState() - }, - getDocumentationPages: () => Services.getAllPlansDocumentation(props.team._id, props.api._humanReadableId, props.api.currentVersion), - importPages: (pages, linked) => Services.importPlanPages(props.team._id, props.api._id, pages, props.api.currentVersion, props.planForEdition._id, linked) - })} /> - ) + importAuthorized={props.plans + .filter((p) => p._id !== props.planForEdition._id) + .some((p) => p.documentation?.pages.length)} + importPage={() => + openApiDocumentationSelectModal({ + api: props.api, + teamId: props.team._id, + onClose: () => { + toast.success(translate('doc.page.import.successfull')); + props.reloadState(); + }, + getDocumentationPages: () => + Services.getAllPlansDocumentation( + props.team._id, + props.api._humanReadableId, + props.api.currentVersion + ), + importPages: (pages, linked) => + Services.importPlanPages( + props.team._id, + props.api._id, + pages, + props.api.currentVersion, + props.planForEdition._id, + linked + ), + }) + } + /> + ); } -} +}; export default class WrapperError extends React.Component { state = { - error: undefined - } + error: undefined, + }; componentDidCatch(error) { - this.setState({ error }) + this.setState({ error }); } reset() { - this.setState({ error: undefined }) + this.setState({ error: undefined }); } render() { - if (this.state.error) - return
Something wrong happened
//@ts-ignore - return this.props.children + if (this.state.error) return
Something wrong happened
; //@ts-ignore + return this.props.children; } -} \ No newline at end of file +} diff --git a/daikoku/javascript/src/components/frontend/api/ApiPricing.tsx b/daikoku/javascript/src/components/frontend/api/ApiPricing.tsx index 2c7ed2e3f..40fc51a32 100644 --- a/daikoku/javascript/src/components/frontend/api/ApiPricing.tsx +++ b/daikoku/javascript/src/components/frontend/api/ApiPricing.tsx @@ -11,13 +11,16 @@ import { GlobalContext } from '../../../contexts/globalContext'; import * as Services from '../../../services'; import { currencies } from '../../../services/currencies'; import { - IApi, IBaseUsagePlan, + IApi, + IBaseUsagePlan, ISubscription, - ISubscriptionDemand, ISubscriptionWithApiInfo, - ITeamSimple, IUsagePlan, + ISubscriptionDemand, + ISubscriptionWithApiInfo, + ITeamSimple, + IUsagePlan, isError, isMiniFreeWithQuotas, - isValidationStepTeamAdmin + isValidationStepTeamAdmin, } from '../../../types'; import { Can, @@ -25,8 +28,10 @@ import { Spinner, access, apikey, - isPublish, isSubscriptionProcessIsAutomatic, - renderPlanInfo, renderPricing + isPublish, + isSubscriptionProcessIsAutomatic, + renderPlanInfo, + renderPricing, } from '../../utils'; import { formatPlanType } from '../../utils/formatters'; import { ApiDocumentation } from './ApiDocumentation'; @@ -35,28 +40,39 @@ import { ApiSwagger } from './ApiSwagger'; export const currency = (plan?: IBaseUsagePlan) => { if (!plan) { - return ""; //todo: return undefined + return ''; //todo: return undefined } const cur = find(currencies, (c) => c.code === plan.currency.code); return `${cur?.name}(${cur?.symbol})`; }; type ApiPricingCardProps = { - plan: IUsagePlan, - api: IApi, - askForApikeys: (x: { team: string, plan: IUsagePlan, apiKey?: ISubscription, motivation?: object }) => Promise, - myTeams: Array, - ownerTeam: ITeamSimple, - subscriptions: Array, - inProgressDemands: Array, -} + plan: IUsagePlan; + api: IApi; + askForApikeys: (x: { + team: string; + plan: IUsagePlan; + apiKey?: ISubscription; + motivation?: object; + }) => Promise; + myTeams: Array; + ownerTeam: ITeamSimple; + subscriptions: Array; + inProgressDemands: Array; +}; const ApiPricingCard = (props: ApiPricingCardProps) => { const { Translation } = useContext(I18nContext); - const { openFormModal, openLoginOrRegisterModal, openApiKeySelectModal, openCustomModal, close } = useContext(ModalContext); + const { + openFormModal, + openLoginOrRegisterModal, + openApiKeySelectModal, + openCustomModal, + close, + } = useContext(ModalContext); const { client } = useContext(getApolloContext()); - const { connectedUser, tenant } = useContext(GlobalContext) + const { connectedUser, tenant } = useContext(GlobalContext); const showApiKeySelectModal = (team: string) => { const { plan } = props; @@ -66,82 +82,108 @@ const ApiPricingCard = (props: ApiPricingCardProps) => { return; } - const askForApikeys = (team: string, plan: IUsagePlan, apiKey?: ISubscription) => { - const adminStep = plan.subscriptionProcess.find(s => isValidationStepTeamAdmin(s)) + const askForApikeys = ( + team: string, + plan: IUsagePlan, + apiKey?: ISubscription + ) => { + const adminStep = plan.subscriptionProcess.find((s) => + isValidationStepTeamAdmin(s) + ); if (adminStep && isValidationStepTeamAdmin(adminStep)) { - console.debug({apiKey, value: apiKey?.metadata}) openFormModal({ title: translate('motivations.modal.title'), schema: adminStep.schema, - onSubmit: (motivation: object) => props.askForApikeys({ team, plan, apiKey, motivation }), + onSubmit: (motivation: object) => + props.askForApikeys({ team, plan, apiKey, motivation }), actionLabel: translate('Send'), - value: apiKey?.customMetadata - }) + value: apiKey?.customMetadata, + }); } else { - props.askForApikeys({ team, plan: plan, apiKey }) - .then(() => close()) + props.askForApikeys({ team, plan: plan, apiKey }).then(() => close()); } - } + }; type IUsagePlanGQL = { - _id: string + _id: string; otoroshiTarget: { - otoroshiSettings: string - } - aggregationApiKeysSecurity: boolean - } + otoroshiSettings: string; + }; + aggregationApiKeysSecurity: boolean; + }; type IApiGQL = { - _id: string - _humanReadableId: string - currentVersion: string - name: string - possibleUsagePlans: IUsagePlanGQL[] - } + _id: string; + _humanReadableId: string; + currentVersion: string; + name: string; + possibleUsagePlans: IUsagePlanGQL[]; + }; Services.getAllTeamSubscriptions(team) - .then((subscriptions) => client.query({ - query: Services.graphql.apisByIdsWithPlans, - variables: { ids: [...new Set(subscriptions.map((s) => s.api))] }, - }) - .then(({ data }) => ({ apis: data.apis, subscriptions })) + .then((subscriptions) => + client + .query({ + query: Services.graphql.apisByIdsWithPlans, + variables: { ids: [...new Set(subscriptions.map((s) => s.api))] }, + }) + .then(({ data }) => ({ apis: data.apis, subscriptions })) ) - .then(({ apis, subscriptions }: { apis: Array, subscriptions: Array }) => { - const int = subscriptions - .map((subscription) => { + .then( + ({ + apis, + subscriptions, + }: { + apis: Array; + subscriptions: Array; + }) => { + const int = subscriptions.map((subscription) => { const api = apis.find((a) => a._id === subscription.api); const plan = Option(api?.possibleUsagePlans) - .flatMap((plans) => Option(plans.find((plan) => plan._id === subscription.plan))) + .flatMap((plans) => + Option(plans.find((plan) => plan._id === subscription.plan)) + ) .getOrNull(); return { subscription, api, plan }; - }) - - const filteredApiKeys = int.filter((infos) => infos.plan?.otoroshiTarget?.otoroshiSettings === - plan?.otoroshiTarget?.otoroshiSettings && (infos.plan?.aggregationApiKeysSecurity) - ) - .map((infos) => infos.subscription); - - if (!plan.aggregationApiKeysSecurity || subscriptions.length <= 0) { - askForApikeys(team, plan); - } else { - openApiKeySelectModal({ - plan, - apiKeys: filteredApiKeys, - onSubscribe: () => askForApikeys(team, plan), - extendApiKey: (apiKey: ISubscription) => askForApikeys(team, plan, apiKey), }); + + const filteredApiKeys = int + .filter( + (infos) => + infos.plan?.otoroshiTarget?.otoroshiSettings === plan?.otoroshiTarget?.otoroshiSettings && + (infos.plan?.aggregationApiKeysSecurity) + ) + .filter(s => !tenant.environmentAggregationApiKeysSecurity || s.subscription.planName === plan.customName) + .map((infos) => infos.subscription); + + if ( + !tenant.aggregationApiKeysSecurity || !plan.aggregationApiKeysSecurity || + filteredApiKeys.length <= 0 + ) { + askForApikeys(team, plan); + } else { + openApiKeySelectModal({ + plan, + apiKeys: filteredApiKeys, + onSubscribe: () => askForApikeys(team, plan), + extendApiKey: (apiKey: ISubscription) => + askForApikeys(team, plan, apiKey), + }); + } } - }); + ); }; const plan = props.plan; const customDescription = plan.customDescription; - const isAutomaticProcess = isSubscriptionProcessIsAutomatic(plan) + const isAutomaticProcess = isSubscriptionProcessIsAutomatic(plan); const authorizedTeams = props.myTeams .filter((t) => !tenant.subscriptionSecurity || t.type !== 'Personal') - .filter((t) => props.api.visibility === 'Public' || - props.api.authorizedTeams.includes(t._id) || - t._id === props.ownerTeam._id + .filter( + (t) => + props.api.visibility === 'Public' || + props.api.authorizedTeams.includes(t._id) || + t._id === props.ownerTeam._id ); const allPossibleTeams = difference( @@ -156,13 +198,15 @@ const ApiPricingCard = (props: ApiPricingCardProps) => { const { translate } = useContext(I18nContext); - let pricing = renderPricing(plan, translate) - - const otoroshiTargetIsDefined = !!plan.otoroshiTarget && plan.otoroshiTarget.authorizedEntities; - const otoroshiEntitiesIsDefined = otoroshiTargetIsDefined && (!!plan.otoroshiTarget?.authorizedEntities?.groups.length || - !!plan.otoroshiTarget?.authorizedEntities?.routes.length || - !!plan.otoroshiTarget?.authorizedEntities?.services.length); + let pricing = renderPricing(plan, translate); + const otoroshiTargetIsDefined = + !!plan.otoroshiTarget && plan.otoroshiTarget.authorizedEntities; + const otoroshiEntitiesIsDefined = + otoroshiTargetIsDefined && + (!!plan.otoroshiTarget?.authorizedEntities?.groups.length || + !!plan.otoroshiTarget?.authorizedEntities?.routes.length || + !!plan.otoroshiTarget?.authorizedEntities?.services.length); const openTeamSelectorModal = () => { openCustomModal({ @@ -184,8 +228,14 @@ const ApiPricingCard = (props: ApiPricingCardProps) => { } return ( -
-
+
+
{plan.customName || formatPlanType(plan, translate)}
@@ -195,16 +245,35 @@ const ApiPricingCard = (props: ApiPricingCardProps) => { {!customDescription && renderPlanInfo(plan)}

{tenant.display === 'environment' && ( -
+
swagger + to={`./${props.plan.customName}/swagger`} + relative="path" + className={classNames('btn btn-sm btn-outline-primary mb-1', { + link__disabled: + !props.plan.swagger?.url && !props.plan.swagger?.content, + })} + > + swagger + test + to={`./${props.plan.customName}/testing`} + relative="path" + className={classNames('btn btn-sm btn-outline-primary mb-1', { + link__disabled: !props.plan.testing?.enabled, + })} + > + test + Documentation + to={`./${props.plan.customName}/documentation`} + relative="path" + className={classNames('btn btn-sm btn-outline-primary', { + link__disabled: !props.plan.documentation?.pages.length, + })} + > + Documentation +
)}
@@ -218,7 +287,8 @@ const ApiPricingCard = (props: ApiPricingCardProps) => { i18nkey="plan.limits" replacements={[plan.maxPerSecond, plan.maxPerMonth]} > - Limits: {plan.maxPerSecond} req./sec, {plan.maxPerMonth} req./month + Limits: {plan.maxPerSecond} req./sec, {plan.maxPerMonth}{' '} + req./month
@@ -232,33 +302,48 @@ const ApiPricingCard = (props: ApiPricingCardProps) => {
{!otoroshiTargetIsDefined && props.api.visibility !== 'AdminOnly' && ( - {translate('otoroshi.missing.target')} + + {translate('otoroshi.missing.target')} + )} - {!otoroshiEntitiesIsDefined && props.api.visibility !== 'AdminOnly' && ( - {translate('otoroshi.missing.entities')} + {!otoroshiEntitiesIsDefined && + props.api.visibility !== 'AdminOnly' && ( + + {translate('otoroshi.missing.entities')} + + )} + {!isPublish(props.api) && props.api.visibility !== 'AdminOnly' && ( + + {translate('api.not.pusblished')} + )} - {(otoroshiTargetIsDefined && otoroshiEntitiesIsDefined || props.api.visibility === 'AdminOnly') && + {((otoroshiTargetIsDefined && otoroshiEntitiesIsDefined) || + props.api.visibility === 'AdminOnly') && (!isAccepted || props.api.visibility === 'AdminOnly') && isPublish(props.api) && ( plan.visibility === 'Public' || team._id === props.ownerTeam._id + (team) => + plan.visibility === 'Public' || + team._id === props.ownerTeam._id )} > {(props.api.visibility === 'AdminOnly' || (plan.otoroshiTarget && !isAccepted)) && ( - - )} + + )} )} {connectedUser.isGuest && ( @@ -267,7 +352,7 @@ const ApiPricingCard = (props: ApiPricingCardProps) => { className="btn btn-sm btn-outline-primary mx-auto mt-3" onClick={() => openLoginOrRegisterModal({ tenant })} > - Get API key + )}
@@ -277,179 +362,241 @@ const ApiPricingCard = (props: ApiPricingCardProps) => { }; type ITeamSelector = { - teams: Array - pendingTeams: Array - acceptedTeams: Array - allowMultipleDemand?: boolean - showApiKeySelectModal: (teamId: string) => void, - plan: IUsagePlan -} + teams: Array; + pendingTeams: Array; + acceptedTeams: Array; + allowMultipleDemand?: boolean; + showApiKeySelectModal: (teamId: string) => void; + plan: IUsagePlan; +}; const TeamSelector = (props: ITeamSelector) => { const { translate } = useContext(I18nContext); const { close } = useContext(ModalContext); const navigate = useNavigate(); - const displayVerifiedBtn = props.plan.subscriptionProcess.some(p => p.type === 'payment') + const displayVerifiedBtn = props.plan.subscriptionProcess.some( + (p) => p.type === 'payment' + ); return (
-
{translate('team.selection.desc.request')}
-
- { - props.teams - .filter(t => !!props.allowMultipleDemand || !props.acceptedTeams.includes(t._id)) - .sort((a, b) => a.name.localeCompare(b.name)) - .map(team => { - const allowed = props.allowMultipleDemand || - (!props.pendingTeams.includes(team._id) && !props.acceptedTeams.includes(team._id) && (!displayVerifiedBtn || team.verified)) - - return ( -
{ - return allowed ? props.showApiKeySelectModal(team._id) : () => { }} - } - > - { - props.pendingTeams.includes(team._id) && - - } - { - displayVerifiedBtn && !team.verified && - - } - {team.name} -
- ) - }) - } +
+ {translate('team.selection.desc.request')} +
+
+ {props.teams + .filter( + (t) => + !!props.allowMultipleDemand || + !props.acceptedTeams.includes(t._id) + ) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((team) => { + const allowed = + props.allowMultipleDemand || + (!props.pendingTeams.includes(team._id) && + !props.acceptedTeams.includes(team._id) && + (!displayVerifiedBtn || team.verified)); + + return ( +
{ + return allowed + ? props.showApiKeySelectModal(team._id) + : () => {}; + }} + > + {props.pendingTeams.includes(team._id) && ( + + )} + {displayVerifiedBtn && !team.verified && ( + + )} + {team.name} +
+ ); + })}
- ) -} + ); +}; type ApiPricingProps = { - api: IApi - myTeams: Array - ownerTeam: ITeamSimple - subscriptions: Array, - inProgressDemands: Array, - askForApikeys: (x: { team: string, plan: IUsagePlan, apiKey?: ISubscription, motivation?: object }) => Promise, -} + api: IApi; + myTeams: Array; + ownerTeam: ITeamSimple; + subscriptions: Array; + inProgressDemands: Array; + askForApikeys: (x: { + team: string; + plan: IUsagePlan; + apiKey?: ISubscription; + motivation?: object; + }) => Promise; +}; export const ApiPricing = (props: ApiPricingProps) => { const queryClient = useQueryClient(); - const usagePlansQuery = useQuery({ queryKey: ['plans', props.api.currentVersion], queryFn: () => Services.getVisiblePlans(props.api._id, props.api.currentVersion) }) + const usagePlansQuery = useQuery({ + queryKey: ['plans', props.api.currentVersion], + queryFn: () => + Services.getVisiblePlans(props.api._id, props.api.currentVersion), + }); + const match = useMatch('/:team/:api/:version/pricing/:env/:tab'); - const match = useMatch('/:team/:api/:version/pricing/:env/:tab') - - const maybeTab = match?.params.tab - const maybeEnv = match?.params.env + const maybeTab = match?.params.tab; + const maybeEnv = match?.params.env; useEffect(() => { - queryClient.invalidateQueries({ queryKey: ['plans'] }) - }, [props.api]) - + queryClient.invalidateQueries({ queryKey: ['plans'] }); + }, [props.api]); if (usagePlansQuery.isLoading) { - return + return ; } else if (usagePlansQuery.data && !isError(usagePlansQuery.data)) { - const possibleUsagePlans = (usagePlansQuery.data as Array) - .filter((plan) => { - return plan.visibility === 'Public' || - props.myTeams.some((team) => team._id === props.ownerTeam._id) || - props.myTeams.some((team) => plan.authorizedTeams.includes(team._id)); - }); + const possibleUsagePlans = ( + usagePlansQuery.data as Array + ).filter((plan) => { + return ( + plan.visibility === 'Public' || + props.myTeams.some((team) => team._id === props.ownerTeam._id) || + props.myTeams.some((team) => plan.authorizedTeams.includes(team._id)) + ); + }); if (maybeEnv && maybeTab) { - const plan = usagePlansQuery.data.find(p => p.customName === maybeEnv)! + const plan = usagePlansQuery.data.find((p) => p.customName === maybeEnv)!; return (
- + -
{plan.customName}
+
{plan.customName}
-
+
swagger + to={`../../${props.api.currentVersion}/pricing/${plan.customName}/swagger`} + relative="path" + className={classNames('btn btn-sm btn-outline-primary mb-1', { + link__disabled: !plan.swagger?.content && !plan.swagger?.url, + disabled: !plan.swagger?.content && !plan.swagger?.url, + })} + > + swagger + test + to={`../../${props.api.currentVersion}/pricing/${plan.customName}/testing`} + relative="path" + className={classNames('btn btn-sm btn-outline-primary mb-1', { + link__disabled: !plan.testing || !plan.testing.enabled, + disabled: !plan.testing || !plan.testing.enabled, + })} + > + test + Documentation + to={`../../${props.api.currentVersion}/pricing/${plan.customName}/documentation`} + relative="path" + className={classNames('btn btn-sm btn-outline-primary', { + link__disabled: + !plan.documentation || !plan.documentation?.pages.length, + disabled: + !plan.documentation || !plan.documentation?.pages.length, + })} + > + Documentation +
- {maybeTab === 'swagger' && } - {maybeTab === 'documentation' && Services.getUsagePlanDocPage(props.api._id, plan._id, pageId)} />} - {maybeTab === 'testing' && } + {maybeTab === 'swagger' && ( + + )} + {maybeTab === 'documentation' && ( + + Services.getUsagePlanDocPage(props.api._id, plan._id, pageId) + } + /> + )} + {maybeTab === 'testing' && ( + + )}
- ) + ); } else { return ( -
+
{possibleUsagePlans - .sort((a, b) => (a.customName || a.type).localeCompare(b.customDescription || b.type)) - .map((plan) => - subs.api === props.api._id && subs.plan === plan._id - )} - inProgressDemands={props.inProgressDemands.filter( - (demand) => demand.api === props.api._id && demand.plan === plan._id - )} - askForApikeys={props.askForApikeys} - /> - )} + .sort((a, b) => + (a.customName || a.type).localeCompare( + b.customDescription || b.type + ) + ) + .map((plan) => ( + + + subs.api === props.api._id && subs.plan === plan._id + )} + inProgressDemands={props.inProgressDemands.filter( + (demand) => + demand.api === props.api._id && demand.plan === plan._id + )} + askForApikeys={props.askForApikeys} + /> + + ))}
); } - } else { - return
+ return
; } - - - -} +}; diff --git a/daikoku/javascript/src/components/frontend/fastMode/FastApiCard.tsx b/daikoku/javascript/src/components/frontend/fastMode/FastApiCard.tsx index eed35a29f..efe6b6178 100644 --- a/daikoku/javascript/src/components/frontend/fastMode/FastApiCard.tsx +++ b/daikoku/javascript/src/components/frontend/fastMode/FastApiCard.tsx @@ -1,156 +1,222 @@ -import { getApolloContext } from "@apollo/client"; -import { useQueryClient } from "@tanstack/react-query"; -import { useContext, useEffect, useState } from "react"; -import Select from "react-select"; -import { toast } from "sonner"; +import { getApolloContext } from '@apollo/client'; +import { useQueryClient } from '@tanstack/react-query'; +import { useContext, useEffect, useState } from 'react'; +import Select from 'react-select'; +import { toast } from 'sonner'; -import { ModalContext } from "../../../contexts"; -import { I18nContext } from "../../../contexts/i18n-context"; -import * as Services from "../../../services"; -import { IFastApi, IFastPlan, IFastSubscription, isError, ISubscription, ISubscriptionWithApiInfo, isValidationStepTeamAdmin, ITeamSimple } from "../../../types"; +import { ModalContext } from '../../../contexts'; +import { I18nContext } from '../../../contexts/i18n-context'; +import * as Services from '../../../services'; +import { + IFastApi, + IFastPlan, + IFastSubscription, + isError, + ISubscription, + ISubscriptionWithApiInfo, + isValidationStepTeamAdmin, + ITeamSimple, +} from '../../../types'; import { isSubscriptionProcessIsAutomatic, Option } from '../../utils'; +import { GlobalContext } from '../../../contexts/globalContext'; type FastApiCardProps = { - team: ITeamSimple, - apisWithAuthorization: Array, - subscriptions: Array>, - showPlan: (plan: IFastPlan) => void - showApiKey: (apiId: string, teamId: string, version: string, plan: IFastPlan) => void - planResearch: string -} + team: ITeamSimple; + apisWithAuthorization: Array; + subscriptions: Array>; + showPlan: (plan: IFastPlan) => void; + showApiKey: ( + apiId: string, + teamId: string, + version: string, + plan: IFastPlan + ) => void; + planResearch: string; +}; export const FastApiCard = (props: FastApiCardProps) => { const { openFormModal, openApiKeySelectModal } = useContext(ModalContext); const queryClient = useQueryClient(); const { client } = useContext(getApolloContext()); + const { tenant } = useContext(GlobalContext); const { translate } = useContext(I18nContext); - const [selectedApiV, setSelectedApiV] = useState(props.apisWithAuthorization.find(a => a.api.isDefault)?.api.currentVersion || props.apisWithAuthorization[0].api.currentVersion); - const [selectedApi, setSelectedApi] = useState(props.apisWithAuthorization.find((api) => api.api.currentVersion === selectedApiV)!) + const [selectedApiV, setSelectedApiV] = useState( + props.apisWithAuthorization.find((a) => a.api.isDefault)?.api + .currentVersion || props.apisWithAuthorization[0].api.currentVersion + ); + const [selectedApi, setSelectedApi] = useState( + props.apisWithAuthorization.find( + (api) => api.api.currentVersion === selectedApiV + )! + ); useEffect(() => { - setSelectedApi(props.apisWithAuthorization.find((api) => api.api.currentVersion === selectedApiV)!) - }, [props.apisWithAuthorization]) - + setSelectedApi( + props.apisWithAuthorization.find( + (api) => api.api.currentVersion === selectedApiV + )! + ); + }, [props.apisWithAuthorization]); const changeApiV = (version: string) => { - setSelectedApiV(version) - setSelectedApi(props.apisWithAuthorization.find((api) => api.api.currentVersion === version)!) - } + setSelectedApiV(version); + setSelectedApi( + props.apisWithAuthorization.find( + (api) => api.api.currentVersion === version + )! + ); + }; - const subscribe = (apiId: string, team: ITeamSimple, plan: IFastPlan, apiKey?: ISubscription) => { - const apiKeyDemand = (motivation?: object) => apiKey - ? Services.extendApiKey(apiId, apiKey._id, team._id, plan._id, motivation) - : Services.askForApiKey(apiId, team._id, plan._id, motivation) + const subscribe = ( + apiId: string, + team: ITeamSimple, + plan: IFastPlan, + apiKey?: ISubscription + ) => { + const apiKeyDemand = (motivation?: object) => + apiKey + ? Services.extendApiKey( + apiId, + apiKey._id, + team._id, + plan._id, + motivation + ) + : Services.askForApiKey(apiId, team._id, plan._id, motivation); - const adminStep = plan.subscriptionProcess.find(s => isValidationStepTeamAdmin(s)) + const adminStep = plan.subscriptionProcess.find((s) => + isValidationStepTeamAdmin(s) + ); if (adminStep && isValidationStepTeamAdmin(adminStep)) { openFormModal<{ motivation: string }>({ title: translate('motivations.modal.title'), schema: adminStep.schema, onSubmit: (motivation) => { - apiKeyDemand(motivation) - .then((response) => { - if (isError(response)) { - toast.error(response.error) - } else { - toast.info(translate( - { - key: 'subscription.plan.waiting', - replacements: [ - plan.customName!, - team.name - ] - })) - queryClient.invalidateQueries({ queryKey: ['data'] }) - } + apiKeyDemand(motivation).then((response) => { + if (isError(response)) { + toast.error(response.error); + } else { + toast.info( + translate({ + key: 'subscription.plan.waiting', + replacements: [plan.customName!, team.name], + }) + ); + queryClient.invalidateQueries({ queryKey: ['data'] }); } - ) - + }); }, - actionLabel: translate('Send') - }) + actionLabel: translate('Send'), + }); } else { - apiKeyDemand() - .then((response) => { - if (isError(response)) { - toast.error(response.error) - } else { - toast.success(translate( - { - key: 'subscription.plan.accepted', - replacements: [ - plan.customName!, - team.name - ] - })) - queryClient.invalidateQueries({ queryKey: ['data'] }) - } - }) + apiKeyDemand().then((response) => { + if (isError(response)) { + toast.error(response.error); + } else { + toast.success( + translate({ + key: 'subscription.plan.accepted', + replacements: [plan.customName!, team.name], + }) + ); + queryClient.invalidateQueries({ queryKey: ['data'] }); + } + }); } - } + }; type IUsagePlanGQL = { - _id: string + _id: string; otoroshiTarget: { - otoroshiSettings: string - } - aggregationApiKeysSecurity: boolean - } + otoroshiSettings: string; + }; + aggregationApiKeysSecurity: boolean; + }; type IApiGQL = { - _id: string - _humanReadableId: string - currentVersion: string - name: string - possibleUsagePlans: IUsagePlanGQL[] - } + _id: string; + _humanReadableId: string; + currentVersion: string; + name: string; + possibleUsagePlans: IUsagePlanGQL[]; + }; - const subscribeOrExtends = (apiId: string, team: ITeamSimple, plan: IFastPlan) => { + const subscribeOrExtends = ( + apiId: string, + team: ITeamSimple, + plan: IFastPlan + ) => { if (client) { Services.getAllTeamSubscriptions(props.team._id) - .then((subscriptions) => client.query({ - query: Services.graphql.apisByIdsWithPlans, - variables: { ids: [...new Set(subscriptions.map((s) => s.api))] }, - }) - .then(({ data }) => ({ apis: data.apis, subscriptions })) + .then((subscriptions) => + client + .query({ + query: Services.graphql.apisByIdsWithPlans, + variables: { ids: [...new Set(subscriptions.map((s) => s.api))] }, + }) + .then(({ data }) => ({ apis: data.apis, subscriptions })) ) - .then(({ apis, subscriptions }: { apis: Array, subscriptions: Array }) => { - const int = subscriptions - .map((subscription) => { + .then( + ({ + apis, + subscriptions, + }: { + apis: Array; + subscriptions: Array; + }) => { + const int = subscriptions.map((subscription) => { const api = apis.find((a) => a._id === subscription.api); const plan = Option(api?.possibleUsagePlans) - .flatMap((plans) => Option(plans.find((plan) => plan._id === subscription.plan))) + .flatMap((plans) => + Option(plans.find((plan) => plan._id === subscription.plan)) + ) .getOrNull(); return { subscription, api, plan }; - }) + }); - const filteredApiKeys = int.filter((infos) => infos.plan?.otoroshiTarget?.otoroshiSettings === - plan?.otoroshiTarget?.otoroshiSettings && infos.plan?.aggregationApiKeysSecurity - ) - .map((infos) => infos.subscription); + const filteredApiKeys = int + .filter( + (infos) => + infos.plan?.otoroshiTarget?.otoroshiSettings === plan?.otoroshiTarget?.otoroshiSettings && + infos.plan?.aggregationApiKeysSecurity + ) + .filter(s => !tenant.environmentAggregationApiKeysSecurity || s.subscription.planName === plan.customName) + .map((infos) => infos.subscription); - if (!plan.aggregationApiKeysSecurity || subscriptions.length <= 0) { - subscribe(apiId, team, plan); - } else { - openApiKeySelectModal({ - plan, - apiKeys: filteredApiKeys, - onSubscribe: () => subscribe(apiId, team, plan), - extendApiKey: (apiKey: ISubscription) => subscribe(apiId, team, plan, apiKey), - }); + if ( + (!plan.aggregationApiKeysSecurity) || + filteredApiKeys.length <= 0 + ) { + subscribe(apiId, team, plan); + } else { + openApiKeySelectModal({ + plan, + apiKeys: filteredApiKeys, + onSubscribe: () => subscribe(apiId, team, plan), + extendApiKey: (apiKey: ISubscription) => + subscribe(apiId, team, plan, apiKey), + }); + } } - }); + ); } - - } + }; return (
{/* TODO: overflow ellips for title*/} -

{selectedApi.api.name}

- {props.apisWithAuthorization.length > 1 && +

+ {selectedApi.api.name} +

+ {props.apisWithAuthorization.length > 1 && (