Skip to content

Commit 2966182

Browse files
authored
Merge pull request #60 from LabSyncro/feat/devops
Feat/devops
2 parents 9cbe738 + 38160ca commit 2966182

41 files changed

Lines changed: 1405 additions & 303 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
node_modules
2+
Dockerfile*
3+
docker-compose*
4+
.dockerignore
5+
.git
6+
.gitignore
7+
README.md
8+
LICENSE
9+
.vscode
10+
Makefile
11+
helm-charts
12+
.env
13+
.editorconfig
14+
.idea
15+
coverage*

.github/workflows/ci.yaml

Lines changed: 0 additions & 44 deletions
This file was deleted.

.github/workflows/deploy.yml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: Deploy
2+
3+
on:
4+
workflow_dispatch:
5+
6+
env:
7+
REGISTRY: ghcr.io
8+
IMAGE_NAME: labsyncro/labsyncro
9+
ORG_USERNAME: ${{ github.actor }}
10+
11+
permissions:
12+
contents: write
13+
pull-requests: write
14+
issues: write
15+
packages: write
16+
17+
jobs:
18+
build:
19+
name: Build Docker Image
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Checkout Code
23+
uses: actions/checkout@v4
24+
25+
- name: Setup Bun
26+
uses: oven-sh/setup-bun@v2
27+
with:
28+
bun-version: "latest"
29+
30+
- name: Setup Environment Variables
31+
run: |
32+
echo DATABASE_USER=${{ secrets.DATABASE_USER }} >> .env
33+
echo DATABASE_HOST=${{ secrets.DATABASE_HOST }} >> .env
34+
echo DATABASE_NAME=${{ secrets.DATABASE_NAME }} >> .env
35+
echo DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }} >> .env
36+
echo DATABASE_PORT=${{ secrets.DATABASE_PORT }} >> .env
37+
echo JWT_SECRET=${{ secrets.JWT_SECRET }} >> .env
38+
echo GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }} >> .env
39+
echo GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }} >> .env
40+
echo NODE_ENV=${{ secrets.NODE_ENV }} >> .env
41+
echo AUTH_SECRET=${{ secrets.AUTH_SECRET }} >> .env
42+
echo DATABASE_URL=${{ secrets.DATABASE_URL }} >> .env
43+
echo PRINT_LABELS_ENDPOINT=${{ secrets.PRINT_LABELS_ENDPOINT }} >> .env
44+
45+
- name: Setup Docker Buildx
46+
uses: docker/setup-buildx-action@v3
47+
48+
- name: Log into GitHub Container Registry
49+
uses: docker/login-action@v3
50+
with:
51+
registry: ${{ env.REGISTRY }}
52+
username: ${{ env.ORG_USERNAME }}
53+
password: ${{ secrets.GITHUB_TOKEN }}
54+
55+
- name: Build and Push Docker Image
56+
uses: docker/build-push-action@v6
57+
with:
58+
context: .
59+
push: true
60+
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
61+
cache-from: type=gha
62+
cache-to: type=gha,mode=max
63+
64+
deploy:
65+
name: Deploy to VPS
66+
runs-on: ubuntu-latest
67+
needs: [build]
68+
steps:
69+
- name: Deploy Application via SSH
70+
uses: appleboy/ssh-action@master
71+
with:
72+
host: ${{ secrets.VPS_HOST }}
73+
port: ${{ secrets.VPS_PORT }}
74+
username: ${{ secrets.VPS_USERNAME }}
75+
password: ${{ secrets.VPS_PASSWORD }}
76+
script: |
77+
export CR_PAT=${{ secrets.GITHUB_TOKEN }}
78+
echo $CR_PAT | docker login ${{ env.REGISTRY }} -u ${{ env.ORG_USERNAME }} --password-stdin
79+
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
80+
81+
cd labsyncro
82+
83+
curl -s https://raw.githubusercontent.com/LabSyncro/LabSyncro/main/docker-compose.yml -o docker-compose.yml
84+
85+
docker compose stop labsyncro
86+
docker compose rm -f labsyncro
87+
88+
docker compose up -d labsyncro
89+
90+
docker logout ${{ env.REGISTRY }}

Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
FROM oven/bun:latest AS base
2+
WORKDIR /usr/src/app
3+
4+
FROM base AS install
5+
RUN mkdir -p /temp/dev
6+
COPY package.json bun.lockb /temp/dev/
7+
RUN cd /temp/dev && bun install --frozen-lockfile
8+
9+
RUN mkdir -p /temp/prod
10+
COPY package.json bun.lockb /temp/prod/
11+
RUN cd /temp/prod && bun install --frozen-lockfile --production
12+
13+
FROM base AS prerelease
14+
COPY --from=install /temp/dev/node_modules node_modules
15+
COPY . .
16+
17+
ENV NODE_ENV=production
18+
RUN bun run build
19+
20+
FROM base AS release
21+
COPY --chown=bun:bun --from=install /temp/prod/node_modules node_modules
22+
COPY --chown=bun:bun --from=prerelease /usr/src/app/.output .
23+
24+
USER bun
25+
ENV NUXT_HOST=0.0.0.0
26+
ENV NUXT_PORT=3000
27+
EXPOSE 3000/tcp
28+
ENTRYPOINT [ "bun", "run", "server/index.mjs" ]

