satellite/console: add notifications for free account limits

Add notifications for free account limits for segment usage
and update to follow the figma designs.

Issue: https://github.com/storj/storj/issues/5482

Change-Id: I8a2fe38d609d53e09bf5074484cedc343223bffd
This commit is contained in:
Lizzy Thomson 2023-02-15 10:54:22 -07:00
parent d48f745668
commit 155b927c5e
8 changed files with 142 additions and 43 deletions

View File

@ -78,6 +78,14 @@ func (c *ProjectLimitCache) GetProjectLimits(ctx context.Context, projectID uuid
defaultUsage := c.defaultMaxUsage.Int64()
projectLimits.Usage = &defaultUsage
}
if projectLimits.Segments == nil {
defaultSegments := c.defaultMaxSegments
projectLimits.Segments = &defaultSegments
}
if projectLimits.Segments == nil {
defaultSegments := c.defaultMaxSegments
projectLimits.Segments = &defaultSegments
}
return projectLimits, nil
}

View File

@ -218,6 +218,18 @@ func (usage *Service) GetProjectBandwidthTotals(ctx context.Context, projectID u
return total, ErrProjectUsage.Wrap(err)
}
// GetProjectSegmentTotals returns total amount of allocated segments used for past 30 days.
func (usage *Service) GetProjectSegmentTotals(ctx context.Context, projectID uuid.UUID) (total int64, err error) {
defer mon.Task()(&ctx, projectID)(&err)
total, err = usage.liveAccounting.GetProjectSegmentUsage(ctx, projectID)
if ErrKeyNotFound.Has(err) {
return 0, nil
}
return total, ErrProjectUsage.Wrap(err)
}
// GetProjectBandwidth returns project allocated bandwidth for the specified year, month and day.
func (usage *Service) GetProjectBandwidth(ctx context.Context, projectID uuid.UUID, year int, month time.Month, day int) (_ int64, err error) {
defer mon.Task()(&ctx, projectID)(&err)
@ -243,6 +255,17 @@ func (usage *Service) GetProjectBandwidthLimit(ctx context.Context, projectID uu
return usage.projectLimitCache.GetProjectBandwidthLimit(ctx, projectID)
}
// GetProjectSegmentLimit returns current project segment limit.
func (usage *Service) GetProjectSegmentLimit(ctx context.Context, projectID uuid.UUID) (_ memory.Size, err error) {
defer mon.Task()(&ctx, projectID)(&err)
limits, err := usage.projectLimitCache.GetProjectLimits(ctx, projectID)
if err != nil {
return 0, ErrProjectUsage.Wrap(err)
}
return memory.Size(*limits.Usage), nil
}
// UpdateProjectLimits sets new value for project's bandwidth and storage limit.
// TODO remove because it's not used.
func (usage *Service) UpdateProjectLimits(ctx context.Context, projectID uuid.UUID, limit memory.Size) (err error) {

View File

@ -11,6 +11,10 @@ type ProjectUsageLimits struct {
BandwidthUsed int64 `json:"bandwidthUsed"`
ObjectCount int64 `json:"objectCount"`
SegmentCount int64 `json:"segmentCount"`
RateLimit int64 `json:"rateLimit"`
SegmentLimit int64 `json:"segmentLimit"`
RateUsed int64 `json:"rateUsed"`
SegmentUsed int64 `json:"segmentUsed"`
}
// UsageLimits represents storage, bandwidth, and segment limits imposed on an entity.

View File

@ -2642,6 +2642,8 @@ func (s *Service) GetProjectUsageLimits(ctx context.Context, projectID uuid.UUID
BandwidthUsed: prUsageLimits.BandwidthUsed,
ObjectCount: prObjectsSegments.ObjectCount,
SegmentCount: prObjectsSegments.SegmentCount,
SegmentLimit: prUsageLimits.SegmentLimit,
SegmentUsed: prUsageLimits.SegmentUsed,
}, nil
}
@ -2695,6 +2697,10 @@ func (s *Service) getProjectUsageLimits(ctx context.Context, projectID uuid.UUID
if err != nil {
return nil, err
}
segmentLimit, err := s.projectUsage.GetProjectSegmentLimit(ctx, projectID)
if err != nil {
return nil, err
}
storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, projectID)
if err != nil {
@ -2704,12 +2710,18 @@ func (s *Service) getProjectUsageLimits(ctx context.Context, projectID uuid.UUID
if err != nil {
return nil, err
}
segmentUsed, err := s.projectUsage.GetProjectSegmentTotals(ctx, projectID)
if err != nil {
return nil, err
}
return &ProjectUsageLimits{
StorageLimit: storageLimit.Int64(),
BandwidthLimit: bandwidthLimit.Int64(),
StorageUsed: storageUsed,
BandwidthUsed: bandwidthUsed,
SegmentLimit: segmentLimit.Int64(),
SegmentUsed: segmentUsed,
}, nil
}

View File

@ -156,6 +156,8 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
limits.storageUsed,
limits.objectCount,
limits.segmentCount,
limits.segmentLimit,
limits.segmentUsed,
);
}

