Skip to content

Commit 549988a

Browse files
committed
improvement(sim-links): count visits best-effort, drop the unused visits index, final cleanup
1 parent 653acf7 commit 549988a

10 files changed

Lines changed: 100 additions & 102 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { cn } from '@sim/emcn'
2+
import { HEADER_ACTION_CLUSTER, PAGE_HEADER_BAR } from '@/components/page-header-bar'
3+
import { PAGE_COLUMN_CLASS } from '@/app/o/[organizationId]/components/organization-page'
4+
5+
export default function SimLinksLoading() {
6+
return (
7+
<div className='flex h-full flex-col bg-[var(--bg)]'>
8+
<div className={PAGE_HEADER_BAR}>
9+
<div className={HEADER_ACTION_CLUSTER} />
10+
</div>
11+
<div className={cn(PAGE_COLUMN_CLASS, 'pt-8')}>
12+
<h1 className='text-[var(--text-primary)] text-lg'>Sim Links</h1>
13+
<p role='status' className='mt-4 text-[var(--text-muted)] text-small'>
14+
Loading Sim links
15+
</p>
16+
</div>
17+
</div>
18+
)
19+
}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { createSerializer, parseAsString } from 'nuqs/server'
2-
import { organizationPageParsers } from '@/app/o/[organizationId]/components/organization-page/search-params'
32

43
/** `create` opens the new-link editor, prefilled with its value as the name. */
54
export const simLinkViewParsers = { create: parseAsString } as const
65

7-
export const serializeSimLinkView = createSerializer({
8-
...organizationPageParsers,
9-
...simLinkViewParsers,
10-
})
6+
export const serializeSimLinkView = createSerializer(simLinkViewParsers)

‎apps/sim/app/o/[organizationId]/links/sim-links.tsx‎

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ const SORT_TABS = [
4646
{ id: 'popular', label: 'Most used' },
4747
] as const satisfies readonly OrganizationPageTab[]
4848

49-
async function copyLink(href: string) {
49+
async function copyLink(path: string) {
5050
try {
51-
await writeTextToClipboard(new URL(href, window.location.origin).href)
51+
await writeTextToClipboard(new URL(path, window.location.origin).href)
5252
} catch (error) {
5353
logger.error('Failed to copy Sim link', { error })
5454
}
@@ -123,44 +123,49 @@ export function SimLinks({ organizationId }: SimLinksProps) {
123123
: 'No Sim links yet. Create one for a page your team opens often, like sim/handbook.'}
124124
</SettingsEmptyState>
125125
) : (
126-
rows.map((link) => (
127-
<SettingsResourceRow
128-
key={link.id}
129-
icon={<Link />}
130-
title={`sim/${link.name}`}
131-
description={link.description ? `${link.description} · ${link.url}` : link.url}
132-
badge={<SimLinkDetails link={link} />}
133-
onClick={() => window.open(simLinkPath(link.name), '_blank', 'noopener,noreferrer')}
134-
clickLabel={`Open sim/${link.name}`}
135-
trailing={
136-
<DropdownMenu>
137-
<DropdownMenuTrigger asChild>
138-
<Chip leftIcon={MoreHorizontal} aria-label={`sim/${link.name} actions`} />
139-
</DropdownMenuTrigger>
140-
<DropdownMenuContent align='end'>
141-
<DropdownMenuItem onSelect={() => void copyLink(simLinkPath(link.name))}>
142-
Copy link
143-
</DropdownMenuItem>
144-
{link.canEdit && (
145-
<>
146-
<DropdownMenuItem onSelect={() => setEditing(link)}>Edit</DropdownMenuItem>
147-
<DropdownMenuSeparator />
148-
<DropdownMenuItem
149-
className='text-[var(--text-error)]'
150-
onSelect={() => {
151-
remove.reset()
152-
setDeleting(link)
153-
}}
154-
>
155-
Delete
156-
</DropdownMenuItem>
157-
</>
158-
)}
159-
</DropdownMenuContent>
160-
</DropdownMenu>
161-
}
162-
/>
163-
))
126+
rows.map((link) => {
127+
const path = simLinkPath(link.name)
128+
return (
129+
<SettingsResourceRow
130+
key={link.id}
131+
icon={<Link />}
132+
title={`sim/${link.name}`}
133+
description={link.description ? `${link.description} · ${link.url}` : link.url}
134+
badge={<SimLinkDetails link={link} />}
135+
onClick={() => window.open(path, '_blank', 'noopener,noreferrer')}
136+
clickLabel={`Open sim/${link.name}`}
137+
trailing={
138+
<DropdownMenu>
139+
<DropdownMenuTrigger asChild>
140+
<Chip leftIcon={MoreHorizontal} aria-label={`sim/${link.name} actions`} />
141+
</DropdownMenuTrigger>
142+
<DropdownMenuContent align='end'>
143+
<DropdownMenuItem onSelect={() => void copyLink(path)}>
144+
Copy link
145+
</DropdownMenuItem>
146+
{link.canEdit && (
147+
<>
148+
<DropdownMenuItem onSelect={() => setEditing(link)}>
149+
Edit
150+
</DropdownMenuItem>
151+
<DropdownMenuSeparator />
152+
<DropdownMenuItem
153+
className='text-[var(--text-error)]'
154+
onSelect={() => {
155+
remove.reset()
156+
setDeleting(link)
157+
}}
158+
>
159+
Delete
160+
</DropdownMenuItem>
161+
</>
162+
)}
163+
</DropdownMenuContent>
164+
</DropdownMenu>
165+
}
166+
/>
167+
)
168+
})
164169
)}
165170
<SettingsListPagination {...links} fallback='Could not load more Sim links' />
166171
</div>

