Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ SAGITTARIUS_CABLE_URL=http://localhost:80/cable
NEXT_PUBLIC_SCULPTOR_VERSION=0.0.0
NEXT_PUBLIC_PICTOR_VERSION=0.15.0
NEXT_PUBLIC_ALLOWED_REDIRECT_DOMAINS=*.code0.tech,*.codezero.build
NEXT_PUBLIC_SUBSCRIPTION_URL=https://codezero.build/subscription

NEXT_PUBLIC_OTEL_SERVICE_NAME=#"sculptor-client"
NEXT_PUBLIC_OTEL_HEADER=#"Authorization: Basic <auth-token>"
Expand Down
3 changes: 3 additions & 0 deletions src/app/(dashboard)/@modal/(.)upgrade/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {UpgradePage} from "@edition/license/pages/UpgradePage";

export default UpgradePage
6 changes: 4 additions & 2 deletions src/app/(dashboard)/@sidebar/namespace/[namespaceId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
import {ProjectListView} from "@edition/project/views/ProjectListView";
import {FlowRecentListView} from "@edition/flow/views/FlowRecentListView";
import {NamespaceMenuView} from "@edition/namespace/views/NamespaceMenuView";
import {NamespaceUpgradeView} from "@edition/namespace/views/NamespaceUpgradeView";
import {Flex} from "@code0-tech/pictor";

export default () => {
return <Flex h={"100%"} pt={0.5} style={{boxSizing: "border-box", flexDirection: 'column', gap: "1.3rem"}}>
<ProjectListView/>
<div style={{borderTop: "1px dashed rgba(255,255,255, .1)"}}/>
<FlowRecentListView/>
<div style={{marginTop: "auto"}}>
<Flex style={{marginTop: "auto", flexDirection: "column", gap: "0.7rem"}}>
<NamespaceUpgradeView/>
<NamespaceMenuView/>
</div>
</Flex>
</Flex>
}
5 changes: 5 additions & 0 deletions src/app/(dashboard)/upgrade/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {UpgradePage} from "@edition/license/pages/UpgradePage";

export default function Page() {
return <UpgradePage/>
}
3 changes: 2 additions & 1 deletion src/app/api/config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ export const dynamic = "force-dynamic"
*/
export function GET() {

const config: ClientOtelConfig = {
const config: ClientOtelConfig & { subscriptionUrl: string | null } = {
tracesEndpoint: process.env.NEXT_PUBLIC_OTEL_TRACES_ENDPOINT ?? null,
logsEndpoint: process.env.NEXT_PUBLIC_OTEL_LOGS_ENDPOINT ?? null,
header: process.env.NEXT_PUBLIC_OTEL_HEADER ?? null,
serviceName: process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME ?? null,
environment: process.env.NEXT_PUBLIC_OTEL_ENVIRONMENT ?? null,
version: process.env.NEXT_PUBLIC_SCULPTOR_VERSION ?? "0.0.0",
edition: process.env.NEXT_PUBLIC_EDITION ?? "edition",
subscriptionUrl: process.env.NEXT_PUBLIC_SUBSCRIPTION_URL ?? null,
}

return NextResponse.json(config, {
Expand Down
8 changes: 8 additions & 0 deletions src/packages/ce/src/license/pages/UpgradePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use client"

import React from "react";
import {notFound} from "next/navigation";

export const UpgradePage: React.FC = () => {
return notFound()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client"

import React from "react";
import {Avatar, Badge, Card, Flex, hashToColor, Text, useService} from "@code0-tech/pictor";
import {IconFolders, IconUsers} from "@tabler/icons-react";
import Link from "next/link";
import {Namespace} from "@code0-tech/sagittarius-graphql-types";
import {OrganizationService} from "@edition/organization/services/Organization.service";
import {UserService} from "@edition/user/services/User.service";
import {getNamespaceName} from "@edition/namespace/util/Namespace.name.util";

export interface NamespaceCardComponentProps {
namespace: Namespace
}

export const NamespaceCardComponent: React.FC<NamespaceCardComponentProps> = (props) => {

const {namespace} = props

const organizationService = useService(OrganizationService)
const userService = useService(UserService)

const number = namespace.id?.match(/Namespace\/(\d+)$/)?.[1]
const name = getNamespaceName(namespace, organizationService, userService) ?? ""
const isPersonal = namespace.parent?.__typename === "User"
const user = namespace.parent?.__typename === "User"
? userService.getById(namespace.parent.id) : undefined

return <Link href={`/namespace/${number}`} prefetch style={{display: "contents"}}>
<Card color={"secondary"} clickable h={"100%"}>
<Flex style={{flexDirection: "column", gap: "1.25rem"}}>
<Flex align={"center"} style={{gap: "0.85rem"}}>
{isPersonal
? <Avatar type={"character"} identifier={user?.username ?? ""} size={24}/>
: <Avatar color={hashToColor(name, 200, 360)}
bg={"transparent"} identifier={name} size={24}/>}
<Flex align={"center"} style={{gap: "0.5rem", minWidth: 0, flex: 1}}>
<Text size={"md"} hierarchy={"primary"} fw={500}>{name}</Text>
{isPersonal && <Badge color={"info"}>Personal</Badge>}
</Flex>
</Flex>
<Flex align={"center"} style={{gap: "1.25rem"}}>
<Flex align={"center"} style={{gap: "0.4rem"}}>
<IconFolders size={15}/>
<Text size={"sm"} hierarchy={"tertiary"}>
{namespace.projects?.count ?? 0} projects
</Text>
</Flex>
<Flex align={"center"} style={{gap: "0.4rem"}}>
<IconUsers size={15}/>
<Text size={"sm"} hierarchy={"tertiary"}>
{namespace.members?.count ?? 0} members
</Text>
</Flex>
</Flex>
</Flex>
</Card>
</Link>
}
52 changes: 6 additions & 46 deletions src/packages/ce/src/namespace/views/NamespaceRowView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@

import React from "react";
import {
Avatar,
Badge,
Button,
ButtonGroup,
Card,
Col,
Flex,
hashToColor,
Menu,
MenuContent,
MenuItem,
Expand All @@ -26,9 +23,7 @@ import {
IconAdjustmentsHorizontal,
IconArrowsSort,
IconCheck,
IconFolders,
IconPlus,
IconUsers
IconPlus
} from "@tabler/icons-react";
import Link from "next/link";
import {Namespace} from "@code0-tech/sagittarius-graphql-types";
Expand All @@ -37,6 +32,7 @@ import {OrganizationService} from "@edition/organization/services/Organization.s
import {UserService} from "@edition/user/services/User.service";
import {useUserSession} from "@edition/user/hooks/User.session.hook";
import {getNamespaceName} from "@edition/namespace/util/Namespace.name.util";
import {NamespaceCardComponent} from "@edition/namespace/components/NamespaceCardComponent";

const filterLabels = {
all: "All",
Expand Down Expand Up @@ -159,47 +155,11 @@ export const NamespaceRowView: React.FC = () => {

{/* ── The grid users choose from; create sits in the same grid ── */}
<Row>
{visibleNamespaces.map(namespace => {
const number = namespace.id?.match(/Namespace\/(\d+)$/)?.[1]
const name = getNamespaceName(namespace, organizationService, userService) ?? ""
const isPersonal = namespace.parent?.__typename === "User"
const user = namespace.parent?.__typename === "User"
? userService.getById(namespace.parent.id) : undefined
return <Col key={namespace.id} xs={6} mb={1}>
<Link href={`/namespace/${number}`} prefetch style={{display: "contents"}}>
<Card color={"secondary"} clickable h={"100%"}>
<Flex style={{flexDirection: "column", gap: "1.25rem"}}>
{/* identity: avatar (user avatar for personal, identicon for org), name and personal marker */}
<Flex align={"center"} style={{gap: "0.85rem"}}>
{isPersonal
? <Avatar type={"character"} identifier={user?.username ?? ""} size={24}/>
: <Avatar color={hashToColor(name, 200, 360)}
bg={"transparent"} identifier={name} size={24}/>}
<Flex align={"center"} style={{gap: "0.5rem", minWidth: 0, flex: 1}}>
<Text size={"md"} hierarchy={"primary"} fw={500}>{name}</Text>
{isPersonal && <Badge color={"info"}>Personal</Badge>}
</Flex>
</Flex>
{/* metadata: one calm, labelled line */}
<Flex align={"center"} style={{gap: "1.25rem"}}>
<Flex align={"center"} style={{gap: "0.4rem"}}>
<IconFolders size={15}/>
<Text size={"sm"} hierarchy={"tertiary"}>
{namespace.projects?.count ?? 0} projects
</Text>
</Flex>
<Flex align={"center"} style={{gap: "0.4rem"}}>
<IconUsers size={15}/>
<Text size={"sm"} hierarchy={"tertiary"}>
{namespace.members?.count ?? 0} members
</Text>
</Flex>
</Flex>
</Flex>
</Card>
</Link>
{visibleNamespaces.map(namespace => (
<Col key={namespace.id} xs={6} mb={1}>
<NamespaceCardComponent namespace={namespace}/>
</Col>
})}
))}

{/* create-workspace affordance, matching card footprint */}
<Col xs={6} mb={1} mih={"100px"}>
Expand Down
3 changes: 3 additions & 0 deletions src/packages/ce/src/namespace/views/NamespaceUpgradeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from "react";

export const NamespaceUpgradeView: React.FC = () => null
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client"

import React from "react";
import {Button, useService} from "@code0-tech/pictor";
import Link from "next/link";
import {usePathname, useSearchParams} from "next/navigation";
import BorderBeam from "border-beam";
import {UserService} from "@edition/user/services/User.service";

export interface UpgradeButtonComponentProps {
children?: React.ReactNode
namespaceId?: string | number
fullWidth?: boolean
paddingSize?: React.ComponentProps<typeof Button>["paddingSize"]
color?: React.ComponentProps<typeof Button>["color"]
beamSize?: React.ComponentProps<typeof BorderBeam>["size"]
}

export const UpgradeButtonComponent: React.FC<UpgradeButtonComponentProps> = ({children, namespaceId, fullWidth, paddingSize, color = "tertiary", beamSize = "sm"}) => {

const userService = useService(UserService)
const pathname = usePathname()
const searchParams = useSearchParams()
const [pending, startTransition] = React.useTransition()

const isSubscription = pathname === "/upgrade"
const namespace = namespaceId?.toString() ?? searchParams.get("namespace")
const href = namespace ? `/upgrade?namespace=${namespace}` : "/upgrade"

const display = fullWidth ? "block" : "inline-block"

const openSubscription = () => {
const target = window.open("about:blank", "_blank")
startTransition(async () => {
const [config, tokenPayload] = await Promise.all([
fetch("/api/config").then(response => response.json()),
userService.usersCreateCraterToken()
])
const subscriptionUrl = config?.subscriptionUrl as string | null | undefined
const token = tokenPayload?.token?.token
if (!subscriptionUrl || !token) {
target?.close()
return
}
const url = new URL(subscriptionUrl)
if (namespace) url.searchParams.set("namespace", namespace)
url.searchParams.set("token", token)
if (target) target.location.href = url.toString()
})
}

const inner = <BorderBeam strength={1} size={beamSize} theme={"dark"} duration={5} style={{display}}>
<Button paddingSize={paddingSize} color={color} disabled={pending}
onClick={isSubscription ? openSubscription : undefined}
justify={"center"} w={fullWidth ? "100%" : undefined}>
{children ?? "Upgrade your plan"}
</Button>
</BorderBeam>

if (isSubscription) return <span style={{display}}>{inner}</span>

return <Link href={href} style={{display}}>
{inner}
</Link>
}
Loading