View File

@ -7,7 +7,7 @@
<div class="modal">
<Icon class="modal__icon" :class="{ warning: severity === 'warning', critical: severity === 'critical' }" />
<h1 class="modal__title">{{ title }}</h1>
<p class="modal__info">To get more storage and bandwidth, upgrade to a Pro Account. You will still get 150GB free storage and bandwidth per month, and only pay what you use beyond that.</p>
<p class="modal__info">To get more {{ limitType }} limit, upgrade to a Pro Account. You will still get 150GB free storage and bandwidth per month, and only pay what you use beyond that.</p>
<div class="modal__buttons">
<VButton
label="Cancel"
@ -25,7 +25,7 @@
font-size="13px"
class="modal__buttons__button upgrade"
:on-press="onUpgrade"
:is-green-white="true"
:is-white-blue="true"
/>
</div>
</div>
@ -54,6 +54,8 @@ export default class LimitWarningModal extends Vue {
private readonly severity: 'warning' | 'critical';
@Prop({ default: '' })
private readonly title: string;
@Prop({ default: '' })
private readonly limitType: string;
@Prop({ default: () => {} })
private readonly onUpgrade: () => Promise<void>;
@Prop({ default: () => {} })

View File

@ -153,6 +153,8 @@ export class ProjectLimits {
public storageUsed: number = 0,
public objectCount: number = 0,
public segmentCount: number = 0,
public segmentLimit: number = 0,
public segmentUsed: number = 0,
) {}
}

View File

