From 29aa99a46275bd55fe8eb5d5188074f9b0a34f05 Mon Sep 17 00:00:00 2001
From: psiddharthdesign <107192927+psiddharthdesign@users.noreply.github.com>
Date: Mon, 29 Jul 2024 23:28:16 +0530
Subject: [PATCH] feature / status labels and urls
---
.../(specific-project-pages)/AllRunsTable.tsx | 24 +-
.../runs/[runId]/ProjectRunDetails.tsx | 219 ++++++++----------
.../[projectSlug]/runs/[runId]/page.tsx | 16 +-
src/data/user/projects.tsx | 2 +-
src/data/user/runs.ts | 41 +++-
src/lib/utils.ts | 20 +-
6 files changed, 169 insertions(+), 153 deletions(-)
diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/AllRunsTable.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/AllRunsTable.tsx
index d446ced5..eaa1d7e4 100644
--- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/AllRunsTable.tsx
+++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/AllRunsTable.tsx
@@ -1,40 +1,32 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tables } from "@/lib/database.types";
+import { ToSnakeCase } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import { Activity } from "lucide-react";
import moment from "moment";
import Link from "next/link";
-import { runStageEnum } from "./enums";
export type StatusColor = {
[key: string]: string;
};
-const statusOrder = [
- runStageEnum.running,
- runStageEnum.queued,
- runStageEnum.pending_approval,
- runStageEnum.pending_apply,
- runStageEnum.succeeded,
- runStageEnum.rejected,
- runStageEnum.failed
-];
-
export const statusColors: StatusColor = {
queued: 'bg-yellow-200/50 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-200',
+ pending_plan: 'bg-amber-200/50 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200',
+ running_plan: 'bg-purple-200/50 text-purple-800 dark:bg-purple-900/50 dark:text-purple-200',
pending_approval: 'bg-blue-200/50 text-blue-800 dark:bg-blue-900/50 dark:text-blue-200',
- running: 'bg-purple-200/50 text-purple-800 dark:bg-purple-900/50 dark:text-purple-200',
+ approved: 'bg-green-200/50 text-green-800 dark:bg-green-900/50 dark:text-green-200',
pending_apply: 'bg-green-200/50 text-green-800 dark:bg-green-900/50 dark:text-green-200',
+ running_apply: 'bg-teal-200/50 text-teal-800 dark:bg-teal-900/50 dark:text-teal-200',
succeeded: 'bg-green-200/50 text-green-800 dark:bg-green-900/50 dark:text-green-200',
failed: 'bg-red-200/50 text-red-800 dark:bg-red-900/50 dark:text-red-200',
+ discarded: 'bg-neutral-200 text-neutral-800 dark:bg-neutral-950 dark:text-neutral-200',
};
export const AllRunsTable = ({ runs, projectSlug }: { runs: Tables<'digger_runs'>[], projectSlug: string }) => {
const sortedRuns = [...runs].sort((a, b) => {
- const statusA = statusOrder.indexOf(a.status.toLowerCase() as runStageEnum);
- const statusB = statusOrder.indexOf(b.status.toLowerCase() as runStageEnum);
- if (statusA !== statusB) return statusA - statusB;
+ // Sort primarily by created_at in descending order (most recent first)
return moment(b.created_at).valueOf() - moment(a.created_at).valueOf();
});
@@ -69,7 +61,7 @@ export const AllRunsTable = ({ runs, projectSlug }: { runs: Tables<'digger_runs'
{run.commit_id}
-
+
{run.status.toUpperCase()}
diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/ProjectRunDetails.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/ProjectRunDetails.tsx
index fcc0cb34..b0659000 100644
--- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/ProjectRunDetails.tsx
+++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/ProjectRunDetails.tsx
@@ -4,118 +4,67 @@ import { T } from "@/components/ui/Typography";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
-import { approveRun, changeRunStatus, rejectRun } from "@/data/user/runs";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { approveRun, rejectRun } from "@/data/user/runs";
import { useSAToastMutation } from "@/hooks/useSAToastMutation";
+import { ToSnakeCase, ToTitleCase } from "@/lib/utils";
import { Table } from "@/types";
import { DotFilledIcon } from "@radix-ui/react-icons";
import { AnimatePresence, motion } from 'framer-motion';
-import { CheckCircle2, Clock, GitPullRequest, Loader2, Play, XCircle } from 'lucide-react';
+import { CheckCircle2, Clock, GitPullRequest, LinkIcon, Loader2, Play, XCircle } from 'lucide-react';
import Image from 'next/image';
+import Link from "next/link";
import { useRouter } from 'next/navigation';
-import React, { useEffect, useState } from 'react';
+import React, { useState } from 'react';
import { statusColors } from "../../(specific-project-pages)/AllRunsTable";
-const logEntries = [
- { time: "2024-07-01T12:34:56Z", author: "Siddharth Ponnapalli", title: "Added basic VPC setup" },
- { time: "2024-07-01T13:45:22Z", author: "Siddharth Ponnapalli", title: "Added subnets and associated them with VPC" },
- { time: "2024-07-02T09:14:33Z", author: "Siddharth Ponnapalli", title: "Created security groups for web servers" },
- { time: "2024-07-02T10:22:12Z", author: "Siddharth Ponnapalli", title: "Added EC2 instances in different subnets" },
- { time: "2024-07-03T11:55:47Z", author: "Siddharth Ponnapalli", title: "Configured S3 buckets and objects" },
- { time: "2024-07-03T14:33:29Z", author: "Siddharth Ponnapalli", title: "Added RDS instance with MySQL" },
- { time: "2024-07-04T08:45:22Z", author: "Siddharth Ponnapalli", title: "Created IAM role, policy, and instance profile" },
- { time: "2024-07-04T10:12:09Z", author: "Siddharth Ponnapalli", title: "Added third EC2 instance with IAM role" },
- { time: "2024-07-05T11:34:56Z", author: "Siddharth Ponnapalli", title: "Updated security group rules for enhanced security" },
- { time: "2024-07-05T13:45:22Z", author: "Siddharth Ponnapalli", title: "Refactored subnet CIDR blocks for better segmentation" },
- { time: "2024-07-06T09:14:33Z", author: "Siddharth Ponnapalli", title: "Improved S3 bucket policies for enhanced security" },
- { time: "2024-07-06T10:22:12Z", author: "Siddharth Ponnapalli", title: "Added outputs for easier infrastructure management" },
- { time: "2024-07-07T11:55:47Z", author: "Siddharth Ponnapalli", title: "Enhanced instance tagging for better identification" },
- { time: "2024-07-07T14:33:29Z", author: "Siddharth Ponnapalli", title: "Updated RDS instance settings for improved performance" },
- { time: "2024-07-08T08:45:22Z", author: "Siddharth Ponnapalli", title: "Optimized VPC and subnet configurations" },
- { time: "2024-07-08T10:12:09Z", author: "Siddharth Ponnapalli", title: "Cleaned up IAM policies for better security practices" },
- { time: "2024-07-09T11:34:56Z", author: "Siddharth Ponnapalli", title: "Added new S3 bucket for log storage" },
- { time: "2024-07-09T13:45:22Z", author: "Siddharth Ponnapalli", title: "Implemented CloudWatch logs for monitoring" },
- { time: "2024-07-10T09:14:33Z", author: "Siddharth Ponnapalli", title: "Configured CloudFront distribution for S3 bucket" },
- { time: "2024-07-10T10:22:12Z", author: "Siddharth Ponnapalli", title: "Added Route 53 records for domain management" },
- { time: "2024-07-11T11:55:47Z", author: "Siddharth Ponnapalli", title: "Updated EC2 instance types for better performance" },
- { time: "2024-07-11T14:33:29Z", author: "Siddharth Ponnapalli", title: "Refined security group rules for better protection" },
- { time: "2024-07-12T08:45:22Z", author: "Siddharth Ponnapalli", title: "Added auto-scaling configuration for EC2 instances" },
- { time: "2024-07-12T10:12:09Z", author: "Siddharth Ponnapalli", title: "Configured RDS backup and retention policies" },
- { time: "2024-07-13T11:34:56Z", author: "Siddharth Ponnapalli", title: "Updated IAM role policies for least privilege access" },
- { time: "2024-07-13T13:45:22Z", author: "Siddharth Ponnapalli", title: "Implemented Lambda function for automated backups" },
- { time: "2024-07-14T09:14:33Z", author: "Siddharth Ponnapalli", title: "Integrated CloudWatch alarms for monitoring" },
- { time: "2024-07-14T10:22:12Z", author: "Siddharth Ponnapalli", title: "Updated S3 bucket lifecycle policies for cost optimization" },
- { time: "2024-07-15T11:55:47Z", author: "Siddharth Ponnapalli", title: "Configured VPC peering for multi-region setup" },
- { time: "2024-07-15T14:33:29Z", author: "Siddharth Ponnapalli", title: "Enhanced CloudFront caching settings" },
- { time: "2024-07-16T08:45:22Z", author: "Siddharth Ponnapalli", title: "Added new security group for database access" },
- { time: "2024-07-16T10:12:09Z", author: "Siddharth Ponnapalli", title: "Updated RDS instance class for better performance" },
- { time: "2024-07-17T11:34:56Z", author: "Siddharth Ponnapalli", title: "Implemented SNS for notification alerts" },
- { time: "2024-07-17T13:45:22Z", author: "Siddharth Ponnapalli", title: "Configured SES for email notifications" },
- { time: "2024-07-18T09:14:33Z", author: "Siddharth Ponnapalli", title: "Added DynamoDB table for session management" },
- { time: "2024-07-18T10:22:12Z", author: "Siddharth Ponnapalli", title: "Updated IAM policies for DynamoDB access" },
- { time: "2024-07-19T11:55:47Z", author: "Siddharth Ponnapalli", title: "Enhanced EC2 instance monitoring with detailed metrics" },
- { time: "2024-07-19T14:33:29Z", author: "Siddharth Ponnapalli", title: "Configured VPC flow logs for network monitoring" },
- { time: "2024-07-20T08:45:22Z", author: "Siddharth Ponnapalli", title: "Updated security group ingress rules for specific IP ranges" },
- { time: "2024-07-20T10:12:09Z", author: "Siddharth Ponnapalli", title: "Added IAM policy for S3 read-only access" },
- { time: "2024-07-21T11:34:56Z", author: "Siddharth Ponnapalli", title: "Refined Lambda function code for efficiency" },
- { time: "2024-07-21T13:45:22Z", author: "Siddharth Ponnapalli", title: "Enhanced VPC subnet configuration for better isolation" },
- { time: "2024-07-22T09:14:33Z", author: "Siddharth Ponnapalli", title: "Configured CloudFormation stack for infrastructure as code" },
- { time: "2024-07-22T10:22:12Z", author: "Siddharth Ponnapalli", title: "Updated EC2 instance AMI for latest security patches" },
- { time: "2024-07-23T11:55:47Z", author: "Siddharth Ponnapalli", title: "Implemented SSM for instance management" },
- { time: "2024-07-23T14:33:29Z", author: "Siddharth Ponnapalli", title: "Enhanced IAM roles for cross-account access" },
- { time: "2024-07-24T08:45:22Z", author: "Siddharth Ponnapalli", title: "Configured RDS read replicas for high availability" },
- { time: "2024-07-24T10:12:09Z", author: "Siddharth Ponnapalli", title: "Added CloudFront invalidation for cache management" },
- { time: "2024-07-25T11:34:56Z", author: "Siddharth Ponnapalli", title: "Updated Route 53 health checks for better uptime monitoring" },
- { time: "2024-07-25T13:45:22Z", author: "Siddharth Ponnapalli", title: "Enhanced S3 bucket encryption settings for data security" },
- { time: "2024-07-26T09:14:33Z", author: "Siddharth Ponnapalli", title: "Configured ELB for load balancing EC2 instances" },
- { time: "2024-07-26T10:22:12Z", author: "Siddharth Ponnapalli", title: "Updated IAM policies for better resource access control" },
- { time: "2024-07-27T11:55:47Z", author: "Siddharth Ponnapalli", title: "Enhanced CloudWatch dashboards for better visibility" },
- { time: "2024-07-27T14:33:29Z", author: "Siddharth Ponnapalli", title: "Configured EC2 instance user data for automation" }
-];
-
-const terraformOutput = `Terraform used the selected providers to generate the following
-execution plan. Resource actions are indicated with the
-following symbols:
- + create
-
-Terraform will perform the following actions:
-
- # null_resource.test5000 will be created
- + resource "null_resource" "test5000" {
- + id = (known after apply)
- }
-
-Plan: 1 to add, 0 to change, 0 to destroy.
-
-Do you want to perform these actions?
- Terraform will perform the actions described above.
- Only 'yes' will be accepted to approve.
-
- Enter a value: yes
-
-null_resource.test5000: Creating...
-null_resource.test5000: Creation complete after 0s [id=1100760775258092881]
-
-Apply complete! Resources: 1 added, 0 changed, 0 destroyed.`;
-
function RenderContent({
activeStage,
run,
+ tfOutput,
+ workflowRunUrl
}: {
activeStage: string;
run: Table<'digger_runs'>;
+ tfOutput: string | null;
+ workflowRunUrl: string | null;
}) {
if (activeStage === 'plan') {
return (
-
Terraform Plan Output
-
- {run.terraform_output || 'No plan output available'}
-
+
+
Terraform Plan Output
+
+ {workflowRunUrl && run.status !== ToTitleCase('queued') && (
+
+
+
+
+
+
+
+
+ View workflow run
+
+
+
+ )}
+
+ {run.status !== ToTitleCase('queued') &&
+ run.status !== ToTitleCase('pending_plan') &&
+ run.status !== ToTitleCase('running_plan') && (
+
+ {run.terraform_output || 'No plan output available'}
+
+ )}
);
} else if (activeStage === 'apply') {
- if (run.status === 'pending_approval' || run.status === 'rejected') {
+ if (run.status === ToTitleCase('pending_approval') || run.status === ToTitleCase('rejected')) {
return (
@@ -129,7 +78,7 @@ function RenderContent({
Apply logs
- {terraformOutput}
+ {tfOutput}
);
@@ -141,22 +90,13 @@ export const ProjectRunDetails: React.FC<{
run: Table<'digger_runs'>,
loggedInUser: Table<'user_profiles'>
isUserOrgAdmin: boolean
-}> = ({ run, loggedInUser, isUserOrgAdmin }) => {
+ tfOutput: string | null
+ workflowRunUrl: string | null
+ fullRepoName: string | null
+}> = ({ run, loggedInUser, isUserOrgAdmin, tfOutput, workflowRunUrl, fullRepoName }) => {
const router = useRouter();
const [activeStage, setActiveStage] = useState<'plan' | 'apply'>('plan');
- useEffect(() => {
- if (run.status === 'pending_apply') {
- const timer = setTimeout(async () => {
- await changeRunStatus(run.id, 'succeeded');
- setActiveStage('apply');
- router.refresh();
- }, 5000);
-
- return () => clearTimeout(timer);
- }
- }, [run.status, run.id, router]);
-
const { mutate: approveMutation, isLoading: isApproving } = useSAToastMutation(
async () => await approveRun(run.id, loggedInUser.id),
{
@@ -181,8 +121,6 @@ export const ProjectRunDetails: React.FC<{
}
);
-
-
return (
-
+
+
{run.status.toUpperCase()}
} />
@@ -212,18 +150,19 @@ export const ProjectRunDetails: React.FC<{
icon={}
name="Plan"
isActive={activeStage === 'plan'}
- isComplete={run.status !== 'pending_approval'}
+ isRunning={run.status === ToTitleCase('running_plan')}
+ isComplete={!['queued', 'pending_plan', 'running_plan'].includes(ToSnakeCase(run.status))}
onClick={() => setActiveStage('plan')}
/>
- {run.status === 'pending_approval' && isUserOrgAdmin && (
+ {run.status === ToTitleCase('pending_approval') && isUserOrgAdmin && (
Do you approve the proposed changes?
-
@@ -234,30 +173,44 @@ export const ProjectRunDetails: React.FC<{
icon={}
name="Apply"
isActive={activeStage === 'apply'}
- isComplete={run.status === 'succeeded'}
- isDisabled={run.status === 'pending_approval' || run.status === 'rejected'}
+ isRunning={run.status === ToTitleCase('running_apply')}
+ isComplete={run.status === ToTitleCase('succeeded')}
+ isDisabled={run.status !== ToTitleCase('succeeded')}
onClick={() => setActiveStage('apply')}
/>
- {run.status === 'pending_apply' && run.is_approved && (
+ {run.status === ToTitleCase('running_plan') && (
+
+
+ Running plan...
+
+ )}
+
+ {run.status === ToTitleCase('running_apply') && (
- Applying changes...
+ Applying changes...
)}
+
- {run.status === 'succeeded' && (
+ {['approved', 'pending_apply', 'running_apply', 'succeeded', 'failed'].includes(ToSnakeCase(run.status)) && (
Approved by:
)}
- {run.status === 'rejected' && (
-
Rejected by:
+ {run.status === ToTitleCase('discarded') && (
+
Discarded by:
)}
- {(run.status === 'succeeded' || run.status === 'rejected') && (
+ {!(["queued", "pending_plan", "running_plan", "pending_approval"].includes(ToSnakeCase(run.status))) && (
-
+
@@ -321,14 +274,16 @@ export const ProjectRunDetails: React.FC<{
);
};
+
const RunStageSidebarItem: React.FC<{
icon: React.ReactNode,
name: string,
isActive: boolean,
+ isRunning: boolean,
isComplete: boolean,
isDisabled?: boolean,
onClick: () => void
-}> = ({ icon, name, isActive, isComplete, isDisabled = false, onClick }) => (
+}> = ({ icon, name, isActive, isRunning, isComplete, isDisabled = false, onClick }) => (
{name}
- {isActive && !isComplete && }
- {isComplete && }
+
+ {isComplete && (
+
+ )}
+ {isActive && (
+
+ )}
+
);
-const DetailItem: React.FC<{ label: string, value: string | React.ReactNode }> = ({ label, value }) => (
+const DetailItem: React.FC<{ label: string, value: string | React.ReactNode, link?: string }> = ({ label, value, link }) => (
{label}:
- {typeof value === 'string' ?
{value}
: value}
+ {link ? (
+
{typeof value === 'string' ? value : value}
+ ) : (
+ typeof value === 'string' ?
{value} : value
+ )}
);
\ No newline at end of file
diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/page.tsx
index c862c521..09c9e9c1 100644
--- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/page.tsx
+++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/page.tsx
@@ -3,7 +3,8 @@ import { PageHeading } from "@/components/PageHeading";
import { T } from "@/components/ui/Typography";
import { getLoggedInUserOrganizationRole } from "@/data/user/organizations";
import { getSlimProjectById } from "@/data/user/projects";
-import { getRunById } from "@/data/user/runs";
+import { getRepoDetails } from "@/data/user/repos";
+import { getBatchIdFromPlanStageId, getRunById, getTFOutputAndWorkflowURLFromBatchId } from "@/data/user/runs";
import { getUserProfile } from "@/data/user/user";
import { Table } from "@/types";
import { serverGetLoggedInUser } from "@/utils/server/serverGetLoggedInUser";
@@ -29,6 +30,9 @@ type ProjectRunDetailsProps = {
run: Table<'digger_runs'>,
loggedInUser: Table<'user_profiles'>,
isUserOrgAdmin: boolean
+ tfOutput: string | null
+ workflowRunUrl: string | null
+ fullRepoName: string | null
}
@@ -50,8 +54,14 @@ export default async function RunDetailPage({
getSlimProjectById(project_id),
serverGetLoggedInUser()
]);
+ const { repo_full_name } = await getRepoDetails(project.repo_id);
+
const userProfile = await getUserProfile(user.id);
- const organizationRole = await getLoggedInUserOrganizationRole(project.organization_id);
+ const [organizationRole, batchId] = await Promise.all([
+ getLoggedInUserOrganizationRole(project.organization_id),
+ getBatchIdFromPlanStageId(run.plan_stage_id)
+ ]);
+ const { terraform_output, workflow_run_url } = await getTFOutputAndWorkflowURLFromBatchId(batchId);
const isOrganizationAdmin =
organizationRole === "admin" || organizationRole === "owner";
@@ -71,7 +81,7 @@ export default async function RunDetailPage({
}
>
-
+
}
diff --git a/src/data/user/projects.tsx b/src/data/user/projects.tsx
index 2c2f68cd..6f59c3f1 100644
--- a/src/data/user/projects.tsx
+++ b/src/data/user/projects.tsx
@@ -14,7 +14,7 @@ export async function getSlimProjectById(projectId: string) {
const supabaseClient = createSupabaseUserServerComponentClient();
const { data, error } = await supabaseClient
.from("projects")
- .select("id,name,project_status,organization_id,team_id,slug")
+ .select("id,name,project_status,organization_id,team_id,slug, repo_id")
.eq("id", projectId)
.single();
if (error) {
diff --git a/src/data/user/runs.ts b/src/data/user/runs.ts
index f4585578..8aa26ce3 100644
--- a/src/data/user/runs.ts
+++ b/src/data/user/runs.ts
@@ -130,7 +130,7 @@ export async function requestRunApproval(
const supabase = createSupabaseUserServerComponentClient();
const { data, error } = await supabase
.from('digger_runs')
- .update({ status: 'pending_approval' })
+ .update({ status: 'Pending Approval' })
.eq('id', runId)
.select('id')
.single();
@@ -149,7 +149,7 @@ export async function approveRun(
const { data, error } = await supabase
.from('digger_runs')
.update({
- status: 'pending_apply',
+ status: 'Approved',
approver_user_id: userId,
is_approved: true,
})
@@ -171,7 +171,7 @@ export async function rejectRun(
const { data, error } = await supabase
.from('digger_runs')
.update({
- status: 'rejected',
+ status: 'Discarded',
approver_user_id: userId,
is_approved: false,
})
@@ -196,3 +196,38 @@ export async function changeRunStatus(runId: string, status: string) {
if (error) throw error;
}
+
+export async function getBatchIdFromPlanStageId(planStageId: string | null) {
+ if (!planStageId) return null;
+ const supabase = createSupabaseUserServerComponentClient();
+ const { error, data } = await supabase
+ .from('digger_run_stages')
+ .select('batch_id')
+ .eq('id', planStageId)
+ .single();
+
+ if (error) throw error;
+
+ return data.batch_id;
+}
+
+export async function getTFOutputAndWorkflowURLFromBatchId(
+ batchId: string | null,
+) {
+ if (!batchId) return { terraform_output: null, workflow_run_url: null };
+
+ const supabase = createSupabaseUserServerComponentClient();
+ const { error, data } = await supabase
+ .from('digger_jobs')
+ .select('terraform_output, workflow_run_url')
+ .eq('batch_id', batchId)
+ .limit(1)
+ .maybeSingle();
+
+ if (error) throw error;
+
+ return {
+ terraform_output: data?.terraform_output ?? null,
+ workflow_run_url: data?.workflow_run_url ?? null,
+ };
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 095cb802..5e8a35ec 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -13,10 +13,24 @@ export const generateSlug = (title: string) => {
strict: true,
replacement: '-',
});
- return slug
-}
+ return slug;
+};
export const nanoid = customAlphabet(
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
7,
-); //
\ No newline at end of file
+); //
+
+export const ToTitleCase = (str: string) => {
+ return str
+ .split('_')
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(' ');
+};
+
+export const ToSnakeCase = (str: string) => {
+ return str
+ .split(' ')
+ .map((word) => word.toLowerCase())
+ .join('_');
+};