Skip to content

Commit 30e3153

Browse files
authored
make create modal prettier (#2377)
* make create modal prettier * ui improvements * redirect to unit details on creation * add last updated * actually we dont want to redirect
1 parent fcb2cb8 commit 30e3153

File tree

9 files changed

+289
-54
lines changed

9 files changed

+289
-54
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as React from 'react'
2+
import { useRouter } from '@tanstack/react-router'
3+
import { getWidgetsAuthToken } from '@/authkit/serverFunctions'
4+
import { useToast } from '@/hooks/use-toast'
5+
import { OrganizationSwitcher, WorkOsWidgets } from '@workos-inc/widgets'
6+
import { DropdownMenu } from '@radix-ui/themes'
7+
8+
import '@workos-inc/widgets/styles.css'
9+
import '@radix-ui/themes/styles.css'
10+
11+
type WorkosOrgSwitcherProps = {
12+
userId: string
13+
organisationId: string
14+
label?: string
15+
redirectTo?: string
16+
/**
17+
* If true, wraps the switcher in WorkOsWidgets provider which applies a full-page layout.
18+
* Leave false for compact embedding in headers/navs to avoid large whitespace.
19+
*/
20+
wrapWithProvider?: boolean
21+
/**
22+
* When true, injects a default extra group with a Settings item in the switcher dropdown.
23+
*/
24+
showSettingsItem?: boolean
25+
}
26+
27+
export default function WorkosOrgSwitcher({
28+
userId,
29+
organisationId,
30+
label = 'My Orgs',
31+
redirectTo = '/dashboard/units',
32+
wrapWithProvider = false,
33+
showSettingsItem = false,
34+
}: WorkosOrgSwitcherProps) {
35+
const router = useRouter()
36+
const { toast } = useToast()
37+
const [authToken, setAuthToken] = React.useState<string | null>(null)
38+
const [error, setError] = React.useState<string | null>(null)
39+
const [loading, setLoading] = React.useState(true)
40+
41+
const handleSwitchToOrganization = async (organizationId: string) => {
42+
try {
43+
const res = await fetch('/api/auth/workos/switch-org', {
44+
method: 'POST',
45+
headers: { 'Content-Type': 'application/json' },
46+
body: JSON.stringify({ organizationId, pathname: redirectTo }),
47+
})
48+
const data = await res.json()
49+
if (!data?.redirectUrl) return
50+
const url: string = data.redirectUrl
51+
const isInternal = url.startsWith('/')
52+
if (isInternal) {
53+
await router.navigate({ to: url })
54+
router.invalidate()
55+
} else {
56+
throw new Error('Cannot redirect to external URL')
57+
}
58+
} catch (e: any) {
59+
toast({
60+
title: 'Failed to switch organization',
61+
description: e?.message ?? 'Failed to switch organization',
62+
variant: 'destructive',
63+
})
64+
console.error('Failed to switch organization', e)
65+
}
66+
}
67+
68+
React.useEffect(() => {
69+
(async () => {
70+
try {
71+
const token = await getWidgetsAuthToken({ data: { userId, organizationId: organisationId } })
72+
setAuthToken(token)
73+
setLoading(false)
74+
} catch (e: any) {
75+
setError(e?.message ?? 'Failed to get WorkOS token')
76+
setLoading(false)
77+
}
78+
})()
79+
}, [userId, organisationId])
80+
81+
if (loading) return <p>Loading WorkOS…</p>
82+
if (error) return <p className="text-red-600">Error: {error}</p>
83+
if (!authToken) return <p>Could not load WorkOS token.</p>
84+
85+
const extraMenu = showSettingsItem ? (
86+
<>
87+
<DropdownMenu.Separator />
88+
<DropdownMenu.Group>
89+
<DropdownMenu.Item onClick={() => router.navigate({ to: '/dashboard/settings/user' })}>
90+
Settings
91+
</DropdownMenu.Item>
92+
</DropdownMenu.Group>
93+
</>
94+
) : null
95+
96+
if (wrapWithProvider) {
97+
return (
98+
<WorkOsWidgets
99+
// Reset WorkOS full-page layout styles so it fits inside the sidebar
100+
style={{ minHeight: 'auto', height: 'auto', padding: 0, display: 'contents' } as any}
101+
>
102+
<div className="w-full">
103+
<OrganizationSwitcher
104+
authToken={authToken}
105+
organizationLabel={label}
106+
switchToOrganization={({ organizationId }) => handleSwitchToOrganization(organizationId)}
107+
>
108+
{extraMenu}
109+
</OrganizationSwitcher>
110+
</div>
111+
</WorkOsWidgets>
112+
)
113+
}
114+
115+
return (
116+
<OrganizationSwitcher
117+
authToken={authToken}
118+
organizationLabel={label}
119+
switchToOrganization={({ organizationId }) => handleSwitchToOrganization(organizationId)}
120+
>
121+
{extraMenu}
122+
</OrganizationSwitcher>
123+
)
124+
}
125+
126+

ui/src/components/WorkosSettings.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import '@workos-inc/widgets/styles.css';
1414
import '@radix-ui/themes/styles.css';
1515
import CreateOrganizationBtn from './CreateOrganisationButtonWOS';
16+
import WorkosOrgSwitcher from './WorkosOrgSwitcher';
1617

1718

1819
type LoaderData = {

ui/src/lib/env.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import { createServerFn } from "@tanstack/react-start"
55

66

7+
// !IMPORTANT: DO NOT ADD ANYTHING SENSITIVE HERE. THIS IS USED ON THE CLIENT SIDE.
78
export type Env = {
89
PUBLIC_URL: string
910
PUBLIC_HOSTNAME: string
1011
STATESMAN_BACKEND_URL: string
12+
WORKOS_REDIRECT_URI: string
1113
}
1214

1315
export const getPublicServerConfig = createServerFn({ method: 'GET' })
@@ -16,5 +18,6 @@ export const getPublicServerConfig = createServerFn({ method: 'GET' })
1618
PUBLIC_URL: process.env.PUBLIC_URL ?? '',
1719
PUBLIC_HOSTNAME: process.env.PUBLIC_URL?.replace('https://', '').replace('http://', '') ?? '',
1820
STATESMAN_BACKEND_URL: process.env.STATESMAN_BACKEND_URL ?? '',
21+
WORKOS_REDIRECT_URI: process.env.WORKOS_REDIRECT_URI ?? '',
1922
} as Env
2023
})