@ -38,13 +38,24 @@
</v-banner>
<v-banner
v-if="limitState.isShown && !isLoading && dashboardContent"
:severity="limitState.severity"
:on-click="() => setIsLimitModalShown(true)"
v-if="limitState.hundredIsShown && !isLoading && dashboardContent"
severity="critical"
:on-click="() => setIsHundredLimitModalShown(true)"
:dashboard-ref="dashboardContent"
>
<template #text>
<p class="medium">{{ limitState.label }}</p>
<p class="medium">{{ limitState.hundredLabel }}</p>
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
</template>
</v-banner>
<v-banner
v-if="limitState.eightyIsShown && !isLoading && dashboardContent"
severity="warning"
:on-click="() => setIsEightyLimitModalShown(true)"
:dashboard-ref="dashboardContent"
>
<template #text>
<p class="medium">{{ limitState.eightyLabel }}</p>
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
</template>
</v-banner>
@ -66,10 +77,19 @@
:initial-seconds="inactivityModalTime / 1000"
/>
<limit-warning-modal
v-if="isLimitModalShown && !isLoading"
:severity="limitState.severity"
:on-close="() => setIsLimitModalShown(false)"
:title="limitState.modalTitle"
v-if="isHundredLimitModalShown && !isLoading"
severity="critical"
:on-close="() => setIsHundredLimitModalShown(false)"
:title="limitState.hundredModalTitle"
:limit-type="limitState.hundredModalLimitType"
:on-upgrade="togglePMModal"
/>
<limit-warning-modal
v-if="isEightyLimitModalShown && !isLoading"
severity="warning"
:on-close="() => setIsEightyLimitModalShown(false)"
:title="limitState.eightyModalTitle"
:limit-type="limitState.eightyModalLimitType"
:on-upgrade="togglePMModal"
/>
<AllModals />
@ -147,7 +167,8 @@ const debugTimerId = ref<ReturnType<typeof setTimeout> | null>();
const inactivityModalShown = ref<boolean>(false);
const isSessionActive = ref<boolean>(false);
const isSessionRefreshing = ref<boolean>(false);
const isLimitModalShown = ref<boolean>(false);
const isHundredLimitModalShown = ref<boolean>(false);
const isEightyLimitModalShown = ref<boolean>(false);
const debugTimerText = ref<string>('');
const dashboardContent = ref<HTMLElement | null>(null);
@ -162,42 +183,62 @@ const isAccountFrozen = computed((): boolean => {
/**
* Returns all needed information for limit banner and modal when bandwidth or storage close to limits.
*/
const limitState = computed((): { isShown: boolean, severity?: 'info' | 'warning' | 'critical', label?: string, modalTitle?: string } => {
if (store.state.usersModule.user.paidTier || isAccountFrozen.value) return { isShown: false };
const limitState = computed((): { eightyIsShown: boolean, hundredIsShown: boolean, eightyLabel?: string, eightyModalTitle?: string, eightyModalLimitType?: string, hundredLabel?: string, hundredModalTitle?: string, hundredModalLimitType?: string } => {
if (store.state.usersModule.user.paidTier || isAccountFrozen.value) return { eightyIsShown: false, hundredIsShown: false };
const EIGHTY_PERCENT = 80;
const HUNDRED_PERCENT = 100;
const result:
{
eightyIsShown: boolean,
hundredIsShown: boolean,
eightyLabel?: string,
eightyModalTitle?: string,
eightyModalLimitType?: string,
hundredLabel?: string,
hundredModalTitle?: string,
hundredModalLimitType?: string
} = { eightyIsShown: false, hundredIsShown: false, eightyLabel: '', hundredLabel: '' };
const result: { isShown: boolean, severity?: 'info' | 'warning' | 'critical', label?: string, modalTitle?: string } = { isShown: false, label: '' };
const { currentLimits } = store.state.projectsModule;
const bandwidthUsedPercent = Math.round(currentLimits.bandwidthUsed * HUNDRED_PERCENT / currentLimits.bandwidthLimit);
const storageUsedPercent = Math.round(currentLimits.storageUsed * HUNDRED_PERCENT / currentLimits.storageLimit);
const limitTypeArr = [
{ name: 'bandwidth', usedPercent: Math.round(currentLimits.bandwidthUsed * 100 / currentLimits.bandwidthLimit) },
{ name: 'storage', usedPercent: Math.round(currentLimits.storageUsed * 100 / currentLimits.storageLimit) },
{ name: 'segment', usedPercent: Math.round(currentLimits.segmentUsed * 100 / currentLimits.segmentLimit) },
];
const isLimitHigh = bandwidthUsedPercent >= EIGHTY_PERCENT || storageUsedPercent >= EIGHTY_PERCENT;
const isLimitCritical = bandwidthUsedPercent === HUNDRED_PERCENT || storageUsedPercent === HUNDRED_PERCENT;
const hundredPercent = [] as string[];
const eightyPercent = [] as string[];
if (isLimitHigh) {
result.isShown = true;
result.severity = isLimitCritical ? 'critical' : 'warning';
if (bandwidthUsedPercent > storageUsedPercent) {
result.label = bandwidthUsedPercent === HUNDRED_PERCENT ?
'URGENT: Youve reached the bandwidth limit for your project. Avoid any service interruptions.'
: `Youve used ${bandwidthUsedPercent}% of your bandwidth limit. Avoid interrupting your usage by upgrading account.`;
result.modalTitle = `Youve used ${bandwidthUsedPercent}% of your free account bandwidth`;
} else if (bandwidthUsedPercent < storageUsedPercent) {
result.label = storageUsedPercent === HUNDRED_PERCENT ?
'URGENT: Youve reached the storage limit for your project. Avoid any service interruptions.'
: `Youve used ${storageUsedPercent}% of your storage limit. Avoid interrupting your usage by upgrading account.`;
result.modalTitle = `Youve used ${storageUsedPercent}% of your free account storage`;
limitTypeArr.forEach((limitType) => {
if (limitType.usedPercent >= 80) {
if (limitType.usedPercent >= 100) {
hundredPercent.push(limitType.name);
} else {
result.label = storageUsedPercent === HUNDRED_PERCENT && bandwidthUsedPercent === HUNDRED_PERCENT ?
'URGENT: Youve reached the storage and bandwidth limits for your project. Avoid any service interruptions.'
: `Youve used ${storageUsedPercent}% of your storage and ${bandwidthUsedPercent}% of bandwidth limit. Avoid interrupting your usage by upgrading account.`;
result.modalTitle = `Youve used ${storageUsedPercent}% storage and ${bandwidthUsedPercent}% of your free account bandwidth`;
eightyPercent.push(limitType.name);
}
}
});
if (eightyPercent.length !== 0) {
result.eightyIsShown = true;
const eightyPercentString = eightyPercent.join(' and ');
result.eightyLabel = `You've used 80% of your ${eightyPercentString} limit. Avoid interrupting your usage by upgrading your account.`;
result.eightyModalTitle = `80% ${eightyPercentString} limit used`;
result.eightyModalLimitType = eightyPercentString;
}
if (hundredPercent.length !== 0) {
result.hundredIsShown = true;
const hundredPercentString = hundredPercent.join(' and ');
result.hundredLabel = `URGENT: Youve reached the ${hundredPercentString} limit for your project. Upgrade to avoid any service interruptions.`;
result.hundredModalTitle = `URGENT: Youve reached the ${hundredPercentString} limit for your project.`;
result.hundredModalLimitType = hundredPercentString;
}
return result;
});
@ -469,8 +510,12 @@ async function handleInactive(): Promise<void> {
}
}
function setIsLimitModalShown(value: boolean): void {
isLimitModalShown.value = value;
function setIsEightyLimitModalShown(value: boolean): void {
isEightyLimitModalShown.value = value;
}
function setIsHundredLimitModalShown(value: boolean): void {
isHundredLimitModalShown.value = value;
}
/**
@ -496,7 +541,8 @@ async function generateNewMFARecoveryCodes(): Promise<void> {
* Opens add payment method modal.
*/
function togglePMModal(): void {
isLimitModalShown.value = false;
isHundredLimitModalShown.value = false;
isEightyLimitModalShown.value = false;
store.commit(APP_STATE_MUTATIONS.UPDATE_ACTIVE_MODAL, MODALS.addPaymentMethod);
}