components/app/Checkout/DeviceKindTable/column.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,51 @@ import type { AugmentedColumnDef } from '~/components/common/DataTable/column';
55
export function createColumns ({
66
onDeviceKindLinkClick,
77
}: {
8-
onDeviceKindLinkClick: (id: string) => void,
8+
onDeviceKindLinkClick: (id: string) => void;
99
}): AugmentedColumnDef<DeviceKindInCartList>[] {
1010
return [
1111
{
1212
id: 'id',
1313
title: 'Mã loại thiết bị',
1414
cell: ({ row }) =>
15-
h(
16-
'p',
17-
{ class: 'text-normal pl-3' },
18-
[row.original.id.toUpperCase()],
19-
),
15+
h('p', { class: 'text-normal pl-3' }, [row.original.id.toUpperCase()]),
2016
},
2117
{
2218
id: 'name',
2319
title: 'Tên loại thiết bị',
2420
cell: ({ row }) =>
2521
h(
2622
'p',
27-
{ class: 'line-clamp-2 text-slate-500 text-normal leading-6 font-normal' },
23+
{
24+
class:
25+
'line-clamp-2 text-slate-500 text-normal leading-6 font-normal',
26+
},
2827
row.original.name,
2928
),
3029
},
30+
{
31+
id: 'category',
32+
title: 'Phân nhóm',
33+
cell: ({ row }) =>
34+
h(
35+
'p',
36+
{
37+
class:
38+
'line-clamp-2 text-slate-500 text-normal leading-6 font-normal',
39+
},
40+
row.original.category,
41+
),
42+
},
3143
{
3244
id: 'quantity',
3345
title: 'SL',
3446
cell: ({ row }) =>
3547
h(
3648
'p',
37-
{ class: 'line-clamp-2 text-slate-500 text-right text-normal leading-6 font-normal' },
49+
{
50+
class:
51+
'line-clamp-2 text-slate-500 text-center text-normal leading-6 font-normal',
52+
},
3853
row.original.quantity,
3954
),
4055
},
@@ -44,7 +59,10 @@ export function createColumns ({
4459
cell: ({ row }) =>
4560
h(
4661
'div',
47-
{ class: 'flex justify-end items-center text-lg hover:cursor-pointer', onClick: () => onDeviceKindLinkClick(row.original.id) },
62+
{
63+
class: 'flex justify-end items-center text-lg hover:cursor-pointer',
64+
onClick: () => onDeviceKindLinkClick(row.original.id),
65+
},
4866
h(Icon, { name: 'i-heroicons-arrow-top-right-on-square' }),
4967
),
5068
},

components/app/Checkout/DeviceKindTable/index.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const props = defineProps<{
77
cart: {
88
id: string;
99
name: string;
10+
category: string;
1011
deviceIds: string[];
1112
}[],
1213
}>();
@@ -28,6 +29,7 @@ async function fetchData (offset: number, length: number, options: { desc?: bool
2829
let deviceKinds = props.cart.map((deviceKind) => ({
2930
id: deviceKind.id,
3031
name: deviceKind.name,
32+
category: deviceKind.category,
3133
quantity: deviceKind.deviceIds.length,
3234
}));
3335
if (options.sortField) {
@@ -45,5 +47,8 @@ async function fetchData (offset: number, length: number, options: { desc?: bool
4547
</script>
4648

4749
<template>
48-
<DataTable :key="cart.flatMap(({ deviceIds }) => deviceIds).join('-')" :selectable="false" :searchable="false" :qrable="false" :fetch-fn="fetchData" :delete-fn="deleteData" :columns="createColumns({ onDeviceKindLinkClick }) as AugmentedColumnDef<unknown>[]" />
50+
<DataTable
51+
:key="cart.flatMap(({ deviceIds }) => deviceIds).join('-')" :selectable="false" :searchable="false"
52+
:qrable="false" :fetch-fn="fetchData" :delete-fn="deleteData"
53+
:columns="createColumns({ onDeviceKindLinkClick }) as AugmentedColumnDef<unknown>[]" />
4954
</template>

components/app/Checkout/DeviceKindTable/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Static } from '@sinclair/typebox';
44
export const DeviceKindInCartList = Type.Object({
55
id: Type.String(),
66
name: Type.String(),
7+
category: Type.String(),
78
quantity: Type.Number(),
89
});
910

components/app/Checkout/DeviceSearchBox.vue

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const emits = defineEmits<{
55
'device-select': [string],
66
}>();
77
8+
const { lab } = useLab();
9+
810
const searchText = ref('');
911
const { isActive: isDropdownActive, setInactive } = useClick(useTemplateRef('dropdown'));
1012
@@ -17,7 +19,7 @@ watch(searchText, async () => {
1719
searchItems.value = [];
1820
return;
1921
}
20-
const data = await deviceKindService.getDeviceKinds(0, numberOfSearchItemsShown, { searchText: searchText.value || undefined, searchFields: ['device_name', 'device_id'] });
22+
const data = await deviceKindService.getDeviceKindsByLabId(lab.value.id, 0, numberOfSearchItemsShown, { searchText: searchText.value || undefined, searchFields: ['device_name', 'device_id'] });
2123
searchItems.value = data.deviceKinds.map(({ name, mainImage, id }) => ({ id, name, image: mainImage }));
2224
});
2325
function focusNextSearchItem () {
@@ -41,18 +43,26 @@ function unfocusSearchItem () {
4143
<div ref="dropdown" class="relative">
4244
<div class="relative">
4345
<input
44-
v-model="searchText"
46+
v-model="searchText"
4547
class="bg-white text-primary-light placeholder:text-primary-light border-2 h-11 w-[100%] pl-10 pr-3 rounded-md text-md placeholder:text-normal"
46-
type="search" placeholder="Tên/Mã loại thiết bị" @keydown.down="focusNextSearchItem" @keydown.up="focusPrevSearchItem" @keydown.enter="focusedSearchItemIndex !== null && goToSearchItem(searchItems[focusedSearchItemIndex!].id)" @keydown.esc="unfocusSearchItem">
48+
type="search" placeholder="Tên/Mã loại thiết bị" @keydown.down="focusNextSearchItem"
49+
@keydown.up="focusPrevSearchItem"
50+
@keydown.enter="focusedSearchItemIndex !== null && goToSearchItem(searchItems[focusedSearchItemIndex!].id)"
51+
@keydown.esc="unfocusSearchItem">
4752
<Icon
48-
aria-hidden class="absolute left-3 top-[12px] text-xl text-primary-dark"
53+
aria-hidden class="absolute left-3 top-[12px] text-xl text-primary-dark"
4954
name="i-heroicons-magnifying-glass" />
5055
</div>
5156

52-
<div :class="`${isDropdownActive && searchItems.length ? 'flex' : 'hidden'} flex-col gap-1 absolute bg-white p-1 mt-1 w-[120%] z-50 shadow-[0_0px_16px_-3px_rgba(0,0,0,0.3)]`">
53-
<a v-for="(item, index) in searchItems" :key="item.id" :class="`px-2 text-normal p-1 flex justify-start gap-2 hover:bg-gray-100 ${focusedSearchItemIndex === index ? 'bg-secondary-light' : ''}`" @click="goToSearchItem(searchItems[index].id)">
57+
<div
58+
:class="`${isDropdownActive && searchItems.length ? 'flex' : 'hidden'} flex-col gap-1 absolute bg-white p-1 mt-1 w-[120%] z-50 shadow-[0_0px_16px_-3px_rgba(0,0,0,0.3)]`">
59+
<a
60+
v-for="(item, index) in searchItems" :key="item.id"
61+
:class="`px-2 text-normal p-1 flex justify-start gap-2 hover:bg-gray-100 ${focusedSearchItemIndex === index ? 'bg-secondary-light' : ''}`"
62+
@click="goToSearchItem(searchItems[index].id)">
5463
<img :src="item.image" class="h-6 w-6 block">
55-
<p class="p-1 px-2 text-nowrap bg-gray-100 border border-gray-300 rounded-md text-normal font-normal leading-none">
64+
<p
65+
class="p-1 px-2 text-nowrap bg-gray-100 border border-gray-300 rounded-md text-normal font-normal leading-none">
5666
{{ item.id.toUpperCase() }}
5767
</p>
5868
<HighlightText class="line-clamp-1" :text="item.name" :match-text="searchText || undefined" />

components/app/Checkout/DeviceSelectModal.vue

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const props = defineProps<{
77
selectedDevices: string[];
88
}>();
99
10+
const { lab } = useLab();
11+
1012
const emits = defineEmits<{
1113
'close-modal': [],
1214
'device-add': [{ kind: string, id: string }],
@@ -16,24 +18,31 @@ const emits = defineEmits<{
1618
const deviceKindMeta = ref<null | DeviceKindResourceDto>(null);
1719
watch(() => [props.kindId], async () => {
1820
if (props.kindId) {
19-
deviceKindMeta.value = await deviceKindService.getById(props.kindId);
21+
deviceKindMeta.value = await deviceKindService.getById(props.kindId, lab.value.id);
2022
return;
2123
}
2224
deviceKindMeta.value = null;
2325
});
2426
</script>
2527

2628
<template>
27-
<div v-if="deviceKindMeta && kindId" class="fixed z-50 top-0 left-0 w-[100vw] h-[100vh] flex justify-center items-center">
29+
<div
30+
v-if="deviceKindMeta && kindId"
31+
class="fixed z-50 top-0 left-0 w-[100vw] h-[100vh] flex justify-center items-center">
2832
<div class="bg-white shadow-[0_0px_16px_1px_rgba(0,0,0,0.3)] w-[400px] sm:w-[90vw] p-6">
2933
<div class="flex justify-between gap-3 items-center">
3034
<h2 class="text-lg"> {{ deviceKindMeta.name }} </h2>
31-
<div class="border border-slate text-slate rounded-full flex justify-center items-center p-2 hover:cursor-pointer" @click="emits('close-modal')">
35+
<div
36+
class="border border-slate text-slate rounded-full flex justify-center items-center p-2 hover:cursor-pointer"
37+
@click="emits('close-modal')">
3238
<Icon class="text-lg" aria-hidden name="i-heroicons-x-mark" />
3339
</div>
3440
</div>
3541
<div class="mt-6">
36-
<CheckoutDeviceSelectTable :selected-devices="selectedDevices" :kind-id="kindId" @device-add="(id) => emits('device-add', { kind: props.kindId!, id })" @device-delete="(id) => emits('device-delete', { kind: props.kindId!, id})" />
42+
<CheckoutDeviceSelectTable
43+
:selected-devices="selectedDevices" :kind-id="kindId"
44+
@device-add="(id) => emits('device-add', { kind: props.kindId!, id })"
45+
@device-delete="(id) => emits('device-delete', { kind: props.kindId!, id })" />
3746
</div>
3847
</div>
3948
</div>

0 commit comments

Comments
 (0)