Skip to content

Commit 519a74c

Browse files
authored
drift (#2315)
* drift * add workos stuff * select it from the list
1 parent 87294ba commit 519a74c

23 files changed

+956
-114
lines changed

ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@tanstack/react-router-devtools": "^1.132.51",
4747
"@tanstack/react-start": "^1.132.51",
4848
"@workos-inc/node": "^7.45.0",
49+
"@workos-inc/widgets": "^1.4.2",
4950
"class-variance-authority": "^0.7.1",
5051
"clsx": "^2.1.1",
5152
"cmdk": "^1.1.1",

ui/src/api/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export async function updateOrgSettings(
134134
slack_channel_name?: string,
135135
}
136136
) {
137-
const response = await fetch(`${process.env.ORCHESTRATOR_ORCHESTRATOR_BACKEND_URL}/api/orgs/settings`, {
137+
const response = await fetch(`${process.env.ORCHESTRATOR_BACKEND_URL}/api/orgs/settings`, {
138138
method: 'PUT',
139139
headers: {
140140
'Authorization': `Bearer ${process.env.ORCHESTRATOR_BACKEND_SECRET}`,

ui/src/api/backend.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,29 @@ export async function fetchRepoDetails(repoId: string, organisationId: string, u
7070
'DIGGER_ORG_SOURCE': 'workos',
7171
},
7272
});
73-
}
73+
}
74+
75+
76+
77+
export async function testSlackWebhook(
78+
notification_url: string
79+
) {
80+
const response = await fetch(`${process.env.DRIFT_REPORTING_BACKEND_URL}/_internal/send_test_slack_notification_for_url`, {
81+
method: 'POST',
82+
headers: {
83+
'Authorization': `Bearer ${process.env.DRIFT_REPORTING_BACKEND_WEBHOOK_SECRET}`,
84+
},
85+
body: JSON.stringify({
86+
notification_url: notification_url
87+
})
88+
})
89+
90+
console.log(response)
91+
if (!response.ok) {
92+
const text = await response.text()
93+
console.log(text)
94+
throw new Error('Failed to test Slack webhook')
95+
}
96+
97+
return response.text()
98+
}

ui/src/api/helpers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
function redirectWithFallback(redirectUri: string, headers?: Headers) {
2+
const newHeaders = headers ? new Headers(headers) : new Headers();
3+
newHeaders.set('Location', redirectUri);
4+
5+
return new Response(null, { status: 307, headers: newHeaders });
6+
}
7+
8+
function errorResponseWithFallback(errorBody: { error: { message: string; description: string } }) {
9+
return new Response(JSON.stringify(errorBody), {
10+
status: 500,
11+
headers: { 'Content-Type': 'application/json' },
12+
});
13+
}
14+
15+
export { redirectWithFallback, errorResponseWithFallback };

ui/src/api/server_functions.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,38 @@
11
import { createServerFn } from "@tanstack/react-start";
2-
import { Job, Repo } from './types'
3-
import { fetchRepos } from "./backend";
4-
import { fetchProjects, updateProject } from "./api";
2+
import { Job, OrgSettings, Project, Repo } from './types'
3+
import { fetchRepos, testSlackWebhook } from "./backend";
4+
import { fetchProject, fetchProjects, getOrgSettings, updateOrgSettings, updateProject } from "./api";
55

6+
export const getOrgSettingsFn = createServerFn({method: 'GET'})
7+
.inputValidator((data : {userId: string, organisationId: string}) => data)
8+
.handler(async ({ data }) => {
9+
const settings : any = await getOrgSettings(data.organisationId, data.userId)
10+
return settings
11+
})
612

13+
export const updateOrgSettingsFn = createServerFn({method: 'POST'})
14+
.inputValidator((data : {userId: string, organisationId: string, settings: OrgSettings}) => data)
15+
.handler(async ({ data }) => {
16+
const settings : any = await updateOrgSettings(data.organisationId, data.userId, data.settings)
17+
return settings.result
18+
})
719

820
export const getProjectsFn = createServerFn({method: 'GET'})
9-
.inputValidator((data : {userId: string, organisationId: string}) => data)
21+
.inputValidator((data : {userId: string, organisationId: string}) => {
22+
if (!data.userId || !data.organisationId) {
23+
throw new Error('Missing required fields: userId and organisationId are required')
24+
}
25+
return data
26+
})
1027
.handler(async ({ data }) => {
11-
const projects : any = await fetchProjects(data.organisationId, data.userId)
12-
return projects.result
13-
})
28+
try {
29+
const projects : any = await fetchProjects(data.organisationId, data.userId)
30+
return projects.result || []
31+
} catch (error) {
32+
console.error('Error in getProjectsFn:', error)
33+
return []
34+
}
35+
})
1436

1537

1638
export const updateProjectFn = createServerFn({method: 'POST'})
@@ -34,6 +56,13 @@ export const getReposFn = createServerFn({method: 'GET'})
3456
return repos
3557
})
3658

