Skip to content

Commit

Permalink
Merge pull request #6256 from signalco-io/feat/uier/db-comments
Browse files Browse the repository at this point in the history
feat(uier): Connected toolbar to API and DB
  • Loading branch information
AleksandarDev authored Nov 16, 2024
2 parents 716d490 + 5c58f7b commit a6def34
Show file tree
Hide file tree
Showing 28 changed files with 365 additions and 135 deletions.
6 changes: 6 additions & 0 deletions infra/apps/uier/Pulumi.next.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
config:
azure-native:clientId: f72781c2-20ca-47bf-bdfc-a39343fbaede
azure-native:clientSecret:
secure: AAABAKNv1KwWFJpsVdaGYA6F6CcjxQ/stOwVwh99ZQzIA3K/9WD9JOITtbIndAgOdipNfvlmmHASUOjuQsW6tosOXJKaVtgk
azure-native:location: westeurope
azure-native:subscriptionId: d22db5bd-ca9a-447c-9b1b-dc56aca401d7
azure-native:tenantId: c7837eb3-77ca-413b-8653-c97b08886678
cloudflare:apiToken:
secure: AAABAAqPzyAV9mvmavpxCxB+Ws+snM4JK8mNiAxa+WqmzniW5gNmJpHXGgXZI69SkL251Lhhrp43BUrvHnTEbAFf3lG8/Wsl
uier:zoneid:
Expand Down
66 changes: 65 additions & 1 deletion infra/apps/uier/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { nextJsApp } from '@infra/pulumi/vercel';
import { dnsRecord } from '@infra/pulumi/cloudflare';
import { ProjectDomain } from '@pulumiverse/vercel';
import { ProjectDomain, ProjectEnvironmentVariable } from '@pulumiverse/vercel';
import { getStack } from '@pulumi/pulumi';
import { ResourceGroup } from '@pulumi/azure-native/resources/index.js';
import { DatabaseAccount, SqlResourceSqlDatabase, SqlResourceSqlContainer, DatabaseAccountOfferType, listDatabaseAccountConnectionStringsOutput } from '@pulumi/azure-native/documentdb/index.js';

const up = async () => {
const stack = getStack();
Expand All @@ -24,6 +26,68 @@ const up = async () => {
dnsRecord('vercel-uier', '@', '76.76.21.21', 'A', false);
}
}

// Azure
const resourceGroupName = `uier-${stack}`;
const resourceGroup = new ResourceGroup(resourceGroupName);

// Create an Azure Cosmos DB Account for SQL API
const cosmosAccountName = 'uierdb';
const databaseName = 'uierdata';
const cosmosDbAccount = new DatabaseAccount(cosmosAccountName, {
resourceGroupName: resourceGroup.name,
databaseAccountOfferType: DatabaseAccountOfferType.Standard,
capabilities: [
{
name: 'EnableServerless', // Use specific capabilities if needed
},
],
consistencyPolicy: {
defaultConsistencyLevel: 'Session', // Adjust the consistency level as needed
},
locations: [
{ locationName: 'West Europe' },
],
});
const cosmosPrimaryConnectionString = listDatabaseAccountConnectionStringsOutput({
resourceGroupName: resourceGroup.name,
accountName: cosmosDbAccount.name,
}).apply(keys => keys.connectionStrings?.at(0)?.connectionString ?? '');

// Create a SQL Database within the Cosmos DB Account
const sqlDatabase = new SqlResourceSqlDatabase(databaseName, {
resourceGroupName: resourceGroup.name,
accountName: cosmosDbAccount.name,
resource: {
id: databaseName,
},
});

// Creating the containers inside the database
const containerNames = ['comments'];
containerNames.map((containerName) =>
new SqlResourceSqlContainer(`dbcontainer-${containerName}`, {
resourceGroupName: resourceGroup.name,
accountName: cosmosDbAccount.name,
databaseName: sqlDatabase.name,
containerName: containerName,
resource: {
id: containerName,
partitionKey: {
kind: 'Hash',
paths: ['/domain'],
},
},
}),
);

