Skip to content

Commit ca4da58

Browse files
authored
feat/onboading flow (#2380)
* make create modal prettier * ui improvements * redirect to unit details on creation * add last updated * actually we dont want to redirect * first onnoarding steps * complete onnoarding flow * handle 0 units case * increase spacing above * add copy button for diggeryml
1 parent 2ac9a84 commit ca4da58

File tree

8 files changed

+667
-285
lines changed

8 files changed

+667
-285
lines changed

action.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ inputs:
8484
description: Setup OpenToFu
8585
required: false
8686
default: "false"
87+
opentofu-tfe-token:
88+
description: the cli credentials token to use with opentofu
89+
required: false
90+
default: ""
91+
opentofu-tfe-hostname:
92+
description: the cli hostname auth to use with opentofu
93+
required: false
94+
default: ""
95+
8796
setup-pulumi:
8897
description: Setup Pulumi
8998
required: false
@@ -109,6 +118,15 @@ inputs:
109118
description: Terraform version
110119
required: false
111120
default: v1.5.5
121+
terraform-tfe-token:
122+
description: the cli credentials token to use with opentofu
123+
required: false
124+
default: ""
125+
terraform-tfe-hostname:
126+
description: the cli hostname auth to use with opentofu
127+
required: false
128+
default: ""
129+
112130
configure-checkout:
113131
description: Configure checkout. Beware that this will overwrite any changes in the working directory
114132
required: false
@@ -392,6 +410,8 @@ runs:
392410
with:
393411
terraform_version: ${{ inputs.terraform-version }}
394412
terraform_wrapper: false
413+
cli_config_credentials_token: ${{ inputs.terraform-tfe-token || '' }}
414+
cli_config_credentials_hostname: ${{ inputs.terraform-tfe-hostname || 'otaco.app' }}
395415
if: inputs.setup-terraform == 'true'
396416

397417
- name: Setup tfenv
@@ -409,6 +429,8 @@ runs:
409429
with:
410430
tofu_version: ${{ inputs.opentofu-version }}
411431
tofu_wrapper: false
432+
cli_config_credentials_token: ${{ inputs.opentofu-tfe-token || '' }}
433+
cli_config_credentials_hostname: ${{ inputs.opentofu-tfe-hostname || 'otaco.app' }}
412434
if: inputs.setup-opentofu == 'true'
413435

414436
- name: Setup Pulumi

ui/src/components/OnboardingSteps.tsx

Lines changed: 291 additions & 67 deletions
Large diffs are not rendered by default.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import * as React from 'react'
2+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
3+
import { Button } from '@/components/ui/button'
4+
import { Label } from '@/components/ui/label'
5+
import { Copy, Check, Github, TerminalSquare } from 'lucide-react'
6+
7+
function CopyButton({ content }: { content: string }) {
8+
const [copied, setCopied] = React.useState(false)
9+
const copy = () => {
10+
navigator.clipboard.writeText(content)
11+
setCopied(true)
12+
setTimeout(() => setCopied(false), 1500)
13+
}
14+
return (
15+
<Button size="icon" variant="ghost" className="absolute top-2 right-2 h-8 w-8" onClick={copy}>
16+
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
17+
</Button>
18+
)
19+
}
20+
21+
type Props = {
22+
unitId: string
23+
organisationId: string
24+
publicHostname: string
25+
onGoToGithub?: () => void
26+
onGoToLocal?: () => void
27+
showNextActions?: boolean
28+
}
29+
30+
export default function UnitConfigureInstructions({ unitId, organisationId, publicHostname, onGoToGithub, onGoToLocal, showNextActions = true }: Props) {
31+
const tfBlock = `terraform {\n cloud {\n hostname = "${publicHostname}"\n organization = "${organisationId}" \n workspaces {\n name = "${unitId}"\n }\n }\n}`
32+
return (
33+
<>
34+
<Card>
35+
<CardHeader>
36+
<CardTitle>Terraform Configuration</CardTitle>
37+
<CardDescription>Add this configuration block to your Terraform code to use this unit</CardDescription>
38+
</CardHeader>
39+
<CardContent>
40+
<div className="mb-4">
41+
<p className="text-sm text-muted-foreground mb-4">
42+
To use this unit in your Terraform configuration, add the following block to your Terraform code:
43+
</p>
44+
<div className="relative">
45+
<pre className="bg-muted p-4 rounded-lg overflow-x-auto font-mono text-sm">{tfBlock}</pre>
46+
<CopyButton content={tfBlock} />
47+
</div>
48+
</div>
49+
50+
<div className="space-y-6">
51+
<div>
52+
<h3 className="font-semibold mb-2">1. Login to the remote backend</h3>
53+
<p className="text-sm text-muted-foreground mb-2">First, authenticate with the remote backend:</p>
54+
<div className="relative">
55+
<pre className="bg-muted p-4 rounded-lg overflow-x-auto font-mono text-sm">terraform login {publicHostname}</pre>
56+
<CopyButton content={`terraform login ${publicHostname}`} />
57+
</div>
58+
</div>
59+
60+
<div>
61+
<h3 className="font-semibold mb-2">2. Initialize Terraform</h3>
62+
<p className="text-sm text-muted-foreground mb-2">After adding the configuration block above, initialize your working directory:</p>
63+
<div className="relative">
64+
<pre className="bg-muted p-4 rounded-lg overflow-x-auto font-mono text-sm">terraform init</pre>
65+
<CopyButton content="terraform init" />
66+
</div>
67+
</div>
68+
69+
<div>
70+
<h3 className="font-semibold mb-2">3. Review Changes</h3>
71+
<p className="text-sm text-muted-foreground mb-2">Preview any changes that will be made to your infrastructure:</p>
72+
<div className="relative">
73+
<pre className="bg-muted p-4 rounded-lg overflow-x-auto font-mono text-sm">terraform plan</pre>
74+
<CopyButton content="terraform plan" />
75+
</div>
76+
</div>
77+
78+
<div>
79+
<h3 className="font-semibold mb-2">4. Apply Changes</h3>
80+
<p className="text-sm text-muted-foreground mb-2">Apply the changes to your infrastructure:</p>
81+
<div className="relative">
82+
<pre className="bg-muted p-4 rounded-lg overflow-x-auto font-mono text-sm">terraform apply</pre>
83+
<CopyButton content="terraform apply" />
84+
</div>
85+
</div>
86+
87+
<div className="mt-6 bg-blue-50 dark:bg-blue-950 p-4 rounded-lg">
88+
<h3 className="font-semibold text-blue-700 dark:text-blue-300 mb-2">Note</h3>
89+
<p className="text-sm text-blue-600 dark:text-blue-400">
90+
After completing these steps, your Terraform state will be managed by this unit. All state operations will be automatically versioned and you can roll back to previous versions if needed.
91+
</p>
92+
</div>
93+
94+
{showNextActions && (
95+
<div className="pt-4">
96+
<Label className="text-right">What would you like to do next?</Label>
97+
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
98+
<div
99+
onClick={onGoToLocal}
100+
className={
101+
`relative flex cursor-pointer items-start gap-4 rounded-lg border p-4 md:p-5 transition-colors hover:bg-muted/50 border-muted`
102+
}
103+
>
104+
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-muted-foreground">
105+
<TerminalSquare className="h-5 w-5" />
106+
</div>
107+
<div className="space-y-1">
108+
<div className="flex items-center gap-2">
109+
<span className="text-base font-semibold">Run locally for now</span>
110+
</div>
111+
<p className="text-sm text-muted-foreground">
112+
Stick to local <code className="font-mono">terraform plan</code>/<code className="font-mono">apply</code>
113+
using the configuration above. You can enable PR automation later.
114+
</p>
115+
</div>
116+
</div>
117+
118+
<div
119+
onClick={onGoToGithub}
120+
className={
121+
`relative flex cursor-pointer items-start gap-4 rounded-lg border p-4 md:p-5 transition-colors hover:bg-muted/50 border-muted`
122+
}
123+
>
124+
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10 text-primary">
125+
<Github className="h-5 w-5" />
126+
</div>
127+
<div className="space-y-1">
128+
<div className="flex items-center gap-2">
129+
<span className="text-base font-semibold">Use PR automation</span>
130+
</div>
131+
<p className="text-sm text-muted-foreground">
132+
Create a pull request that touches your Terraform directories. Digger will
133+
comment with plans and manage runs automatically.
134+
</p>
135+
</div>
136+
</div>
137+
</div>
138+
</div>
139+
)}
140+
</div>
141+
</CardContent>
142+
</Card>
143+
</>
144+
)
145+
}
146+
147+
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import * as React from 'react'
2+
import { Input } from '@/components/ui/input'
3+
import { Label } from '@/components/ui/label'
4+
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
5+
import { Badge } from '@/components/ui/badge'
6+
import { Button } from '@/components/ui/button'
7+
import { createUnitFn } from '@/api/statesman_serverFunctions'
8+
import { Cloud, HardDrive } from 'lucide-react'
9+
10+
type UnitCreateFormProps = {
11+
userId: string
12+
email: string
13+
organisationId: string
14+
onCreated: (unit: { id: string; name: string }) => void
15+
onBringOwnState: () => void
16+
showBringOwnState?: boolean
17+
}
18+
19+
export default function UnitCreateForm({ userId, email, organisationId, onCreated, onBringOwnState, showBringOwnState = true }: UnitCreateFormProps) {
20+
const [unitName, setUnitName] = React.useState('')
21+
const [unitType, setUnitType] = React.useState<'local' | 'remote'>('local')
22+
const [isCreating, setIsCreating] = React.useState(false)
23+
const [error, setError] = React.useState<string | null>(null)
24+
25+
const handleCreate = async () => {
26+
if (!unitName.trim()) return
27+
setIsCreating(true)
28+
setError(null)
29+
try {
30+
const unit = await createUnitFn({
31+
data: {
32+
userId,
33+
organisationId,
34+
email,
35+
name: unitName.trim(),
36+
},
37+
})
38+
onCreated({ id: unit.id, name: unit.name })
39+
} catch (e: any) {
40+
setError(e?.message ?? 'Failed to create unit')
41+
} finally {
42+
setIsCreating(false)
43+
}
44+
}
45+
46+
return (
47+
<div className="space-y-4">
48+
<div>
49+
<Label htmlFor="unit-name">Unit Name</Label>
50+
<Input
51+
id="unit-name"
52+
value={unitName}
53+
onChange={(e) => setUnitName(e.target.value)}
54+
onKeyDown={(e) => {
55+
if (e.key === 'Enter') {
56+
e.preventDefault()
57+
handleCreate()
58+
}
59+
}}
60+
placeholder="my-terraform-state"
61+
className="mt-1"
62+
/>
63+
</div>
64+
65+
<div>
66+
<Label className="text-right">Unit Type</Label>
67+
<RadioGroup
68+
value={unitType}
69+
onValueChange={(v) => setUnitType(v as 'local' | 'remote')}
70+
className="mt-3 grid grid-cols-1 gap-3"
71+
>
72+
<label
73+
htmlFor="unit-type-local"
74+
className={`relative flex cursor-pointer items-start gap-4 rounded-lg border p-4 md:p-5 transition-colors hover:bg-muted/50 ${unitType === 'local' ? 'ring-2 ring-primary border-primary' : 'border-muted'}`}
75+
onClick={() => setUnitType('local')}
76+
>
77+
<RadioGroupItem id="unit-type-local" value="local" className="sr-only" />
78+
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10 text-primary">
79+
<HardDrive className="h-5 w-5" />
80+
</div>
81+
<div className="space-y-1">
82+
<div className="flex items-center gap-2">
83+
<span className="text-base font-semibold">Local</span>
84+
</div>
85+
<p className="text-sm text-muted-foreground">
86+
Mainly using units as states manager. Useful for teams that want full
87+
control and integrating pipelines with own CI.
88+
</p>
89+
</div>
90+
</label>
91+
92+
<label
93+
htmlFor="unit-type-remote"
94+
className={`relative flex cursor-not-allowed items-start gap-4 rounded-lg border p-4 md:p-5 opacity-60 bg-muted/30`}
95+
>
96+
<RadioGroupItem id="unit-type-remote" value="remote" disabled className="sr-only" />
97+
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-muted-foreground">
98+
<Cloud className="h-5 w-5" />
99+
</div>
100+
<div className="space-y-1">
101+
<div className="flex items-center gap-2">
102+
<span className="text-base font-semibold">Remote</span>
103+
<Badge variant="secondary">Coming soon</Badge>
104+
</div>
105+
<p className="text-sm text-muted-foreground">
106+
Fully managed terraform runs. Run terraform locally and stream logs from
107+
remote runs. Best for teams that want seamless automation for their
108+
terraform runs without much configuration.
109+
</p>
110+
</div>
111+
</label>
112+
</RadioGroup>
113+
</div>
114+
115+
{error && <p className="text-sm text-destructive">{error}</p>}
116+
{showBringOwnState ? (
117+
<div className="flex items-center justify-between">
118+
<Button variant="ghost" type="button" onClick={onBringOwnState}>
119+
I want to bring my own state
120+
</Button>
121+
<Button onClick={handleCreate} disabled={!unitName.trim() || isCreating}>
122+
{isCreating ? 'Creating...' : 'Create Unit'}
123+
</Button>
124+
</div>
125+
) : (
126+
<div className="flex items-center justify-end">
127+
<Button onClick={handleCreate} disabled={!unitName.trim() || isCreating}>
128+
{isCreating ? 'Creating...' : 'Create Unit'}
129+
</Button>
130+
</div>
131+
)}
132+
</div>
133+
)
134+
}
135+
136+

ui/src/routes/_authenticated/_dashboard/dashboard/onboarding.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ export const Route = createFileRoute(
1414
'/_authenticated/_dashboard/dashboard/onboarding',
1515
)({
1616
component: RouteComponent,
17+
loader: async ({ context }) => {
18+
const { user, organisationId, publicServerConfig } = context
19+
const publicHostname = publicServerConfig?.PUBLIC_HOSTNAME || ''
20+
return { user, organisationId, publicHostname }
21+
},
1722
})
1823

1924
function RouteComponent() {
25+
const { user, organisationId, publicHostname } = Route.useLoaderData()
2026
const [repoInfo, setRepoInfo] = useState<RepoInfo | null>(null)
2127
const router = useRouter()
2228
const handleOnboardingComplete = () => {
23-
router.navigate({ to: "/dashboard/repos" })
29+
router.navigate({ to: "/dashboard/units" })
2430
}
2531

2632
return (
@@ -36,10 +42,20 @@ function RouteComponent() {
3642
<OnboardingSteps
3743
repoName={repoInfo.name}
3844
repoOwner={repoInfo.owner}
45+
userId={user?.id || ''}
46+
email={user?.email || ''}
47+
organisationId={organisationId || ''}
48+
publicHostname={publicHostname}
3949
onComplete={handleOnboardingComplete}
4050
/>
4151
) : (
42-
<OnboardingSteps onComplete={handleOnboardingComplete} />
52+
<OnboardingSteps
53+
userId={user?.id || ''}
54+
email={user?.email || ''}
55+
organisationId={organisationId || ''}
56+
publicHostname={publicHostname}
57+
onComplete={handleOnboardingComplete}
58+
/>
4359
)}
4460
</div>
4561
)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function RouteComponent() {
5353
Connect your first repository to start running Terraform with Digger.
5454
</p>
5555
<Button asChild>
56-
<Link to="/dashboard/onboarding">
56+
<Link to="/dashboard/onboarding" search={{ step: 'github' } as any}>
5757
Connect your first repository <PlusCircle className="ml-2 h-4 w-4" />
5858
</Link>
5959
</Button>
@@ -117,7 +117,7 @@ function RouteComponent() {
117117
const ConnectMoreRepositoriesButton = () => {
118118
return (
119119
<Button variant="ghost" asChild>
120-
<Link to="/dashboard/onboarding">
120+
<Link to="/dashboard/onboarding" search={{ step: 'github' } as any}>
121121
Connect More Repositories <PlusCircle className="ml-2 h-4 w-4" />
122122
</Link>
123123
</Button>

0 commit comments

Comments
 (0)