Skip to content

Commit d20b6b7

Browse files
committed
feat: badges, usage limit; fix(note): iframe security; improvement(s-modal): sizing
1 parent 7deeb50 commit d20b6b7

File tree

11 files changed

+498
-434
lines changed

11 files changed

+498
-434
lines changed

apps/sim/app/api/billing/route.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export async function GET(request: NextRequest) {
9393
const { searchParams } = new URL(request.url)
9494
const context = searchParams.get('context') || 'user'
9595
const contextId = searchParams.get('id')
96+
const includeOrg = searchParams.get('includeOrg') === 'true'
9697

9798
// Validate context parameter
9899
if (!['user', 'organization'].includes(context)) {
@@ -115,14 +116,38 @@ export async function GET(request: NextRequest) {
115116
if (context === 'user') {
116117
// Get user billing (may include organization if they're part of one)
117118
billingData = await getSimplifiedBillingSummary(session.user.id, contextId || undefined)
119+
118120
// Attach effective billing blocked status (includes org owner check)
119121
const billingStatus = await getEffectiveBillingStatus(session.user.id)
122+
120123
billingData = {
121124
...billingData,
122125
billingBlocked: billingStatus.billingBlocked,
123126
billingBlockedReason: billingStatus.billingBlockedReason,
124127
blockedByOrgOwner: billingStatus.blockedByOrgOwner,
125128
}
129+
130+
// Optionally include organization membership and role
131+
if (includeOrg) {
132+
const userMembership = await db
133+
.select({
134+
organizationId: member.organizationId,
135+
role: member.role,
136+
})
137+
.from(member)
138+
.where(eq(member.userId, session.user.id))
139+
.limit(1)
140+
141+
if (userMembership.length > 0) {
142+
billingData = {
143+
...billingData,
144+
organization: {
145+
id: userMembership[0].organizationId,
146+
role: userMembership[0].role as 'owner' | 'admin' | 'member',
147+
},
148+
}
149+
}
150+
}
126151
} else {
127152
// Get user role in organization for permission checks first
128153
const memberRecord = await db

apps/sim/app/workspace/[workspaceId]/logs/utils.ts

Lines changed: 36 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -5,146 +5,78 @@ import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
55
import { getBlock } from '@/blocks/registry'
66

77
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook'] as const
8-
const RUNNING_COLOR = '#22c55e' as const
9-
const PENDING_COLOR = '#f59e0b' as const
108

9+
/** Possible execution status values for workflow logs */
1110
export type LogStatus = 'error' | 'pending' | 'running' | 'info'
1211

13-
/**
14-
* Checks if a hex color is gray/neutral (low saturation) or too light/dark
15-
*/
16-
export function isGrayOrNeutral(hex: string): boolean {
17-
const r = Number.parseInt(hex.slice(1, 3), 16)
18-
const g = Number.parseInt(hex.slice(3, 5), 16)
19-
const b = Number.parseInt(hex.slice(5, 7), 16)
20-
21-
const max = Math.max(r, g, b)
22-
const min = Math.min(r, g, b)
23-
const lightness = (max + min) / 2 / 255
24-
25-
const delta = max - min
26-
const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1)) / 255
27-
28-
return saturation < 0.2 || lightness > 0.8 || lightness < 0.25
29-
}
30-
31-
/**
32-
* Converts a hex color to a background variant with appropriate opacity
33-
*/
34-
export function hexToBackground(hex: string): string {
35-
const r = Number.parseInt(hex.slice(1, 3), 16)
36-
const g = Number.parseInt(hex.slice(3, 5), 16)
37-
const b = Number.parseInt(hex.slice(5, 7), 16)
38-
return `rgba(${r}, ${g}, ${b}, 0.2)`
12+
/** Configuration mapping log status to Badge variant and display label */
13+
const STATUS_VARIANT_MAP: Record<
14+
LogStatus,
15+
{ variant: React.ComponentProps<typeof Badge>['variant']; label: string }
16+
> = {
17+
error: { variant: 'red', label: 'Error' },
18+
pending: { variant: 'amber', label: 'Pending' },
19+
running: { variant: 'green', label: 'Running' },
20+
info: { variant: 'gray', label: 'Info' },
3921
}
4022

41-
/**
42-
* Lightens a hex color to make it more vibrant for text
43-
*/
44-
export function lightenColor(hex: string, percent = 30): string {
45-
const r = Number.parseInt(hex.slice(1, 3), 16)
46-
const g = Number.parseInt(hex.slice(3, 5), 16)
47-
const b = Number.parseInt(hex.slice(5, 7), 16)
48-
49-
const newR = Math.min(255, Math.round(r + (255 - r) * (percent / 100)))
50-
const newG = Math.min(255, Math.round(g + (255 - g) * (percent / 100)))
51-
const newB = Math.min(255, Math.round(b + (255 - b) * (percent / 100)))
52-
53-
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`
23+
/** Configuration mapping core trigger types to Badge color variants */
24+
const TRIGGER_VARIANT_MAP: Record<string, React.ComponentProps<typeof Badge>['variant']> = {
25+
manual: 'gray-secondary',
26+
api: 'blue',
27+
schedule: 'teal',
28+
chat: 'purple',
29+
webhook: 'orange',
5430
}
5531

5632
interface StatusBadgeProps {
33+
/** The execution status to display */
5734
status: LogStatus
5835
}
5936

6037
/**
61-
* Displays a styled badge for a log execution status
38+
* Renders a colored badge indicating log execution status.
39+
* @param props - Component props containing the status
40+
* @returns A Badge with dot indicator and status label
6241
*/
6342
export const StatusBadge = React.memo(({ status }: StatusBadgeProps) => {
64-
const config = {
65-
error: {
66-
bg: 'var(--terminal-status-error-bg)',
67-
color: 'var(--text-error)',
68-
label: 'Error',
69-
},
70-
pending: {
71-
bg: hexToBackground(PENDING_COLOR),
72-
color: lightenColor(PENDING_COLOR, 65),
73-
label: 'Pending',
74-
},
75-
running: {
76-
bg: hexToBackground(RUNNING_COLOR),
77-
color: lightenColor(RUNNING_COLOR, 65),
78-
label: 'Running',
79-
},
80-
info: {
81-
bg: 'var(--terminal-status-info-bg)',
82-
color: 'var(--terminal-status-info-color)',
83-
label: 'Info',
84-
},
85-
}[status]
86-
87-
return React.createElement(
88-
'div',
89-
{
90-
className:
91-
'inline-flex items-center gap-[6px] rounded-[6px] px-[9px] py-[2px] font-medium text-[12px]',
92-
style: { backgroundColor: config.bg, color: config.color },
93-
},
94-
React.createElement('div', {
95-
className: 'h-[6px] w-[6px] rounded-[2px]',
96-
style: { backgroundColor: config.color },
97-
}),
98-
config.label
99-
)
43+
const config = STATUS_VARIANT_MAP[status]
44+
return React.createElement(Badge, { variant: config.variant, dot: true }, config.label)
10045
})
10146

10247
StatusBadge.displayName = 'StatusBadge'
10348

10449
interface TriggerBadgeProps {
50+
/** The trigger type identifier (e.g., 'manual', 'api', or integration block type) */
10551
trigger: string
10652
}
10753

10854
/**
109-
* Displays a styled badge for a workflow trigger type
55+
* Renders a colored badge indicating the workflow trigger type.
56+
* Core triggers display with their designated colors; integrations show with icons.
57+
* @param props - Component props containing the trigger type
58+
* @returns A Badge with appropriate styling for the trigger type
11059
*/
11160
export const TriggerBadge = React.memo(({ trigger }: TriggerBadgeProps) => {
11261
const metadata = getIntegrationMetadata(trigger)
11362
const isIntegration = !(CORE_TRIGGER_TYPES as readonly string[]).includes(trigger)
11463
const block = isIntegration ? getBlock(trigger) : null
11564
const IconComponent = block?.icon
11665

117-
const isUnknownIntegration = isIntegration && trigger !== 'generic' && !block
118-
if (
119-
trigger === 'manual' ||
120-
trigger === 'generic' ||
121-
isUnknownIntegration ||
122-
isGrayOrNeutral(metadata.color)
123-
) {
66+
const coreVariant = TRIGGER_VARIANT_MAP[trigger]
67+
if (coreVariant) {
68+
return React.createElement(Badge, { variant: coreVariant }, metadata.label)
69+
}
70+
71+
if (IconComponent) {
12472
return React.createElement(
12573
Badge,
126-
{
127-
variant: 'default',
128-
className:
129-
'inline-flex items-center gap-[6px] rounded-[6px] px-[9px] py-[2px] font-medium text-[12px]',
130-
},
131-
IconComponent && React.createElement(IconComponent, { className: 'h-[12px] w-[12px]' }),
74+
{ variant: 'gray-secondary', icon: IconComponent },
13275
metadata.label
13376
)
13477
}
13578

136-
const textColor = lightenColor(metadata.color, 65)
137-
138-
return React.createElement(
139-
'div',
140-
{
141-
className:
142-
'inline-flex items-center gap-[6px] rounded-[6px] px-[9px] py-[2px] font-medium text-[12px]',
143-
style: { backgroundColor: hexToBackground(metadata.color), color: textColor },
144-
},
145-
IconComponent && React.createElement(IconComponent, { className: 'h-[12px] w-[12px]' }),
146-
metadata.label
147-
)
79+
return React.createElement(Badge, { variant: 'gray-secondary' }, metadata.label)
14880
})
14981

15082
TriggerBadge.displayName = 'TriggerBadge'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,11 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
126126
<iframe
127127
src={`https://www.youtube.com/embed/${videoId}`}
128128
title='YouTube video'
129-
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
129+
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share'
130130
allowFullScreen
131+
loading='lazy'
132+
referrerPolicy='strict-origin-when-cross-origin'
133+
sandbox='allow-scripts allow-same-origin allow-presentation allow-popups'
131134
className='aspect-video w-full'
132135
/>
133136
</span>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx

Lines changed: 8 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use client'
22

33
import { useCallback, useEffect, useState } from 'react'
4-
import clsx from 'clsx'
54
import {
5+
Badge,
66
Button,
77
Modal,
88
ModalBody,
@@ -731,29 +731,10 @@ interface StatusBadgeProps {
731731

732732
function StatusBadge({ isWarning }: StatusBadgeProps) {
733733
const label = isWarning ? 'Update deployment' : 'Live'
734-
735734
return (
736-
<div
737-
className={clsx(
738-
'flex h-[24px] items-center justify-start gap-[8px] rounded-[6px] border px-[9px]',
739-
isWarning ? 'border-[#A16207] bg-[#452C0F]' : 'border-[#22703D] bg-[#14291B]'
740-
)}
741-
>
742-
<div
743-
className='h-[6px] w-[6px] rounded-[2px]'
744-
style={{
745-
backgroundColor: isWarning ? '#EAB308' : '#4ADE80',
746-
}}
747-
/>
748-
<span
749-
className='font-medium text-[11.5px]'
750-
style={{
751-
color: isWarning ? '#EAB308' : '#86EFAC',
752-
}}
753-
>
754-
{label}
755-
</span>
756-
</div>
735+
<Badge variant={isWarning ? 'amber' : 'green'} size='lg' dot>
736+
{label}
737+
</Badge>
757738
)
758739
}
759740

@@ -773,35 +754,10 @@ function TemplateStatusBadge({ status, views, stars }: TemplateStatusBadgeProps)
773754
: null
774755

775756
return (
776-
<div
777-
className={clsx(
778-
'flex h-[24px] items-center justify-start gap-[8px] rounded-[6px] border px-[9px]',
779-
isPending ? 'border-[#A16207] bg-[#452C0F]' : 'border-[#22703D] bg-[#14291B]'
780-
)}
781-
>
782-
<div
783-
className='h-[6px] w-[6px] rounded-[2px]'
784-
style={{
785-
backgroundColor: isPending ? '#EAB308' : '#4ADE80',
786-
}}
787-
/>
788-
<span
789-
className='font-medium text-[11.5px]'
790-
style={{
791-
color: isPending ? '#EAB308' : '#86EFAC',
792-
}}
793-
>
794-
{label}
795-
</span>
796-
{statsText && (
797-
<span
798-
className='font-medium text-[11.5px]'
799-
style={{ color: isPending ? '#EAB308' : '#86EFAC' }}
800-
>
801-
{statsText}
802-
</span>
803-
)}
804-
</div>
757+
<Badge variant={isPending ? 'amber' : 'green'} size='lg' dot>
758+
{label}
759+
{statsText && <span>{statsText}</span>}
760+
</Badge>
805761
)
806762
}
807763

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import Link from 'next/link'
2525
import { useShallow } from 'zustand/react/shallow'
2626
import {
27+
Badge,
2728
Button,
2829
Code,
2930
Input,
@@ -1327,33 +1328,9 @@ export function Terminal() {
13271328
{/* Status */}
13281329
<div className={clsx(COLUMN_WIDTHS.STATUS, COLUMN_BASE_CLASS)}>
13291330
{statusInfo ? (
1330-
<div
1331-
className={clsx(
1332-
'flex h-[24px] w-[56px] items-center justify-start rounded-[6px] border pl-[9px]',
1333-
statusInfo.isError
1334-
? 'gap-[5px] border-[var(--terminal-status-error-border)] bg-[var(--terminal-status-error-bg)]'
1335-
: 'gap-[8px] border-[var(--terminal-status-info-border)] bg-[var(--terminal-status-info-bg)]'
1336-
)}
1337-
>
1338-
<div
1339-
className='h-[6px] w-[6px] rounded-[2px]'
1340-
style={{
1341-
backgroundColor: statusInfo.isError
1342-
? 'var(--text-error)'
1343-
: 'var(--terminal-status-info-color)',
1344-
}}
1345-
/>
1346-
<span
1347-
className='font-medium text-[11.5px]'
1348-
style={{
1349-
color: statusInfo.isError
1350-
? 'var(--text-error)'
1351-
: 'var(--terminal-status-info-color)',
1352-
}}
1353-
>
1354-
{statusInfo.label}
1355-
</span>
1356-
</div>
1331+
<Badge variant={statusInfo.isError ? 'red' : 'gray'} dot>
1332+
{statusInfo.label}
1333+
</Badge>
13571334
) : (
13581335
<span className={ROW_TEXT_CLASS}>-</span>
13591336
)}

0 commit comments

Comments
 (0)