mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-27 00:19:39 +01:00
Merge branch 'main' into 1-3490
This commit is contained in:
commit
1e8bdda5c4
frontend/src
component
admin
billing/BillingDashboard/BillingPlan
network/NetworkTrafficUsage/hooks
users/CreateUser/SeatCostWarning
demo/DemoDialog/DemoDialogPlans
feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem
interfaces
src
website
@ -8,8 +8,8 @@ import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||
import {
|
||||
BILLING_INCLUDED_REQUESTS,
|
||||
BILLING_PAYG_DEFAULT_MINIMUM_SEATS,
|
||||
BILLING_PAYG_USER_PRICE,
|
||||
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||
BILLING_PAYG_SEAT_PRICE,
|
||||
BILLING_TRAFFIC_PRICE,
|
||||
} from './BillingPlan';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useOverageCost } from './useOverageCost';
|
||||
@ -34,11 +34,16 @@ export const BillingDetailsPAYG = ({
|
||||
|
||||
const eligibleUsers = users.filter((user) => user.email);
|
||||
|
||||
const seatPrice =
|
||||
instanceStatus.prices?.payg?.seat ?? BILLING_PAYG_SEAT_PRICE;
|
||||
const trafficPrice =
|
||||
instanceStatus.prices?.payg?.traffic ?? BILLING_TRAFFIC_PRICE;
|
||||
|
||||
const minSeats =
|
||||
instanceStatus.minSeats ?? BILLING_PAYG_DEFAULT_MINIMUM_SEATS;
|
||||
|
||||
const billableUsers = Math.max(eligibleUsers.length, minSeats);
|
||||
const usersCost = BILLING_PAYG_USER_PRICE * billableUsers;
|
||||
const usersCost = seatPrice * billableUsers;
|
||||
|
||||
const includedTraffic = BILLING_INCLUDED_REQUESTS;
|
||||
const overageCost = useOverageCost(includedTraffic);
|
||||
@ -66,7 +71,7 @@ export const BillingDetailsPAYG = ({
|
||||
</GridColLink>
|
||||
</Typography>
|
||||
<StyledInfoLabel>
|
||||
${BILLING_PAYG_USER_PRICE}/month per paid member
|
||||
${seatPrice}/month per paid member
|
||||
</StyledInfoLabel>
|
||||
</GridCol>
|
||||
<GridCol>
|
||||
@ -93,8 +98,8 @@ export const BillingDetailsPAYG = ({
|
||||
</GridColLink>
|
||||
</Typography>
|
||||
<StyledInfoLabel>
|
||||
${BILLING_TRAFFIC_BUNDLE_PRICE} per 1
|
||||
million started above included data
|
||||
${trafficPrice} per 1 million started above
|
||||
included data
|
||||
</StyledInfoLabel>
|
||||
</GridCol>
|
||||
<GridCol>
|
||||
|
@ -9,10 +9,10 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||
import {
|
||||
BILLING_INCLUDED_REQUESTS,
|
||||
BILLING_PLAN_PRICES,
|
||||
BILLING_PRO_DEFAULT_INCLUDED_SEATS,
|
||||
BILLING_PRO_USER_PRICE,
|
||||
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||
BILLING_PRO_BASE_PRICE,
|
||||
BILLING_PRO_SEAT_PRICE,
|
||||
BILLING_TRAFFIC_PRICE,
|
||||
} from './BillingPlan';
|
||||
import { useOverageCost } from './useOverageCost';
|
||||
|
||||
@ -41,12 +41,17 @@ export const BillingDetailsPro = ({
|
||||
|
||||
const eligibleUsers = users.filter((user) => user.email);
|
||||
|
||||
const planPrice = BILLING_PLAN_PRICES[instanceStatus.plan];
|
||||
const planPrice =
|
||||
instanceStatus.prices?.pro?.base ?? BILLING_PRO_BASE_PRICE;
|
||||
const seatPrice =
|
||||
instanceStatus.prices?.pro?.seat ?? BILLING_PRO_SEAT_PRICE;
|
||||
const trafficPrice =
|
||||
instanceStatus.prices?.pro?.traffic ?? BILLING_TRAFFIC_PRICE;
|
||||
const seats = BILLING_PRO_DEFAULT_INCLUDED_SEATS;
|
||||
|
||||
const freeAssigned = Math.min(eligibleUsers.length, seats);
|
||||
const paidAssigned = eligibleUsers.length - freeAssigned;
|
||||
const paidAssignedPrice = BILLING_PRO_USER_PRICE * paidAssigned;
|
||||
const paidAssignedPrice = seatPrice * paidAssigned;
|
||||
const includedTraffic = BILLING_INCLUDED_REQUESTS;
|
||||
const overageCost = useOverageCost(includedTraffic);
|
||||
|
||||
@ -96,7 +101,7 @@ export const BillingDetailsPro = ({
|
||||
</GridColLink>
|
||||
</Typography>
|
||||
<StyledInfoLabel>
|
||||
${BILLING_PRO_USER_PRICE}/month per paid member
|
||||
${seatPrice}/month per paid member
|
||||
</StyledInfoLabel>
|
||||
</GridCol>
|
||||
<GridCol>
|
||||
@ -123,8 +128,8 @@ export const BillingDetailsPro = ({
|
||||
</GridColLink>
|
||||
</Typography>
|
||||
<StyledInfoLabel>
|
||||
${BILLING_TRAFFIC_BUNDLE_PRICE} per 1
|
||||
million started above included data
|
||||
${trafficPrice} per 1 million started above
|
||||
included data
|
||||
</StyledInfoLabel>
|
||||
</GridCol>
|
||||
<GridCol>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Alert, Grid, styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { InstanceState, InstancePlan } from 'interfaces/instance';
|
||||
import { InstanceState } from 'interfaces/instance';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { trialHasExpired, isTrialInstance } from 'utils/instanceTrial';
|
||||
import { GridRow } from 'component/common/GridRow/GridRow';
|
||||
@ -9,16 +9,14 @@ import { Badge } from 'component/common/Badge/Badge';
|
||||
import { BillingDetails } from './BillingDetails';
|
||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||
|
||||
export const BILLING_PLAN_PRICES: Record<string, number> = {
|
||||
[InstancePlan.PRO]: 80,
|
||||
};
|
||||
export const BILLING_PRO_BASE_PRICE = 80;
|
||||
export const BILLING_PRO_SEAT_PRICE = 15;
|
||||
export const BILLING_PAYG_SEAT_PRICE = 75;
|
||||
export const BILLING_TRAFFIC_PRICE = 5;
|
||||
|
||||
export const BILLING_PAYG_USER_PRICE = 75;
|
||||
export const BILLING_PAYG_DEFAULT_MINIMUM_SEATS = 5;
|
||||
export const BILLING_PRO_USER_PRICE = 15;
|
||||
export const BILLING_PRO_DEFAULT_INCLUDED_SEATS = 5;
|
||||
export const BILLING_INCLUDED_REQUESTS = 53_000_000;
|
||||
export const BILLING_TRAFFIC_BUNDLE_PRICE = 5;
|
||||
|
||||
const StyledPlanBox = styled('aside')(({ theme }) => ({
|
||||
padding: theme.spacing(2.5),
|
||||
@ -77,7 +75,8 @@ export const BillingPlan = () => {
|
||||
);
|
||||
|
||||
const expired = trialHasExpired(instanceStatus);
|
||||
const planPrice = BILLING_PLAN_PRICES[instanceStatus.plan] ?? 0;
|
||||
const baseProPrice =
|
||||
instanceStatus.prices?.pro?.base ?? BILLING_PRO_BASE_PRICE;
|
||||
const plan = `${instanceStatus.plan}${isPAYG ? ' Pay-as-You-Go' : ''}`;
|
||||
const inactive = instanceStatus.state !== InstanceState.ACTIVE;
|
||||
|
||||
@ -131,10 +130,10 @@ export const BillingPlan = () => {
|
||||
</GridCol>
|
||||
<GridCol>
|
||||
<ConditionallyRender
|
||||
condition={planPrice > 0}
|
||||
condition={!isPAYG}
|
||||
show={
|
||||
<StyledPriceSpan>
|
||||
${planPrice.toFixed(2)}
|
||||
${baseProPrice.toFixed(2)}
|
||||
</StyledPriceSpan>
|
||||
}
|
||||
/>
|
||||
|
@ -5,7 +5,8 @@ import {
|
||||
calculateOverageCost,
|
||||
calculateTotalUsage,
|
||||
} from 'utils/traffic-calculations';
|
||||
import { BILLING_TRAFFIC_BUNDLE_PRICE } from './BillingPlan';
|
||||
import { BILLING_TRAFFIC_PRICE } from './BillingPlan';
|
||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||
|
||||
export const useOverageCost = (includedTraffic: number) => {
|
||||
if (!includedTraffic) {
|
||||
@ -17,6 +18,12 @@ export const useOverageCost = (includedTraffic: number) => {
|
||||
const from = formatDate(startOfMonth(now));
|
||||
const to = formatDate(endOfMonth(now));
|
||||
|
||||
const { instanceStatus } = useInstanceStatus();
|
||||
const trafficPrice =
|
||||
instanceStatus?.prices?.[
|
||||
instanceStatus?.billing === 'pay-as-you-go' ? 'payg' : 'pro'
|
||||
]?.traffic ?? BILLING_TRAFFIC_PRICE;
|
||||
|
||||
const { result } = useTrafficSearch('daily', { from, to });
|
||||
const overageCost = useMemo(() => {
|
||||
if (result.state !== 'success') {
|
||||
@ -24,12 +31,8 @@ export const useOverageCost = (includedTraffic: number) => {
|
||||
}
|
||||
|
||||
const totalUsage = calculateTotalUsage(result.data);
|
||||
return calculateOverageCost(
|
||||
totalUsage,
|
||||
includedTraffic,
|
||||
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||
);
|
||||
}, [includedTraffic, JSON.stringify(result)]);
|
||||
return calculateOverageCost(totalUsage, includedTraffic, trafficPrice);
|
||||
}, [includedTraffic, JSON.stringify(result), trafficPrice]);
|
||||
|
||||
return overageCost;
|
||||
};
|
||||
|
@ -12,10 +12,11 @@ import {
|
||||
calculateOverageCost,
|
||||
calculateTotalUsage,
|
||||
} from 'utils/traffic-calculations';
|
||||
import { BILLING_TRAFFIC_BUNDLE_PRICE } from '../../../billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||
import { BILLING_TRAFFIC_PRICE } from '../../../billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||
import { averageTrafficPreviousMonths } from '../average-traffic-previous-months';
|
||||
import { useConnectionsConsumption } from 'hooks/api/getters/useConnectionsConsumption/useConnectionsConsumption';
|
||||
import { useRequestsConsumption } from 'hooks/api/getters/useRequestsConsumption/useRequestsConsumption';
|
||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||
|
||||
export const useTrafficStats = (
|
||||
includedTraffic: number,
|
||||
@ -26,6 +27,12 @@ export const useTrafficStats = (
|
||||
chartDataSelection.grouping,
|
||||
toDateRange(chartDataSelection, currentDate),
|
||||
);
|
||||
const { instanceStatus } = useInstanceStatus();
|
||||
const trafficPrice =
|
||||
instanceStatus?.prices?.[
|
||||
instanceStatus?.billing === 'pay-as-you-go' ? 'payg' : 'pro'
|
||||
]?.traffic ?? BILLING_TRAFFIC_PRICE;
|
||||
|
||||
const results = useMemo(() => {
|
||||
if (result.state !== 'success') {
|
||||
return {
|
||||
@ -43,14 +50,14 @@ export const useTrafficStats = (
|
||||
const overageCost = calculateOverageCost(
|
||||
usageTotal,
|
||||
includedTraffic,
|
||||
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||
trafficPrice,
|
||||
);
|
||||
|
||||
const estimatedMonthlyCost = calculateEstimatedMonthlyCost(
|
||||
traffic.apiData,
|
||||
includedTraffic,
|
||||
currentDate,
|
||||
BILLING_TRAFFIC_BUNDLE_PRICE,
|
||||
trafficPrice,
|
||||
);
|
||||
|
||||
const requestSummaryUsage =
|
||||
@ -69,6 +76,7 @@ export const useTrafficStats = (
|
||||
JSON.stringify(result),
|
||||
includedTraffic,
|
||||
JSON.stringify(chartDataSelection),
|
||||
trafficPrice,
|
||||
]);
|
||||
|
||||
return results;
|
||||
|
@ -2,11 +2,15 @@ import type { VFC } from 'react';
|
||||
import { Alert } from '@mui/material';
|
||||
import { useUsersPlan } from 'hooks/useUsersPlan';
|
||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||
import { BILLING_PRO_USER_PRICE } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||
import { BILLING_PRO_SEAT_PRICE } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||
|
||||
export const SeatCostWarning: VFC = () => {
|
||||
const { users } = useUsers();
|
||||
const { isBillingUsers, seats, planUsers } = useUsersPlan(users);
|
||||
const { instanceStatus } = useInstanceStatus();
|
||||
const seatPrice =
|
||||
instanceStatus?.prices?.pro?.seat ?? BILLING_PRO_SEAT_PRICE;
|
||||
|
||||
if (!isBillingUsers || planUsers.length < seats) {
|
||||
return null;
|
||||
@ -20,9 +24,8 @@ export const SeatCostWarning: VFC = () => {
|
||||
<p>
|
||||
<strong>Heads up!</strong> You are exceeding your allocated free
|
||||
members included in your plan ({planUsers.length} of {seats}).
|
||||
Creating this user will add{' '}
|
||||
<strong>${BILLING_PRO_USER_PRICE}/month</strong> to your
|
||||
invoice, starting with your next payment.
|
||||
Creating this user will add <strong>${seatPrice}/month</strong>{' '}
|
||||
to your invoice, starting with your next payment.
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
|
@ -6,11 +6,11 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import {
|
||||
BILLING_PAYG_DEFAULT_MINIMUM_SEATS,
|
||||
BILLING_PAYG_USER_PRICE,
|
||||
BILLING_PLAN_PRICES,
|
||||
BILLING_PAYG_SEAT_PRICE,
|
||||
BILLING_PRO_BASE_PRICE,
|
||||
BILLING_PRO_DEFAULT_INCLUDED_SEATS,
|
||||
} from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||
import { InstancePlan } from 'interfaces/instance';
|
||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||
|
||||
const StyledDemoDialog = styled(DemoDialog)(({ theme }) => ({
|
||||
'& .MuiDialog-paper': {
|
||||
@ -91,6 +91,12 @@ interface IDemoDialogPlansProps {
|
||||
export const DemoDialogPlans = ({ open, onClose }: IDemoDialogPlansProps) => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
const isEnterprisePaygEnabled = useUiFlag('enterprise-payg');
|
||||
const { instanceStatus } = useInstanceStatus();
|
||||
|
||||
const paygSeatPrice =
|
||||
instanceStatus?.prices?.payg?.seat ?? BILLING_PAYG_SEAT_PRICE;
|
||||
const proBasePrice =
|
||||
instanceStatus?.prices?.pro?.base ?? BILLING_PRO_BASE_PRICE;
|
||||
|
||||
return (
|
||||
<StyledDemoDialog open={open} onClose={onClose}>
|
||||
@ -139,7 +145,7 @@ export const DemoDialogPlans = ({ open, onClose }: IDemoDialogPlansProps) => {
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography variant='h6' fontWeight='normal'>
|
||||
${BILLING_PAYG_USER_PRICE} per user/month
|
||||
${paygSeatPrice} per user/month
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
{BILLING_PAYG_DEFAULT_MINIMUM_SEATS} users
|
||||
@ -174,7 +180,7 @@ export const DemoDialogPlans = ({ open, onClose }: IDemoDialogPlansProps) => {
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography variant='h6' fontWeight='normal'>
|
||||
${BILLING_PLAN_PRICES[InstancePlan.PRO]}/month
|
||||
${proBasePrice}/month
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
includes {BILLING_PRO_DEFAULT_INCLUDED_SEATS}{' '}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { type DragEventHandler, type RefObject, useRef } from 'react';
|
||||
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { useMediaQuery, useTheme } from '@mui/material';
|
||||
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
@ -34,8 +34,6 @@ type ProjectEnvironmentStrategyDraggableItemProps = {
|
||||
onDragEnd?: () => void;
|
||||
};
|
||||
|
||||
const onDragNoOp = () => () => {};
|
||||
|
||||
export const ProjectEnvironmentStrategyDraggableItem = ({
|
||||
className,
|
||||
strategy,
|
||||
@ -43,9 +41,9 @@ export const ProjectEnvironmentStrategyDraggableItem = ({
|
||||
environmentName,
|
||||
otherEnvironments,
|
||||
isDragging,
|
||||
onDragStartRef = onDragNoOp,
|
||||
onDragOver = onDragNoOp,
|
||||
onDragEnd = onDragNoOp,
|
||||
onDragStartRef,
|
||||
onDragOver,
|
||||
onDragEnd,
|
||||
}: ProjectEnvironmentStrategyDraggableItemProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
@ -75,67 +73,59 @@ export const ProjectEnvironmentStrategyDraggableItem = ({
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={className}
|
||||
key={strategy.id}
|
||||
ref={ref}
|
||||
onDragOver={onDragOver(ref, index)}
|
||||
sx={{ opacity: isDragging ? '0.5' : '1' }}
|
||||
>
|
||||
<StrategyDraggableItem
|
||||
strategy={strategy}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragStartRef={onDragStartRef}
|
||||
onDragOver={onDragOver}
|
||||
index={index}
|
||||
isDragging={isDragging}
|
||||
headerItemsRight={
|
||||
<>
|
||||
{draftChange && !isSmallScreen ? (
|
||||
<ChangeRequestDraftStatusBadge
|
||||
sx={{ mr: 1.5 }}
|
||||
changeAction={draftChange.change.action}
|
||||
/>
|
||||
) : null}
|
||||
<StrategyDraggableItem
|
||||
strategy={strategy}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragStartRef={onDragStartRef}
|
||||
onDragOver={onDragOver}
|
||||
index={index}
|
||||
isDragging={isDragging}
|
||||
headerItemsRight={
|
||||
<>
|
||||
{draftChange && !isSmallScreen ? (
|
||||
<ChangeRequestDraftStatusBadge
|
||||
sx={{ mr: 1.5 }}
|
||||
changeAction={draftChange.change.action}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{scheduledChanges &&
|
||||
scheduledChanges.length > 0 &&
|
||||
!isSmallScreen ? (
|
||||
<ChangesScheduledBadge
|
||||
scheduledChangeRequestIds={(
|
||||
scheduledChanges ?? []
|
||||
).map((scheduledChange) => scheduledChange.id)}
|
||||
/>
|
||||
) : null}
|
||||
{otherEnvironments && otherEnvironments?.length > 0 ? (
|
||||
<CopyStrategyIconMenu
|
||||
environmentId={environmentName}
|
||||
environments={otherEnvironments as string[]}
|
||||
strategy={strategy}
|
||||
/>
|
||||
) : null}
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_FEATURE_STRATEGY}
|
||||
environmentId={environmentName}
|
||||
projectId={projectId}
|
||||
component={Link}
|
||||
to={editStrategyPath}
|
||||
tooltipProps={{
|
||||
title: 'Edit strategy',
|
||||
}}
|
||||
data-testid={`STRATEGY_EDIT-${strategy.name}`}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
<MenuStrategyRemove
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
{scheduledChanges &&
|
||||
scheduledChanges.length > 0 &&
|
||||
!isSmallScreen ? (
|
||||
<ChangesScheduledBadge
|
||||
scheduledChangeRequestIds={(
|
||||
scheduledChanges ?? []
|
||||
).map((scheduledChange) => scheduledChange.id)}
|
||||
/>
|
||||
) : null}
|
||||
{otherEnvironments && otherEnvironments?.length > 0 ? (
|
||||
<CopyStrategyIconMenu
|
||||
environmentId={environmentName}
|
||||
environments={otherEnvironments as string[]}
|
||||
strategy={strategy}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_FEATURE_STRATEGY}
|
||||
environmentId={environmentName}
|
||||
projectId={projectId}
|
||||
component={Link}
|
||||
to={editStrategyPath}
|
||||
tooltipProps={{
|
||||
title: 'Edit strategy',
|
||||
}}
|
||||
data-testid={`STRATEGY_EDIT-${strategy.name}`}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
<MenuStrategyRemove
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={environmentName}
|
||||
strategy={strategy}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -8,8 +8,6 @@ import { Box } from '@mui/material';
|
||||
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import { StrategyItem } from './StrategyItem/StrategyItem';
|
||||
|
||||
const onDragNoOp = () => () => {};
|
||||
|
||||
type StrategyDraggableItemProps = {
|
||||
headerItemsRight: ReactNode;
|
||||
strategy: IFeatureStrategy;
|
||||
@ -30,9 +28,9 @@ export const StrategyDraggableItem = ({
|
||||
strategy,
|
||||
index,
|
||||
isDragging,
|
||||
onDragStartRef = onDragNoOp,
|
||||
onDragOver = onDragNoOp,
|
||||
onDragEnd = onDragNoOp,
|
||||
onDragStartRef,
|
||||
onDragOver,
|
||||
onDragEnd,
|
||||
headerItemsRight,
|
||||
}: StrategyDraggableItemProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -41,13 +39,13 @@ export const StrategyDraggableItem = ({
|
||||
<Box
|
||||
key={strategy.id}
|
||||
ref={ref}
|
||||
onDragOver={onDragOver(ref, index)}
|
||||
onDragOver={onDragOver?.(ref, index)}
|
||||
sx={{ opacity: isDragging ? '0.5' : '1' }}
|
||||
>
|
||||
<StrategyItem
|
||||
headerItemsRight={headerItemsRight}
|
||||
strategy={strategy}
|
||||
onDragStart={onDragStartRef(ref, index)}
|
||||
onDragStart={onDragStartRef?.(ref, index)}
|
||||
onDragEnd={onDragEnd}
|
||||
/>
|
||||
</Box>
|
||||
|
@ -1,3 +1,17 @@
|
||||
type InstancePrices = {
|
||||
pro?: {
|
||||
base?: number;
|
||||
seat?: number;
|
||||
traffic?: number;
|
||||
};
|
||||
payg?: {
|
||||
seat?: number;
|
||||
traffic?: number;
|
||||
};
|
||||
};
|
||||
|
||||
type InstanceBilling = 'pay-as-you-go' | 'subscription';
|
||||
|
||||
export interface IInstanceStatus {
|
||||
plan: InstancePlan;
|
||||
trialExpiry?: string;
|
||||
@ -8,6 +22,8 @@ export interface IInstanceStatus {
|
||||
seats?: number;
|
||||
minSeats?: number;
|
||||
isCustomBilling?: boolean;
|
||||
prices?: InstancePrices;
|
||||
billing?: InstanceBilling;
|
||||
}
|
||||
|
||||
export enum InstanceState {
|
||||
|
@ -457,8 +457,9 @@ export class AccessService {
|
||||
async getRootRoleForUser(userId: number): Promise<IRole> {
|
||||
const rootRole = await this.store.getRootRoleForUser(userId);
|
||||
if (!rootRole) {
|
||||
const defaultRole = await this.getPredefinedRole(RoleName.VIEWER);
|
||||
return defaultRole;
|
||||
// this should never happen, but before breaking we want to know if it does.
|
||||
this.logger.warn(`Could not find root role for user=${userId}.`);
|
||||
return this.getPredefinedRole(RoleName.VIEWER);
|
||||
}
|
||||
return rootRole;
|
||||
}
|
||||
|
20
src/migrations/20250320121200-all-users-have-a-root-role.js
Normal file
20
src/migrations/20250320121200-all-users-have-a-root-role.js
Normal file
@ -0,0 +1,20 @@
|
||||
exports.up = function (db, cb) {
|
||||
// add root role Viewer (id 3) to all users who don't have a root role
|
||||
db.runSql(
|
||||
`INSERT INTO role_user(role_id, user_id, project) SELECT 3, u.id, 'default'
|
||||
FROM users u
|
||||
WHERE u.id > 0 AND u.deleted_at IS NULL AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM role_user ru
|
||||
JOIN roles r ON ru.role_id = r.id
|
||||
WHERE ru.user_id = u.id
|
||||
AND r.type IN ('root', 'root-custom')
|
||||
);`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
// No rollback
|
||||
callback();
|
||||
};
|
@ -235,7 +235,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.8.3":
|
||||
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2, @babel/code-frame@npm:^7.8.3":
|
||||
version: 7.26.2
|
||||
resolution: "@babel/code-frame@npm:7.26.2"
|
||||
dependencies:
|
||||
@ -494,12 +494,12 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/helpers@npm:^7.26.0":
|
||||
version: 7.26.0
|
||||
resolution: "@babel/helpers@npm:7.26.0"
|
||||
version: 7.26.10
|
||||
resolution: "@babel/helpers@npm:7.26.10"
|
||||
dependencies:
|
||||
"@babel/template": "npm:^7.25.9"
|
||||
"@babel/types": "npm:^7.26.0"
|
||||
checksum: 10c0/343333cced6946fe46617690a1d0789346960910225ce359021a88a60a65bc0d791f0c5d240c0ed46cf8cc63b5fd7df52734ff14e43b9c32feae2b61b1647097
|
||||
"@babel/template": "npm:^7.26.9"
|
||||
"@babel/types": "npm:^7.26.10"
|
||||
checksum: 10c0/f99e1836bcffce96db43158518bb4a24cf266820021f6461092a776cba2dc01d9fc8b1b90979d7643c5c2ab7facc438149064463a52dd528b21c6ab32509784f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -514,6 +514,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/parser@npm:^7.26.9":
|
||||
version: 7.26.10
|
||||
resolution: "@babel/parser@npm:7.26.10"
|
||||
dependencies:
|
||||
"@babel/types": "npm:^7.26.10"
|
||||
bin:
|
||||
parser: ./bin/babel-parser.js
|
||||
checksum: 10c0/c47f5c0f63cd12a663e9dc94a635f9efbb5059d98086a92286d7764357c66bceba18ccbe79333e01e9be3bfb8caba34b3aaebfd8e62c3d5921c8cf907267be75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.9":
|
||||
version: 7.25.9
|
||||
resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9"
|
||||
@ -1482,6 +1493,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/template@npm:^7.26.9":
|
||||
version: 7.26.9
|
||||
resolution: "@babel/template@npm:7.26.9"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.26.2"
|
||||
"@babel/parser": "npm:^7.26.9"
|
||||
"@babel/types": "npm:^7.26.9"
|
||||
checksum: 10c0/019b1c4129cc01ad63e17529089c2c559c74709d225f595eee017af227fee11ae8a97a6ab19ae6768b8aa22d8d75dcb60a00b28f52e9fa78140672d928bc1ae9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.25.9":
|
||||
version: 7.25.9
|
||||
resolution: "@babel/traverse@npm:7.25.9"
|
||||
@ -1507,6 +1529,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.26.10, @babel/types@npm:^7.26.9":
|
||||
version: 7.26.10
|
||||
resolution: "@babel/types@npm:7.26.10"
|
||||
dependencies:
|
||||
"@babel/helper-string-parser": "npm:^7.25.9"
|
||||
"@babel/helper-validator-identifier": "npm:^7.25.9"
|
||||
checksum: 10c0/7a7f83f568bfc3dfabfaf9ae3a97ab5c061726c0afa7dcd94226d4f84a81559da368ed79671e3a8039d16f12476cf110381a377ebdea07587925f69628200dac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@braintree/sanitize-url@npm:^7.0.1":
|
||||
version: 7.1.1
|
||||
resolution: "@braintree/sanitize-url@npm:7.1.1"
|
||||
|
Loading…
Reference in New Issue
Block a user