1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-06 01:15:28 +02:00

Merge branch 'main' into 1-3490

This commit is contained in:
Tymoteusz Czech 2025-03-21 13:00:56 +01:00
commit 1e8bdda5c4
No known key found for this signature in database
GPG Key ID: 133555230D88D75F
13 changed files with 208 additions and 122 deletions

View File

@ -8,8 +8,8 @@ import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import { import {
BILLING_INCLUDED_REQUESTS, BILLING_INCLUDED_REQUESTS,
BILLING_PAYG_DEFAULT_MINIMUM_SEATS, BILLING_PAYG_DEFAULT_MINIMUM_SEATS,
BILLING_PAYG_USER_PRICE, BILLING_PAYG_SEAT_PRICE,
BILLING_TRAFFIC_BUNDLE_PRICE, BILLING_TRAFFIC_PRICE,
} from './BillingPlan'; } from './BillingPlan';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useOverageCost } from './useOverageCost'; import { useOverageCost } from './useOverageCost';
@ -34,11 +34,16 @@ export const BillingDetailsPAYG = ({
const eligibleUsers = users.filter((user) => user.email); 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 = const minSeats =
instanceStatus.minSeats ?? BILLING_PAYG_DEFAULT_MINIMUM_SEATS; instanceStatus.minSeats ?? BILLING_PAYG_DEFAULT_MINIMUM_SEATS;
const billableUsers = Math.max(eligibleUsers.length, minSeats); const billableUsers = Math.max(eligibleUsers.length, minSeats);
const usersCost = BILLING_PAYG_USER_PRICE * billableUsers; const usersCost = seatPrice * billableUsers;
const includedTraffic = BILLING_INCLUDED_REQUESTS; const includedTraffic = BILLING_INCLUDED_REQUESTS;
const overageCost = useOverageCost(includedTraffic); const overageCost = useOverageCost(includedTraffic);
@ -66,7 +71,7 @@ export const BillingDetailsPAYG = ({
</GridColLink> </GridColLink>
</Typography> </Typography>
<StyledInfoLabel> <StyledInfoLabel>
${BILLING_PAYG_USER_PRICE}/month per paid member ${seatPrice}/month per paid member
</StyledInfoLabel> </StyledInfoLabel>
</GridCol> </GridCol>
<GridCol> <GridCol>
@ -93,8 +98,8 @@ export const BillingDetailsPAYG = ({
</GridColLink> </GridColLink>
</Typography> </Typography>
<StyledInfoLabel> <StyledInfoLabel>
${BILLING_TRAFFIC_BUNDLE_PRICE} per 1 ${trafficPrice} per 1 million started above
million started above included data included data
</StyledInfoLabel> </StyledInfoLabel>
</GridCol> </GridCol>
<GridCol> <GridCol>

View File

@ -9,10 +9,10 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import { import {
BILLING_INCLUDED_REQUESTS, BILLING_INCLUDED_REQUESTS,
BILLING_PLAN_PRICES,
BILLING_PRO_DEFAULT_INCLUDED_SEATS, BILLING_PRO_DEFAULT_INCLUDED_SEATS,
BILLING_PRO_USER_PRICE, BILLING_PRO_BASE_PRICE,
BILLING_TRAFFIC_BUNDLE_PRICE, BILLING_PRO_SEAT_PRICE,
BILLING_TRAFFIC_PRICE,
} from './BillingPlan'; } from './BillingPlan';
import { useOverageCost } from './useOverageCost'; import { useOverageCost } from './useOverageCost';
@ -41,12 +41,17 @@ export const BillingDetailsPro = ({
const eligibleUsers = users.filter((user) => user.email); 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 seats = BILLING_PRO_DEFAULT_INCLUDED_SEATS;
const freeAssigned = Math.min(eligibleUsers.length, seats); const freeAssigned = Math.min(eligibleUsers.length, seats);
const paidAssigned = eligibleUsers.length - freeAssigned; const paidAssigned = eligibleUsers.length - freeAssigned;
const paidAssignedPrice = BILLING_PRO_USER_PRICE * paidAssigned; const paidAssignedPrice = seatPrice * paidAssigned;
const includedTraffic = BILLING_INCLUDED_REQUESTS; const includedTraffic = BILLING_INCLUDED_REQUESTS;
const overageCost = useOverageCost(includedTraffic); const overageCost = useOverageCost(includedTraffic);
@ -96,7 +101,7 @@ export const BillingDetailsPro = ({
</GridColLink> </GridColLink>
</Typography> </Typography>
<StyledInfoLabel> <StyledInfoLabel>
${BILLING_PRO_USER_PRICE}/month per paid member ${seatPrice}/month per paid member
</StyledInfoLabel> </StyledInfoLabel>
</GridCol> </GridCol>
<GridCol> <GridCol>
@ -123,8 +128,8 @@ export const BillingDetailsPro = ({
</GridColLink> </GridColLink>
</Typography> </Typography>
<StyledInfoLabel> <StyledInfoLabel>
${BILLING_TRAFFIC_BUNDLE_PRICE} per 1 ${trafficPrice} per 1 million started above
million started above included data included data
</StyledInfoLabel> </StyledInfoLabel>
</GridCol> </GridCol>
<GridCol> <GridCol>

View File

@ -1,6 +1,6 @@
import { Alert, Grid, styled } from '@mui/material'; import { Alert, Grid, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; 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 useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { trialHasExpired, isTrialInstance } from 'utils/instanceTrial'; import { trialHasExpired, isTrialInstance } from 'utils/instanceTrial';
import { GridRow } from 'component/common/GridRow/GridRow'; import { GridRow } from 'component/common/GridRow/GridRow';
@ -9,16 +9,14 @@ import { Badge } from 'component/common/Badge/Badge';
import { BillingDetails } from './BillingDetails'; import { BillingDetails } from './BillingDetails';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus'; import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
export const BILLING_PLAN_PRICES: Record<string, number> = { export const BILLING_PRO_BASE_PRICE = 80;
[InstancePlan.PRO]: 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_PAYG_DEFAULT_MINIMUM_SEATS = 5;
export const BILLING_PRO_USER_PRICE = 15;
export const BILLING_PRO_DEFAULT_INCLUDED_SEATS = 5; export const BILLING_PRO_DEFAULT_INCLUDED_SEATS = 5;
export const BILLING_INCLUDED_REQUESTS = 53_000_000; export const BILLING_INCLUDED_REQUESTS = 53_000_000;
export const BILLING_TRAFFIC_BUNDLE_PRICE = 5;
const StyledPlanBox = styled('aside')(({ theme }) => ({ const StyledPlanBox = styled('aside')(({ theme }) => ({
padding: theme.spacing(2.5), padding: theme.spacing(2.5),
@ -77,7 +75,8 @@ export const BillingPlan = () => {
); );
const expired = trialHasExpired(instanceStatus); 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 plan = `${instanceStatus.plan}${isPAYG ? ' Pay-as-You-Go' : ''}`;
const inactive = instanceStatus.state !== InstanceState.ACTIVE; const inactive = instanceStatus.state !== InstanceState.ACTIVE;
@ -131,10 +130,10 @@ export const BillingPlan = () => {
</GridCol> </GridCol>
<GridCol> <GridCol>
<ConditionallyRender <ConditionallyRender
condition={planPrice > 0} condition={!isPAYG}
show={ show={
<StyledPriceSpan> <StyledPriceSpan>
${planPrice.toFixed(2)} ${baseProPrice.toFixed(2)}
</StyledPriceSpan> </StyledPriceSpan>
} }
/> />

View File

@ -5,7 +5,8 @@ import {
calculateOverageCost, calculateOverageCost,
calculateTotalUsage, calculateTotalUsage,
} from 'utils/traffic-calculations'; } 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) => { export const useOverageCost = (includedTraffic: number) => {
if (!includedTraffic) { if (!includedTraffic) {
@ -17,6 +18,12 @@ export const useOverageCost = (includedTraffic: number) => {
const from = formatDate(startOfMonth(now)); const from = formatDate(startOfMonth(now));
const to = formatDate(endOfMonth(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 { result } = useTrafficSearch('daily', { from, to });
const overageCost = useMemo(() => { const overageCost = useMemo(() => {
if (result.state !== 'success') { if (result.state !== 'success') {
@ -24,12 +31,8 @@ export const useOverageCost = (includedTraffic: number) => {
} }
const totalUsage = calculateTotalUsage(result.data); const totalUsage = calculateTotalUsage(result.data);
return calculateOverageCost( return calculateOverageCost(totalUsage, includedTraffic, trafficPrice);
totalUsage, }, [includedTraffic, JSON.stringify(result), trafficPrice]);
includedTraffic,
BILLING_TRAFFIC_BUNDLE_PRICE,
);
}, [includedTraffic, JSON.stringify(result)]);
return overageCost; return overageCost;
}; };

View File

@ -12,10 +12,11 @@ import {
calculateOverageCost, calculateOverageCost,
calculateTotalUsage, calculateTotalUsage,
} from 'utils/traffic-calculations'; } 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 { averageTrafficPreviousMonths } from '../average-traffic-previous-months';
import { useConnectionsConsumption } from 'hooks/api/getters/useConnectionsConsumption/useConnectionsConsumption'; import { useConnectionsConsumption } from 'hooks/api/getters/useConnectionsConsumption/useConnectionsConsumption';
import { useRequestsConsumption } from 'hooks/api/getters/useRequestsConsumption/useRequestsConsumption'; import { useRequestsConsumption } from 'hooks/api/getters/useRequestsConsumption/useRequestsConsumption';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
export const useTrafficStats = ( export const useTrafficStats = (
includedTraffic: number, includedTraffic: number,
@ -26,6 +27,12 @@ export const useTrafficStats = (
chartDataSelection.grouping, chartDataSelection.grouping,
toDateRange(chartDataSelection, currentDate), 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(() => { const results = useMemo(() => {
if (result.state !== 'success') { if (result.state !== 'success') {
return { return {
@ -43,14 +50,14 @@ export const useTrafficStats = (
const overageCost = calculateOverageCost( const overageCost = calculateOverageCost(
usageTotal, usageTotal,
includedTraffic, includedTraffic,
BILLING_TRAFFIC_BUNDLE_PRICE, trafficPrice,
); );
const estimatedMonthlyCost = calculateEstimatedMonthlyCost( const estimatedMonthlyCost = calculateEstimatedMonthlyCost(
traffic.apiData, traffic.apiData,
includedTraffic, includedTraffic,
currentDate, currentDate,
BILLING_TRAFFIC_BUNDLE_PRICE, trafficPrice,
); );
const requestSummaryUsage = const requestSummaryUsage =
@ -69,6 +76,7 @@ export const useTrafficStats = (
JSON.stringify(result), JSON.stringify(result),
includedTraffic, includedTraffic,
JSON.stringify(chartDataSelection), JSON.stringify(chartDataSelection),
trafficPrice,
]); ]);
return results; return results;

View File

@ -2,11 +2,15 @@ import type { VFC } from 'react';
import { Alert } from '@mui/material'; import { Alert } from '@mui/material';
import { useUsersPlan } from 'hooks/useUsersPlan'; import { useUsersPlan } from 'hooks/useUsersPlan';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; 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 = () => { export const SeatCostWarning: VFC = () => {
const { users } = useUsers(); const { users } = useUsers();
const { isBillingUsers, seats, planUsers } = useUsersPlan(users); const { isBillingUsers, seats, planUsers } = useUsersPlan(users);
const { instanceStatus } = useInstanceStatus();
const seatPrice =
instanceStatus?.prices?.pro?.seat ?? BILLING_PRO_SEAT_PRICE;
if (!isBillingUsers || planUsers.length < seats) { if (!isBillingUsers || planUsers.length < seats) {
return null; return null;
@ -20,9 +24,8 @@ export const SeatCostWarning: VFC = () => {
<p> <p>
<strong>Heads up!</strong> You are exceeding your allocated free <strong>Heads up!</strong> You are exceeding your allocated free
members included in your plan ({planUsers.length} of {seats}). members included in your plan ({planUsers.length} of {seats}).
Creating this user will add{' '} Creating this user will add <strong>${seatPrice}/month</strong>{' '}
<strong>${BILLING_PRO_USER_PRICE}/month</strong> to your to your invoice, starting with your next payment.
invoice, starting with your next payment.
</p> </p>
</Alert> </Alert>
); );

View File

@ -6,11 +6,11 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { import {
BILLING_PAYG_DEFAULT_MINIMUM_SEATS, BILLING_PAYG_DEFAULT_MINIMUM_SEATS,
BILLING_PAYG_USER_PRICE, BILLING_PAYG_SEAT_PRICE,
BILLING_PLAN_PRICES, BILLING_PRO_BASE_PRICE,
BILLING_PRO_DEFAULT_INCLUDED_SEATS, BILLING_PRO_DEFAULT_INCLUDED_SEATS,
} from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan'; } 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 }) => ({ const StyledDemoDialog = styled(DemoDialog)(({ theme }) => ({
'& .MuiDialog-paper': { '& .MuiDialog-paper': {
@ -91,6 +91,12 @@ interface IDemoDialogPlansProps {
export const DemoDialogPlans = ({ open, onClose }: IDemoDialogPlansProps) => { export const DemoDialogPlans = ({ open, onClose }: IDemoDialogPlansProps) => {
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
const isEnterprisePaygEnabled = useUiFlag('enterprise-payg'); 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 ( return (
<StyledDemoDialog open={open} onClose={onClose}> <StyledDemoDialog open={open} onClose={onClose}>
@ -139,7 +145,7 @@ export const DemoDialogPlans = ({ open, onClose }: IDemoDialogPlansProps) => {
</Typography> </Typography>
<div> <div>
<Typography variant='h6' fontWeight='normal'> <Typography variant='h6' fontWeight='normal'>
${BILLING_PAYG_USER_PRICE} per user/month ${paygSeatPrice} per user/month
</Typography> </Typography>
<Typography variant='body2'> <Typography variant='body2'>
{BILLING_PAYG_DEFAULT_MINIMUM_SEATS} users {BILLING_PAYG_DEFAULT_MINIMUM_SEATS} users
@ -174,7 +180,7 @@ export const DemoDialogPlans = ({ open, onClose }: IDemoDialogPlansProps) => {
</Typography> </Typography>
<div> <div>
<Typography variant='h6' fontWeight='normal'> <Typography variant='h6' fontWeight='normal'>
${BILLING_PLAN_PRICES[InstancePlan.PRO]}/month ${proBasePrice}/month
</Typography> </Typography>
<Typography variant='body2'> <Typography variant='body2'>
includes {BILLING_PRO_DEFAULT_INCLUDED_SEATS}{' '} includes {BILLING_PRO_DEFAULT_INCLUDED_SEATS}{' '}

View File

@ -1,5 +1,5 @@
import { type DragEventHandler, type RefObject, useRef } from 'react'; 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 { IFeatureEnvironment } from 'interfaces/featureToggle';
import type { IFeatureStrategy } from 'interfaces/strategy'; import type { IFeatureStrategy } from 'interfaces/strategy';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
@ -34,8 +34,6 @@ type ProjectEnvironmentStrategyDraggableItemProps = {
onDragEnd?: () => void; onDragEnd?: () => void;
}; };
const onDragNoOp = () => () => {};
export const ProjectEnvironmentStrategyDraggableItem = ({ export const ProjectEnvironmentStrategyDraggableItem = ({
className, className,
strategy, strategy,
@ -43,9 +41,9 @@ export const ProjectEnvironmentStrategyDraggableItem = ({
environmentName, environmentName,
otherEnvironments, otherEnvironments,
isDragging, isDragging,
onDragStartRef = onDragNoOp, onDragStartRef,
onDragOver = onDragNoOp, onDragOver,
onDragEnd = onDragNoOp, onDragEnd,
}: ProjectEnvironmentStrategyDraggableItemProps) => { }: ProjectEnvironmentStrategyDraggableItemProps) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
@ -75,67 +73,59 @@ export const ProjectEnvironmentStrategyDraggableItem = ({
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
return ( return (
<Box <StrategyDraggableItem
className={className} strategy={strategy}
key={strategy.id} onDragEnd={onDragEnd}
ref={ref} onDragStartRef={onDragStartRef}
onDragOver={onDragOver(ref, index)} onDragOver={onDragOver}
sx={{ opacity: isDragging ? '0.5' : '1' }} index={index}
> isDragging={isDragging}
<StrategyDraggableItem headerItemsRight={
strategy={strategy} <>
onDragEnd={onDragEnd} {draftChange && !isSmallScreen ? (
onDragStartRef={onDragStartRef} <ChangeRequestDraftStatusBadge
onDragOver={onDragOver} sx={{ mr: 1.5 }}
index={index} changeAction={draftChange.change.action}
isDragging={isDragging} />
headerItemsRight={ ) : null}
<>
{draftChange && !isSmallScreen ? (
<ChangeRequestDraftStatusBadge
sx={{ mr: 1.5 }}
changeAction={draftChange.change.action}
/>
) : null}
{scheduledChanges && {scheduledChanges &&
scheduledChanges.length > 0 && scheduledChanges.length > 0 &&
!isSmallScreen ? ( !isSmallScreen ? (
<ChangesScheduledBadge <ChangesScheduledBadge
scheduledChangeRequestIds={( scheduledChangeRequestIds={(
scheduledChanges ?? [] scheduledChanges ?? []
).map((scheduledChange) => scheduledChange.id)} ).map((scheduledChange) => scheduledChange.id)}
/> />
) : null} ) : null}
{otherEnvironments && otherEnvironments?.length > 0 ? ( {otherEnvironments && otherEnvironments?.length > 0 ? (
<CopyStrategyIconMenu <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}
environmentId={environmentName} environmentId={environmentName}
environments={otherEnvironments as string[]}
strategy={strategy} strategy={strategy}
/> />
</> ) : null}
} <PermissionIconButton
/> permission={UPDATE_FEATURE_STRATEGY}
</Box> 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}
/>
</>
}
/>
); );
}; };

View File

@ -8,8 +8,6 @@ import { Box } from '@mui/material';
import type { IFeatureStrategy } from 'interfaces/strategy'; import type { IFeatureStrategy } from 'interfaces/strategy';
import { StrategyItem } from './StrategyItem/StrategyItem'; import { StrategyItem } from './StrategyItem/StrategyItem';
const onDragNoOp = () => () => {};
type StrategyDraggableItemProps = { type StrategyDraggableItemProps = {
headerItemsRight: ReactNode; headerItemsRight: ReactNode;
strategy: IFeatureStrategy; strategy: IFeatureStrategy;
@ -30,9 +28,9 @@ export const StrategyDraggableItem = ({
strategy, strategy,
index, index,
isDragging, isDragging,
onDragStartRef = onDragNoOp, onDragStartRef,
onDragOver = onDragNoOp, onDragOver,
onDragEnd = onDragNoOp, onDragEnd,
headerItemsRight, headerItemsRight,
}: StrategyDraggableItemProps) => { }: StrategyDraggableItemProps) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -41,13 +39,13 @@ export const StrategyDraggableItem = ({
<Box <Box
key={strategy.id} key={strategy.id}
ref={ref} ref={ref}
onDragOver={onDragOver(ref, index)} onDragOver={onDragOver?.(ref, index)}
sx={{ opacity: isDragging ? '0.5' : '1' }} sx={{ opacity: isDragging ? '0.5' : '1' }}
> >
<StrategyItem <StrategyItem
headerItemsRight={headerItemsRight} headerItemsRight={headerItemsRight}
strategy={strategy} strategy={strategy}
onDragStart={onDragStartRef(ref, index)} onDragStart={onDragStartRef?.(ref, index)}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
/> />
</Box> </Box>

View File

@ -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 { export interface IInstanceStatus {
plan: InstancePlan; plan: InstancePlan;
trialExpiry?: string; trialExpiry?: string;
@ -8,6 +22,8 @@ export interface IInstanceStatus {
seats?: number; seats?: number;
minSeats?: number; minSeats?: number;
isCustomBilling?: boolean; isCustomBilling?: boolean;
prices?: InstancePrices;
billing?: InstanceBilling;
} }
export enum InstanceState { export enum InstanceState {

View File

@ -457,8 +457,9 @@ export class AccessService {
async getRootRoleForUser(userId: number): Promise<IRole> { async getRootRoleForUser(userId: number): Promise<IRole> {
const rootRole = await this.store.getRootRoleForUser(userId); const rootRole = await this.store.getRootRoleForUser(userId);
if (!rootRole) { if (!rootRole) {
const defaultRole = await this.getPredefinedRole(RoleName.VIEWER); // this should never happen, but before breaking we want to know if it does.
return defaultRole; this.logger.warn(`Could not find root role for user=${userId}.`);
return this.getPredefinedRole(RoleName.VIEWER);
} }
return rootRole; return rootRole;
} }

View 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();
};

View File

@ -235,7 +235,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 7.26.2
resolution: "@babel/code-frame@npm:7.26.2" resolution: "@babel/code-frame@npm:7.26.2"
dependencies: dependencies:
@ -494,12 +494,12 @@ __metadata:
linkType: hard linkType: hard
"@babel/helpers@npm:^7.26.0": "@babel/helpers@npm:^7.26.0":
version: 7.26.0 version: 7.26.10
resolution: "@babel/helpers@npm:7.26.0" resolution: "@babel/helpers@npm:7.26.10"
dependencies: dependencies:
"@babel/template": "npm:^7.25.9" "@babel/template": "npm:^7.26.9"
"@babel/types": "npm:^7.26.0" "@babel/types": "npm:^7.26.10"
checksum: 10c0/343333cced6946fe46617690a1d0789346960910225ce359021a88a60a65bc0d791f0c5d240c0ed46cf8cc63b5fd7df52734ff14e43b9c32feae2b61b1647097 checksum: 10c0/f99e1836bcffce96db43158518bb4a24cf266820021f6461092a776cba2dc01d9fc8b1b90979d7643c5c2ab7facc438149064463a52dd528b21c6ab32509784f
languageName: node languageName: node
linkType: hard linkType: hard
@ -514,6 +514,17 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.9":
version: 7.25.9 version: 7.25.9
resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9" resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9"
@ -1482,6 +1493,17 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@babel/traverse@npm:^7.25.9":
version: 7.25.9 version: 7.25.9
resolution: "@babel/traverse@npm:7.25.9" resolution: "@babel/traverse@npm:7.25.9"
@ -1507,6 +1529,16 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@braintree/sanitize-url@npm:^7.0.1":
version: 7.1.1 version: 7.1.1
resolution: "@braintree/sanitize-url@npm:7.1.1" resolution: "@braintree/sanitize-url@npm:7.1.1"