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() defaultUsage := c.defaultMaxUsage.Int64()
projectLimits.Usage = &defaultUsage 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 return projectLimits, nil
} }

View File

@ -218,6 +218,18 @@ func (usage *Service) GetProjectBandwidthTotals(ctx context.Context, projectID u
return total, ErrProjectUsage.Wrap(err) 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. // 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) { 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) 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) 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. // UpdateProjectLimits sets new value for project's bandwidth and storage limit.
// TODO remove because it's not used. // TODO remove because it's not used.
func (usage *Service) UpdateProjectLimits(ctx context.Context, projectID uuid.UUID, limit memory.Size) (err error) { 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"` BandwidthUsed int64 `json:"bandwidthUsed"`
ObjectCount int64 `json:"objectCount"` ObjectCount int64 `json:"objectCount"`
SegmentCount int64 `json:"segmentCount"` 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. // 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, BandwidthUsed: prUsageLimits.BandwidthUsed,
ObjectCount: prObjectsSegments.ObjectCount, ObjectCount: prObjectsSegments.ObjectCount,
SegmentCount: prObjectsSegments.SegmentCount, SegmentCount: prObjectsSegments.SegmentCount,
SegmentLimit: prUsageLimits.SegmentLimit,
SegmentUsed: prUsageLimits.SegmentUsed,
}, nil }, nil
} }
@ -2695,6 +2697,10 @@ func (s *Service) getProjectUsageLimits(ctx context.Context, projectID uuid.UUID
if err != nil { if err != nil {
return nil, err return nil, err
} }
segmentLimit, err := s.projectUsage.GetProjectSegmentLimit(ctx, projectID)
if err != nil {
return nil, err
}
storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, projectID) storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, projectID)
if err != nil { if err != nil {
@ -2704,12 +2710,18 @@ func (s *Service) getProjectUsageLimits(ctx context.Context, projectID uuid.UUID
if err != nil { if err != nil {
return nil, err return nil, err
} }
segmentUsed, err := s.projectUsage.GetProjectSegmentTotals(ctx, projectID)
if err != nil {
return nil, err
}
return &ProjectUsageLimits{ return &ProjectUsageLimits{
StorageLimit: storageLimit.Int64(), StorageLimit: storageLimit.Int64(),
BandwidthLimit: bandwidthLimit.Int64(), BandwidthLimit: bandwidthLimit.Int64(),
StorageUsed: storageUsed, StorageUsed: storageUsed,
BandwidthUsed: bandwidthUsed, BandwidthUsed: bandwidthUsed,
SegmentLimit: segmentLimit.Int64(),
SegmentUsed: segmentUsed,
}, nil }, nil
} }

View File

@ -153,9 +153,11 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
limits.bandwidthLimit, limits.bandwidthLimit,
limits.bandwidthUsed, limits.bandwidthUsed,
limits.storageLimit, limits.storageLimit,
limits.storageUsed, limits.storageUsed,
limits.objectCount, limits.objectCount,
limits.segmentCount, limits.segmentCount,
limits.segmentLimit,
limits.segmentUsed,
); );
} }

View File

@ -7,7 +7,7 @@
<div class="modal"> <div class="modal">
<Icon class="modal__icon" :class="{ warning: severity === 'warning', critical: severity === 'critical' }" /> <Icon class="modal__icon" :class="{ warning: severity === 'warning', critical: severity === 'critical' }" />
<h1 class="modal__title">{{ title }}</h1> <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"> <div class="modal__buttons">
<VButton <VButton
label="Cancel" label="Cancel"
@ -25,7 +25,7 @@
font-size="13px" font-size="13px"
class="modal__buttons__button upgrade" class="modal__buttons__button upgrade"
:on-press="onUpgrade" :on-press="onUpgrade"
:is-green-white="true" :is-white-blue="true"
/> />
</div> </div>
</div> </div>
@ -54,6 +54,8 @@ export default class LimitWarningModal extends Vue {
private readonly severity: 'warning' | 'critical'; private readonly severity: 'warning' | 'critical';
@Prop({ default: '' }) @Prop({ default: '' })
private readonly title: string; private readonly title: string;
@Prop({ default: '' })
private readonly limitType: string;
@Prop({ default: () => {} }) @Prop({ default: () => {} })
private readonly onUpgrade: () => Promise<void>; private readonly onUpgrade: () => Promise<void>;
@Prop({ default: () => {} }) @Prop({ default: () => {} })

View File

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