Skip to content

Commit 2e81826

Browse files
committed
feat(settings): prototype plans-in-settings section for local Plan & Credits
1 parent 5a00823 commit 2e81826

6 files changed

Lines changed: 470 additions & 14 deletions

File tree

src/components/dialog/content/setting/CreditsPanel.vue

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,52 @@
11
<template>
2-
<div class="credits-container flex h-full flex-col gap-4">
3-
<div>
2+
<div
3+
:class="
4+
cn(
5+
'credits-container flex flex-col gap-4',
6+
embedded ? 'shrink-0' : 'h-full'
7+
)
8+
"
9+
>
10+
<div v-if="!embedded">
411
<h2 class="mb-2 text-2xl font-bold">
512
{{ $t('credits.credits') }}
613
</h2>
714
<Divider />
815
</div>
916

10-
<CreditsTile />
17+
<div v-if="embedded" class="rounded-2xl border border-interface-stroke p-6">
18+
<div class="mb-4 flex items-center justify-between">
19+
<h3 class="m-0 text-base font-semibold">
20+
{{ $t('credits.workspaceCredits') }}
21+
</h3>
22+
<Button
23+
variant="secondary"
24+
size="lg"
25+
@click="handleCreditsHistoryClick"
26+
>
27+
{{ $t('subscription.manageBilling') }}
28+
</Button>
29+
</div>
30+
<CreditsTile class="max-w-md" />
31+
</div>
32+
<CreditsTile v-else />
1133

12-
<div class="flex items-center justify-between">
34+
<div v-if="!embedded" class="flex items-center justify-between">
1335
<h3 class="m-0">{{ $t('credits.activity') }}</h3>
1436
<Button variant="muted-textonly" @click="handleCreditsHistoryClick">
1537
<i class="pi pi-arrow-up-right" />
1638
{{ $t('credits.invoiceHistory') }}
1739
</Button>
1840
</div>
1941

20-
<UsageLogsTable ref="usageLogsTableRef" />
42+
<UsageLogsTable v-if="!embedded" ref="usageLogsTableRef" />
2143

22-
<div class="flex flex-row gap-2">
44+
<SubscriptionFooterLinks
45+
v-if="embedded && showFooterLinks"
46+
class="mt-auto"
47+
:show-invoice-history="false"
48+
/>
49+
<div v-else-if="!embedded" class="flex flex-row gap-2">
2350
<Button variant="muted-textonly" @click="handleFaqClick">
2451
<i class="pi pi-question-circle" />
2552
{{ $t('credits.faqs') }}
@@ -38,17 +65,24 @@
3865

3966
<script setup lang="ts">
4067
import Divider from 'primevue/divider'
68+
import { cn } from '@comfyorg/tailwind-utils'
4169
import { ref, watch } from 'vue'
4270
4371
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
4472
import Button from '@/components/ui/button/Button.vue'
73+
import SubscriptionFooterLinks from '@/platform/cloud/subscription/components/SubscriptionFooterLinks.vue'
4574
import { useAuthActions } from '@/composables/auth/useAuthActions'
4675
import { useExternalLink } from '@/composables/useExternalLink'
4776
import CreditsTile from '@/platform/cloud/subscription/components/CreditsTile.vue'
4877
import { useTelemetry } from '@/platform/telemetry'
4978
import { useAuthStore } from '@/stores/authStore'
5079
import { useCommandStore } from '@/stores/commandStore'
5180
81+
const { embedded = false, showFooterLinks = true } = defineProps<{
82+
embedded?: boolean
83+
showFooterLinks?: boolean
84+
}>()
85+
5286
const { buildDocsUrl, docsPaths } = useExternalLink()
5387
const authStore = useAuthStore()
5488
const authActions = useAuthActions()