// Assign app environment variables
new ProjectEnvironmentVariable('vercel-uier-env-cosmos', {
projectId: app.projectId,
key: 'COSMOSDB_CONNECTION_STRING',
value: cosmosPrimaryConnectionString,
targets: stack === 'production' ? ['production'] : ['preview'],
});
};

export default up;
1 change: 1 addition & 0 deletions web/apps/uier/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COSMOSDB_CONNECTION_STRING=secret
10 changes: 0 additions & 10 deletions web/apps/uier/app/(rest)/(marketing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,6 @@ const data: SectionData[] = [
header: 'Product',
ctas: [
{ label: 'uier', href: KnownPages.App },
{ label: 'uier', href: KnownPages.App },
{ label: 'uier', href: KnownPages.App },
]
},
{
header: 'Product',
ctas: [
{ label: 'uier', href: KnownPages.App },
{ label: 'uier', href: KnownPages.App },
{ label: 'uier', href: KnownPages.App },
]
},
{
Expand Down
33 changes: 33 additions & 0 deletions web/apps/uier/app/api/comments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createComment, getComments, updateComment } from '../../../src/lib/repo/commentsRepo';

export async function GET(request: Request) {
const domain = request.headers.get('host');
if (!domain)
return new Response('Couldn\'t resolve domain', { status: 402 });

const comments = await getComments(domain);
return Response.json(comments);
}

export async function POST(request: Request) {
const domain = request.headers.get('host');
if (!domain)
return new Response('Couldn\'t resolve domain', { status: 402 });

const comment = await request.json() as {
id?: string,
path: string,
position: object, // TODO: Types
thread: object, // TODO: Types
device?: object, // TODO: Types
resolved?: boolean
};

if (comment.id) {
await updateComment(domain, comment.id, comment);
return Response.json({ id: comment.id });
} else {
const id = await createComment({ domain, ...comment });
return Response.json({ id }, { status: 201 });
}
}
2 changes: 1 addition & 1 deletion web/apps/uier/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const nextConfig = {
[
knownSecureHeadersExternalUrls.vercel,
knownSecureHeadersExternalUrls.googleFonts,
{ scriptSrc: 'http://localhost:4005', styleSrc: 'http://localhost:4005' },
{ scriptSrc: 'http://localhost:4005', styleSrc: 'http://localhost:4005', connectSrc: 'http://localhost:4005' },
]
))
}];
Expand Down
2 changes: 2 additions & 0 deletions web/apps/uier/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"lint": "next lint"
},
"dependencies": {
"@azure/cosmos": "4.1.1",
"@enterwell/react-hooks": "0.5.0",
"@next/env": "15.0.3",
"@signalco/cms-components-marketing": "workspace:*",
Expand All @@ -33,6 +34,7 @@
"@tanstack/react-query-devtools": "5.60.5",
"@vercel/analytics": "1.4.0",
"classix": "2.2.0",
"nanoid": "5.0.8",
"next": "15.0.3",
"next-secure-headers": "2.2.0",
"next-themes": "0.4.3",
Expand Down
26 changes: 26 additions & 0 deletions web/apps/uier/src/lib/db/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type Container, CosmosClient, type Database } from '@azure/cosmos';

let client: CosmosClient | null = null;
let dataDb: Database | null = null;

let commentsContainer: Container | null = null;

function cosmosClient() {
if (client == null) {
const connectionString = process.env.COSMOSDB_CONNECTION_STRING;
if (!connectionString)
throw new Error('COSMOSDB_CONNECTION_STRING is not available in the environment. Please set it before using this feature.');

client = new CosmosClient(connectionString);
}

return client;
}

function cosmosDataDb() {
return dataDb = dataDb ?? cosmosClient().database('uierdata');
}

export function cosmosDataContainerComments() {
return commentsContainer = commentsContainer ?? cosmosDataDb().container('comments');
}
9 changes: 9 additions & 0 deletions web/apps/uier/src/lib/db/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type DbComment = {
id: string;
domain: string;
path: string;
position: object, // TODO: Type
thread: object, // TODO: Type
device?: object, // TODO: Type
resolved?: boolean;
};
37 changes: 37 additions & 0 deletions web/apps/uier/src/lib/repo/commentsRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { nanoid } from 'nanoid';
import { DbComment } from '../db/schema';
import { cosmosDataContainerComments } from '../db/client';