‎apps/sim/lib/sim-links/application/use-cases.ts‎

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { AuditAction, AuditResourceType } from '@sim/audit'
22
import { db } from '@sim/db'
3+
import { createLogger } from '@sim/logger'
34
import { isOrgAdminRole } from '@sim/platform-authz/workspace'
4-
import { getPostgresErrorCode } from '@sim/utils/errors'
5+
import { getPostgresErrorCode, toError } from '@sim/utils/errors'
56
import {
67
defineAuthorizedOrganizationUseCase,
78
type OrganizationUseCaseContext,
@@ -17,6 +18,8 @@ import type { SimLinkAccess, SimLinkRecord, SimLinkViewer } from '@/lib/sim-link
1718
import * as repository from '@/lib/sim-links/repository'
1819
import type { SimLinkFields, SimLinkSort } from '@/lib/sim-links/validation'
1920

21+
const logger = createLogger('SimLinks')
22+
2023
interface OrganizationInput {
2124
organizationId: string
2225
}
@@ -94,20 +97,24 @@ async function withUniqueName<T>(
9497
function auditOf(
9598
action: WorkspaceUseCaseAuditEntry['action'],
9699
verb: string,
97-
link: Pick<SimLinkRecord, 'id' | 'name'> & Partial<SimLinkRecord>
100+
link: { id: string; name: string },
101+
metadata?: Record<string, unknown>
98102
): WorkspaceUseCaseAuditEntry {
99103
return {
100104
action,
101105
resourceType: AuditResourceType.SIM_LINK,
102106
resourceId: link.id,
103107
resourceName: `sim/${link.name}`,
104108
description: `${verb} sim/${link.name}`,
105-
...(link.url !== undefined && {
106-
metadata: { url: link.url, unlisted: link.unlisted, restrictEdit: link.restrictEdit },
107-
}),
109+
metadata,
108110
}
109111
}
110112

113+
/** What a saved link now does, for its audit entry. */
114+
function savedMetadata(link: SimLinkView): Record<string, unknown> {
115+
return { url: link.url, unlisted: link.unlisted, restrictEdit: link.restrictEdit }
116+
}
117+
111118
/** Read in the writing transaction, so the result and its audit describe exactly this write. */
112119
async function readView(
113120
tx: DbTransaction,
@@ -150,7 +157,8 @@ export const createSimLink = defineAuthorizedOrganizationUseCase({
150157
})
151158
)
152159
},
153-
projectAudit: ({ result }) => auditOf(AuditAction.SIM_LINK_CREATED, 'Created', result),
160+
projectAudit: ({ result }) =>
161+
auditOf(AuditAction.SIM_LINK_CREATED, 'Created', result, savedMetadata(result)),
154162
})
155163