59+
export const getProjectFn = createServerFn({method: 'GET'})
60+
.inputValidator((data : {projectId: string, organisationId: string, userId: string}) => data)
61+
.handler(async ({ data }) => {
62+
const project : any = await fetchProject(data.projectId, data.organisationId, data.userId)
63+
return project
64+
})
65+
3766

3867
export const getRepoDetailsFn = createServerFn({method: 'GET'})
3968
.inputValidator((data : {repoId: string, organisationId: string, userId: string}) => data)
@@ -68,3 +97,19 @@ export const getRepoDetailsFn = createServerFn({method: 'GET'})
6897

6998
return { repo, allJobs }
7099
})
100+
101+
export const switchToOrganizationFn = createServerFn({method: 'POST'})
102+
.inputValidator((data : { organisationId: string, pathname: string}) => data)
103+
.handler(async ({ data }) => {
104+
return null
105+
})
106+
107+
108+
109+
export const testSlackWebhookFn = createServerFn({method: 'POST'})
110+
.inputValidator((data : {notification_url: string}) => data)
111+
.handler(async ({ data }) => {
112+
const response : any = await testSlackWebhook(data.notification_url)
113+
return response
114+
})
115+

ui/src/api/slack_actions.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
"use server";
2-
3-
import { updateOrgSettings } from "./api";
4-
51
// helpers/slack.ts
62
// ──────────────────────────────────────────────────────────────────────────────
73
// CONFIG

ui/src/api/types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ export interface Organisation {
99
ExternalId: string
1010
}
1111

12-
12+
export interface OrgSettings {
13+
drift_webhook_url: string
14+
drift_enabled: boolean
15+
drift_cron_tab: string
16+
}
1317

1418

1519
export interface Project {
@@ -19,8 +23,10 @@ export interface Project {
1923
repo_full_name: string
2024
drift_enabled: boolean
2125
drift_status: string
26+
latest_drift_check: string | null
27+
drift_terraform_plan: string | null
2228
}
23-
29+
2430

2531
export interface Repo {
2632
id: number

ui/src/authkit/serverFunctions.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { getConfig } from './ssr/config';
44
import { terminateSession, withAuth } from './ssr/session';
55
import { getWorkOS } from './ssr/workos';
66
import type { GetAuthURLOptions, NoUserInfo, UserInfo } from './ssr/interfaces';
7+
import { Organization } from '@workos-inc/node';
8+
import { WidgetScope } from 'node_modules/@workos-inc/node/lib/widgets/interfaces/get-token';
79

810
export const getAuthorizationUrl = createServerFn({ method: 'GET' })
911
.inputValidator((options?: GetAuthURLOptions) => options)
@@ -19,6 +21,32 @@ export const getAuthorizationUrl = createServerFn({ method: 'GET' })
1921
});
2022
});
2123