export async function getComments(domain: string) {
return (await cosmosDataContainerComments().items.query<DbComment>({
query: 'SELECT * FROM c WHERE c.domain = @domain',
parameters: [{ name: '@domain', value: domain }]
}).fetchAll()).resources;
}

export async function createComment({ domain, path, position, thread, device }: { domain: string, path: string, position: object, thread: object, device?: object }) {
const container = cosmosDataContainerComments();
const commentId = `comment_${nanoid()}`;
await container.items.create<DbComment>({
id: commentId,
domain,
path,
position, // TODO: Sanitize
thread, // TODO: Sanitize
device, // TODO: Sanitize
});
return commentId;
}

export async function updateComment(domain: string, id: string, comment: { path: string, position: object, thread: object, device?: object, resolved?: boolean }) {
const container = cosmosDataContainerComments();
await container.item(id, domain).replace<DbComment>({
id,
domain,
...comment
});
}

export async function deleteComment(domain: string, id: string) {
await cosmosDataContainerComments().item(id, domain).delete();
}
2 changes: 1 addition & 1 deletion web/packages/uier-toolbar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"./index.css": "./dist/index.css"
},
"scripts": {
"dev:build": "tsup --watch --env.NODE_ENV development",
"dev:build": "tsup --watch ./src --env.NODE_ENV development",
"dev:serve": "pnpm dlx serve dist -l 4005",
"dev": "pnpm run /^dev:.*/",
"build": "tsup --minify --env.NODE_ENV production",
Expand Down
6 changes: 3 additions & 3 deletions web/packages/uier-toolbar/src/components/CommentBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { useCommentItemRects } from '../hooks/useCommentItemRects';
import { CommentThread } from './CommentThread';
import { CommentSelectionHighlight } from './CommentSelectionHighlight';
import { CommentsBootstrapperContext } from './CommentsBootstrapperContext';
import { CommentItem } from './Comments';
import { CommentIcon } from './CommentIcon';
import { CommentItem } from './@types/Comments';

type CommentBubbleProps = HTMLAttributes<HTMLDivElement> & {
defaultOpen?: boolean;
Expand Down Expand Up @@ -45,7 +45,7 @@ export function CommentBubble({
e.preventDefault();
const formData = new FormData(e.currentTarget);
e.currentTarget.reset();
const commentId = await upsert.mutateAsync({
const { id } = await upsert.mutateAsync({
...commentItem,
thread: {
...commentItem.thread,
Expand All @@ -60,7 +60,7 @@ export function CommentBubble({
});

if (creating) {
onCreated?.(commentId);
onCreated?.(id);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useRef } from 'react';
import { cx } from '@signalco/ui-primitives/cx';
import { getElementSelector } from '@signalco/js';
import { useWindowEvent } from '@signalco/hooks/useWindowEvent';
import { CommentPoint } from './Comments';
import { CommentPoint } from './@types/Comments';

type CommentPointOverlayProps = {
onPoint: (commentPoint: CommentPoint) => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { HTMLAttributes } from 'react';
import { cx } from '@signalco/ui-primitives/cx';
import { useCommentItemRects } from '../hooks/useCommentItemRects';
import { CommentSelection } from './Comments';
import { CommentSelection } from './@types/Comments';

type CommentSelectionHighlightProps = HTMLAttributes<HTMLDivElement> & {
commentSelection: CommentSelection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Button } from '@signalco/ui-primitives/Button';
import { Comment } from '@signalco/ui-icons';
import { orderBy } from '@signalco/js';
import { useResizeObserver } from '@enterwell/react-hooks';
import { popoverWidth, popoverWindowMargin } from './Comments';
import { popoverWidth, popoverWindowMargin } from './@types/Comments';

export function CommentSelectionPopover({ rects, onCreate }: { rects: DOMRect[]; onCreate: () => void; }) {
const [popoverHeight, setPopoverHeight] = useState(0);
Expand Down
4 changes: 2 additions & 2 deletions web/packages/uier-toolbar/src/components/CommentThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { Stack } from '@signalco/ui-primitives/Stack';
import { Divider } from '@signalco/ui-primitives/Divider';
import { Button } from '@signalco/ui-primitives/Button';
import { CommentThreadItem } from './CommentThreadItem';
import { CommentItem } from './Comments';
import { CommentItem } from './@types/Comments';

export function CommentThread({ commentItem, onResolve }: { commentItem: CommentItem; onResolve?: () => void; }) {
export function CommentThread({ commentItem, onResolve }: { commentItem: CommentItem; onResolve: () => void; }) {
const [expanded, setExpanded] = useState(false);
if (!commentItem.thread.items.length) return null;

Expand Down
52 changes: 9 additions & 43 deletions web/packages/uier-toolbar/src/components/CommentThreadItem.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,15 @@
'use client';

import { useContext, useState } from 'react';
import { Typography } from '@signalco/ui-primitives/Typography';
import { Stack } from '@signalco/ui-primitives/Stack';
import { Row } from '@signalco/ui-primitives/Row';
import { Popper } from '@signalco/ui-primitives/Popper';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@signalco/ui-primitives/Menu';
import { IconButton } from '@signalco/ui-primitives/IconButton';
import { Avatar } from '@signalco/ui-primitives/Avatar';
import { Check, Desktop, Mobile, MoreHorizontal, Tablet } from '@signalco/ui-icons';
import { Check, MoreHorizontal } from '@signalco/ui-icons';
import { Timeago } from '@signalco/ui/Timeago';
import { camelToSentenceCase } from '@signalco/js';
import { CommentsBootstrapperContext } from './CommentsBootstrapperContext';
import { CommentItem, CommentItemThreadItem } from './Comments';

function DeviceInfo({ device }: { device?: CommentItem['device'] }) {
const { rootElement } = useContext(CommentsBootstrapperContext);
const [open, setOpen] = useState(false);
const deviceSize = device?.size;
const DeviceSizeIcon = deviceSize === 'desktop'
? Desktop
: (deviceSize === 'tablet'
? Tablet
: (deviceSize === 'mobile'
? Mobile
: null));

if (!DeviceSizeIcon) return null;

return (
<Popper
open={open}
// onMouseLeave={() => setOpen(false)}
container={rootElement}
anchor={(
<DeviceSizeIcon size={14} className="text-secondary-foreground" onMouseEnter={() => setOpen(true)} />
)}>
<Stack spacing={0.5} className="rounded-md px-2 py-1 text-background">
{device?.browser && <Typography level="body2" title={device.userAgent}>{device.browser}</Typography>}
{device?.os && <Typography level="body2">{device.os}</Typography>}
{deviceSize && <Typography level="body2">{camelToSentenceCase(deviceSize)}</Typography>}
{device?.windowSize && <Typography level="body2">{device.windowSize[0]}x{device.windowSize[1]}</Typography>}
{device?.pixelRatio && <Typography level="body2">(x{device.pixelRatio})</Typography>}
</Stack>
</Popper>
);
}
import { DeviceInfo } from './info/DeviceInfo';
import { CommentItem, CommentItemThreadItem } from './@types/Comments';

export function CommentThreadItem({ comment, threadItem, first, onDone }: { comment: CommentItem, threadItem: CommentItemThreadItem; first?: boolean; onDone?: () => void; }) {
const { text } = threadItem;
Expand All @@ -59,12 +23,14 @@ export function CommentThreadItem({ comment, threadItem, first, onDone }: { comm
<Row spacing={0.5}>
<Avatar size="sm">{avatarFallback}</Avatar>
<Typography className="text-sm text-foreground">{author}</Typography>
{first && (
{/* TODO Enable when we fix device info UI */}
{/* {first && (
<DeviceInfo device={comment.device} />
)}
<span className="text-sm text-secondary-foreground">
)} */}
{/* TODO Enable when we have this info */}
{/* <span className="text-sm text-secondary-foreground">
<Timeago format="nano" date={new Date()} />
</span>
</span> */}
</Row>
<Row>
{first && (
Expand Down
Loading

0 comments on commit a6def34

Please sign in to comment.