Skip to content

Commit

Permalink
can create and delete services on EasyPanel
Browse files Browse the repository at this point in the history
  • Loading branch information
sangdth committed Nov 27, 2023
1 parent 26c225f commit 48898fb
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 37 deletions.
12 changes: 8 additions & 4 deletions app/(dashboard)/sites/[id]/components/SiteDeleteForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import {
useToast,
} from '@/hooks';
import { DeleteIcon } from '@/icons';
import { deleteProject } from '@/lib/actions/easypanel';
import { deleteRepo } from '@/lib/actions/github';
import { deleteSite } from '@/lib/actions/site';
import { delayAsync } from '@/lib/utils';
import type { Site } from '@/types';

export type SiteDeleteFormProps = {
Expand All @@ -59,20 +59,24 @@ export const SiteDeleteForm = ({ site }: SiteDeleteFormProps) => {

const handleDelete = async () => {
try {
// if (!session || !session.user) {
// toast({ title: 'Authentication issue', status: 'error' });

// return;
// }
if (!site) return;

setIsLoading(true);

toast({ title: 'Start deleting...' });

await delayAsync();

await deleteRepo(siteId);
toast({ title: 'Deleted GitHub repo...' });

await delayAsync();
await deleteProject({ name: site.id });

const deletedSite = await deleteSite(site);

toast({ title: 'Finally, deleted site!' });

setIsLoading(false);
Expand Down
12 changes: 7 additions & 5 deletions app/(dashboard)/sites/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
SiteDeleteForm,
SiteGeneralSettingsForm,
SiteQuickLinks,
SiteTemplateEditor,
} from './components';
// import { getProject } from '@/lib/actions/easypanel';

export type SingleSitePageProps = {
params: {
Expand All @@ -26,10 +26,14 @@ export default async function SingleSitePage({ params }: SingleSitePageProps) {
return null;
}

const repo = await checkRepoExisting(params.id);
const id = decodeURIComponent(params.id);

const repo = await checkRepoExisting(id);

// const project = await getProject({ projectName: id });

const site = await prisma.site.findUnique({
where: { id: decodeURIComponent(params.id) },
where: { id },
});

if (session.user.role !== UserRole.ADMIN && site?.userId !== session.user.id) {
Expand All @@ -44,8 +48,6 @@ export default async function SingleSitePage({ params }: SingleSitePageProps) {

{site && <SiteCustomDomainForm site={site} />}

{site && <SiteTemplateEditor site={site} />}

{site && <SiteDeleteForm site={site} />}
</Stack>
);
Expand Down
8 changes: 7 additions & 1 deletion app/(dashboard)/sites/components/Sites/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ export const Sites = async ({ limit }: { limit?: number }) => {
No Sites Yet
</Heading>

<NextImage alt="missing site" src="/web-design.svg" width={400} height={400} />
<NextImage
priority
alt="missing site"
src="/web-design.svg"
width={400}
height={400}
/>

<Text className="text-lg text-stone-500">
You do not have any sites yet. Create one to get started.
Expand Down
11 changes: 6 additions & 5 deletions components/client/CreateSiteButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from '@/hooks';
import { addCollaborator, createRepo } from '@/lib/actions/github';
import { createSite } from '@/lib/actions/site';
import { createProject } from '@/lib/actions/easypanel';
import { createProject, createService } from '@/lib/actions/easypanel';
import { UserRole } from '@prisma/client';
import { AddIcon } from '@/icons';

Expand Down Expand Up @@ -85,15 +85,15 @@ export const CreateSiteButton = () => {
toast({ title: 'Creating your site...' });

await createRepo({ ...newSite, template });
toast({ title: 'GitHub repo is cloned successfully...' });

toast({ title: 'adding you as collaborator...' });
toast({ title: '...GitHub repo is cloned successfully' });

if (session.user.role !== UserRole.ADMIN) {
toast({ title: '...adding you as collaborator' });
await addCollaborator(newSite.id, session.user.username);
}

await createProject({ name: newSite.id });
toast({ title: '...creating services on EasyPanel' });
await createProject(newSite);

toast({ title: 'Done!' });

Expand All @@ -103,6 +103,7 @@ export const CreateSiteButton = () => {
router.push(`/sites/${newSite.id}`);
}
} catch (error: any) {
console.log('### error: ', { error });
toast({ status: 'error', title: error.message });
}
};
Expand Down
141 changes: 127 additions & 14 deletions lib/actions/easypanel.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,113 @@
'use server';

import { Client as EasyPanelClient, ProjectName, ProjectQueryConf } from 'easypanel.js';
import {
easypanel as easypanelClient,
ProjectName,
ProjectQueryConf,
CreateService,
ServiceType,
} from 'easypanel-sdk';
import { getSession } from '@/lib/auth';
import { delayAsync } from '@/lib/utils';
import { EnvironmentVariables, Site } from '@/types';

const easypanel = new EasyPanelClient({
endpoint: process.env.EASYPANEL_URL ?? '',
token: process.env.EASYPANEL_API_KEY ?? '',
credentials: { email: '', password: '' }, // to silent the TypeScript error
});
declare global {
// eslint-disable-next-line no-var, vars-on-top
var easypanel: ReturnType<typeof easypanelClient> | undefined; // declare needs var
}

export const createProject = async ({ name }: ProjectName) => {
const session = await getSession();
const easypanel =
global.easypanel ??
easypanelClient({
endpoint: process.env.EASYPANEL_URL ?? '',
token: process.env.EASYPANEL_API_KEY ?? '',
});

if (!session?.user.id) {
throw new Error('Unauthorized');
}
if (process.env.NODE_ENV === 'development') {
global.easypanel = easypanel;
}

const response = await easypanel.projects.create({ name });
export const createProject = async (site: Site) => {
try {
const session = await getSession();

return response;
if (!session?.user.id) {
throw new Error('Unauthorized');
}

const { production } = site.environmentVariables as EnvironmentVariables;

const response = await easypanel.projects.create({ name: site.id });

const databaseUrl = `postgres://postgres:${production.POSTGRES_PASSWORD}@${site.id}_postgres:5432/${site.id}`;

await delayAsync(500);

await easypanel.services.create('app', {
projectName: site.id,
serviceName: 'nextjs',
domains: [{ host: '$(EASYPANEL_DOMAIN)' }],
});

await delayAsync(500);

// @ts-expect-error // postgres typo name
await easypanel.services.create('postgres', {
projectName: site.id,
serviceName: 'postgres',
domains: [{ host: '$(EASYPANEL_DOMAIN)' }],
image: 'postgres:16',
password: production.POSTGRES_PASSWORD,
});

await delayAsync(500);

await easypanel.services.updateSourceGithub('app', {
projectName: site.id,
serviceName: 'nextjs',
owner: 'sieutoc-customers',
repo: site.id,
ref: 'master',
path: '/',
});

await delayAsync(1000);

await easypanel.services.updateEnv('app', {
projectName: site.id,
serviceName: 'nextjs',
env: Object.entries({
...production,
NEXTAUTH_URL: '$(PRIMARY_DOMAIN)',
DATABASE_URL: databaseUrl,
})
.map(([k, v]) => `${k}=${v}`)
.join('\n'),
});

await delayAsync(500);

await easypanel.services.updateBuild('app', {
projectName: site.id,
serviceName: 'nextjs',
build: { type: 'nixpacks' },
});

await delayAsync(500);

// do the deploy later, it takes too long
// const deployed = await easypanel.services.deploy('app', {
// projectName: site.id,
// serviceName: 'nextjs',
// });
// console.log('### deployed: ', deployed);

await delayAsync(500);

return response;
} catch (error) {
console.error(error);
}
};

export const getProject = async ({ projectName }: ProjectQueryConf) => {
Expand All @@ -40,8 +129,32 @@ export const deleteProject = async ({ name }: ProjectName) => {
throw new Error('Unauthorized');
}

const foundProject = await easypanel.projects.inspect({ projectName: name });

// We have to delete all services inside before deleting the project
const destroyRequests = foundProject.result.data.json.services.map((service) => {
return easypanel.services.destroy(service.type, {
projectName: service.projectName,
// @ts-expect-error
serviceName: service.name, // wrong type return in inspect service
});
});

await Promise.all(destroyRequests);

const response = await easypanel.projects.destory({ name });
console.log('### delete response: ', { response });

return response;
};

export const createService = async (serviceType: ServiceType, input: CreateService) => {
const session = await getSession();

if (!session) {
throw new Error('Unauthorized');
}

const response = await easypanel.services.create(serviceType, input);

return response;
};
4 changes: 2 additions & 2 deletions lib/actions/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { generateSecret } from '@/lib/utils';
import { getSession } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { Site } from '@/types';
import { EnvironmentVariables, Site } from '@/types';

export type CreateSiteDto = Pick<Site, 'name' | 'description' | 'slug'>;

Expand All @@ -15,7 +15,7 @@ export const createSite = async ({ name, description, slug }: CreateSiteDto) =>
}

try {
const environmentVariables = {
const environmentVariables: EnvironmentVariables = {
production: {
NEXTAUTH_SECRET: generateSecret(),
ARGON_SECRET: generateSecret(),
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"ai": "2.2.25",
"date-fns": "2.30.0",
"deepmerge": "4.3.1",
"easypanel-sdk": "0.3.1",
"easypanel.js": "1.0.0",
"fast-deep-equal": "3.1.3",
"framer-motion": "10.16.5",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ model Site {
customDomain String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
environmentVariables Json @default("{}")
environmentVariables Json @default("{\"preview\":{},\"production\":{}}")
user User? @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
userId String?
template String?
Expand Down
11 changes: 6 additions & 5 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ export enum HttpMethod {

export type JsonObject = Prisma.JsonObject;

export interface EnvironmentVariables extends Prisma.JsonObject {
production: Record<string, string>;
preview: Record<string, string>;
}

export type ReposResponse = Endpoints['GET /repos/{owner}/{repo}']['response'];

export type EnvironmentType = 'production' | 'preview';

export type EnvironmentVariables = {
[key in EnvironmentType]: Record<string, string | undefined>;
};

0 comments on commit 48898fb

Please sign in to comment.