24+
export const getOrganisationDetails = createServerFn({method: 'GET'})
25+
.inputValidator((data: {organizationId: string}) => data)
26+
.handler(async ({data: {organizationId}}) : Promise<Organization> => {
27+
return getWorkOS().organizations.getOrganization(organizationId).then(organization => organization);
28+
});
29+
30+
31+
export const createOrganization = createServerFn({method: 'POST'})
32+
.inputValidator((data: {name: string, userId: string}) => data)
33+
.handler(async ({data: {name, userId}}) : Promise<Organization> => {
34+
try {
35+
const organization = await getWorkOS().organizations.createOrganization({ name: name });
36+
37+
await getWorkOS().userManagement.createOrganizationMembership({
38+
organizationId: organization.id,
39+
userId: userId,
40+
roleSlug: "admin",
41+
});
42+
43+
return organization;
44+
} catch (error) {
45+
console.error('Error creating organization:', error);
46+
throw error;
47+
}
48+
});
49+
2250
export const getSignInUrl = createServerFn({ method: 'GET' })
2351
.inputValidator((data?: string) => data)
2452
.handler(async ({ data: returnPathname }) => {
@@ -44,3 +72,27 @@ export const getAuth = createServerFn({ method: 'GET' }).handler(async (): Promi
4472
const organisationId = auth.organizationId!;
4573
return {auth, organisationId};
4674
});
75+
76+
export const getOrganization = createServerFn({method: 'GET'})
77+
.inputValidator((data: {organizationId: string}) => data)
78+
.handler(async ({data: {organizationId}}) : Promise<Organization> => {
79+
return getWorkOS().organizations.getOrganization(organizationId);
80+
});
81+
82+
export const ensureOrgExists = createServerFn({method: 'GET'})
83+
.inputValidator((data: {organizationId: string}) => data)
84+
.handler(async ({data: {organizationId}}) : Promise<Organization> => {
85+
return getWorkOS().organizations.getOrganization(organizationId);
86+
});
87+
88+
89+
export const getWidgetsAuthToken = createServerFn({method: 'GET'})
90+
.inputValidator((args: {userId: string, organizationId: string, scopes?: WidgetScope[]}) => args)
91+
.handler(async ({data: {userId, organizationId, scopes}}) : Promise<string> => {
92+
return getWorkOS().widgets.getToken({
93+
userId: userId,
94+
organizationId: organizationId,
95+
scopes: scopes ?? ['widgets:users-table:manage'] as WidgetScope[],
96+
});
97+
})
98+
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { createOrganization } from "@/authkit/serverFunctions";
2+
import { Button } from "@/components/ui/button";
3+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
4+
import { Input } from "@/components/ui/input";
5+
import { Label } from "@/components/ui/label";
6+
import { useState } from "react";
7+
import { useToast } from "@/hooks/use-toast";
8+
9+
10+
export default function CreateOrganizationBtn({ userId }: { userId: string }) {
11+
const [name, setName] = useState("");
12+
const [open, setOpen] = useState(false);
13+
const { toast } = useToast();
14+
async function handleSubmit(e: React.FormEvent) {
15+
e.preventDefault();
16+
17+
try {
18+
const organization = await createOrganization({ data: { name: name, userId: userId } });
19+
toast({
20+
title: "Organization created",
21+
description: "The page will now reload to refresh organisations list. To use this new organization, select it from the list.",
22+
duration: 5000,
23+
variant: "default"
24+
});
25+
setOpen(false);
26+
window.setTimeout(() => {
27+
window.location.reload();
28+
}, 5000);
29+
} catch (error) {
30+
console.error("Failed to create organization:", error);
31+
}
32+
33+
}
34+
35+
return (
36+
<Dialog open={open} onOpenChange={setOpen}>
37+
<DialogTrigger asChild>
38+
<Button onClick={() => setOpen(true)} variant="outline" className="w-full">Create New Organization</Button>
39+
</DialogTrigger>
40+
<DialogContent>
41+
<DialogHeader>
42+
<DialogTitle>Create New Organization</DialogTitle>
43+
<DialogDescription>
44+
Create a new organization to collaborate with your team.
45+
</DialogDescription>
46+
</DialogHeader>
47+
<form onSubmit={handleSubmit} className="space-y-4">
48+
<div>
49+
<Label htmlFor="name">Organization Name</Label>
50+
<Input
51+
id="name"
52+
value={name}
53+
onChange={(e) => setName(e.target.value)}
54+
placeholder="Enter organization name"
55+
required
56+
/>
57+
</div>
58+
<Button type="submit">Create</Button>
59+
</form>
60+
</DialogContent>
61+
</Dialog>
62+
);
63+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import * as React from 'react';
2+
import { createFileRoute } from '@tanstack/react-router';
3+
import { getWidgetsAuthToken } from '@/authkit/serverFunctions';
4+
5+
import {
6+
OrganizationSwitcher,
7+
UserProfile,
8+
UserSecurity,
9+
WorkOsWidgets,
10+
UsersManagement,
11+
} from '@workos-inc/widgets';
12+
13+
import '@workos-inc/widgets/styles.css';
14+
import '@radix-ui/themes/styles.css';
15+
import CreateOrganizationBtn from './CreateOrganisationButtonWOS';
16+
17+
type LoaderData = {
18+
organisationId: string;
19+
role: 'admin' | 'member' | string;
20+
// You can also supply userId from your auth loader if you want
21+
userId: string;
22+
};
23+
24+
25+
26+
27+
type WorkosSettingsProps = {
28+
userId: string;
29+
organisationId: string;
30+
role: 'admin' | 'member' | string;
31+
};
32+
33+
export function WorkosSettings({ userId, organisationId, role }: WorkosSettingsProps) {
34+
const [authToken, setAuthToken] = React.useState<string | null>(null);
35+
const [error, setError] = React.useState<string | null>(null);
36+
const [loading, setLoading] = React.useState(true);
37+
38+
React.useEffect(() => {
39+
(async () => {
40+
try {
41+
const authToken = await getWidgetsAuthToken({ data: { userId, organizationId: organisationId } });
42+
setAuthToken(authToken);
43+
setLoading(false);
44+
} catch (e: any) {
45+
setError(e?.message ?? 'Failed to get WorkOS token');
46+
setLoading(false);
47+
}
48+
})();
49+
}, [userId, organisationId]);
50+
51+
52+
if (loading) return <p>Loading WorkOS…</p>;
53+
if (error) return <p className="text-red-600">Error: {error}</p>;
54+
if (!authToken) return <p>Could not load WorkOS token.</p>;
55+
56+
return (
57+
<div className="max-w-3xl mx-auto py-6">
58+
<WorkOsWidgets>
59+
<OrganizationSwitcher
60+
authToken={authToken}
61+
organizationLabel="My Orgs"
62+
switchToOrganization={async ({ organizationId }) => {
63+
// Call your own server action if needed
64+
}}
65+
/>
66+
<div className="h-4" />
67+
{/* Add your org creation UI here */}
68+
<CreateOrganizationBtn userId={userId} />
69+
<div className="h-4" />
70+
<UserProfile authToken={authToken} />
71+
<div className="h-4" />
72+
<UserSecurity authToken={authToken} />
73+
<div className="h-4" />
74+
{role === 'admin' && <UsersManagement authToken={authToken} />}
75+
</WorkOsWidgets>
76+
</div>
77+
);
78+
}

0 commit comments

Comments
 (0)