src/locales/en/main.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2623,8 +2623,46 @@
26232623
},
26242624
"personalDataConsentRequired": "You must agree to the processing of your personal data."
26252625
},
2626+
"settingsPlans": {
2627+
"title": "Plans",
2628+
"subtitle": "Plans run on Comfy Cloud and their credits work on this install too.",
2629+
"personal": "Personal",
2630+
"teams": "Teams",
2631+
"billedYearlyToggle": "Billed yearly",
2632+
"billedYearlyTotal": "{total} billed yearly",
2633+
"billedMonthly": "Billed monthly",
2634+
"creditsAYear": "{credits} credits a year",
2635+
"creditsAMonth": "{credits} credits a month",
2636+
"perDollar": "{credits} per dollar",
2637+
"whatsIncluded": "What's included:",
2638+
"everythingInPlus": "Everything in {tier}, plus:",
2639+
"choosePlan": "Choose {tier}",
2640+
"teamTitle": "Team Plan",
2641+
"teamSubtitle": "Choose your own monthly credit subscription. Get a larger discount with a larger credit subscription.",
2642+
"creditsPerMonth": "{credits} credits per month",
2643+
"subscribeTeamYearly": "Subscribe to Team Yearly",
2644+
"subscribeTeam": "Subscribe to Team",
2645+
"detailsTitle": "Details",
2646+
"everythingInProPlus": "Everything in Pro, plus:",
2647+
"perkInviteMembers": "Invite team members",
2648+
"perkConcurrentRuns": "Members can run workflows concurrently",
2649+
"perkSharedCreditPool": "Shared credit pool for all members",
2650+
"perkRolePermissions": "Role-based permissions",
2651+
"comingSoonLabel": "Coming soon:",
2652+
"comingSharedWorkflows": "Shared workflows & assets",
2653+
"comingProjects": "Projects",
2654+
"enterpriseLabel": "Enterprise",
2655+
"enterpriseCopy": "Need more members? Looking for more flexibility or custom features?",
2656+
"contactUs": "Contact us",
2657+
"checkoutCaption": "Checkout happens right here: payment details handled securely by Stripe. Prefer no plan? Keep topping up, anytime.",
2658+
"benefitRuntimeStandard": "30 minute max workflow runtime",
2659+
"benefitAddCredits": "Add more credits anytime",
2660+
"benefitImportModels": "Import your own models",
2661+
"benefitRuntimePro": "Longer workflow runtime (up to 1 hr)"
2662+
},
26262663
"credits": {
26272664
"activity": "Activity",
2665+
"workspaceCredits": "Workspace credits",
26282666
"insufficient": {
26292667
"memberTitle": "This workspace is out of credits",
26302668
"memberDescription": "Your team has used all its credits. Your workspace admins need to add more credits to run workflows.",

src/platform/cloud/subscription/components/SubscriptionFooterLinks.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<template>
2-
<div
3-
class="flex items-center justify-between border-t border-interface-stroke pt-3"
4-
>
2+
<div class="flex items-center justify-between pt-3 pb-6">
53
<div class="flex gap-2">
64
<Button
75
variant="muted-textonly"

src/platform/workspace/components/dialogs/settings/PlanCreditsPanelContent.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,29 @@ import enMessages from '@/locales/en/main.json'
88

99
import PlanCreditsPanelContent from './PlanCreditsPanelContent.vue'
1010

11+
const mockDistribution = vi.hoisted(() => ({ cloud: true }))
12+
vi.mock('@/platform/distribution/types', () => ({
13+
get isCloud() {
14+
return mockDistribution.cloud
15+
}
16+
}))
17+
1118
const refreshSpy = vi.hoisted(() => vi.fn(() => Promise.resolve()))
1219

1320
const stubs = {
1421
SubscriptionPanelContentWorkspace: {
1522
template: '<div data-testid="credits-body" />'
1623
},
24+
CreditsPanel: {
25+
props: ['embedded'],
26+
template: '<div data-testid="legacy-credits-body" />'
27+
},
28+
SettingsPlansSection: {
29+
template: '<div data-testid="plans-section" />'
30+
},
31+
SubscriptionFooterLinks: {
32+
template: '<div data-testid="footer-links" />'
33+
},
1734
UsageLogsTable: {
1835
template: '<div data-testid="usage-logs" />',
1936
methods: {
@@ -22,7 +39,8 @@ const stubs = {
2239
}
2340
}
2441

25-
function renderPanel() {
42+
function renderPanel({ cloud = true } = {}) {
43+
mockDistribution.cloud = cloud
2644
const i18n = createI18n({
2745
legacy: false,
2846
locale: 'en',
@@ -40,7 +58,16 @@ describe('PlanCreditsPanelContent', () => {
4058
expect(screen.queryByTestId('usage-logs')).toBeNull()
4159
})
4260

61+
it('shows the embedded credits body and plans section on local', () => {
62+
renderPanel({ cloud: false })
63+
expect(screen.getByTestId('legacy-credits-body')).toBeTruthy()
64+
expect(screen.getByTestId('plans-section')).toBeTruthy()
65+
expect(screen.getByTestId('footer-links')).toBeTruthy()
66+
expect(screen.queryByTestId('credits-body')).toBeNull()
67+
})
68+
4369
it('loads the usage log on the Activity tab', async () => {
70+
refreshSpy.mockClear()
4471
renderPanel()
4572

4673
await userEvent.click(screen.getByRole('button', { name: 'Activity' }))
@@ -51,6 +78,7 @@ describe('PlanCreditsPanelContent', () => {
5178

5279
it('reports usage-log refresh failures', async () => {
5380
const error = new Error('refresh failed')
81+
refreshSpy.mockClear()
5482
refreshSpy.mockRejectedValueOnce(error)
5583
const consoleError = vi
5684
.spyOn(console, 'error')

src/platform/workspace/components/dialogs/settings/PlanCreditsPanelContent.vue

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,63 @@
1616
</div>
1717
</div>
1818

19-
<SubscriptionPanelContentWorkspace v-if="activeView === 'overview'" />
20-
<UsageLogsTable v-else ref="usageLogsTable" />
19+
<template v-if="activeView === 'overview'">
20+
<SubscriptionPanelContentWorkspace v-if="isCloud" />
21+
<div v-else class="flex min-h-0 flex-1 flex-col gap-8 overflow-y-auto">
22+
<CreditsPanel embedded :show-footer-links="false" />
23+
<SettingsPlansSection />
24+
<SubscriptionFooterLinks
25+
class="shrink-0"
26+
:show-invoice-history="false"
27+
/>
28+
</div>
29+
</template>
30+
<template v-else>
31+
<UsageLogsTable ref="usageLogsTable" />
32+
<div class="flex items-center pt-3 pb-6">
33+
<Button
34+
variant="muted-textonly"
35+
class="text-xs text-text-secondary"
36+
@click="openFullActivity"
37+
>
38+
{{ $t('workspacePanel.activity.fullActivity') }}
39+
<i class="pi pi-external-link text-xs text-text-secondary" />
40+
</Button>
41+
</div>
42+
</template>
2143
</div>
2244
</template>
2345

2446
<script setup lang="ts">
2547
import { computed, ref, useTemplateRef, watch } from 'vue'
2648
import { useI18n } from 'vue-i18n'
2749
50+
import CreditsPanel from '@/components/dialog/content/setting/CreditsPanel.vue'
2851
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
2952
import Button from '@/components/ui/button/Button.vue'
53+
import { getComfyPlatformBaseUrl } from '@/config/comfyApi'
54+
import SubscriptionFooterLinks from '@/platform/cloud/subscription/components/SubscriptionFooterLinks.vue'
55+
import { isCloud } from '@/platform/distribution/types'
3056
import SubscriptionPanelContentWorkspace from '@/platform/workspace/components/SubscriptionPanelContentWorkspace.vue'
57+
import SettingsPlansSection from '@/platform/workspace/components/dialogs/settings/SettingsPlansSection.vue'
3158
3259
type View = 'overview' | 'activity'
3360
3461
const { t } = useI18n()
3562
36-
// The owner-only Invoices tab is added by FE-1245, which owns the
37-
// next-invoice banner + Stripe portal link that fill it.
3863
const tabs = computed<{ key: View; label: string }[]>(() => [
3964
{ key: 'overview', label: t('workspacePanel.planCredits.tabs.overview') },
4065
{ key: 'activity', label: t('workspacePanel.planCredits.tabs.activity') }
4166
])
4267
4368
const activeView = ref<View>('overview')
4469
70+
const fullActivityUrl = `${getComfyPlatformBaseUrl()}/profile/usage`
71+
72+
function openFullActivity() {
73+
window.open(fullActivityUrl, '_blank', 'noopener,noreferrer')
74+
}
75+
4576
const usageLogsTable = useTemplateRef('usageLogsTable')
4677
watch(usageLogsTable, (table) => {
4778
table?.refresh().catch((error) => {

0 commit comments

Comments
 (0)