ui/src/routes/_authenticated/_dashboard.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,45 @@ import { getSignInUrl } from '../../authkit/serverFunctions';
33
import { SidebarProvider, Sidebar, SidebarHeader, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarTrigger } from '@/components/ui/sidebar';
44
import { Link } from '@tanstack/react-router';
55
import { GitBranch, Folders, Waves, Settings, CreditCard, LogOut, Cuboid} from 'lucide-react';
6+
import WorkosOrgSwitcher from '@/components/WorkosOrgSwitcher';
7+
import { WorkOsWidgets } from '@workos-inc/widgets';
68

79
export const Route = createFileRoute('/_authenticated/_dashboard')({
810
component: DashboardComponent,
911
loader: async ({ context }) => {
10-
const { user, organisationName } = context;
11-
return { user, organisationName };
12+
const { user, organisationName, organisationId, publicServerConfig } = context;
13+
return { user, organisationName, organisationId, publicServerConfig };
1214
},
1315
});
1416

1517
function DashboardComponent() {
16-
const { user, organisationName } = Route.useLoaderData();
18+
const { user, organisationName, organisationId, publicServerConfig } = Route.useLoaderData();
19+
const workosEnabled = publicServerConfig.WORKOS_REDIRECT_URI !== '';
1720
const location = useLocation();
1821
return (
1922
<SidebarProvider>
23+
<WorkOsWidgets
24+
style={{ display: 'contents', minHeight: 'auto', height: 'auto' } as any}
25+
theme={{ panelBackground: 'solid', radius: 'none' } as any}
26+
>
2027
<div className="flex h-screen w-full">
2128
<Sidebar>
2229
<SidebarHeader className="text-center">
2330
<h2 className="text-xl font-bold mb-2">🌮 OpenTACO</h2>
2431
<div className="px-4">
2532
<div className="h-[1px] bg-border mb-2" />
26-
<h3>
33+
{!workosEnabled && <h3>
2734
<Link
2835
to="/dashboard/settings/user"
2936
className="text-sm text-muted-foreground hover:text-primary transition-colors duration-200"
3037
>
3138
{organisationName}
3239
</Link>
33-
</h3>
40+
41+
</h3>}
42+
<div className="mt-2" />
43+
{workosEnabled && <WorkosOrgSwitcher userId={user?.id || ''} organisationId={organisationId || ''} showSettingsItem />}
44+
3445
<div className="h-[1px] bg-border mt-2" />
3546
</div>
3647
</SidebarHeader>
@@ -106,6 +117,7 @@ function DashboardComponent() {
106117
</div>
107118
</main>
108119
</div>
120+
</WorkOsWidgets>
109121
</SidebarProvider>
110122
)
111123
};

ui/src/routes/_authenticated/_dashboard/dashboard/repos.index.tsx

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -43,51 +43,70 @@ function RouteComponent() {
4343
<CardDescription>List of repositories Connected to digger and their latest runs</CardDescription>
4444
</CardHeader>
4545
<CardContent>
46-
<Table>
47-
<TableHeader>
48-
<TableRow>
49-
<TableHead>Type</TableHead>
50-
<TableHead>Name</TableHead>
51-
<TableHead>URL</TableHead>
52-
{/* <TableHead>Latest Run</TableHead> */}
53-
<TableHead>Details</TableHead>
54-
</TableRow>
55-
</TableHeader>
56-
<TableBody>
57-
{repos.map((repo : Repo) => {
58-
const Icon = iconMap[repo.vcs]
59-
return (
60-
<TableRow key={repo.id}>
61-
<TableCell>
62-
<Icon className="h-5 w-5" />
63-
</TableCell>
64-
<TableCell>{repo.name}</TableCell>
65-
<TableCell>
66-
<a
67-
href={repo.repo_url}
68-
target="_blank"
69-
rel="noopener noreferrer"
70-
className="text-blue-500 hover:underline"
71-
>
72-
{repo.repo_url}
73-
</a>
74-
</TableCell>
75-
{/* <TableCell>{repo.latestRun}</TableCell> */}
76-
<TableCell>
77-
<Button variant="ghost" asChild>
78-
<Link to="/dashboard/repos/$repoId" params={{ repoId: String(repo.id) }}>
79-
View Details <ExternalLink className="ml-2 h-4 w-4" />
80-
</Link>
81-
</Button>
82-
</TableCell>
46+
{repos.length === 0 ? (
47+
<div className="text-center py-12">
48+
<div className="inline-flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 mb-4">
49+
<Github className="h-6 w-6 text-primary" />
50+
</div>
51+
<h2 className="text-lg font-semibold mb-2">No Repositories Connected</h2>
52+
<p className="text-muted-foreground max-w-sm mx-auto mb-6">
53+
Connect your first repository to start running Terraform with Digger.
54+
</p>
55+
<Button asChild>
56+
<Link to="/dashboard/onboarding">
57+
Connect your first repository <PlusCircle className="ml-2 h-4 w-4" />
58+
</Link>
59+
</Button>
60+
</div>
61+
) : (
62+
<>
63+
<Table>
64+
<TableHeader>
65+
<TableRow>
66+
<TableHead>Type</TableHead>
67+
<TableHead>Name</TableHead>
68+
<TableHead>URL</TableHead>
69+
{/* <TableHead>Latest Run</TableHead> */}
70+
<TableHead>Details</TableHead>
8371
</TableRow>
84-
)
85-
})}
86-
</TableBody>
87-
</Table>
88-
<div className="mt-4">
89-
<ConnectMoreRepositoriesButton />
90-
</div>
72+
</TableHeader>
73+
<TableBody>
74+
{repos.map((repo : Repo) => {
75+
const Icon = iconMap[repo.vcs]
76+
return (
77+
<TableRow key={repo.id}>
78+
<TableCell>
79+
<Icon className="h-5 w-5" />
80+
</TableCell>
81+
<TableCell>{repo.name}</TableCell>
82+
<TableCell>
83+
<a
84+
href={repo.repo_url}
85+
target="_blank"
86+
rel="noopener noreferrer"
87+
className="text-blue-500 hover:underline"
88+
>
89+
{repo.repo_url}
90+
</a>
91+
</TableCell>
92+
{/* <TableCell>{repo.latestRun}</TableCell> */}
93+
<TableCell>
94+
<Button variant="ghost" asChild>
95+
<Link to="/dashboard/repos/$repoId" params={{ repoId: String(repo.id) }}>
96+
View Details <ExternalLink className="ml-2 h-4 w-4" />
97+
</Link>
98+
</Button>
99+
</TableCell>
100+
</TableRow>
101+
)
102+
})}
103+
</TableBody>
104+
</Table>
105+
<div className="mt-4">
106+
<ConnectMoreRepositoriesButton />
107+
</div>
108+
</>
109+
)}
91110
</CardContent>
92111
</Card>
93112
<Outlet />

ui/src/routes/_authenticated/_dashboard/dashboard/units.$unitId.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ function RouteComponent() {
317317
ID: {unit.id}
318318
</CardDescription>
319319
<CardDescription>
320-
Version {unit.version}Last updated {formatDate(unit.updated)}{formatBytes(unit.size)}
320+
Last updated {formatDate(unit.updated)}{formatBytes(unit.size)}
321321
</CardDescription>
322322
</CardHeader>
323323
</Card>

0 commit comments

Comments
 (0)