156164
export const updateSimLink = defineAuthorizedOrganizationUseCase({
@@ -176,7 +184,8 @@ export const updateSimLink = defineAuthorizedOrganizationUseCase({
176184
})
177185
)
178186
},
179-
projectAudit: ({ result }) => auditOf(AuditAction.SIM_LINK_UPDATED, 'Updated', result),
187+
projectAudit: ({ result }) =>
188+
auditOf(AuditAction.SIM_LINK_UPDATED, 'Updated', result, savedMetadata(result)),
180189
})
181190

182191
export const deleteSimLink = defineAuthorizedOrganizationUseCase({
@@ -192,6 +201,19 @@ export const deleteSimLink = defineAuthorizedOrganizationUseCase({
192201
projectAudit: ({ result }) => auditOf(AuditAction.SIM_LINK_DELETED, 'Deleted', result),
193202
})
194203

204+
/** Visit counts rank links; failing to record one must never block the redirect it describes. */
205+
async function countVisit(organizationId: string, linkId: string): Promise<void> {
206+
try {
207+
await repository.recordSimLinkVisit(organizationId, linkId)
208+
} catch (error) {
209+
logger.warn('Could not count a Sim link visit', {
210+
organizationId,
211+
linkId,
212+
error: toError(error),
213+
})
214+
}
215+
}
216+
195217
/**
196218
* Unlisted links resolve for every member: unlisting hides a link from browsing, not from use.
197219
* Only a redirect counts as a visit; a link opened without the values it needs does not.
@@ -207,8 +229,7 @@ export const resolveSimLink = defineAuthorizedOrganizationUseCase({
207229
const link = await repository.findSimLinkDestination(input.organizationId, input.name)
208230
if (!link) return { kind: 'not_found' }
209231
const target = resolveSimLinkTarget(link, input.values)
210-
if (target.kind === 'redirect')
211-
await repository.recordSimLinkVisit(input.organizationId, link.id)
232+
if (target.kind === 'redirect') await countVisit(input.organizationId, link.id)
212233
return target
213234
},
214235
})

‎apps/sim/lib/sim-links/destination.test.ts‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ describe('Sim link destinations', () => {
2828
'https://example.com@{*}',
2929
'https://example.com:{*}',
3030
'https://{*}@corp.example.com/',
31-
'https://user:{*}@corp.example.com/',
3231
'https://corp.example.com:{*}/path',
3332
'{*}',
34-
'https:{*}',
3533
'javascript:alert(1)',
3634
'data:text/html,hi',
3735
'ftp://example.com/file',
@@ -70,15 +68,9 @@ describe('Sim link destinations', () => {
7068
it.each(['https://{*}@corp.example.com/', 'https://user:{*}@corp.example.com/'])(
7169
'refuses to resolve %s, whose values could move the host',
7270
(url) => {
73-
for (const values of [
74-
['evil.com', 'x'],
75-
['443', 'x'],
76-
['a b', 'x:y'],
77-
]) {
78-
expect(() => resolveSimLinkTarget({ url, defaultUrl: null }, values)).toThrow(
79-
'no fixed http(s) origin'
80-
)
81-
}
71+
expect(() => resolveSimLinkTarget({ url, defaultUrl: null }, ['evil.com', 'x'])).toThrow(
72+
'no fixed http(s) origin'
73+
)
8274
}
8375
)
8476

‎packages/db/migrations/0447_sim_links.sql‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,4 @@ CREATE TABLE "sim_link" (
2020
ALTER TABLE "sim_link" ADD CONSTRAINT "sim_link_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
2121
ALTER TABLE "sim_link" ADD CONSTRAINT "sim_link_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
2222
CREATE UNIQUE INDEX "sim_link_organization_name_unique" ON "sim_link" USING btree ("organization_id","normalized_name");--> statement-breakpoint
23-
CREATE INDEX "sim_link_organization_visits_idx" ON "sim_link" USING btree ("organization_id","visit_count" DESC NULLS LAST,"normalized_name");--> statement-breakpoint
2423
CREATE INDEX "sim_link_created_by_idx" ON "sim_link" USING btree ("created_by");

‎packages/db/migrations/meta/0447_snapshot.json‎

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"id": "42b7ec5a-241f-44fc-84a2-d311b4ebe879",
2+
"id": "bf055cdf-b4c5-4124-9871-4d465c57fc9e",
33
"prevId": "659943cb-6ede-48d1-b6df-5246d3eff9ac",
44
"version": "7",
55
"dialect": "postgresql",
@@ -20446,33 +20446,6 @@
2044620446
"method": "btree",
2044720447
"with": {}
2044820448
},
20449-
"sim_link_organization_visits_idx": {
20450-
"name": "sim_link_organization_visits_idx",
20451-
"columns": [
20452-
{
20453-
"expression": "organization_id",
20454-
"isExpression": false,
20455-
"asc": true,
20456-
"nulls": "last"
20457-
},
20458-
{
20459-
"expression": "visit_count",
20460-
"isExpression": false,
20461-
"asc": false,
20462-
"nulls": "last"
20463-
},
20464-
{
20465-
"expression": "normalized_name",
20466-
"isExpression": false,
20467-
"asc": true,
20468-
"nulls": "last"
20469-
}
20470-
],
20471-
"isUnique": false,
20472-
"concurrently": false,
20473-
"method": "btree",
20474-
"with": {}
20475-
},
2047620449
"sim_link_created_by_idx": {
2047720450
"name": "sim_link_created_by_idx",
2047820451
"columns": [

‎packages/db/migrations/meta/_journal.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3127,7 +3127,7 @@
31273127
{
31283128
"idx": 447,
31293129
"version": "7",
3130-
"when": 1793400456832,
3130+
"when": 1793401421656,
31313131
"tag": "0447_sim_links",
31323132
"breakpoints": true
31333133
}

‎packages/db/schema.ts‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -966,11 +966,6 @@ export const simLink = pgTable(
966966
},
967967
(table) => [
968968
uniqueIndex('sim_link_organization_name_unique').on(table.organizationId, table.normalizedName),
969-
index('sim_link_organization_visits_idx').on(
970-
table.organizationId,
971-
table.visitCount.desc(),
972-
table.normalizedName
973-
),
974969
index('sim_link_created_by_idx').on(table.createdBy),
975970
]
976971
)

‎scripts/check-unused-exports.baseline.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,6 @@
222222
"apps/sim/app/cli/auth/cli-auth-request.ts#CliAuthRequest",
223223
"apps/sim/app/cli/auth/search-params.ts#CLI_AUTH_SCOPES",
224224
"apps/sim/app/invite/components/index.ts#InvitationWorkspaceAccess",
225-
"apps/sim/app/o/[organizationId]/components/organization-page/index.ts#OrganizationPageTab",
226-
"apps/sim/app/o/[organizationId]/components/organization-page/organization-page.tsx#OrganizationPageTab",
227225
"apps/sim/app/o/[organizationId]/components/organization-sidebar/hooks/index.ts#useOrganizationWorkspaces",
228226
"apps/sim/app/o/[organizationId]/integrations/search-params.ts#integrationConnectionParams",
229227
"apps/sim/app/o/[organizationId]/settings/components/integrations/search-params.ts#organizationIntegrationsTabParam",

0 commit comments

Comments
 (0)