Merge branch 'feature/main-js' of http://192.168.10.110/Autoflow/autoflow-web-console into feature/main-js
commit
7aeaf9bb7e
@ -0,0 +1,3 @@
|
|||||||
|
NODE_ENV = "dev"
|
||||||
|
VITE_APP_API_SERVER_URL = "http://localhost:80"
|
||||||
|
VITE_ROOT_PATH = ""
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
NODE_ENV = "prod"
|
||||||
|
VITE_APP_API_SERVER_URL = "http://cuuva.com:2480/autoflow-server-mgmt"
|
||||||
|
VITE_ROOT_PATH = "/autoflow"
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@vue/typescript/recommended",
|
||||||
|
"plugin:vue/vue3-recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
"vue/comment-directive": "off",
|
||||||
|
"vue/no-v-html": "off",
|
||||||
|
"no-console": process.env.NODE_ENV === "prod" ? "warn" : "off",
|
||||||
|
"no-debugger": process.env.NODE_ENV === "prod" ? "warn" : "off",
|
||||||
|
"vue/no-deprecated-slot-attribute": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
endOfLine: "auto",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
FROM nginx:stable-alpine
|
||||||
|
|
||||||
|
RUN apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime
|
||||||
|
|
||||||
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
COPY dist/. /usr/share/nginx/html/autoflow
|
||||||
|
EXPOSE 80
|
||||||
|
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /autoflow/index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,9 @@
|
|||||||
import vuetify from 'eslint-config-vuetify'
|
import vuetify from 'eslint-config-vuetify'
|
||||||
|
|
||||||
export default vuetify()
|
export default {
|
||||||
|
vuetify(),
|
||||||
|
'vue/attributes-order': 'off',
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: autoflow
|
||||||
|
namespace: autoflow
|
||||||
|
labels:
|
||||||
|
app: autoflow
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: autoflow
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: autoflow
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: 192.168.10.120:32100/autoflow:2025.07.005
|
||||||
|
name: autoflow
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 100Mi
|
||||||
|
# limits:
|
||||||
|
# cpu: 1000m
|
||||||
|
# memory: 2Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: autoflow
|
||||||
|
namespace: autoflow
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: autoflow
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
namespace: autoflow
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: 1g
|
||||||
|
name: autoflow
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /autoflow
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: autoflow
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["onClick"]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emit("onClick");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-tooltip location="bottom" text="Down">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-arrow-down"
|
||||||
|
class="ma-1"
|
||||||
|
color="brack"
|
||||||
|
density="comfortable"
|
||||||
|
elevation="0"
|
||||||
|
@click="onClick"
|
||||||
|
size="small"
|
||||||
|
v-bind="props"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["onClick"]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emit("onClick");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-tooltip location="bottom" text="Up">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-arrow-up"
|
||||||
|
class="ma-1"
|
||||||
|
color="brack"
|
||||||
|
density="comfortable"
|
||||||
|
elevation="0"
|
||||||
|
@click="onClick"
|
||||||
|
size="small"
|
||||||
|
v-bind="props"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["onClick"]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emit("onClick");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-tooltip location="bottom" text="배포">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-server"
|
||||||
|
class="ma-1"
|
||||||
|
color="error"
|
||||||
|
density="comfortable"
|
||||||
|
elevation="0"
|
||||||
|
@click="onClick"
|
||||||
|
size="small"
|
||||||
|
v-bind="props"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["onClick"]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emit("onClick");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-tooltip location="bottom" text="다운로드">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
@click="onClick"
|
||||||
|
class="ma-1"
|
||||||
|
icon="mdi-download"
|
||||||
|
color="success"
|
||||||
|
density="comfortable"
|
||||||
|
elevation="0"
|
||||||
|
size="small"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["onClick"]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emit("onClick");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-tooltip location="bottom" text="정보">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
@click="onClick"
|
||||||
|
class="ma-1"
|
||||||
|
icon="mdi-file-document"
|
||||||
|
color="info"
|
||||||
|
density="comfortable"
|
||||||
|
elevation="0"
|
||||||
|
size="small"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["onClick"]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emit("onClick");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-tooltip location="bottom" text="설정">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
@click="onClick"
|
||||||
|
class="ma-1"
|
||||||
|
icon="mdi-cog-play-outline"
|
||||||
|
color="success"
|
||||||
|
density="comfortable"
|
||||||
|
elevation="0"
|
||||||
|
size="small"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editData: Object,
|
||||||
|
mode: String,
|
||||||
|
userOption: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["handle-data", "close-modal"]);
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const form = ref({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
file: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 다이얼로그 타이틀
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (props.mode === "create") return "Create Dataset";
|
||||||
|
if (props.mode === "edit") return "Edit Dataset";
|
||||||
|
return "Clone Execution";
|
||||||
|
});
|
||||||
|
|
||||||
|
const onChooseFile = () => {
|
||||||
|
fileInput.value?.click();
|
||||||
|
};
|
||||||
|
const submit = () => {
|
||||||
|
emit("handle-data", form.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="rounded-lg overflow-hidden">
|
||||||
|
<!-- 타이틀 영역 -->
|
||||||
|
<v-card-title
|
||||||
|
class="text-white font-weight-bold text-h6"
|
||||||
|
style="background-color: #1976d2"
|
||||||
|
>
|
||||||
|
{{ dialogTitle }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<v-form @submit.prevent="submit">
|
||||||
|
<v-row dense class="mb-6">
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-subheader class="font-weight-medium white--text mb-2">
|
||||||
|
Dataset Title
|
||||||
|
</v-subheader>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.name"
|
||||||
|
variant="outlined"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
outlined
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-subheader class="font-weight-medium white--text mb-2">
|
||||||
|
Dataset Version
|
||||||
|
</v-subheader>
|
||||||
|
<v-text-field
|
||||||
|
variant="outlined"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
outlined
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Description
|
||||||
|
</label>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.description"
|
||||||
|
variant="outlined"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Upload File
|
||||||
|
</label>
|
||||||
|
<v-file-input
|
||||||
|
v-model="form.file"
|
||||||
|
label="Upload File"
|
||||||
|
@click:append-outer="onChooseFile"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="justify-end" style="padding: 16px 24px">
|
||||||
|
<v-btn color="success" @click="submit">Save</v-btn>
|
||||||
|
<v-btn text class="white--text" @click="$emit('close-modal')"
|
||||||
|
>Close</v-btn
|
||||||
|
>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconArrowDown from "@/components/atoms/button/IconArrowDown.vue";
|
||||||
|
import IconArrowUp from "@/components/atoms/button/IconArrowUp.vue";
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
const steps = ref([
|
||||||
|
{ order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" },
|
||||||
|
{
|
||||||
|
order: 2,
|
||||||
|
stepName: "Preprocessing",
|
||||||
|
type: "Preprocess",
|
||||||
|
status: "Not Configured",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
order: 3,
|
||||||
|
stepName: "Train Model",
|
||||||
|
type: "Train",
|
||||||
|
status: "Not Configured",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editData: Object,
|
||||||
|
mode: String,
|
||||||
|
userOption: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["handle-data", "close-modal"]);
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
emit("handle-data", form.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="rounded-lg overflow-hidden">
|
||||||
|
<!-- 타이틀 영역 -->
|
||||||
|
<v-card-title
|
||||||
|
class="text-white font-weight-bold text-h6"
|
||||||
|
style="background-color: #1976d2"
|
||||||
|
>
|
||||||
|
Deploy Model
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<div class="text-subtitle-1 font-weight-medium mb-4">
|
||||||
|
Select Model : ImageClassifier
|
||||||
|
</div>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-form @submit.prevent="submit">
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>OTA
|
||||||
|
</label>
|
||||||
|
<v-row dense class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-subheader class="font-weight-medium white--text mb-2">
|
||||||
|
Select Package
|
||||||
|
</v-subheader>
|
||||||
|
<v-select
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
outlined
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-text class="pt-6 pb-4 px-6">
|
||||||
|
<v-subheader class="font-weight-medium mb-2">
|
||||||
|
Package Preview
|
||||||
|
</v-subheader>
|
||||||
|
<v-sheet class="pa-4 mb-6" elevation="1" rounded>
|
||||||
|
<v-row dense>
|
||||||
|
<!-- Linux -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<div class="font-weight-medium mb-2">Linux</div>
|
||||||
|
<v-text-field
|
||||||
|
label="File Name"
|
||||||
|
placeholder="4_EdgeInfra_Perception.sh"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
label="File Path"
|
||||||
|
placeholder="/home/etri/TeslaSystem/EdgeInfraVision/RUN"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Windows -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<div class="font-weight-medium mb-2">Windows</div>
|
||||||
|
<v-text-field
|
||||||
|
label="File Name"
|
||||||
|
placeholder="4_EdgeInfra_Perception.exe"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
label="File Path"
|
||||||
|
placeholder="C:/etri/TeslaSystem/EdgeInfraVision/RUN"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-sheet>
|
||||||
|
<!-- Package Preview 끝 -->
|
||||||
|
|
||||||
|
<!-- Software Name / Version -->
|
||||||
|
<v-row dense class="mb-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
label="Software Name"
|
||||||
|
placeholder="Enter software Name"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
label="Software Version"
|
||||||
|
placeholder="Enter software Version"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Executed 여부 -->
|
||||||
|
<v-row dense class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-radio-group row>
|
||||||
|
<v-radio label="Executed" value="executed" />
|
||||||
|
<v-radio label="Not Executed" value="not_executed" />
|
||||||
|
</v-radio-group>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="justify-end" style="padding: 16px 24px">
|
||||||
|
<v-btn color="success" @click="submit">Save</v-btn>
|
||||||
|
<v-btn text class="white--text" @click="$emit('close-modal')"
|
||||||
|
>Close</v-btn
|
||||||
|
>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed, defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
mode: "create" | "edit" | "clone";
|
||||||
|
selectedData: any;
|
||||||
|
workflowList: string[];
|
||||||
|
executionTypes: string[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", v: boolean): void;
|
||||||
|
(e: "save", payload: any): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 로컬 state
|
||||||
|
const internalWorkflow = ref(props.selectedData?.workflow || "");
|
||||||
|
const internalExecType = ref(props.selectedData?.execType || "");
|
||||||
|
const internalName = ref(props.selectedData?.name || "");
|
||||||
|
const internalDesc = ref(props.selectedData?.description || "");
|
||||||
|
const internalExperiment = ref(props.selectedData?.experiment || "");
|
||||||
|
|
||||||
|
// 다이얼로그 열릴 때 복사
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open && props.selectedData) {
|
||||||
|
internalWorkflow.value = props.selectedData.workflow;
|
||||||
|
internalExecType.value = props.selectedData.execType;
|
||||||
|
internalName.value = props.selectedData.name;
|
||||||
|
internalDesc.value = props.selectedData.description;
|
||||||
|
internalExperiment.value = props.selectedData.experiment;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 다이얼로그 타이틀
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (props.mode === "create") return "Create Execution";
|
||||||
|
if (props.mode === "edit") return "Edit Execution";
|
||||||
|
return "Clone Execution";
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSave() {
|
||||||
|
emit("save", {
|
||||||
|
workflow: internalWorkflow.value,
|
||||||
|
execType: internalExecType.value,
|
||||||
|
name: internalName.value,
|
||||||
|
description: internalDesc.value,
|
||||||
|
experiment: internalExperiment.value,
|
||||||
|
});
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
}
|
||||||
|
function onClose() {
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="rounded-lg overflow-hidden">
|
||||||
|
<v-card-title
|
||||||
|
class="text-white font-weight-bold text-h6"
|
||||||
|
style="background-color: #1976d2"
|
||||||
|
>{{ dialogTitle }}</v-card-title
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 본문 -->
|
||||||
|
<v-card-text class="white--text pa-6">
|
||||||
|
<!-- Workflow Information 헤더 -->
|
||||||
|
<v-card-subtitle class="white--text mb-4"
|
||||||
|
>Workflow Information</v-card-subtitle
|
||||||
|
>
|
||||||
|
|
||||||
|
<v-row dense class="mb-6">
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-subheader class="font-weight-medium white--text mb-2">
|
||||||
|
Select Workflow
|
||||||
|
</v-subheader>
|
||||||
|
<v-select
|
||||||
|
v-model="internalWorkflow"
|
||||||
|
:items="workflowList"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
outlined
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-subheader class="font-weight-medium white--text mb-2">
|
||||||
|
Execution Type
|
||||||
|
</v-subheader>
|
||||||
|
<v-select
|
||||||
|
v-model="internalExecType"
|
||||||
|
:items="executionTypes"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
outlined
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Execution Name -->
|
||||||
|
<v-row dense class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-subheader class="font-weight-medium white--text mb-2">
|
||||||
|
Execution Type
|
||||||
|
</v-subheader>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="internalName"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<v-row dense class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-subheader class="font-weight-medium white--text mb-2">
|
||||||
|
Description
|
||||||
|
</v-subheader>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="internalName"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Experiment -->
|
||||||
|
<v-row dense class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-subheader class="font-weight-medium white--text mb-2">
|
||||||
|
Experiment
|
||||||
|
</v-subheader>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="internalName"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- 액션 -->
|
||||||
|
<v-card-actions class="justify-end pa-4">
|
||||||
|
<v-btn color="success" @click="onSave">Start</v-btn>
|
||||||
|
<v-btn text class="white--text" @click="onClose">Close</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editData: Object,
|
||||||
|
mode: String,
|
||||||
|
userOption: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["handle-data", "close-modal"]);
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
emit("handle-data", form.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="rounded-lg overflow-hidden">
|
||||||
|
<!-- 타이틀 영역 -->
|
||||||
|
<v-card-title
|
||||||
|
class="text-white font-weight-bold text-h6"
|
||||||
|
style="background-color: #1976d2"
|
||||||
|
>
|
||||||
|
Create Experiment
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<v-form @submit.prevent="submit">
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Experiment Name</label
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.name"
|
||||||
|
variant="outlined"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Description</label
|
||||||
|
>
|
||||||
|
<v-textarea
|
||||||
|
v-model="form.description"
|
||||||
|
variant="outlined"
|
||||||
|
rows="3"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="justify-end" style="padding: 16px 24px">
|
||||||
|
<v-btn color="success" @click="submit">Save</v-btn>
|
||||||
|
<v-btn text class="white--text" @click="$emit('close-modal')"
|
||||||
|
>Close</v-btn
|
||||||
|
>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
// 부모에서 전달받을 Props 정의
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
selectedData: { workflow: string; stepName: string } | null;
|
||||||
|
workflowList: string[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// v-model 및 save 이벤트를 위한 Emit 정의
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: boolean): void;
|
||||||
|
(e: "save", payload: { workflow: string; stepName: string }): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 다이얼로그 내부에서 사용할 로컬 상태
|
||||||
|
const internalWorkflow = ref(props.selectedData?.workflow || "");
|
||||||
|
const internalStepName = ref(props.selectedData?.stepName || "");
|
||||||
|
|
||||||
|
// 다이얼로그가 열릴 때 외부 데이터를 내부로 복사
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open && props.selectedData) {
|
||||||
|
internalWorkflow.value = props.selectedData.workflow;
|
||||||
|
internalStepName.value = props.selectedData.stepName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save 버튼 클릭
|
||||||
|
const onSave = () => {
|
||||||
|
emit("save", {
|
||||||
|
workflow: internalWorkflow.value,
|
||||||
|
stepName: internalStepName.value,
|
||||||
|
});
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close 버튼 클릭
|
||||||
|
const onClose = () => {
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="rounded-lg overflow-hidden">
|
||||||
|
<!-- 타이틀 바 -->
|
||||||
|
|
||||||
|
<v-card-title
|
||||||
|
class="text-white font-weight-bold text-h6"
|
||||||
|
style="background-color: #1976d2"
|
||||||
|
>Edit Workflow Step Config</v-card-title
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 본문 -->
|
||||||
|
<v-card-text class="pt-6 px-6 pb-4">
|
||||||
|
<!-- Select Workflow -->
|
||||||
|
<v-row class="mb-6" dense>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<div class="font-weight-medium white--text">Select Workflow</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-select
|
||||||
|
v-model="internalWorkflow"
|
||||||
|
:items="workflowList"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
placeholder="Select Workflow"
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Workflow Step Name -->
|
||||||
|
<v-row class="mb-6" dense>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="font-weight-medium white--text mb-2">
|
||||||
|
Workflow Step Name
|
||||||
|
</div>
|
||||||
|
<v-text-field
|
||||||
|
v-model="internalStepName"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
placeholder="Enter Workflow Step"
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- 액션 버튼 -->
|
||||||
|
<v-card-actions class="justify-end" style="padding: 16px 24px">
|
||||||
|
<v-btn color="success" @click="onSave">Save</v-btn>
|
||||||
|
<v-btn text class="white--text" @click="onClose">Close</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editData: Object,
|
||||||
|
mode: String,
|
||||||
|
userOption: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["handle-data", "close-modal"]);
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const form = ref({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
file: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 다이얼로그 타이틀
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (props.mode === "create") return "Create Training Script";
|
||||||
|
if (props.mode === "edit") return "Edit Training Script";
|
||||||
|
return "Clone Execution";
|
||||||
|
});
|
||||||
|
|
||||||
|
const onChooseFile = () => {
|
||||||
|
fileInput.value?.click();
|
||||||
|
};
|
||||||
|
const submit = () => {
|
||||||
|
emit("handle-data", form.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="rounded-lg overflow-hidden">
|
||||||
|
<!-- 타이틀 영역 -->
|
||||||
|
<v-card-title
|
||||||
|
class="text-white font-weight-bold text-h6"
|
||||||
|
style="background-color: #1976d2"
|
||||||
|
>
|
||||||
|
{{ dialogTitle }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<v-form @submit.prevent="submit">
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Training Script Title
|
||||||
|
</label>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.name"
|
||||||
|
variant="outlined"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>File
|
||||||
|
</label>
|
||||||
|
<v-file-input
|
||||||
|
v-model="form.file"
|
||||||
|
label="Upload File"
|
||||||
|
@click:append-outer="onChooseFile"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Description
|
||||||
|
</label>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.description"
|
||||||
|
variant="outlined"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="justify-end" style="padding: 16px 24px">
|
||||||
|
<v-btn color="success" @click="submit">Save</v-btn>
|
||||||
|
<v-btn text class="white--text" @click="$emit('close-modal')"
|
||||||
|
>Close</v-btn
|
||||||
|
>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
// 부모에서 전달받을 Props 정의
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
selectedData: { workflow: string; stepName: string } | null;
|
||||||
|
workflowList: string[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// v-model 및 save 이벤트를 위한 Emit 정의
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: boolean): void;
|
||||||
|
(e: "save", payload: { workflow: string; stepName: string }): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 다이얼로그 내부에서 사용할 로컬 상태
|
||||||
|
const internalWorkflow = ref(props.selectedData?.workflow || "");
|
||||||
|
const internalStepName = ref(props.selectedData?.stepName || "");
|
||||||
|
|
||||||
|
// 다이얼로그가 열릴 때 외부 데이터를 내부로 복사
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open && props.selectedData) {
|
||||||
|
internalWorkflow.value = props.selectedData.workflow;
|
||||||
|
internalStepName.value = props.selectedData.stepName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save 버튼 클릭
|
||||||
|
const onSave = () => {
|
||||||
|
emit("save", {
|
||||||
|
workflow: internalWorkflow.value,
|
||||||
|
stepName: internalStepName.value,
|
||||||
|
});
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close 버튼 클릭
|
||||||
|
const onClose = () => {
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card flat>
|
||||||
|
<!-- 타이틀 바 -->
|
||||||
|
<v-toolbar flat color="primary" dark>
|
||||||
|
<v-toolbar-title>Edit Workflow</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn icon @click="onClose">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<!-- 본문 -->
|
||||||
|
<v-card-text class="pt-6 px-6 pb-4">
|
||||||
|
<!-- Select Workflow -->
|
||||||
|
<v-row class="mb-6" dense>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<div class="font-weight-medium white--text">Select Workflow</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-select
|
||||||
|
v-model="internalWorkflow"
|
||||||
|
:items="workflowList"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
placeholder="Select Workflow"
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Workflow Step Name -->
|
||||||
|
<v-row class="mb-6" dense>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="font-weight-medium white--text mb-2">
|
||||||
|
Workflow Step Name
|
||||||
|
</div>
|
||||||
|
<v-text-field
|
||||||
|
v-model="internalStepName"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
placeholder="Enter Workflow Step"
|
||||||
|
style="background: #1e1e1e; color: #fff"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- 액션 버튼 -->
|
||||||
|
<v-card-actions class="justify-end" style="padding: 16px 24px">
|
||||||
|
<v-btn color="success" @click="onSave">Save</v-btn>
|
||||||
|
<v-btn text class="white--text" @click="onClose">Close</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,207 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconArrowDown from "@/components/atoms/button/IconArrowDown.vue";
|
||||||
|
import IconArrowUp from "@/components/atoms/button/IconArrowUp.vue";
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { AutoflowService } from "@/components/service/management/AutoflowService";
|
||||||
|
import { storage } from "@/utils/storage";
|
||||||
|
import { Workflow } from "@/components/models/management/Autoflow";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useAutoflowStore } from "@/stores/autoflowStore";
|
||||||
|
const saving = ref(false);
|
||||||
|
const errorMsg = ref("");
|
||||||
|
const { projectId } = storeToRefs(useAutoflowStore());
|
||||||
|
const steps = ref([
|
||||||
|
{ order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" },
|
||||||
|
{
|
||||||
|
order: 2,
|
||||||
|
stepName: "Preprocessing",
|
||||||
|
type: "Preprocess",
|
||||||
|
status: "Not Configured",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
order: 3,
|
||||||
|
stepName: "Train Model",
|
||||||
|
type: "Train",
|
||||||
|
status: "Not Configured",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editData: Object,
|
||||||
|
mode: String,
|
||||||
|
userOption: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["handle-data", "close-modal", "saved"]);
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const nowLocalIso = (): string => {
|
||||||
|
const t = new Date(Date.now() - new Date().getTimezoneOffset() * 60000);
|
||||||
|
return t.toISOString().slice(0, 23); // 밀리초까지 포함, 끝의 'Z' 제거
|
||||||
|
};
|
||||||
|
const now = nowLocalIso();
|
||||||
|
const submit = async () => {
|
||||||
|
errorMsg.value = "";
|
||||||
|
if (!form.value.name.trim()) {
|
||||||
|
errorMsg.value = "Workflow Name은 필수입니다.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authObj =
|
||||||
|
(typeof storage?.getAuth === "function" ? storage.getAuth() : null) ??
|
||||||
|
JSON.parse(localStorage.getItem("autoflow-auth") || "{}");
|
||||||
|
|
||||||
|
// 2) username 추출 (userinfo / userInfo 모두 대응)
|
||||||
|
const regUserId =
|
||||||
|
authObj?.userInfo?.username ??
|
||||||
|
authObj?.userinfo?.username ??
|
||||||
|
authObj?.username ??
|
||||||
|
authObj?.userId ??
|
||||||
|
"";
|
||||||
|
|
||||||
|
if (!regUserId) {
|
||||||
|
errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: Workflow = {
|
||||||
|
workflowName: form.value.name.trim(),
|
||||||
|
workflowDescription: form.value.description?.trim() || "",
|
||||||
|
uploadYn: "Y",
|
||||||
|
regUserId,
|
||||||
|
regDt: now,
|
||||||
|
modDt: now,
|
||||||
|
projectId: projectId.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
saving.value = true;
|
||||||
|
const { data } = await AutoflowService.add(payload);
|
||||||
|
emit("saved", data);
|
||||||
|
emit("close-modal");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("워크플로우 저장 실패:", e);
|
||||||
|
errorMsg.value = "저장에 실패했습니다. 잠시 후 다시 시도하세요.";
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="rounded-lg overflow-hidden">
|
||||||
|
<!-- 타이틀 영역 -->
|
||||||
|
<v-card-title
|
||||||
|
class="text-white font-weight-bold text-h6"
|
||||||
|
style="background-color: #1976d2"
|
||||||
|
>
|
||||||
|
Create Workflow
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<div class="text-subtitle-1 font-weight-medium mb-4">
|
||||||
|
Workflow Information
|
||||||
|
</div>
|
||||||
|
<v-form @submit.prevent="submit">
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Workflow Name</label
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.name"
|
||||||
|
variant="outlined"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Workflow Description</label
|
||||||
|
>
|
||||||
|
<v-textarea
|
||||||
|
v-model="form.description"
|
||||||
|
variant="outlined"
|
||||||
|
rows="3"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-text class="pt-6 pb-4 px-6">
|
||||||
|
<div class="text-subtitle-1 font-weight-medium mb-4">Workflow Steps</div>
|
||||||
|
<v-row class="align-center mb-4">
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn color="primary" small>
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Step
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn color="success" small>Save</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn color="grey" small>Cancel</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-spacer />
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-simple-table dense>
|
||||||
|
<thead>
|
||||||
|
<tr class="grey lighten-2">
|
||||||
|
<th class="text-center" style="width: 5%">Order</th>
|
||||||
|
<th class="text-center">Step Name</th>
|
||||||
|
<th class="text-center">Component Type</th>
|
||||||
|
<th class="text-center">Status</th>
|
||||||
|
<th class="text-center" style="width: 20%">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(step, i) in steps"
|
||||||
|
:key="step.order"
|
||||||
|
style="border: 1px solid #ccc"
|
||||||
|
>
|
||||||
|
<td class="text-center">{{ step.order }}</td>
|
||||||
|
<td class="text-center">{{ step.stepName }}</td>
|
||||||
|
<td class="d-flex justify-center align-center">
|
||||||
|
<v-select
|
||||||
|
v-model="step.type"
|
||||||
|
:items="['DataPrep', 'Preprocess', 'Train']"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
style="max-width: 180px"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">{{ step.status }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<IconArrowUp />
|
||||||
|
<IconArrowDown />
|
||||||
|
|
||||||
|
<IconModifyBtn />
|
||||||
|
<IconDeleteBtn />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-simple-table>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="justify-end" style="padding: 16px 24px">
|
||||||
|
<v-btn color="success" @click="submit">Save</v-btn>
|
||||||
|
<v-btn text class="white--text" @click="$emit('close-modal')"
|
||||||
|
>Close</v-btn
|
||||||
|
>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editData: Object,
|
||||||
|
mode: String,
|
||||||
|
userOption: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["handle-data", "close-modal"]);
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const form = ref({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
file: "",
|
||||||
|
});
|
||||||
|
const onChooseFile = () => {
|
||||||
|
fileInput.value?.click();
|
||||||
|
};
|
||||||
|
const submit = () => {
|
||||||
|
emit("handle-data", form.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="rounded-lg overflow-hidden">
|
||||||
|
<!-- 타이틀 영역 -->
|
||||||
|
<v-card-title
|
||||||
|
class="text-white font-weight-bold text-h6"
|
||||||
|
style="background-color: #1976d2"
|
||||||
|
>
|
||||||
|
Workflow Information
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<v-form @submit.prevent="submit">
|
||||||
|
<div class="text-subtitle-1 font-weight-medium mb-4">
|
||||||
|
Workflow Information
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Workflow Name
|
||||||
|
</label>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.name"
|
||||||
|
variant="outlined"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Workflow Description
|
||||||
|
</label>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.description"
|
||||||
|
variant="outlined"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
||||||
|
>Upload File
|
||||||
|
</label>
|
||||||
|
<v-file-input
|
||||||
|
v-model="form.file"
|
||||||
|
label="Upload File"
|
||||||
|
@click:append-outer="onChooseFile"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="justify-end" style="padding: 16px 24px">
|
||||||
|
<v-btn color="success" @click="submit">Save</v-btn>
|
||||||
|
<v-btn text class="white--text" @click="$emit('close-modal')"
|
||||||
|
>Close</v-btn
|
||||||
|
>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,312 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import Plotly from "plotly.js-dist-min";
|
||||||
|
|
||||||
|
const pieChartRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (pieChartRef.value) {
|
||||||
|
Plotly.newPlot(
|
||||||
|
pieChartRef.value,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
values: [40, 25, 20, 15],
|
||||||
|
labels: ["Success", "Pending", "Failed", "Cancelled"],
|
||||||
|
type: "pie",
|
||||||
|
marker: {
|
||||||
|
colors: ["#4caf50", "#ff9800", "#f44336", "#9e9e9e"],
|
||||||
|
},
|
||||||
|
textinfo: "label+percent",
|
||||||
|
textfont: { color: "#fff", size: 14 },
|
||||||
|
hole: 0.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
paper_bgcolor: "#1e1e1e",
|
||||||
|
plot_bgcolor: "#1e1e1e",
|
||||||
|
showlegend: true,
|
||||||
|
legend: {
|
||||||
|
font: { color: "#ffffff", size: 12 },
|
||||||
|
orientation: "h",
|
||||||
|
x: 0.5,
|
||||||
|
xanchor: "center",
|
||||||
|
y: -0.2,
|
||||||
|
},
|
||||||
|
margin: { t: 20, b: 40, l: 0, r: 0 },
|
||||||
|
},
|
||||||
|
{ displayModeBar: false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentRuns = [
|
||||||
|
{ name: "Model A - v1", status: "success", time: "2025-05-12 09:12" },
|
||||||
|
{ name: "Model B - tuning", status: "success", time: "2025-05-14 08:59" },
|
||||||
|
{ name: "Model C - test run", status: "failed", time: "2025-05-13 18:13" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const datasetUpdates = [
|
||||||
|
{ name: "DrivingLog2025", count: 7 },
|
||||||
|
{ name: "CameraFrames", count: 3 },
|
||||||
|
{ name: "LidarScans", count: 2 },
|
||||||
|
{ name: "Traffic_log", count: 2 },
|
||||||
|
{ name: "Traffic_log2", count: 2 },
|
||||||
|
];
|
||||||
|
const dummyWorkflow = [
|
||||||
|
{ title: "Volcano Test Pipeline", date: "2025-05-13 08:12" },
|
||||||
|
{ title: "Volcano Test Pipeline", date: "2025-05-13 08:12" },
|
||||||
|
{ title: "XGBoost Training", date: "2025-05-13 08:12" },
|
||||||
|
{ title: "Data Preprocess Flow", date: "2025-05-13 08:12" },
|
||||||
|
];
|
||||||
|
const tableHeader = [
|
||||||
|
{ label: "Model Name", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Version", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Deployed At", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Status", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Download", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
deviceKey: "1",
|
||||||
|
name: "LaneDetectionModel",
|
||||||
|
version: "v1.2.0",
|
||||||
|
time: "2025-05-13 14:32",
|
||||||
|
status: "Active",
|
||||||
|
download: "Finished",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceKey: "2",
|
||||||
|
name: "TrafficSignClassifier",
|
||||||
|
version: "v0.9.3",
|
||||||
|
time: "2025-05-13 09:00",
|
||||||
|
status: "Pending",
|
||||||
|
download: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceKey: "3",
|
||||||
|
name: "PathPlannerModel",
|
||||||
|
version: "v2.0.1",
|
||||||
|
time: "2025-05-12 17:44",
|
||||||
|
status: "Failed",
|
||||||
|
download: "Failed",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
});
|
||||||
|
const handleRefresh = () => {
|
||||||
|
alert("Refresh 작업 진행중...");
|
||||||
|
};
|
||||||
|
const getSelectedAllData = () => {
|
||||||
|
data.value.selected = data.value.allSelected
|
||||||
|
? data.value.results.map(({ deviceKey }) => ({ deviceKey }))
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-6">
|
||||||
|
<h2 class="text-h6 font-weight-bold">배터리 상태 예측 모델 프로젝트</h2>
|
||||||
|
<v-btn color="primary" prepend-icon="mdi-refresh" @click="handleRefresh"
|
||||||
|
>Refresh</v-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
|
||||||
|
<div style="padding: 16px; border-bottom: 1px solid #ccc">
|
||||||
|
<h3 class="text-subtitle-1 font-weight-bold mb-0">
|
||||||
|
Workflow Success Rate
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div style="overflow-y: auto; padding: 8px 16px">
|
||||||
|
<div ref="pieChartRef" style="height: 280px"></div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
|
||||||
|
<div style="padding: 16px; border-bottom: 1px solid #ccc">
|
||||||
|
<h3 class="text-subtitle-1 font-weight-bold mb-0">
|
||||||
|
Recently Registered Workflow
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px">
|
||||||
|
<v-list density="comfortable" nav>
|
||||||
|
<v-list-item v-for="(item, idx) in dummyWorkflow" :key="idx">
|
||||||
|
<template #title>
|
||||||
|
<div class="d-flex justify-space-between align-center w-100">
|
||||||
|
<span class="text-body-2 font-weight-medium">{{
|
||||||
|
item.title
|
||||||
|
}}</span>
|
||||||
|
<span class="text-caption text-grey-lighten-1">{{
|
||||||
|
item.date
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mt-4">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
|
||||||
|
<div style="padding: 16px; border-bottom: 1px solid #ccc">
|
||||||
|
<h3 class="text-subtitle-1 font-weight-bold mb-0">Recent Run</h3>
|
||||||
|
</div>
|
||||||
|
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px">
|
||||||
|
<v-list density="comfortable">
|
||||||
|
<v-list-item
|
||||||
|
v-for="(run, idx) in recentRuns"
|
||||||
|
:key="idx"
|
||||||
|
class="py-2"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-avatar
|
||||||
|
size="28"
|
||||||
|
:color="
|
||||||
|
run.status === 'success'
|
||||||
|
? 'green lighten-1'
|
||||||
|
: 'red lighten-1'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon size="20" color="white">
|
||||||
|
{{ run.status === "success" ? "mdi-check" : "mdi-close" }}
|
||||||
|
</v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="d-flex flex-column text-right ml-4"
|
||||||
|
style="flex: 1"
|
||||||
|
>
|
||||||
|
<span class="font-weight-medium text-body-2">
|
||||||
|
{{ run.name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-caption text-grey-darken-1">
|
||||||
|
{{ run.time }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
|
||||||
|
<div style="padding: 16px; border-bottom: 1px solid #ccc">
|
||||||
|
<h3 class="text-subtitle-1 font-weight-bold mb-0">
|
||||||
|
Dataset Update Activity
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px">
|
||||||
|
<v-list dense>
|
||||||
|
<v-list-item v-for="(data, idx) in datasetUpdates" :key="idx">
|
||||||
|
<v-list-item-title>{{ data.name }}</v-list-item-title>
|
||||||
|
<v-progress-linear
|
||||||
|
:model-value="data.count * 10"
|
||||||
|
height="8"
|
||||||
|
color="primary"
|
||||||
|
class="mt-1"
|
||||||
|
/>
|
||||||
|
<v-list-item-subtitle
|
||||||
|
>{{ data.count }} updates</v-list-item-subtitle
|
||||||
|
>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-card class="rounded-lg pa-4 mt-4">
|
||||||
|
<div class="d-flex justify-space-between align-center mt-8 mb-2 px-2">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<span class="text-subtitle-1 font-weight-bold">Model Deployment</span>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
class="text-caption font-weight-bold"
|
||||||
|
append-icon="mdi-arrow-right"
|
||||||
|
style="text-transform: none"
|
||||||
|
>
|
||||||
|
Go to Model Deploy
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table density="comfortable" fixed-header height="625">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 5%" />
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.allSelected"
|
||||||
|
style="min-width: 36px"
|
||||||
|
:indeterminate="data.allSelected === true"
|
||||||
|
hide-details
|
||||||
|
@change="getSelectedAllData"
|
||||||
|
></v-checkbox>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="item.style"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="(item, i) in data.results"
|
||||||
|
:key="i"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.selected"
|
||||||
|
hide-details
|
||||||
|
:value="{ deviceKey: item.deviceKey }"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.version }}</td>
|
||||||
|
<td>{{ item.time }}</td>
|
||||||
|
<td>{{ item.status }}</td>
|
||||||
|
<td>{{ item.download }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
</v-col>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
export interface Workflow {
|
||||||
|
workflowName: string;
|
||||||
|
workflowDescription?: string;
|
||||||
|
uploadYn: "Y" | "N";
|
||||||
|
regUserId: string;
|
||||||
|
regDt: string;
|
||||||
|
modDt: string;
|
||||||
|
projectId: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
export interface UserSearch {
|
||||||
|
searchType: string;
|
||||||
|
searchText: string;
|
||||||
|
pageSize: number;
|
||||||
|
pageNum: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
userId: string;
|
||||||
|
userPw: string;
|
||||||
|
userNm: string;
|
||||||
|
userAuth: string;
|
||||||
|
delYn: string;
|
||||||
|
regDate: string;
|
||||||
|
regUserId: string;
|
||||||
|
regUserNm: string;
|
||||||
|
modDate: string;
|
||||||
|
modUserId: string;
|
||||||
|
modUserNm: string;
|
||||||
|
logingDate: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProjectMap {
|
||||||
|
userId: string;
|
||||||
|
prjCd: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
export interface ApiProject {
|
||||||
|
id: number | null;
|
||||||
|
prjCd: string;
|
||||||
|
prjNm: string;
|
||||||
|
prjDesc: string;
|
||||||
|
prjStartDt: string;
|
||||||
|
prjEndDt: string;
|
||||||
|
delYn: string;
|
||||||
|
regDate: string;
|
||||||
|
regUserId: string;
|
||||||
|
regUserNm: string;
|
||||||
|
modDate: string;
|
||||||
|
modUserId: string;
|
||||||
|
modUserNm: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiProject {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
creator: string;
|
||||||
|
date: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Permission = "CREATE" | "READ" | "UPDATE" | "DELETE";
|
||||||
|
|
||||||
|
export interface ProjectAuthority {
|
||||||
|
projectId: number;
|
||||||
|
userId: number | string;
|
||||||
|
permissions: Permission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectSearchParams {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
keyword: string;
|
||||||
|
searchType: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
sortField: string;
|
||||||
|
sortDirection: string;
|
||||||
|
projectId: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export interface Token {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { commonStore, loadingStore } from "@/stores/commonStore";
|
||||||
|
import { storage } from "@/utils/storage";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
|
const loading = loadingStore();
|
||||||
|
|
||||||
|
const API_URL = import.meta.env.VITE_APP_API_SERVER_URL;
|
||||||
|
console.log("API URL:", API_URL);
|
||||||
|
export const request = {
|
||||||
|
post: (uri: string, param: any): any => {
|
||||||
|
return axios.post(`${API_URL}${uri}`, param);
|
||||||
|
},
|
||||||
|
get: (uri: string, param: any): any => {
|
||||||
|
return axios.get(`${API_URL}${uri}`, { params: param });
|
||||||
|
},
|
||||||
|
delete: (uri: string, param: any): any => {
|
||||||
|
return axios.delete(`${API_URL}${uri}`, param);
|
||||||
|
},
|
||||||
|
put: (uri: string, param: any): any => {
|
||||||
|
return axios.put(`${API_URL}${uri}`, param);
|
||||||
|
},
|
||||||
|
postFile: (uri: string, param: any, attachment: any, progress: any): any => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
for (const key in attachment) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(attachment, key)) {
|
||||||
|
const value = attachment[key];
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const file of value) {
|
||||||
|
formData.append(key, file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formData.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.append("param", JSON.stringify(param));
|
||||||
|
|
||||||
|
return axios.post(`${API_URL}${uri}`, formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
onUploadProgress: progress,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
postResponseFile: (
|
||||||
|
uri: string,
|
||||||
|
param: any,
|
||||||
|
responseParam: any,
|
||||||
|
attachment: any,
|
||||||
|
progress: any,
|
||||||
|
): any => {
|
||||||
|
const formData = new FormData();
|
||||||
|
for (const key in attachment) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(attachment, key)) {
|
||||||
|
const value = attachment[key];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const file of value) {
|
||||||
|
formData.append(key, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.append("param", JSON.stringify(param));
|
||||||
|
formData.append("responseParam", JSON.stringify(responseParam));
|
||||||
|
|
||||||
|
return axios.post(`${API_URL}${uri}`, formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
onUploadProgress: progress,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
postNoParam: (uri: string): any => {
|
||||||
|
return axios.post(`${API_URL}${uri}`, null);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.defaults.headers.common["Access-Control-Allow-Origin"] = "*";
|
||||||
|
axios.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
loading.setLoading(true);
|
||||||
|
|
||||||
|
const token = storage.getToken();
|
||||||
|
const refresh = storage.getRefreshToken();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
if (token) (config.headers as any)["cuuva-jwt"] = token;
|
||||||
|
if (refresh) (config.headers as any)["cuuva-jwt-refresh"] = refresh;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
loading.setLoading(false);
|
||||||
|
|
||||||
|
console.log("request error", error);
|
||||||
|
|
||||||
|
const store = commonStore();
|
||||||
|
store.setSnackbarMsg({
|
||||||
|
text: "에러가 발생하였습니다.",
|
||||||
|
color: "red",
|
||||||
|
result: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
loading.setLoading(false);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
loading.setLoading(false);
|
||||||
|
|
||||||
|
const store = commonStore();
|
||||||
|
store.setSnackbarMsg({
|
||||||
|
text: "에러가 발생하였습니다.",
|
||||||
|
color: "red",
|
||||||
|
result: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { Workflow } from "@/components/models/management/Autoflow";
|
||||||
|
import { request } from "@/components/service/index";
|
||||||
|
export const AutoflowService = {
|
||||||
|
add: (payload: Workflow) => {
|
||||||
|
return request.post("/api/workflows", payload);
|
||||||
|
},
|
||||||
|
getAll: () => request.get("/api/workflows", {}),
|
||||||
|
|
||||||
|
delete: (id: Number) => {
|
||||||
|
return request.delete(`/api/workflows/${id}`, {});
|
||||||
|
},
|
||||||
|
view: (id: Number) => {
|
||||||
|
return request.get(`/api/workflows/${id}`, {});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { request } from "@/components/service/index";
|
||||||
|
import { UserSearch, User } from "@/components/models/management/User";
|
||||||
|
|
||||||
|
export const UserManagerService = {
|
||||||
|
// 로그인
|
||||||
|
signIn: (payload: User) => {
|
||||||
|
return request.post("/api/auth/signin", payload);
|
||||||
|
},
|
||||||
|
// 회원가입
|
||||||
|
signUp: (payload: User) => {
|
||||||
|
return request.post("/api/auth/signup", payload);
|
||||||
|
},
|
||||||
|
// 로그아웃
|
||||||
|
signOut: () => {
|
||||||
|
return request.post("/api/auth/signout", {});
|
||||||
|
},
|
||||||
|
// 토큰 갱신
|
||||||
|
refreshtoken: () => {
|
||||||
|
return request.post("/api/auth/refreshtoken", {});
|
||||||
|
},
|
||||||
|
// 전체 사용자 조회
|
||||||
|
getAll: () => {
|
||||||
|
return request.get("/api/auth/users", {});
|
||||||
|
},
|
||||||
|
// 사용자 조회
|
||||||
|
getUser: (userId: number) => {
|
||||||
|
return request.get(`/api/auth/users/${userId}`, {});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { request } from "@/components/service/index";
|
||||||
|
import {
|
||||||
|
ApiProject,
|
||||||
|
ProjectAuthority,
|
||||||
|
ProjectSearchParams,
|
||||||
|
} from "@/components/models/project/Project";
|
||||||
|
|
||||||
|
export const ProjectService = {
|
||||||
|
// ID로 프로젝트 조회
|
||||||
|
fetchProjectById: (id: number) => {
|
||||||
|
return request.get(`/api/projects/${id}`, {});
|
||||||
|
},
|
||||||
|
// 프로젝트 수정
|
||||||
|
update: (id: number, payload: ApiProject) => {
|
||||||
|
return request.put(`/api/projects/${id}`, payload);
|
||||||
|
},
|
||||||
|
// 프로젝트 삭제
|
||||||
|
delete: (id: number) => {
|
||||||
|
return request.delete(`/api/projects/${id}`, {});
|
||||||
|
},
|
||||||
|
// 전체 프로젝트 목록 조회
|
||||||
|
search: () => {
|
||||||
|
return request.get("/api/projects", {});
|
||||||
|
},
|
||||||
|
// 프로젝트 생성
|
||||||
|
add: (payload: ApiProject) => {
|
||||||
|
return request.post("/api/projects", payload);
|
||||||
|
},
|
||||||
|
// 검색 및 페이지네이션 프로젝트 목록 조회
|
||||||
|
searchProjects: (params: ProjectSearchParams) =>
|
||||||
|
request.get("/api/projects/search", params),
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 프로젝트 권한
|
||||||
|
projectAuthority: (projectId: number, payload: ProjectAuthority) => {
|
||||||
|
return request.post(`/api/projects/${projectId}/users`, payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteProjectAuthority: (projectId: number, userId: number) => {
|
||||||
|
return request.delete(
|
||||||
|
`/api/projects/${projectId}/users/${userId}/permissions`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { request } from "@/components/service/index";
|
||||||
|
import { Token } from "@/components/models/token/token";
|
||||||
|
|
||||||
|
export const TokenService = {
|
||||||
|
refreshToken: (): Promise<{ data: Token }> =>
|
||||||
|
request.postNoParam("/api/auth/refreshtoken"),
|
||||||
|
};
|
||||||
@ -0,0 +1,519 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
import ViewComponent from "@/components/templates/Datasets/ViewComponent.vue";
|
||||||
|
import DatasetsBaseDoalog from "@/components/atoms/organisms/DatasetsBaseDoalog.vue";
|
||||||
|
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
|
||||||
|
// const store = commonStore();
|
||||||
|
|
||||||
|
const openView = ref(false);
|
||||||
|
const openModify = ref(false);
|
||||||
|
const tableHeader = [
|
||||||
|
{
|
||||||
|
label: "Title",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "File Name",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "File Path",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Description",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Created Data",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Modified Data",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Action",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchOptions = [
|
||||||
|
{
|
||||||
|
searchType: "전체",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 별칭",
|
||||||
|
searchText: "deviceAlias",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 키",
|
||||||
|
searchText: "deviceKey",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "사용자",
|
||||||
|
searchText: "userId",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 이름",
|
||||||
|
searchText: "deviceName",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 모델",
|
||||||
|
searchText: "deviceModel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 OS",
|
||||||
|
searchText: "deviceOs",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageSizeOptions = [
|
||||||
|
{ text: "10 페이지", value: 10 },
|
||||||
|
{ text: "50 페이지", value: 50 },
|
||||||
|
{ text: "100 페이지", value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isCreateVisible: false,
|
||||||
|
isUploadVisible: false,
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
const params = { ...data.value.params };
|
||||||
|
if (params.searchType === "" || params.searchText === "") {
|
||||||
|
delete params.searchType;
|
||||||
|
delete params.searchText;
|
||||||
|
}
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
title: "배터리 상태 예측 모델 프로젝트",
|
||||||
|
fileName: "train.py",
|
||||||
|
filePath: "/kubeflow-users/battery/train.py",
|
||||||
|
description: "배터리 상태 예측 스크립트",
|
||||||
|
createdData: "2025-04-28 12:01:00",
|
||||||
|
modifiedData: "2025-04-28 12:01:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "상태 추적 모델",
|
||||||
|
fileName: "detection.py",
|
||||||
|
filePath: "/kubeflow-users/status/detection.py",
|
||||||
|
description: "상태 추적 스크립트",
|
||||||
|
createdData: "2025-04-20 12:01:00",
|
||||||
|
modifiedData: "2025-04-28 12:01:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = 5;
|
||||||
|
// DeviceService.search(params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.results = d.data.deviceList;
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "디바이스 조회 실패",
|
||||||
|
// color: "error",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// DeviceService.search().then((d) => {
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveData = () => {
|
||||||
|
if (data.value.selected.length === 0) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제 할 데이터를 선택해주세요. ",
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.value.allSelected || data.value.selected.length !== 1) {
|
||||||
|
data.value.isConfirmDialogVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//리스트로 삭제 할때
|
||||||
|
removeData(undefined);
|
||||||
|
};
|
||||||
|
const closeDetail = () => {
|
||||||
|
openView.value = false;
|
||||||
|
};
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
const openSettingModal = (selectedItem) => {
|
||||||
|
data.value.selectedData = selectedItem;
|
||||||
|
data.value.modalMode = "setting";
|
||||||
|
openView.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "create";
|
||||||
|
data.value.isCreateVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModifyModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "edit";
|
||||||
|
data.value.isUploadVisible = true;
|
||||||
|
};
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.isCreateVisible = null;
|
||||||
|
};
|
||||||
|
const closeModifyModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.isUploadVisible = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedAllData = () => {
|
||||||
|
data.value.selected = data.value.allSelected
|
||||||
|
? data.value.results.map((item) => {
|
||||||
|
return {
|
||||||
|
deviceKey: item.deviceKey,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-100" v-if="!openView">
|
||||||
|
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>-->
|
||||||
|
<!-- <FormComponent-->
|
||||||
|
<!-- :edit-data="data.selectedData"-->
|
||||||
|
<!-- :mode="data.modalMode"-->
|
||||||
|
<!-- @close-modal="closeModal"-->
|
||||||
|
<!-- @handle-data="saveData"-->
|
||||||
|
<!-- :user-option="data.userOption"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </v-dialog>-->
|
||||||
|
<!-- <v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">-->
|
||||||
|
<!-- <ConfirmDialogComponent-->
|
||||||
|
<!-- @cancel="data.isConfirmDialogVisible = false"-->
|
||||||
|
<!-- @delete="removeData(undefined)"-->
|
||||||
|
<!-- @init="(data.selected = []), (data.allSelected = false)"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </v-dialog>-->
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">Datasets</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card flat class="bg-shades-transparent mb-4">
|
||||||
|
<div class="d-flex justify-center flex-wrap align-center">
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
<v-responsive min-width="540" max-width="540">
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.params.searchText"
|
||||||
|
label="검색어"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
required
|
||||||
|
class="mt-3 mb-3"
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="changePageNum(1)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-responsive>
|
||||||
|
|
||||||
|
<div class="ml-3">
|
||||||
|
<v-btn
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
:rounded="5"
|
||||||
|
@click="changePageNum(1)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-sheet
|
||||||
|
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
||||||
|
>
|
||||||
|
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
||||||
|
<v-sheet
|
||||||
|
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
||||||
|
>
|
||||||
|
<v-chip color="primary"
|
||||||
|
>총 {{ data.totalDataLength.toLocaleString() }}개
|
||||||
|
</v-chip>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="bg-shades-transparent">
|
||||||
|
<v-responsive max-width="140" min-width="140" class="mb-2">
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.pageSize"
|
||||||
|
density="compact"
|
||||||
|
:items="pageSizeOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="changePageNum(1)"
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2">
|
||||||
|
<v-btn color="info" @click="openCreateModal"
|
||||||
|
>Create Dataset
|
||||||
|
</v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<v-card class="rounded-lg pa-8">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table
|
||||||
|
density="comfortable"
|
||||||
|
fixed-header
|
||||||
|
height="625"
|
||||||
|
col-md-12
|
||||||
|
col-12
|
||||||
|
overflow-x-auto
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="`${item.style}`"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="(item, i) in data.results"
|
||||||
|
:key="i"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<td>{{ item.title }}</td>
|
||||||
|
<td>{{ item.fileName }}</td>
|
||||||
|
<td>{{ item.filePath }}</td>
|
||||||
|
<td>{{ item.description }}</td>
|
||||||
|
<td>{{ item.createdData }}</td>
|
||||||
|
<td>{{ item.modifiedData }}</td>
|
||||||
|
<td style="white-space: nowrap">
|
||||||
|
<IconInfoBtn @on-click="openSettingModal(item)" />
|
||||||
|
<IconModifyBtn @on-click="openModifyModal()" />
|
||||||
|
<IconDeleteBtn
|
||||||
|
@on-click="
|
||||||
|
removeData([{ deviceKey: item.deviceKey }])
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
<v-card-actions class="text-center mt-8 justify-center">
|
||||||
|
<v-pagination
|
||||||
|
v-model="data.params.pageNum"
|
||||||
|
:length="data.pageLength"
|
||||||
|
:total-visible="10"
|
||||||
|
color="primary"
|
||||||
|
rounded="circle"
|
||||||
|
@update:model-value="getData"
|
||||||
|
></v-pagination>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
<v-dialog v-model="data.isCreateVisible" max-width="600" persistent>
|
||||||
|
<DatasetsBaseDoalog
|
||||||
|
:edit-data="data.selectedData"
|
||||||
|
:mode="data.modalMode"
|
||||||
|
@close-modal="closeCreateModal"
|
||||||
|
@handle-data="saveData"
|
||||||
|
:user-option="data.userOption"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
<v-dialog v-model="data.isUploadVisible" max-width="600" persistent>
|
||||||
|
<DatasetsBaseDoalog
|
||||||
|
:edit-data="data.selectedData"
|
||||||
|
:mode="data.modalMode"
|
||||||
|
@close-modal="closeModifyModal"
|
||||||
|
@handle-data="saveData"
|
||||||
|
:user-option="data.userOption"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-100" v-else>
|
||||||
|
<ViewComponent @close="closeDetail" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,257 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
// const store = commonStore();
|
||||||
|
const experimentInfo = ref({
|
||||||
|
datasetTitle: "자율주행차량 배터리 상태 예측 모델 구축",
|
||||||
|
projectName: "배터리 상태 예측 모델 프로젝트",
|
||||||
|
version: "2.0",
|
||||||
|
createdDate: "2025-02-06",
|
||||||
|
createdId: "ADMIN_001",
|
||||||
|
modifiedDate: "2025-04-30",
|
||||||
|
modifiedId: "USER_002",
|
||||||
|
description: "날씨, 조도, 도로 상태 등의 주행환경 데이터",
|
||||||
|
fileName: "environment_log.csv",
|
||||||
|
fileSize: "58KB",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">View Details</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Dataset Title
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{
|
||||||
|
experimentInfo.datasetTitle
|
||||||
|
}}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Version </v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.version }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<!-- Experiment Name -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Project Name
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.projectName
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Created Date
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{
|
||||||
|
experimentInfo.createdDate
|
||||||
|
}}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Created ID </v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.createdId }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Modified Date
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{
|
||||||
|
experimentInfo.modifiedDate
|
||||||
|
}}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Modified ID
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.modifiedId }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Description</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.description
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">File</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{ experimentInfo.fileName }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
<v-sheet class="d-flex justify-end mb-2">
|
||||||
|
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card-text {
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 전체 테이블 1px 테두리 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card-text th {
|
||||||
|
font-size: 20px;
|
||||||
|
min-width: 400px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.v-card-text td {
|
||||||
|
font-size: 16px;
|
||||||
|
min-width: 600px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
.v-card-text tr:nth-child(odd) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,599 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, watch, computed } from "vue";
|
||||||
|
import { commonStore } from "@/stores/commonStore";
|
||||||
|
import { storage } from "@/utils/storage.js";
|
||||||
|
|
||||||
|
import { ProjectService } from "@/components/service/project/projectService";
|
||||||
|
import { UserManagerService } from "@/components/service/management/userManagerService";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Permission,
|
||||||
|
ApiProject,
|
||||||
|
} from "@/components/models/project/Project";
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// 권한/공통
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
const store = commonStore();
|
||||||
|
|
||||||
|
const DEFAULT_PERMISSIONS: Permission[] = [
|
||||||
|
"CREATE",
|
||||||
|
"READ",
|
||||||
|
"UPDATE",
|
||||||
|
"DELETE",
|
||||||
|
];
|
||||||
|
|
||||||
|
const roles = ref<string[]>([]);
|
||||||
|
const refreshRoles = () => {
|
||||||
|
const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null;
|
||||||
|
const r = auth?.userInfo?.roles ?? auth?.roles ?? [];
|
||||||
|
roles.value = Array.isArray(r) ? r : [];
|
||||||
|
};
|
||||||
|
const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN"));
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// 테이블/검색 상태 (UI 그대로 사용)
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
const tableHeader = [
|
||||||
|
{ label: "No", width: "5%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Project Name", width: "20%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Description", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Select Users", width: "12%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Created DateTime", width: "18%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Action", width: "13%", style: "word-break: keep-all;" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchOptions = [
|
||||||
|
{ searchType: "전체", searchText: "" },
|
||||||
|
{ searchType: "프로젝트명", searchText: "prjNm" },
|
||||||
|
{ searchType: "설명", searchText: "prjDesc" },
|
||||||
|
{ searchType: "생성자", searchText: "regUserId" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageSizeOptions = [
|
||||||
|
{ text: "10 페이지", value: 10 },
|
||||||
|
{ text: "50 페이지", value: 50 },
|
||||||
|
{ text: "100 페이지", value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
type Row = {
|
||||||
|
no: number;
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
users: string[];
|
||||||
|
registDt: string;
|
||||||
|
deviceKey: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: { pageNum: 1, pageSize: 10, searchType: "", searchText: "" },
|
||||||
|
results: [] as Row[],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "" as "create" | "edit" | "",
|
||||||
|
selectedData: null as Row | null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [] as Array<{ deviceKey: number }>,
|
||||||
|
isCreateVisible: false,
|
||||||
|
isUploadVisible: false,
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fmtDate = (v?: string) => (v ? v.replace("T", " ").slice(0, 19) : "-");
|
||||||
|
const splitCsv = (v?: string) =>
|
||||||
|
String(v ?? "")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
/** 사용자 목록 (v-select items) */
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
type UserOption = { id: number | string; username: string };
|
||||||
|
const userOptions = ref<UserOption[]>([]);
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
const { data } = await UserManagerService.getAll();
|
||||||
|
const raw = data as Array<{ id: number | string; username: string }>;
|
||||||
|
userOptions.value = raw.map((u) => ({ id: u.id, username: u.username }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
/** 목록 로드: 카드형 로직과 동일한 응답을 테이블 Row로 매핑 */
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
function toRow(p: any, index: number, offset: number): Row {
|
||||||
|
return {
|
||||||
|
no: offset + index + 1,
|
||||||
|
name: p.prjNm ?? "-",
|
||||||
|
desc: p.prjDesc ?? "-",
|
||||||
|
users: splitCsv(p.regUserId ?? p.regUserNm),
|
||||||
|
registDt: fmtDate(p.regDate ?? p.prjStartDt),
|
||||||
|
deviceKey: p.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
const { pageNum, pageSize } = data.value.params;
|
||||||
|
const startIndex = (pageNum - 1) * pageSize;
|
||||||
|
|
||||||
|
const res = await ProjectService.search();
|
||||||
|
const raw = Array.isArray(res.data) ? res.data : (res.data?.content ?? []);
|
||||||
|
|
||||||
|
data.value.totalDataLength = Array.isArray(res.data)
|
||||||
|
? raw.length
|
||||||
|
: (res.data?.totalElements ?? raw.length);
|
||||||
|
|
||||||
|
const slice = Array.isArray(res.data)
|
||||||
|
? raw.slice(startIndex, startIndex + pageSize)
|
||||||
|
: raw;
|
||||||
|
data.value.results = slice.map((p: any, i: number) =>
|
||||||
|
toRow(p, i, startIndex),
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = data.value.totalDataLength || 0;
|
||||||
|
data.value.pageLength =
|
||||||
|
total % data.value.params.pageSize === 0
|
||||||
|
? total / data.value.params.pageSize
|
||||||
|
: Math.ceil(total / data.value.params.pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
/** 폼 & 권한 부여 & 저장 흐름 (카드형과 동일) */
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
const form = ref({
|
||||||
|
prjCd: "",
|
||||||
|
prjNm: "",
|
||||||
|
prjDesc: "",
|
||||||
|
selectedUsers: [] as string[],
|
||||||
|
});
|
||||||
|
const editingProjectId = ref<number | null>(null);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.value.prjCd = `PRJ${Date.now()}`;
|
||||||
|
form.value.prjNm = "";
|
||||||
|
form.value.prjDesc = "";
|
||||||
|
form.value.selectedUsers = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildApiPayload = (): ApiProject => {
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
const nowIso = new Date().toISOString();
|
||||||
|
const namesCsv = form.value.selectedUsers.join(",");
|
||||||
|
return {
|
||||||
|
id: data.value.modalMode === "edit" ? editingProjectId.value! : null,
|
||||||
|
prjCd: form.value.prjCd,
|
||||||
|
prjNm: form.value.prjNm,
|
||||||
|
prjDesc: form.value.prjDesc,
|
||||||
|
prjStartDt: today,
|
||||||
|
prjEndDt: today,
|
||||||
|
delYn: "N",
|
||||||
|
regDate: nowIso,
|
||||||
|
regUserId: namesCsv,
|
||||||
|
regUserNm: namesCsv,
|
||||||
|
modDate: nowIso,
|
||||||
|
modUserId: namesCsv,
|
||||||
|
modUserNm: namesCsv,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async function grantDefaultPermissions(projectId: number, usernames: string[]) {
|
||||||
|
if (!usernames?.length) return;
|
||||||
|
const set = new Set(usernames);
|
||||||
|
const numericIds = userOptions.value
|
||||||
|
.filter((u) => set.has(u.username))
|
||||||
|
.map((u) => Number(u.id))
|
||||||
|
.filter((n) => Number.isFinite(n));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
numericIds.map((uid) =>
|
||||||
|
ProjectService.projectAuthority(projectId, {
|
||||||
|
projectId,
|
||||||
|
userId: uid,
|
||||||
|
permissions: DEFAULT_PERMISSIONS,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProject() {
|
||||||
|
try {
|
||||||
|
const payload = buildApiPayload();
|
||||||
|
let projectId: number;
|
||||||
|
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
const res = await ProjectService.add(payload);
|
||||||
|
projectId = res.data.id;
|
||||||
|
} else {
|
||||||
|
await ProjectService.update(editingProjectId.value!, payload);
|
||||||
|
projectId = editingProjectId.value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
await grantDefaultPermissions(projectId, form.value.selectedUsers);
|
||||||
|
await getData();
|
||||||
|
data.value.isCreateVisible = false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`${data.value.modalMode} 실패:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// 삭제
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
async function deleteRows(targetList?: Array<{ deviceKey: number }>) {
|
||||||
|
const removeList = targetList ?? data.value.selected;
|
||||||
|
if (!removeList?.length) return;
|
||||||
|
|
||||||
|
const ids = removeList.map((x) => x.deviceKey);
|
||||||
|
|
||||||
|
const remove = (id: number) =>
|
||||||
|
ProjectService.delete(id).then((res) => {
|
||||||
|
if (res.status < 200 || res.status >= 300) return Promise.reject(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
const after = async () => {
|
||||||
|
if (
|
||||||
|
ids.length >= data.value.results.length &&
|
||||||
|
data.value.params.pageNum > 1
|
||||||
|
) {
|
||||||
|
data.value.params.pageNum -= 1;
|
||||||
|
}
|
||||||
|
await getData();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ids.length === 1) {
|
||||||
|
try {
|
||||||
|
await remove(ids[0]);
|
||||||
|
store.setSnackbarMsg?.({
|
||||||
|
color: "success",
|
||||||
|
text: "삭제되었습니다.",
|
||||||
|
result: 200,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
store.setSnackbarMsg?.({
|
||||||
|
color: "warning",
|
||||||
|
text: "삭제 실패",
|
||||||
|
result: 500,
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
after();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Promise.all(ids.map(remove))
|
||||||
|
.then(() =>
|
||||||
|
store.setSnackbarMsg?.({
|
||||||
|
color: "success",
|
||||||
|
text: "모두 삭제되었습니다.",
|
||||||
|
result: 200,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
store.setSnackbarMsg?.({
|
||||||
|
color: "warning",
|
||||||
|
text: "일부 삭제 실패",
|
||||||
|
result: 500,
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
})
|
||||||
|
.finally(after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// UI 핸들러 (모달 열기/수정 열기 등)
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
function getSelectedAllData() {
|
||||||
|
data.value.selected = data.value.allSelected
|
||||||
|
? data.value.results.map((r) => ({ deviceKey: r.deviceKey }))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePageNum(page: number) {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateModal() {
|
||||||
|
data.value.modalMode = "create";
|
||||||
|
editingProjectId.value = null;
|
||||||
|
resetForm();
|
||||||
|
data.value.isCreateVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(row: Row) {
|
||||||
|
data.value.modalMode = "edit";
|
||||||
|
editingProjectId.value = row.deviceKey;
|
||||||
|
|
||||||
|
form.value.prjCd = row.name;
|
||||||
|
form.value.prjNm = row.name;
|
||||||
|
form.value.prjDesc = row.desc === "-" ? "" : row.desc;
|
||||||
|
form.value.selectedUsers = Array.isArray(row.users) ? [...row.users] : [];
|
||||||
|
|
||||||
|
// v-select에 없는 사용자명이 있으면 임시 아이템으로 추가해 칩이 보이게 함
|
||||||
|
const known = new Set(userOptions.value.map((u) => u.username));
|
||||||
|
const missing = form.value.selectedUsers
|
||||||
|
.filter((u) => !known.has(u))
|
||||||
|
.map((u) => ({ id: u, username: u }));
|
||||||
|
if (missing.length) userOptions.value = [...userOptions.value, ...missing];
|
||||||
|
|
||||||
|
data.value.isCreateVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetailModal(row: Row) {
|
||||||
|
data.value.selectedData = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetail() {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모달 닫힐 때 리프레시
|
||||||
|
watch(
|
||||||
|
() => data.value.isCreateVisible,
|
||||||
|
(now, prev) => {
|
||||||
|
if (prev && !now) getData();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// 초기 로딩
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
onMounted(async () => {
|
||||||
|
refreshRoles();
|
||||||
|
await Promise.all([loadUsers(), getData()]);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-100">
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
||||||
|
>
|
||||||
|
<!-- 헤더 -->
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">Project</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 검색/페이지 -->
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card flat class="bg-shades-transparent mb-4">
|
||||||
|
<div class="d-flex justify-center flex-wrap align-center">
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-responsive>
|
||||||
|
|
||||||
|
<v-responsive min-width="540" max-width="540">
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.params.searchText"
|
||||||
|
label="검색어"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
required
|
||||||
|
class="mt-3 mb-3"
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="changePageNum(1)"
|
||||||
|
/>
|
||||||
|
</v-responsive>
|
||||||
|
|
||||||
|
<div class="ml-3">
|
||||||
|
<v-btn
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
:rounded="5"
|
||||||
|
@click="changePageNum(1)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-sheet
|
||||||
|
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
||||||
|
>
|
||||||
|
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
||||||
|
<v-sheet
|
||||||
|
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
||||||
|
>
|
||||||
|
<v-chip color="primary"
|
||||||
|
>총 {{ data.totalDataLength.toLocaleString() }}개</v-chip
|
||||||
|
>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="bg-shades-transparent">
|
||||||
|
<v-responsive max-width="140" min-width="140" class="mb-2">
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.pageSize"
|
||||||
|
density="compact"
|
||||||
|
:items="pageSizeOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="changePageNum(1)"
|
||||||
|
/>
|
||||||
|
</v-responsive>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<v-sheet class="justify-end mb-2">
|
||||||
|
<v-btn color="info" @click="openCreateModal"
|
||||||
|
>Create Project</v-btn
|
||||||
|
>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<!-- 테이블 -->
|
||||||
|
<v-card class="rounded-lg pa-8">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table
|
||||||
|
density="comfortable"
|
||||||
|
fixed-header
|
||||||
|
height="625"
|
||||||
|
overflow-x-auto
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 5%" />
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.allSelected"
|
||||||
|
style="min-width: 36px"
|
||||||
|
:indeterminate="data.allSelected === true"
|
||||||
|
hide-details
|
||||||
|
@change="getSelectedAllData"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="`${item.style}`"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="(item, i) in data.results"
|
||||||
|
:key="i"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.selected"
|
||||||
|
hide-details
|
||||||
|
:value="{ deviceKey: item.deviceKey }"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{ item.no }}</td>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
|
||||||
|
<!-- ✅ Description -->
|
||||||
|
<td>
|
||||||
|
<div class="truncate-2">{{ item.desc }}</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- ✅ Select Users -->
|
||||||
|
<td>
|
||||||
|
<template v-if="item.users?.length">
|
||||||
|
<v-chip
|
||||||
|
v-for="u in item.users"
|
||||||
|
:key="u"
|
||||||
|
size="small"
|
||||||
|
class="ma-1"
|
||||||
|
color="blue-lighten-2"
|
||||||
|
text-color="white"
|
||||||
|
>
|
||||||
|
{{ u }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{ item.registDt }}</td>
|
||||||
|
|
||||||
|
<td style="white-space: nowrap">
|
||||||
|
<IconModifyBtn @on-click="openEditModal(item)" />
|
||||||
|
<IconDeleteBtn
|
||||||
|
@on-click="
|
||||||
|
deleteRows([{ deviceKey: item.deviceKey }])
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<v-card-actions class="text-center mt-8 justify-center">
|
||||||
|
<v-pagination
|
||||||
|
v-model="data.params.pageNum"
|
||||||
|
:length="data.pageLength"
|
||||||
|
:total-visible="10"
|
||||||
|
color="primary"
|
||||||
|
rounded="circle"
|
||||||
|
@update:model-value="changePageNum"
|
||||||
|
/>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<!-- 생성/수정 다이얼로그 -->
|
||||||
|
<v-dialog v-model="data.isCreateVisible" max-width="560" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">
|
||||||
|
{{
|
||||||
|
data.modalMode === "create" ? "Create Project" : "Modify Project"
|
||||||
|
}}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form>
|
||||||
|
<v-text-field label="Project Name" v-model="form.prjNm" required />
|
||||||
|
<v-textarea
|
||||||
|
label="Description"
|
||||||
|
v-model="form.prjDesc"
|
||||||
|
rows="3"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
label="Select Users"
|
||||||
|
v-model="form.selectedUsers"
|
||||||
|
:items="userOptions"
|
||||||
|
item-title="username"
|
||||||
|
item-value="username"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
closable-chips
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn text @click="data.isCreateVisible = false">Cancel</v-btn>
|
||||||
|
<v-btn color="primary" @click="saveProject">
|
||||||
|
{{ data.modalMode === "create" ? "Create" : "Save" }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,551 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
import ViewComponent from "@/components/templates/deployment/ViewComponent.vue";
|
||||||
|
import DeploymentDialog from "@/components/atoms/organisms/DeploymentDialog.vue";
|
||||||
|
// const store = commonStore();
|
||||||
|
|
||||||
|
const openView = ref(false);
|
||||||
|
const isEditVisible = ref(false);
|
||||||
|
const tableHeader = [
|
||||||
|
{ label: "No", width: "5%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Model Name", width: "20%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Version", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Duration", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Deploy Status", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Download Status", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Deployed At", width: "15%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Action", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchOptions = [
|
||||||
|
{ searchType: "전체", searchText: "" },
|
||||||
|
{ searchType: "디바이스 별칭", searchText: "deviceAlias" },
|
||||||
|
{ searchType: "디바이스 키", searchText: "deviceKey" },
|
||||||
|
{ searchType: "사용자", searchText: "userId" },
|
||||||
|
{ searchType: "디바이스 이름", searchText: "deviceName" },
|
||||||
|
{ searchType: "디바이스 모델", searchText: "deviceModel" },
|
||||||
|
{ searchType: "디바이스 OS", searchText: "deviceOs" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageSizeOptions = [
|
||||||
|
{ text: "10 페이지", value: 10 },
|
||||||
|
{ text: "50 페이지", value: 50 },
|
||||||
|
{ text: "100 페이지", value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isEditVisible: false,
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
no: 23,
|
||||||
|
modelName: "ImageClassifier",
|
||||||
|
version: 2,
|
||||||
|
duration: "-",
|
||||||
|
deployStatus: "failure",
|
||||||
|
downloadStatus: "failure",
|
||||||
|
deployedAt: "2025-04-28",
|
||||||
|
key: 23,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 22,
|
||||||
|
modelName: "ImageClassifier",
|
||||||
|
version: 1,
|
||||||
|
duration: "21s",
|
||||||
|
deployStatus: "failure",
|
||||||
|
downloadStatus: "failure",
|
||||||
|
deployedAt: "2025-03-21",
|
||||||
|
key: 22,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 21,
|
||||||
|
modelName: "LaneDetection",
|
||||||
|
version: 1,
|
||||||
|
duration: "12s",
|
||||||
|
deployStatus: "success",
|
||||||
|
downloadStatus: "failure",
|
||||||
|
deployedAt: "2025-03-19",
|
||||||
|
key: 21,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 20,
|
||||||
|
modelName: "CustomerChurn",
|
||||||
|
version: 1,
|
||||||
|
duration: "-",
|
||||||
|
deployStatus: "in_progress",
|
||||||
|
downloadStatus: "-",
|
||||||
|
deployedAt: "2025-02-08",
|
||||||
|
key: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 19,
|
||||||
|
modelName: "AnomalyDetection",
|
||||||
|
version: 1,
|
||||||
|
duration: "20s",
|
||||||
|
deployStatus: "success",
|
||||||
|
downloadStatus: "success",
|
||||||
|
deployedAt: "2025-02-06",
|
||||||
|
key: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 18,
|
||||||
|
modelName: "TimeSeriesForecast",
|
||||||
|
version: 2,
|
||||||
|
duration: "19s",
|
||||||
|
deployStatus: "success",
|
||||||
|
downloadStatus: "success",
|
||||||
|
deployedAt: "2025-01-28",
|
||||||
|
key: 18,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 17,
|
||||||
|
modelName: "TextSentiment",
|
||||||
|
version: 2,
|
||||||
|
duration: "20s",
|
||||||
|
deployStatus: "success",
|
||||||
|
downloadStatus: "success",
|
||||||
|
deployedAt: "2025-01-28",
|
||||||
|
key: 17,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = data.value.results.length;
|
||||||
|
const total = data.value.totalDataLength;
|
||||||
|
const size = data.value.params.pageSize;
|
||||||
|
data.value.pageLength =
|
||||||
|
total % size === 0 ? total / size : Math.ceil(total / size);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveData = () => {
|
||||||
|
if (data.value.selected.length === 0) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제 할 데이터를 선택해주세요. ",
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.value.allSelected || data.value.selected.length !== 1) {
|
||||||
|
data.value.isConfirmDialogVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//리스트로 삭제 할때
|
||||||
|
removeData(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
alert("Refresh 작업 진행중...");
|
||||||
|
};
|
||||||
|
const openInfoModal = () => {
|
||||||
|
data.value.modalMode = "info";
|
||||||
|
openView.value = true;
|
||||||
|
};
|
||||||
|
const closeDetail = () => {
|
||||||
|
openView.value = false;
|
||||||
|
};
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
const openSettingModal = (selectedItem) => {
|
||||||
|
data.value.selectedData = selectedItem;
|
||||||
|
data.value.modalMode = "setting";
|
||||||
|
openView.value = true;
|
||||||
|
};
|
||||||
|
const openModifyModal = (selectedItem) => {
|
||||||
|
data.value.selectedData = selectedItem;
|
||||||
|
data.value.modalMode = "modify";
|
||||||
|
data.value.isModalVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeploymentModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "create";
|
||||||
|
isEditVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.selectedData = null;
|
||||||
|
};
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
isEditVisible.value = null;
|
||||||
|
};
|
||||||
|
const getSelectedAllData = () => {
|
||||||
|
data.value.selected = data.value.allSelected
|
||||||
|
? data.value.results.map((item) => {
|
||||||
|
return {
|
||||||
|
deviceKey: item.deviceKey,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-100" v-if="!openView">
|
||||||
|
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>-->
|
||||||
|
<!-- <FormComponent-->
|
||||||
|
<!-- :edit-data="data.selectedData"-->
|
||||||
|
<!-- :mode="data.modalMode"-->
|
||||||
|
<!-- @close-modal="closeModal"-->
|
||||||
|
<!-- @handle-data="saveData"-->
|
||||||
|
<!-- :user-option="data.userOption"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </v-dialog>-->
|
||||||
|
<!-- <v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">-->
|
||||||
|
<!-- <ConfirmDialogComponent-->
|
||||||
|
<!-- @cancel="data.isConfirmDialogVisible = false"-->
|
||||||
|
<!-- @delete="removeData(undefined)"-->
|
||||||
|
<!-- @init="(data.selected = []), (data.allSelected = false)"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </v-dialog>-->
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">Deployment</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card flat class="bg-shades-transparent mb-4">
|
||||||
|
<div class="d-flex justify-center flex-wrap align-center">
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
<v-responsive min-width="540" max-width="540">
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.params.searchText"
|
||||||
|
label="검색어"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
required
|
||||||
|
class="mt-3 mb-3"
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="changePageNum(1)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-responsive>
|
||||||
|
|
||||||
|
<div class="ml-3">
|
||||||
|
<v-btn
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
:rounded="5"
|
||||||
|
@click="changePageNum(1)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-sheet
|
||||||
|
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
||||||
|
>
|
||||||
|
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
||||||
|
<v-sheet
|
||||||
|
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
||||||
|
>
|
||||||
|
<v-chip color="primary"
|
||||||
|
>총 {{ data.totalDataLength.toLocaleString() }}개
|
||||||
|
</v-chip>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="bg-shades-transparent">
|
||||||
|
<v-responsive max-width="140" min-width="140" class="mb-2">
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.pageSize"
|
||||||
|
density="compact"
|
||||||
|
:items="pageSizeOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="changePageNum(1)"
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2">
|
||||||
|
<v-btn color="info" class="mr-4" @click="handleRefresh"
|
||||||
|
>Refresh
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="info" @click="openDeploymentModal"
|
||||||
|
>Deployment</v-btn
|
||||||
|
>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<v-card class="rounded-lg pa-8">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table
|
||||||
|
density="comfortable"
|
||||||
|
fixed-header
|
||||||
|
height="625"
|
||||||
|
col-md-12
|
||||||
|
col-12
|
||||||
|
overflow-x-auto
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 5%" />
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.allSelected"
|
||||||
|
style="min-width: 36px"
|
||||||
|
:indeterminate="data.allSelected === true"
|
||||||
|
hide-details
|
||||||
|
@change="getSelectedAllData"
|
||||||
|
></v-checkbox>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="`${item.style}`"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="item in data.results"
|
||||||
|
:key="item.no"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.selected"
|
||||||
|
:value="{ deviceKey: item.deviceKey }"
|
||||||
|
hide-details
|
||||||
|
style="min-width: 36px"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.no }}</td>
|
||||||
|
<td class="text-start">{{ item.modelName }}</td>
|
||||||
|
<td>{{ item.version }}</td>
|
||||||
|
<td>{{ item.duration }}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<v-icon
|
||||||
|
v-if="item.deployStatus === 'success'"
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
mdi-checkbox-marked-circle
|
||||||
|
</v-icon>
|
||||||
|
<v-icon
|
||||||
|
v-else-if="item.deployStatus === 'in_progress'"
|
||||||
|
spin
|
||||||
|
>
|
||||||
|
mdi-loading
|
||||||
|
</v-icon>
|
||||||
|
<v-icon v-else color="error"> mdi-close-circle </v-icon>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<v-icon
|
||||||
|
v-if="item.downloadStatus === 'success'"
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
mdi-checkbox-marked-circle
|
||||||
|
</v-icon>
|
||||||
|
<v-icon
|
||||||
|
v-else-if="item.downloadStatus === 'in_progress'"
|
||||||
|
spin
|
||||||
|
>
|
||||||
|
mdi-loading
|
||||||
|
</v-icon>
|
||||||
|
<v-icon
|
||||||
|
v-else-if="item.downloadStatus === 'failure'"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
mdi-close-circle
|
||||||
|
</v-icon>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.deployedAt }}</td>
|
||||||
|
<td style="white-space: nowrap">
|
||||||
|
<IconInfoBtn @on-click="openInfoModal(item)" />
|
||||||
|
<IconDeployment @click="redeploy(item)" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
<v-card-actions class="text-center mt-8 justify-center">
|
||||||
|
<v-pagination
|
||||||
|
v-model="data.params.pageNum"
|
||||||
|
:length="data.pageLength"
|
||||||
|
:total-visible="10"
|
||||||
|
color="primary"
|
||||||
|
rounded="circle"
|
||||||
|
@update:model-value="getData"
|
||||||
|
></v-pagination>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
<v-dialog v-model="isEditVisible" max-width="800" persistent>
|
||||||
|
<DeploymentDialog
|
||||||
|
:edit-data="data.selectedData"
|
||||||
|
:mode="data.modalMode"
|
||||||
|
@close-modal="closeCreateModal"
|
||||||
|
@handle-data="saveData"
|
||||||
|
:user-option="data.userOption"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
<div class="w-100" v-else>
|
||||||
|
<ViewComponent @close="closeDetail" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,397 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
|
// const store = commonStore();
|
||||||
|
|
||||||
|
const experimentInfo = ref({
|
||||||
|
modelName: "ImageClassifier",
|
||||||
|
projectName: "배터리 상태 예측 모델 프로젝트",
|
||||||
|
experimentName: "Baseline Model Training",
|
||||||
|
executionName: "run-batch32-lr0.001",
|
||||||
|
deployDate: "2025-02-06",
|
||||||
|
createdId: "ADMIN_001",
|
||||||
|
description: "기본 모델 구조로 학습 성능 측정",
|
||||||
|
});
|
||||||
|
|
||||||
|
const otaInfo = ref({
|
||||||
|
packageName: "자율주행 타차량 예측",
|
||||||
|
os: "Linux",
|
||||||
|
packageFileName: "4_EdgeInfra_Perception.sh",
|
||||||
|
packageFilePath: "/home/etri/TeslaSystem/EdgeInfraVision/RUN",
|
||||||
|
softwareName: "4_EdgeInfra_Perception.sh",
|
||||||
|
softwareVersion: "v2.0",
|
||||||
|
execute: "Not Executed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
const params = { ...data.value.params };
|
||||||
|
if (params.searchType === "" || params.searchText === "") {
|
||||||
|
delete params.searchType;
|
||||||
|
delete params.searchText;
|
||||||
|
}
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
name: "run-batch32-lr0.001",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/2",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-10T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch64-lr0.001",
|
||||||
|
status: "Failed",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "1/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-09T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch32-lr0.0005",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch64-lr0.0005",
|
||||||
|
status: "Running",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "1/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-05-29T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-augmented-data",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-05-31T00:00:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = 5;
|
||||||
|
setPaginationLength();
|
||||||
|
// DeviceService.search(params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.results = d.data.deviceList;
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "디바이스 조회 실패",
|
||||||
|
// color: "error",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// DeviceService.search().then((d) => {
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">View Details</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Deploy Model Information </span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<!-- Experiment Name -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Model Name </v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{ experimentInfo.modelName }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Project Name -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Project Name</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.projectName
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Experiment Name
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.experimentName
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<!-- Created Date / ID -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Execution Name</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.executionName
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Deploy Date
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.deployDate }}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Created ID</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.createdId }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Description</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.description
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Deploy Status
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">-</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Download Status
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">-</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Deploy OTA Information</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Package Name</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="9">{{ otaInfo.packageName }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">OS</v-col>
|
||||||
|
<v-col cols="9">{{ otaInfo.os }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Package File Name</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="9">{{ otaInfo.packageFileName }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Package File Path</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="9">{{ otaInfo.packageFilePath }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Software Name</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="3">{{ otaInfo.softwareName }}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Software Version</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="3">{{ otaInfo.softwareVersion }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Execute</v-col>
|
||||||
|
<v-col cols="9">{{ otaInfo.execute }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
<v-sheet class="d-flex justify-end mb-2">
|
||||||
|
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card-text {
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 전체 테이블 1px 테두리 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card-text th {
|
||||||
|
font-size: 20px;
|
||||||
|
min-width: 400px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.v-card-text td {
|
||||||
|
font-size: 16px;
|
||||||
|
min-width: 600px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
.v-card-text tr:nth-child(odd) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,526 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref, watch, onBeforeUnmount } from "vue";
|
||||||
|
import "monaco-editor/min/vs/editor/editor.main.css";
|
||||||
|
// const store = commonStore();
|
||||||
|
const activeTab = ref<"details" | "Visualizations">("details");
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
const params = { ...data.value.params };
|
||||||
|
if (params.searchType === "" || params.searchText === "") {
|
||||||
|
delete params.searchType;
|
||||||
|
delete params.searchText;
|
||||||
|
}
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
name: "run-batch32-lr0.001",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/2",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-10T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch64-lr0.001",
|
||||||
|
status: "Failed",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "1/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-09T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch32-lr0.0005",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch64-lr0.0005",
|
||||||
|
status: "Running",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "1/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-05-29T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-augmented-data",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-05-31T00:00:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = 5;
|
||||||
|
|
||||||
|
// DeviceService.search(params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.results = d.data.deviceList;
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "디바이스 조회 실패",
|
||||||
|
// color: "error",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// DeviceService.search().then((d) => {
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const activePlot = ref<"parallel" | "scatter" | "box" | "contour">("parallel");
|
||||||
|
|
||||||
|
// 공통 옵션 예시
|
||||||
|
const parameters = ["alpha", "l1_ratio", "max_depth", "n_estimators"];
|
||||||
|
const metrics = ["mae", "rmse", "r2", "accuracy"];
|
||||||
|
|
||||||
|
// 각 플롯별 선택값
|
||||||
|
const selectedParams = ref<string[]>([]);
|
||||||
|
const selectedX = ref("");
|
||||||
|
const selectedY = ref("");
|
||||||
|
const selectedZ = ref("");
|
||||||
|
const reverseColor = ref(false);
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
selectedParams.value = [];
|
||||||
|
selectedX.value = "";
|
||||||
|
selectedY.value = "";
|
||||||
|
selectedZ.value = "";
|
||||||
|
reverseColor.value = false;
|
||||||
|
}
|
||||||
|
const runA = {
|
||||||
|
id: "77578d20d81542f387149c1e3474f633",
|
||||||
|
name: "defiant-horse-63",
|
||||||
|
start: "2025-02-13 15:12",
|
||||||
|
end: "2025-02-13 15:14",
|
||||||
|
duration: "2.0s",
|
||||||
|
params: { alpha: 0.2, l1_ratio: 0.2 },
|
||||||
|
metrics: { mae: 0.564, r2: 0.237, rmse: 0.734 },
|
||||||
|
tags: {
|
||||||
|
estimator_class: "sklearn.pipeline.Pipeline",
|
||||||
|
estimator_name: "Pipeline",
|
||||||
|
},
|
||||||
|
artifacts: ["estimator.html", "metrics.json"],
|
||||||
|
};
|
||||||
|
const runB = {
|
||||||
|
id: "9a5da7476f0f4ad28117e0b2e8d58515",
|
||||||
|
name: "magnificent-eel-8",
|
||||||
|
start: "2025-02-13 15:12",
|
||||||
|
end: "2025-02-13 15:13",
|
||||||
|
duration: "1.9s",
|
||||||
|
params: { alpha: 0.1, l1_ratio: 0.1 },
|
||||||
|
metrics: { mae: 0.546, r2: 0.28, rmse: 0.713 },
|
||||||
|
tags: {
|
||||||
|
estimator_class: "sklearn.pipeline.Pipeline",
|
||||||
|
estimator_name: "Pipeline",
|
||||||
|
},
|
||||||
|
artifacts: ["estimator.html", "metrics.json"],
|
||||||
|
};
|
||||||
|
// const setPaginationLength = () => {
|
||||||
|
// if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
// data.value.pageLength =
|
||||||
|
// data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
// } else {
|
||||||
|
// data.value.pageLength = Math.ceil(
|
||||||
|
// data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column justify-center w-100"
|
||||||
|
>
|
||||||
|
<!-- 1) Workflow Information -->
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">Compare Executions</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
<v-tabs
|
||||||
|
v-model="activeTab"
|
||||||
|
background-color="grey lighten-4"
|
||||||
|
style="max-width: 360px"
|
||||||
|
grow
|
||||||
|
>
|
||||||
|
<v-tab value="details">Details</v-tab>
|
||||||
|
<v-tab value="Visualizations">Visualizations</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
<div v-if="activeTab === 'details'" fluid flat>
|
||||||
|
<v-card class="bordered-box mb-6 w-100 rounded-lg pa-8">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Run Detail </span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<!-- Workflow Name / Version -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold"> Run ID: </v-col>
|
||||||
|
<v-col cols="5">{{ runA.id }}</v-col>
|
||||||
|
<v-col cols="5">{{ runB.id }}</v-col>
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold">
|
||||||
|
Run Name:
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="5">{{ runA.name }}</v-col>
|
||||||
|
<v-col cols="5">{{ runB.name }}</v-col>
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold">
|
||||||
|
Start Name:
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="5">{{ runA.start }}</v-col>
|
||||||
|
<v-col cols="5">{{ runB.start }}</v-col>
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold">
|
||||||
|
End Name:
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="5">{{ runA.end }}</v-col>
|
||||||
|
<v-col cols="5">{{ runB.end }}</v-col>
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold">
|
||||||
|
Duration:
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="5">{{ runA.duration }}</v-col>
|
||||||
|
<v-col cols="5">{{ runB.duration }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Parameters </span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<!-- Workflow Name / Version -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold"> alpha </v-col>
|
||||||
|
<v-col cols="5">{{ runA.params.alpha }}</v-col>
|
||||||
|
<v-col cols="5">{{ runB.params.alpha }}</v-col>
|
||||||
|
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold">
|
||||||
|
l1_ratio
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="5">{{ runA.params.l1_ratio }}</v-col>
|
||||||
|
<v-col cols="5">{{ runB.params.l1_ratio }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Metrics</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<!-- Workflow Name / Version -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold"> mae </v-col>
|
||||||
|
<v-col cols="5">{{ runA.metrics.mae }}</v-col>
|
||||||
|
<v-col cols="5">{{ runA.metrics.mae }}</v-col>
|
||||||
|
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold"> r2 </v-col>
|
||||||
|
<v-col cols="5">{{ runA.metrics.r2 }}</v-col>
|
||||||
|
<v-col cols="5">{{ runA.metrics.r2 }}</v-col>
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold"> rmse </v-col>
|
||||||
|
<v-col cols="5">{{ runA.metrics.rmse }}</v-col>
|
||||||
|
<v-col cols="5">{{ runA.metrics.rmse }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Tags </span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<!-- Workflow Name / Version -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold">
|
||||||
|
estimator_class
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="5">{{ runA.tags.estimator_class }}</v-col>
|
||||||
|
<v-col cols="5">{{ runA.tags.estimator_class }}</v-col>
|
||||||
|
<v-col cols="2" class="text-h6 font-weight-bold">
|
||||||
|
estimator_name
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="5">{{ runA.tags.estimator_name }}</v-col>
|
||||||
|
<v-col cols="5">{{ runA.tags.estimator_name }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Artifacts</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-sheet class="d-flex justify-end mt-4">
|
||||||
|
<v-btn color="primary" @click="emit('close')">Back to List</v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
<div v-show="activeTab === 'Visualizations'">
|
||||||
|
<!-- 2차 탭: 플롯 종류 -->
|
||||||
|
<v-tabs
|
||||||
|
v-model="activePlot"
|
||||||
|
density="compact"
|
||||||
|
background-color="transparent"
|
||||||
|
class="sub-tabs mt-4"
|
||||||
|
grow
|
||||||
|
show-arrows
|
||||||
|
slider-color="secondary"
|
||||||
|
>
|
||||||
|
<v-tab value="parallel">Parallel Coordinates Plot</v-tab>
|
||||||
|
<v-tab value="scatter">Scatter Plot</v-tab>
|
||||||
|
<v-tab value="box">Box Plot</v-tab>
|
||||||
|
<v-tab value="contour">Contour Plot</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<!-- 컨텐츠 카드 -->
|
||||||
|
<v-card flat class="px-6" elevation="2">
|
||||||
|
<v-row>
|
||||||
|
<!-- 좌측 컨트롤 -->
|
||||||
|
<v-col cols="12" md="4" class="pr-4 pt-8">
|
||||||
|
<v-sheet class="pa-4" elevation="1" style="background: #1e1e1e">
|
||||||
|
<div v-if="activePlot === 'parallel'">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedParams"
|
||||||
|
:items="parameters"
|
||||||
|
label="Parameters"
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="selectedY"
|
||||||
|
:items="metrics"
|
||||||
|
label="Metrics"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-btn text @click="clearAll">Clear All</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="activePlot === 'scatter'">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedX"
|
||||||
|
:items="parameters"
|
||||||
|
label="X-axis"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="selectedY"
|
||||||
|
:items="metrics"
|
||||||
|
label="Y-axis"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-btn text @click="clearAll">Clear All</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="activePlot === 'box'">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedX"
|
||||||
|
:items="parameters"
|
||||||
|
label="X-axis"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="selectedY"
|
||||||
|
:items="metrics"
|
||||||
|
label="Y-axis"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-btn text @click="clearAll">Clear All</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="activePlot === 'contour'">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedX"
|
||||||
|
:items="metrics"
|
||||||
|
label="X-axis"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="selectedY"
|
||||||
|
:items="metrics"
|
||||||
|
label="Y-axis"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="selectedZ"
|
||||||
|
:items="[
|
||||||
|
'training_score',
|
||||||
|
'validation_score',
|
||||||
|
'mae',
|
||||||
|
'rmse',
|
||||||
|
]"
|
||||||
|
label="Z-axis"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="reverseColor"
|
||||||
|
label="Reverse color"
|
||||||
|
dense
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<v-btn text @click="clearAll" class="mt-4">Clear All</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-sheet>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- 우측 그래프 placeholder -->
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-sheet class="placeholder-box" height="400px" elevation="1" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-sheet class="d-flex justify-end my-4">
|
||||||
|
<v-btn color="primary" @click="emit('close')">Back to List</v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 900px;
|
||||||
|
max-height: 900px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,607 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue";
|
||||||
|
import CompareComponent from "@/components/templates/run/executions/CompareComponent.vue";
|
||||||
|
import ViewComponent from "@/components/templates/run/executions/ViewComponent.vue";
|
||||||
|
import ExecutionBaseDialog from "@/components/atoms/organisms/ExecutionBaseDialog.vue";
|
||||||
|
// const store = commonStore();
|
||||||
|
|
||||||
|
const openCompare = ref(false);
|
||||||
|
const openView = ref(false);
|
||||||
|
const tableHeader = [
|
||||||
|
{ label: "No", width: "5%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Execution Name", width: "20%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Status", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Duration", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Experiment", width: "15%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Workflow", width: "15%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Start Time", width: "15%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Registry Status", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Action", width: "5%", style: "word-break: keep-all;" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchOptions = [
|
||||||
|
{ searchType: "All", searchText: "" },
|
||||||
|
{ searchType: "Execution Name", searchText: "name" },
|
||||||
|
{ searchType: "Status", searchText: "status" },
|
||||||
|
{ searchType: "Duration", searchText: "duration" },
|
||||||
|
{ searchType: "Experiment", searchText: "experiment" },
|
||||||
|
{ searchType: "Workflow", searchText: "workflow" },
|
||||||
|
{ searchType: "Registry Status", searchText: "registryStatus" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const execDialogOpen = ref(false);
|
||||||
|
const execMode = ref<"create" | "edit" | "clone">("create");
|
||||||
|
const execSelected = ref<any>(null);
|
||||||
|
const searchExperimentOptions = [{ searchType: "Experiment", searchText: "" }];
|
||||||
|
const searchWorkflowOptions = [{ searchType: "Workflow", searchText: "" }];
|
||||||
|
|
||||||
|
const workflowList = ref(["pipeline-a", "pipeline-b", "pipeline-c"]);
|
||||||
|
const executionTypes = ref(["One-off", "Recurring"]);
|
||||||
|
|
||||||
|
const pageSizeOptions = [
|
||||||
|
{ text: "10 페이지", value: 10 },
|
||||||
|
{ text: "50 페이지", value: 50 },
|
||||||
|
{ text: "100 페이지", value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
const params = { ...data.value.params };
|
||||||
|
if (params.searchType === "" || params.searchText === "") {
|
||||||
|
delete params.searchType;
|
||||||
|
delete params.searchText;
|
||||||
|
}
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
no: 11,
|
||||||
|
name: "run-batch32-lr0.001",
|
||||||
|
status: "Succeeded",
|
||||||
|
duration: "0:00:21",
|
||||||
|
experiment: "Baseline Model Training",
|
||||||
|
workflow: "baseline_train_pipeline",
|
||||||
|
startTime: "2025-05-20 10:12",
|
||||||
|
registryStatus: "Registered",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 10,
|
||||||
|
name: "run-batch64-lr0.001",
|
||||||
|
status: "Failed",
|
||||||
|
duration: "0:00:20",
|
||||||
|
experiment: "Baseline Model Training",
|
||||||
|
workflow: "baseline_train_pipeline",
|
||||||
|
startTime: "2025-05-20 09:10",
|
||||||
|
registryStatus: "Not Registered",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 9,
|
||||||
|
name: "run-batch32-lr0.0005",
|
||||||
|
status: "Succeeded",
|
||||||
|
duration: "0:00:21",
|
||||||
|
experiment: "Baseline Model Training",
|
||||||
|
workflow: "baseline_train_pipeline",
|
||||||
|
startTime: "2025-05-19 10:12",
|
||||||
|
registryStatus: "Registered",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 8,
|
||||||
|
name: "run-batch64-lr0.0005",
|
||||||
|
status: "Running",
|
||||||
|
duration: "0:00:20",
|
||||||
|
experiment: "Baseline Model Training",
|
||||||
|
workflow: "baseline_train_pipeline",
|
||||||
|
startTime: "2025-05-18 11:50",
|
||||||
|
registryStatus: "Not Registered",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 7,
|
||||||
|
name: "run-augmented-data",
|
||||||
|
status: "Succeeded",
|
||||||
|
duration: "0:00:20",
|
||||||
|
experiment: "Baseline Model Training",
|
||||||
|
workflow: "baseline_train_pipeline",
|
||||||
|
startTime: "2025-05-17 09:12",
|
||||||
|
registryStatus: "Registered",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 6,
|
||||||
|
name: "run-augmented-data",
|
||||||
|
status: "Succeeded",
|
||||||
|
duration: "0:00:20",
|
||||||
|
experiment: "Baseline Model Training",
|
||||||
|
workflow: "baseline_train_pipeline",
|
||||||
|
startTime: "2025-05-17 09:12",
|
||||||
|
registryStatus: "Registered",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 5,
|
||||||
|
name: "run-augmented-data",
|
||||||
|
status: "Succeeded",
|
||||||
|
duration: "0:00:20",
|
||||||
|
experiment: "Baseline Model Training",
|
||||||
|
workflow: "baseline_train_pipeline",
|
||||||
|
startTime: "2025-05-17 09:12",
|
||||||
|
registryStatus: "Registered",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = data.value.results.length;
|
||||||
|
setPaginationLength();
|
||||||
|
// DeviceService.search(params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.results = d.data.deviceList;
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "디바이스 조회 실패",
|
||||||
|
// color: "error",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// DeviceService.search().then((d) => {
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveData = () => {
|
||||||
|
if (data.value.selected.length === 0) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제 할 데이터를 선택해주세요. ",
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.value.allSelected || data.value.selected.length !== 1) {
|
||||||
|
data.value.isConfirmDialogVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//리스트로 삭제 할때
|
||||||
|
removeData(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTerminate = () => {
|
||||||
|
alert("Terminate 작업 진행중...");
|
||||||
|
};
|
||||||
|
const handleRetry = () => {
|
||||||
|
alert("Retry 작업 진행중...");
|
||||||
|
};
|
||||||
|
const handleClone = () => {
|
||||||
|
alert("Clone 작업 진행중...");
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
const openCreateExecution = () => {
|
||||||
|
execMode.value = "create";
|
||||||
|
execSelected.value = null;
|
||||||
|
execDialogOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openComparePage = () => {
|
||||||
|
openCompare.value = true;
|
||||||
|
openView.value = false;
|
||||||
|
};
|
||||||
|
const openInfoModal = () => {
|
||||||
|
openView.value = true;
|
||||||
|
openCompare.value = false;
|
||||||
|
};
|
||||||
|
const openModifyModal = (selectedItem) => {
|
||||||
|
execMode.value = "edit";
|
||||||
|
execDialogOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDownloadModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "download";
|
||||||
|
};
|
||||||
|
function closeCompare() {
|
||||||
|
openCompare.value = false;
|
||||||
|
}
|
||||||
|
function closeView() {
|
||||||
|
openView.value = false;
|
||||||
|
}
|
||||||
|
const closeModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.selectedData = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedAllData = () => {
|
||||||
|
data.value.selected = data.value.allSelected
|
||||||
|
? data.value.results.map((item) => {
|
||||||
|
return {
|
||||||
|
deviceKey: item.deviceKey,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-100" v-if="!openCompare && !openView">
|
||||||
|
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>
|
||||||
|
<FormComponent
|
||||||
|
:edit-data="data.selectedData"
|
||||||
|
:mode="data.modalMode"
|
||||||
|
@close-modal="closeModal"
|
||||||
|
@handle-data="saveData"
|
||||||
|
:user-option="data.userOption"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
<v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">
|
||||||
|
<ConfirmDialogComponent
|
||||||
|
@cancel="data.isConfirmDialogVisible = false"
|
||||||
|
@delete="removeData(undefined)"
|
||||||
|
@init="((data.selected = []), (data.allSelected = false))"
|
||||||
|
/>
|
||||||
|
</v-dialog> -->
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">Executions</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card flat class="bg-shades-transparent mb-4">
|
||||||
|
<div class="d-flex justify-center flex-wrap align-center">
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchExperimentOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchWorkflowOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
<v-responsive min-width="540" max-width="540">
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.params.searchText"
|
||||||
|
label="검색어"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
required
|
||||||
|
class="mt-3 mb-3"
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="changePageNum(1)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-responsive>
|
||||||
|
|
||||||
|
<div class="ml-3">
|
||||||
|
<v-btn
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
:rounded="5"
|
||||||
|
@click="changePageNum(1)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-sheet
|
||||||
|
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
||||||
|
>
|
||||||
|
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
||||||
|
<v-sheet
|
||||||
|
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
||||||
|
>
|
||||||
|
<v-chip color="primary"
|
||||||
|
>총 {{ data.totalDataLength.toLocaleString() }}개
|
||||||
|
</v-chip>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="bg-shades-transparent">
|
||||||
|
<v-responsive max-width="140" min-width="140" class="mb-2">
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.pageSize"
|
||||||
|
density="compact"
|
||||||
|
:items="pageSizeOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="changePageNum(1)"
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2 mr-3" @click="handleTerminate">
|
||||||
|
<v-btn color="primary">Terminate </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2 mr-3" @click="handleRetry">
|
||||||
|
<v-btn color="primary">Retry </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2 mr-3" @click="handleClone">
|
||||||
|
<v-btn color="primary">Clone </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2 mr-3" @click="openComparePage">
|
||||||
|
<v-btn color="primary">Compare </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2" @click="openCreateExecution">
|
||||||
|
<v-btn color="primary">Execution </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<v-card class="rounded-lg pa-8">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table
|
||||||
|
density="comfortable"
|
||||||
|
fixed-header
|
||||||
|
height="625"
|
||||||
|
col-md-12
|
||||||
|
col-12
|
||||||
|
overflow-x-auto
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 5%" />
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.allSelected"
|
||||||
|
style="min-width: 36px"
|
||||||
|
:indeterminate="data.allSelected === true"
|
||||||
|
hide-details
|
||||||
|
@change="getSelectedAllData"
|
||||||
|
></v-checkbox>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="`${item.style}`"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="item in data.results"
|
||||||
|
:key="item.no"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.selected"
|
||||||
|
:value="{ deviceKey: item.deviceKey }"
|
||||||
|
hide-details
|
||||||
|
style="min-width: 36px"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.no }}</td>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>
|
||||||
|
<v-icon v-if="item.status === 'Succeeded'" color="green"
|
||||||
|
>mdi-check-circle</v-icon
|
||||||
|
>
|
||||||
|
<v-icon v-else-if="item.status === 'Failed'" color="red"
|
||||||
|
>mdi-close-circle</v-icon
|
||||||
|
>
|
||||||
|
<v-icon v-else color="grey">mdi-loading</v-icon>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.duration }}</td>
|
||||||
|
<td>{{ item.experiment }}</td>
|
||||||
|
<td>{{ item.workflow }}</td>
|
||||||
|
<td>{{ item.startTime }}</td>
|
||||||
|
<td>{{ item.registryStatus }}</td>
|
||||||
|
<td style="white-space: nowrap">
|
||||||
|
<IconInfoBtn @on-click="openInfoModal(item)" />
|
||||||
|
<IconModifyBtn @on-click="openModifyModal(item)" />
|
||||||
|
<IconDownloadBtn @on-click="openDownloadModal(item)" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
<v-card-actions class="text-center mt-8 justify-center">
|
||||||
|
<v-pagination
|
||||||
|
v-model="data.params.pageNum"
|
||||||
|
:length="data.pageLength"
|
||||||
|
:total-visible="10"
|
||||||
|
color="primary"
|
||||||
|
rounded="circle"
|
||||||
|
@update:model-value="getData"
|
||||||
|
></v-pagination>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
<v-dialog v-model="execDialogOpen" max-width="800" persistent>
|
||||||
|
<ExecutionBaseDialog
|
||||||
|
:model-value="execDialogOpen"
|
||||||
|
:mode="execMode"
|
||||||
|
:selectedData="execSelected"
|
||||||
|
:workflowList="workflowList"
|
||||||
|
:executionTypes="executionTypes"
|
||||||
|
@update:modelValue="execDialogOpen = $event"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
<div class="w-100" v-else-if="openCompare">
|
||||||
|
<CompareComponent @close="closeCompare" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-100" v-else-if="openView">
|
||||||
|
<ViewComponent @close="closeView" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,339 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
|
// const store = commonStore();
|
||||||
|
|
||||||
|
const experimentInfo = ref({
|
||||||
|
executionsName: "run-batch32-lr0.001",
|
||||||
|
status: "Succeeded",
|
||||||
|
duration: "0:00:21",
|
||||||
|
experiment: "Baseline Model Training",
|
||||||
|
workflow: "baseline_train_pipeline",
|
||||||
|
startTime: "2025-05-20 10:12",
|
||||||
|
registryStatus: "Registered",
|
||||||
|
});
|
||||||
|
|
||||||
|
const otaInfo = ref({
|
||||||
|
packageName: "자율주행 타차량 예측",
|
||||||
|
os: "Linux",
|
||||||
|
packageFileName: "4_EdgeInfra_Perception.sh",
|
||||||
|
packageFilePath: "/home/etri/TeslaSystem/EdgeInfraVision/RUN",
|
||||||
|
softwareName: "4_EdgeInfra_Perception.sh",
|
||||||
|
softwareVersion: "v2.0",
|
||||||
|
execute: "Not Executed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
const params = { ...data.value.params };
|
||||||
|
if (params.searchType === "" || params.searchText === "") {
|
||||||
|
delete params.searchType;
|
||||||
|
delete params.searchText;
|
||||||
|
}
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
name: "run-batch32-lr0.001",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/2",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-10T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch64-lr0.001",
|
||||||
|
status: "Failed",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "1/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-09T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch32-lr0.0005",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch64-lr0.0005",
|
||||||
|
status: "Running",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "1/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-05-29T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-augmented-data",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-05-31T00:00:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = 5;
|
||||||
|
setPaginationLength();
|
||||||
|
// DeviceService.search(params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.results = d.data.deviceList;
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "디바이스 조회 실패",
|
||||||
|
// color: "error",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// DeviceService.search().then((d) => {
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">View Details</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Deploy Model Information </span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<!-- Experiment Name -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Executions Name
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.executionsName
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Project Name -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Status</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">
|
||||||
|
<v-icon v-if="experimentInfo.status === 'Succeeded'" color="green"
|
||||||
|
>mdi-check-circle</v-icon
|
||||||
|
>
|
||||||
|
<v-icon v-else-if="experimentInfo.status === 'Failed'" color="red"
|
||||||
|
>mdi-close-circle</v-icon
|
||||||
|
>
|
||||||
|
<v-icon v-else color="grey">mdi-loading</v-icon></v-col
|
||||||
|
>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Duration </v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{ experimentInfo.duration }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<!-- Created Date / ID -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Experiment</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{ experimentInfo.experiment }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Workflow </v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.workflow }}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Start Time</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.startTime }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Registry Status</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.registryStatus
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
</v-card-text>
|
||||||
|
<v-sheet class="d-flex justify-end mb-2">
|
||||||
|
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card-text {
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 전체 테이블 1px 테두리 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card-text th {
|
||||||
|
font-size: 20px;
|
||||||
|
min-width: 400px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.v-card-text td {
|
||||||
|
font-size: 16px;
|
||||||
|
min-width: 600px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
.v-card-text tr:nth-child(odd) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,489 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
|
import ViewComponent from "@/components/templates/run/experiment/ViewComponent.vue";
|
||||||
|
import ExperimentCreateDialog from "@/components/atoms/organisms/ExperimentCreateDialog.vue";
|
||||||
|
|
||||||
|
// const store = commonStore();
|
||||||
|
const detailDialog = ref(false);
|
||||||
|
const openView = ref(false);
|
||||||
|
const selectedExperiment = ref<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdDate: string;
|
||||||
|
createdID: string;
|
||||||
|
} | null>(null);
|
||||||
|
const tableHeader = [
|
||||||
|
{
|
||||||
|
label: "Experiment Name",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Description",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Created Date",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Created ID",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Action",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchOptions = [
|
||||||
|
{ searchType: "All", searchText: "" },
|
||||||
|
{ searchType: "Experiment Name", searchText: "name" },
|
||||||
|
{ searchType: "Description", searchText: "description" },
|
||||||
|
{ searchType: "Created Date", searchText: "createdDate" },
|
||||||
|
{ searchType: "Created ID", searchText: "createdID" },
|
||||||
|
];
|
||||||
|
const pageSizeOptions = [
|
||||||
|
{ text: "10 페이지", value: 10 },
|
||||||
|
{ text: "50 페이지", value: 50 },
|
||||||
|
{ text: "100 페이지", value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
const params = { ...data.value.params };
|
||||||
|
if (params.searchType === "" || params.searchText === "") {
|
||||||
|
delete params.searchType;
|
||||||
|
delete params.searchText;
|
||||||
|
}
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
name: "Baseline Model Training",
|
||||||
|
description: "기본 모델 구조로 학습 성능 측정",
|
||||||
|
createdDate: "2025-04-28",
|
||||||
|
createdID: "ADMIN_001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Batch Size Tuning",
|
||||||
|
description: "배치 사이즈 변경에 따른 학습 성능",
|
||||||
|
createdDate: "2025-04-20",
|
||||||
|
createdID: "ADMIN_001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Learning Rate Sweep",
|
||||||
|
description: "러닝레이트 변경에 따른 손실 ",
|
||||||
|
createdDate: "2025-04-20",
|
||||||
|
createdID: "ADMIN_001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Optimizer Comparison",
|
||||||
|
description: "Adam, SGD 등 옵티마이저 종류",
|
||||||
|
createdDate: "2025-04-20",
|
||||||
|
createdID: "ADMIN_001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Model Architecture A vs B",
|
||||||
|
description: "서로 다른 모델 구조",
|
||||||
|
createdDate: "2025-01-28",
|
||||||
|
createdID: "ADMIN_001",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = data.value.results.length;
|
||||||
|
setPaginationLength();
|
||||||
|
// DeviceService.search(params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.results = d.data.deviceList;
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "디바이스 조회 실패",
|
||||||
|
// color: "error",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// DeviceService.search().then((d) => {
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveData = () => {
|
||||||
|
if (data.value.selected.length === 0) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제 할 데이터를 선택해주세요. ",
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.value.allSelected || data.value.selected.length !== 1) {
|
||||||
|
data.value.isConfirmDialogVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//리스트로 삭제 할때
|
||||||
|
removeData(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDetail = (item: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdDate: string;
|
||||||
|
createdID: string;
|
||||||
|
}) => {
|
||||||
|
selectedExperiment.value = item;
|
||||||
|
openView.value = true;
|
||||||
|
};
|
||||||
|
const closeDetail = () => {
|
||||||
|
openView.value = false;
|
||||||
|
selectedExperiment.value = null;
|
||||||
|
};
|
||||||
|
const openCreateModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "create";
|
||||||
|
data.value.isModalVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.selectedData = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedAllData = () => {
|
||||||
|
data.value.selected = data.value.allSelected
|
||||||
|
? data.value.results.map((item) => {
|
||||||
|
return {
|
||||||
|
deviceKey: item.deviceKey,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-100" v-if="!openView">
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">Experiment</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card flat class="bg-shades-transparent mb-4">
|
||||||
|
<div class="d-flex justify-center flex-wrap align-center">
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
<v-responsive min-width="540" max-width="540">
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.params.searchText"
|
||||||
|
label="검색어"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
required
|
||||||
|
class="mt-3 mb-3"
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="changePageNum(1)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-responsive>
|
||||||
|
|
||||||
|
<div class="ml-3">
|
||||||
|
<v-btn
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
:rounded="5"
|
||||||
|
@click="changePageNum(1)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-sheet
|
||||||
|
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
||||||
|
>
|
||||||
|
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
||||||
|
<v-sheet
|
||||||
|
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
||||||
|
>
|
||||||
|
<v-chip color="primary"
|
||||||
|
>총 {{ data.totalDataLength.toLocaleString() }}개
|
||||||
|
</v-chip>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="bg-shades-transparent">
|
||||||
|
<v-responsive max-width="140" min-width="140" class="mb-2">
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.pageSize"
|
||||||
|
density="compact"
|
||||||
|
:items="pageSizeOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="changePageNum(1)"
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2">
|
||||||
|
<v-btn color="primary" @click="openCreateModal"
|
||||||
|
>Create Experiment
|
||||||
|
</v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<v-card class="rounded-lg pa-8">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table
|
||||||
|
density="comfortable"
|
||||||
|
fixed-header
|
||||||
|
height="625"
|
||||||
|
col-md-12
|
||||||
|
col-12
|
||||||
|
overflow-x-auto
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<!-- <th>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.allSelected"
|
||||||
|
style="min-width: 36px"
|
||||||
|
:indeterminate="data.allSelected === true"
|
||||||
|
hide-details
|
||||||
|
@change="getSelectedAllData"
|
||||||
|
></v-checkbox>
|
||||||
|
</th> -->
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="`${item.style}`"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="(item, i) in data.results"
|
||||||
|
:key="i"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<!-- <td>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.selected"
|
||||||
|
hide-details
|
||||||
|
:value="{
|
||||||
|
deviceKey: item.deviceKey,
|
||||||
|
}"
|
||||||
|
></v-checkbox>
|
||||||
|
</td> -->
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.description }}</td>
|
||||||
|
<td>{{ item.createdDate }}</td>
|
||||||
|
<td>{{ item.createdID }}</td>
|
||||||
|
<td style="white-space: nowrap">
|
||||||
|
<IconInfoBtn @on-click="openDetail(item)" />
|
||||||
|
<IconDeleteBtn
|
||||||
|
@on-click="
|
||||||
|
removeData([{ deviceKey: item.deviceKey }])
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
<v-card-actions class="text-center mt-8 justify-center">
|
||||||
|
<v-pagination
|
||||||
|
v-model="data.params.pageNum"
|
||||||
|
:length="data.pageLength"
|
||||||
|
:total-visible="10"
|
||||||
|
color="primary"
|
||||||
|
rounded="circle"
|
||||||
|
@update:model-value="getData"
|
||||||
|
></v-pagination>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
<v-dialog v-model="data.isModalVisible" max-width="600" persistent>
|
||||||
|
<ExperimentCreateDialog
|
||||||
|
:edit-data="data.selectedData"
|
||||||
|
:mode="data.modalMode"
|
||||||
|
@close-modal="closeModal"
|
||||||
|
@handle-data="saveData"
|
||||||
|
:user-option="data.userOption"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
<div class="w-100" v-else>
|
||||||
|
<ViewComponent :experiment="selectedExperiment" @close="closeDetail" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,405 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
|
// const store = commonStore();
|
||||||
|
|
||||||
|
const tableHeader = [
|
||||||
|
{
|
||||||
|
label: "Run Name",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Status",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Duration",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pipeline",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Start Time",
|
||||||
|
width: "20%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const experimentInfo = ref({
|
||||||
|
experimentName: "Baseline Model Training",
|
||||||
|
projectName: "배터리 상태 예측 모델 프로젝트",
|
||||||
|
createdDate: "2025-02-06",
|
||||||
|
createdId: "ADMIN_001",
|
||||||
|
description: "기본 모델 구조로 학습 성능 측정",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
const params = { ...data.value.params };
|
||||||
|
if (params.searchType === "" || params.searchText === "") {
|
||||||
|
delete params.searchType;
|
||||||
|
delete params.searchText;
|
||||||
|
}
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
name: "run-batch32-lr0.001",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/2",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-10T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch64-lr0.001",
|
||||||
|
status: "Failed",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "1/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-09T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch32-lr0.0005",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-06-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-batch64-lr0.0005",
|
||||||
|
status: "Running",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "1/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-05-29T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run-augmented-data",
|
||||||
|
status: "Succeeded",
|
||||||
|
Duration: "0:00:21",
|
||||||
|
configProgress: "0/3",
|
||||||
|
Pipeline: "baseline_train_pipeline",
|
||||||
|
registDt: "2025-05-31T00:00:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = 5;
|
||||||
|
setPaginationLength();
|
||||||
|
// DeviceService.search(params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.results = d.data.deviceList;
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "디바이스 조회 실패",
|
||||||
|
// color: "error",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// DeviceService.search().then((d) => {
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">View Details</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Experiment Information</span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<!-- Experiment Name -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Experiment Name</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.experimentName
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Project Name -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Project Name</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.projectName
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Created Date / ID -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Created Date</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="3" class="pa-2">{{
|
||||||
|
experimentInfo.createdDate
|
||||||
|
}}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Created ID</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.createdId }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Description</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.description
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card class="rounded-lg pa-8">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Runs</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table
|
||||||
|
density="comfortable"
|
||||||
|
fixed-header
|
||||||
|
height="300"
|
||||||
|
col-md-12
|
||||||
|
col-12
|
||||||
|
overflow-x-auto
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="`${item.style}`"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="(item, i) in data.results"
|
||||||
|
:key="i"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.status }}</td>
|
||||||
|
<td>{{ item.Duration }}</td>
|
||||||
|
<td>{{ item.Pipeline }}</td>
|
||||||
|
<td>{{ item.registDt }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
<v-card-actions class="text-center mt-8 justify-center">
|
||||||
|
<v-pagination
|
||||||
|
v-model="data.params.pageNum"
|
||||||
|
:length="data.pageLength"
|
||||||
|
:total-visible="10"
|
||||||
|
color="primary"
|
||||||
|
rounded="circle"
|
||||||
|
@update:model-value="getData"
|
||||||
|
></v-pagination>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
<v-sheet class="d-flex justify-end mb-2">
|
||||||
|
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card-text {
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 전체 테이블 1px 테두리 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card-text th {
|
||||||
|
font-size: 20px;
|
||||||
|
min-width: 400px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.v-card-text td {
|
||||||
|
font-size: 16px;
|
||||||
|
min-width: 600px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
.v-card-text tr:nth-child(odd) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,536 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
import ViewComponent from "@/components/templates/stepconfig/ViewComponent.vue";
|
||||||
|
import StapComfigDialog from "@/components/atoms/organisms/StapComfigDialog.vue";
|
||||||
|
// const store = commonStore();
|
||||||
|
|
||||||
|
const openView = ref(false);
|
||||||
|
const openModify = ref(false);
|
||||||
|
const tableHeader = [
|
||||||
|
{ label: "No", width: "5%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Step Name", width: "15%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Type", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Dataset", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Script", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Hyper Parameters", width: "15%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Resource", width: "15%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Status", width: "5%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Workflow", width: "10%", style: "word-break: keep-all;" },
|
||||||
|
{ label: "Action", width: "5%", style: "word-break: keep-all;" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchOptions = [
|
||||||
|
{ searchType: "전체", searchText: "" },
|
||||||
|
{ searchType: "디바이스 별칭", searchText: "deviceAlias" },
|
||||||
|
{ searchType: "디바이스 키", searchText: "deviceKey" },
|
||||||
|
{ searchType: "사용자", searchText: "userId" },
|
||||||
|
{ searchType: "디바이스 이름", searchText: "deviceName" },
|
||||||
|
{ searchType: "디바이스 모델", searchText: "deviceModel" },
|
||||||
|
{ searchType: "디바이스 OS", searchText: "deviceOs" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageSizeOptions = [
|
||||||
|
{ text: "10 페이지", value: 10 },
|
||||||
|
{ text: "50 페이지", value: 50 },
|
||||||
|
{ text: "100 페이지", value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const workflowList = ["pipeline-a", "pipeline-b", "pipeline-c"];
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
// 더미 데이터: No 7 → 1
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
no: 7,
|
||||||
|
stepName: "Data Ingest",
|
||||||
|
type: "Preprocessing",
|
||||||
|
dataset: "raw_data",
|
||||||
|
script: "ingest.py",
|
||||||
|
hyperParameters: "-",
|
||||||
|
resource: "CPU:1, MEM:2Gi",
|
||||||
|
status: "success",
|
||||||
|
workflow: "pipeline-a",
|
||||||
|
deviceKey: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 6,
|
||||||
|
stepName: "Data Preprocess",
|
||||||
|
type: "Preprocessing",
|
||||||
|
dataset: "raw_data",
|
||||||
|
script: "preprocess.py",
|
||||||
|
hyperParameters: "normalize=True",
|
||||||
|
resource: "CPU:2, MEM:4Gi",
|
||||||
|
status: "success",
|
||||||
|
workflow: "pipeline-a",
|
||||||
|
deviceKey: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 5,
|
||||||
|
stepName: "Model Training",
|
||||||
|
type: "Training",
|
||||||
|
dataset: "processed_data",
|
||||||
|
script: "train.py",
|
||||||
|
hyperParameters: "lr=0.01, epochs=10",
|
||||||
|
resource: "GPU:1, MEM:8Gi",
|
||||||
|
status: "warning",
|
||||||
|
workflow: "pipeline-a",
|
||||||
|
deviceKey: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 4,
|
||||||
|
stepName: "Model Evaluation",
|
||||||
|
type: "Evaluation",
|
||||||
|
dataset: "test_data",
|
||||||
|
script: "evaluate.py",
|
||||||
|
hyperParameters: "-",
|
||||||
|
resource: "CPU:1, MEM:4Gi",
|
||||||
|
status: "success",
|
||||||
|
workflow: "pipeline-a",
|
||||||
|
deviceKey: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 3,
|
||||||
|
stepName: "Model Validation",
|
||||||
|
type: "Validation",
|
||||||
|
dataset: "test_data",
|
||||||
|
script: "validate.py",
|
||||||
|
hyperParameters: "metrics=['accuracy']",
|
||||||
|
resource: "CPU:1, MEM:4Gi",
|
||||||
|
status: "success",
|
||||||
|
workflow: "pipeline-a",
|
||||||
|
deviceKey: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 2,
|
||||||
|
stepName: "Package Model",
|
||||||
|
type: "Packaging",
|
||||||
|
dataset: "trained_model",
|
||||||
|
script: "package.py",
|
||||||
|
hyperParameters: "format='tar.gz'",
|
||||||
|
resource: "CPU:1, MEM:2Gi",
|
||||||
|
status: "success",
|
||||||
|
workflow: "pipeline-a",
|
||||||
|
deviceKey: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
stepName: "Deploy",
|
||||||
|
type: "Deployment",
|
||||||
|
dataset: "package",
|
||||||
|
script: "deploy.py",
|
||||||
|
hyperParameters: "env='prod'",
|
||||||
|
resource: "CPU:1, MEM:2Gi",
|
||||||
|
status: "success",
|
||||||
|
workflow: "pipeline-a",
|
||||||
|
deviceKey: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = data.value.results.length;
|
||||||
|
// 페이지 길이 재계산
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveData = () => {
|
||||||
|
if (data.value.selected.length === 0) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제 할 데이터를 선택해주세요. ",
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.value.allSelected || data.value.selected.length !== 1) {
|
||||||
|
data.value.isConfirmDialogVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//리스트로 삭제 할때
|
||||||
|
removeData(undefined);
|
||||||
|
};
|
||||||
|
const closeDetail = () => {
|
||||||
|
openView.value = false;
|
||||||
|
};
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
const openDetailModal = (selectedItem) => {
|
||||||
|
data.value.selectedData = selectedItem;
|
||||||
|
openView.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = ({
|
||||||
|
workflow,
|
||||||
|
stepName,
|
||||||
|
}: {
|
||||||
|
workflow: string;
|
||||||
|
stepName: string;
|
||||||
|
}) => {
|
||||||
|
if (data.value.selectedData) {
|
||||||
|
data.value.selectedData.workflow = workflow;
|
||||||
|
data.value.selectedData.stepName = stepName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const openModifyModal = (item: { workflow: string; stepName: string }) => {
|
||||||
|
data.value.selectedData = {
|
||||||
|
workflow: item.workflow,
|
||||||
|
stepName: item.stepName,
|
||||||
|
};
|
||||||
|
openModify.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "create";
|
||||||
|
data.value.isModalVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.selectedData = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedAllData = () => {
|
||||||
|
data.value.selected = data.value.allSelected
|
||||||
|
? data.value.results.map((item) => {
|
||||||
|
return {
|
||||||
|
deviceKey: item.deviceKey,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-100" v-if="!openView">
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">Workflows Step Config</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card flat class="bg-shades-transparent mb-4">
|
||||||
|
<div class="d-flex justify-center flex-wrap align-center">
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
<v-responsive min-width="540" max-width="540">
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.params.searchText"
|
||||||
|
label="검색어"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
required
|
||||||
|
class="mt-3 mb-3"
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="changePageNum(1)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-responsive>
|
||||||
|
|
||||||
|
<div class="ml-3">
|
||||||
|
<v-btn
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
:rounded="5"
|
||||||
|
@click="changePageNum(1)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-sheet
|
||||||
|
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
||||||
|
>
|
||||||
|
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
||||||
|
<v-sheet
|
||||||
|
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
||||||
|
>
|
||||||
|
<v-chip color="primary"
|
||||||
|
>총 {{ data.totalDataLength.toLocaleString() }}개
|
||||||
|
</v-chip>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="bg-shades-transparent">
|
||||||
|
<v-responsive max-width="140" min-width="140" class="mb-2">
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.pageSize"
|
||||||
|
density="compact"
|
||||||
|
:items="pageSizeOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="changePageNum(1)"
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<v-card class="rounded-lg pa-8">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table
|
||||||
|
density="comfortable"
|
||||||
|
fixed-header
|
||||||
|
height="625"
|
||||||
|
col-md-12
|
||||||
|
col-12
|
||||||
|
overflow-x-auto
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 5%" />
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.allSelected"
|
||||||
|
style="min-width: 36px"
|
||||||
|
:indeterminate="data.allSelected === true"
|
||||||
|
hide-details
|
||||||
|
@change="getSelectedAllData"
|
||||||
|
></v-checkbox>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="`${item.style}`"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="item in data.results"
|
||||||
|
:key="item.no"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.selected"
|
||||||
|
:value="{ deviceKey: item.deviceKey }"
|
||||||
|
hide-details
|
||||||
|
style="min-width: 36px"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.no }}</td>
|
||||||
|
<td>{{ item.stepName }}</td>
|
||||||
|
<td>{{ item.type }}</td>
|
||||||
|
<td>{{ item.dataset }}</td>
|
||||||
|
<td>{{ item.script }}</td>
|
||||||
|
<td>{{ item.hyperParameters }}</td>
|
||||||
|
<td>{{ item.resource }}</td>
|
||||||
|
<td>
|
||||||
|
<v-icon
|
||||||
|
v-if="item.status === 'success'"
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
mdi-checkbox-marked-circle
|
||||||
|
</v-icon>
|
||||||
|
<v-icon v-else color="warning">
|
||||||
|
mdi-alert-circle
|
||||||
|
</v-icon>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.workflow }}</td>
|
||||||
|
<td style="white-space: nowrap">
|
||||||
|
<IconInfoBtn @on-click="openDetailModal(item)" />
|
||||||
|
<IconModifyBtn @on-click="openModifyModal(item)" />
|
||||||
|
|
||||||
|
<!-- <IconModifyBtn @on-click="openModify = true" /> -->
|
||||||
|
<IconDeleteBtn
|
||||||
|
@on-click="
|
||||||
|
removeData([{ deviceKey: item.deviceKey }])
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
<v-card-actions class="text-center mt-8 justify-center">
|
||||||
|
<v-pagination
|
||||||
|
v-model="data.params.pageNum"
|
||||||
|
:length="data.pageLength"
|
||||||
|
:total-visible="10"
|
||||||
|
color="primary"
|
||||||
|
rounded="circle"
|
||||||
|
@update:model-value="getData"
|
||||||
|
></v-pagination>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
<div class="w-100" v-else>
|
||||||
|
<ViewComponent @close="closeDetail" />
|
||||||
|
</div>
|
||||||
|
<v-dialog v-model="openModify" max-width="600px">
|
||||||
|
<StapComfigDialog
|
||||||
|
v-model="openModify"
|
||||||
|
:selectedData="data.selectedData"
|
||||||
|
:workflowList="workflowList"
|
||||||
|
@save="handleSave"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<!-- 상단 타이틀 -->
|
||||||
|
<v-card flat class="bg-shades-transparent w-100 mb-6">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">View Details</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Workflow Step Information -->
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Workflow Step Information</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="py-4">
|
||||||
|
<v-row align="center" class="mb-2">
|
||||||
|
<v-col cols="3" class="font-weight-bold">Workflow Step Name</v-col>
|
||||||
|
<v-col cols="3">Train Model</v-col>
|
||||||
|
<v-col cols="3" class="font-weight-bold">Workflow Name</v-col>
|
||||||
|
<v-col cols="3">sentiment-analysis</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider />
|
||||||
|
<v-row align="center" class="mt-2">
|
||||||
|
<v-col cols="3" class="font-weight-bold">Created Date</v-col>
|
||||||
|
<v-col cols="3">2025-02-06</v-col>
|
||||||
|
<v-col cols="3" class="font-weight-bold">Created ID</v-col>
|
||||||
|
<v-col cols="3">ADMIN_001</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Dataset -->
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Dataset</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="py-4">
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col cols="3" class="font-weight-bold">Dataset Name</v-col>
|
||||||
|
<v-col cols="3">야간 주행용 레이더 데이터셋 구성</v-col>
|
||||||
|
<v-col cols="3" class="font-weight-bold">Version</v-col>
|
||||||
|
<v-col cols="3">v2.0</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Training Script -->
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Training Script</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="py-4">
|
||||||
|
<v-row align="center" class="mb-2">
|
||||||
|
<v-col cols="3" class="font-weight-bold">Script File</v-col>
|
||||||
|
<v-col cols="9">baseline_train.py</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider />
|
||||||
|
<v-row align="center" class="mt-2">
|
||||||
|
<v-col cols="3" class="font-weight-bold">Script File Path</v-col>
|
||||||
|
<v-col cols="9" style="word-break: break-all">
|
||||||
|
/mnt/nfs/model_code/baseline/baseline_train.py
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Hyperparameters -->
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Hyperparameters</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="py-4">
|
||||||
|
<v-row align="center" class="mb-2">
|
||||||
|
<v-col cols="3" class="font-weight-bold">Batch Size</v-col>
|
||||||
|
<v-col cols="3">64</v-col>
|
||||||
|
<v-col cols="3" class="font-weight-bold">Learning Rate</v-col>
|
||||||
|
<v-col cols="3">0.001</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider />
|
||||||
|
<v-row align="center" class="mt-2">
|
||||||
|
<v-col cols="3" class="font-weight-bold">Optimizer</v-col>
|
||||||
|
<v-col cols="3">Adam</v-col>
|
||||||
|
<v-col cols="3" class="font-weight-bold">Epochs</v-col>
|
||||||
|
<v-col cols="3">20</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Resource & Scheduling -->
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Resource & Scheduling</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="py-4">
|
||||||
|
<v-row align="center" class="mb-2">
|
||||||
|
<v-col cols="3" class="font-weight-bold">CPU Cores</v-col>
|
||||||
|
<v-col cols="3">4</v-col>
|
||||||
|
<v-col cols="3" class="font-weight-bold">Memory</v-col>
|
||||||
|
<v-col cols="3">16Gi</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider />
|
||||||
|
<v-row align="center" class="mt-2">
|
||||||
|
<v-col cols="3" class="font-weight-bold">GPU</v-col>
|
||||||
|
<v-col cols="9">1 (NVIDIA A100)</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Docker image -->
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-6" elevation="2">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Docker image</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="py-4">
|
||||||
|
<v-sheet class="pa-2" elevation="2">
|
||||||
|
kubeflownotebookswg/jupyter-pytorch-cuda-full:v1.9.2
|
||||||
|
</v-sheet>
|
||||||
|
</v-card-text>
|
||||||
|
<v-sheet class="d-flex justify-end mt-4">
|
||||||
|
<v-btn color="primary" @click="emit('close')">Back to List</v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineEmits } from "vue";
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,519 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
import ViewComponent from "@/components/templates/trainingscript/ViewComponent.vue";
|
||||||
|
import TrainingScriptBaseDoalog from "@/components/atoms/organisms/TrainingScriptBaseDoalog.vue";
|
||||||
|
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
|
||||||
|
// const store = commonStore();
|
||||||
|
|
||||||
|
const openView = ref(false);
|
||||||
|
const openModify = ref(false);
|
||||||
|
const tableHeader = [
|
||||||
|
{
|
||||||
|
label: "Title",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "File Name",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "File Path",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Description",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Created Data",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Modified Data",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Action",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchOptions = [
|
||||||
|
{
|
||||||
|
searchType: "전체",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 별칭",
|
||||||
|
searchText: "deviceAlias",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 키",
|
||||||
|
searchText: "deviceKey",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "사용자",
|
||||||
|
searchText: "userId",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 이름",
|
||||||
|
searchText: "deviceName",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 모델",
|
||||||
|
searchText: "deviceModel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 OS",
|
||||||
|
searchText: "deviceOs",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageSizeOptions = [
|
||||||
|
{ text: "10 페이지", value: 10 },
|
||||||
|
{ text: "50 페이지", value: 50 },
|
||||||
|
{ text: "100 페이지", value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isCreateVisible: false,
|
||||||
|
isUploadVisible: false,
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
const params = { ...data.value.params };
|
||||||
|
if (params.searchType === "" || params.searchText === "") {
|
||||||
|
delete params.searchType;
|
||||||
|
delete params.searchText;
|
||||||
|
}
|
||||||
|
data.value.results = [
|
||||||
|
{
|
||||||
|
title: "배터리 상태 예측 모델 프로젝트",
|
||||||
|
fileName: "train.py",
|
||||||
|
filePath: "/kubeflow-users/battery/train.py",
|
||||||
|
description: "배터리 상태 예측 스크립트",
|
||||||
|
createdData: "2025-04-28 12:01:00",
|
||||||
|
modifiedData: "2025-04-28 12:01:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "상태 추적 모델",
|
||||||
|
fileName: "detection.py",
|
||||||
|
filePath: "/kubeflow-users/status/detection.py",
|
||||||
|
description: "상태 추적 스크립트",
|
||||||
|
createdData: "2025-04-20 12:01:00",
|
||||||
|
modifiedData: "2025-04-28 12:01:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
data.value.totalDataLength = 5;
|
||||||
|
// DeviceService.search(params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.results = d.data.deviceList;
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "디바이스 조회 실패",
|
||||||
|
// color: "error",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// DeviceService.search().then((d) => {
|
||||||
|
// data.value.totalDataLength = d.data.totalCount;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setPaginationLength();
|
||||||
|
// }, 200);
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
changePageNum();
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveData = () => {
|
||||||
|
if (data.value.selected.length === 0) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제 할 데이터를 선택해주세요. ",
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.value.allSelected || data.value.selected.length !== 1) {
|
||||||
|
data.value.isConfirmDialogVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//리스트로 삭제 할때
|
||||||
|
removeData(undefined);
|
||||||
|
};
|
||||||
|
const closeDetail = () => {
|
||||||
|
openView.value = false;
|
||||||
|
};
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
const openSettingModal = (selectedItem) => {
|
||||||
|
data.value.selectedData = selectedItem;
|
||||||
|
data.value.modalMode = "setting";
|
||||||
|
openView.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "create";
|
||||||
|
data.value.isCreateVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModifyModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "edit";
|
||||||
|
data.value.isUploadVisible = true;
|
||||||
|
};
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.isCreateVisible = null;
|
||||||
|
};
|
||||||
|
const closeModifyModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.isUploadVisible = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedAllData = () => {
|
||||||
|
data.value.selected = data.value.allSelected
|
||||||
|
? data.value.results.map((item) => {
|
||||||
|
return {
|
||||||
|
deviceKey: item.deviceKey,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-100" v-if="!openView">
|
||||||
|
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>-->
|
||||||
|
<!-- <FormComponent-->
|
||||||
|
<!-- :edit-data="data.selectedData"-->
|
||||||
|
<!-- :mode="data.modalMode"-->
|
||||||
|
<!-- @close-modal="closeModal"-->
|
||||||
|
<!-- @handle-data="saveData"-->
|
||||||
|
<!-- :user-option="data.userOption"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </v-dialog>-->
|
||||||
|
<!-- <v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">-->
|
||||||
|
<!-- <ConfirmDialogComponent-->
|
||||||
|
<!-- @cancel="data.isConfirmDialogVisible = false"-->
|
||||||
|
<!-- @delete="removeData(undefined)"-->
|
||||||
|
<!-- @init="(data.selected = []), (data.allSelected = false)"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </v-dialog>-->
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">Training Script</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card flat class="bg-shades-transparent mb-4">
|
||||||
|
<div class="d-flex justify-center flex-wrap align-center">
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
<v-responsive min-width="540" max-width="540">
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.params.searchText"
|
||||||
|
label="검색어"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
required
|
||||||
|
class="mt-3 mb-3"
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="changePageNum(1)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-responsive>
|
||||||
|
|
||||||
|
<div class="ml-3">
|
||||||
|
<v-btn
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
:rounded="5"
|
||||||
|
@click="changePageNum(1)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-sheet
|
||||||
|
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
||||||
|
>
|
||||||
|
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
||||||
|
<v-sheet
|
||||||
|
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
||||||
|
>
|
||||||
|
<v-chip color="primary"
|
||||||
|
>총 {{ data.totalDataLength.toLocaleString() }}개
|
||||||
|
</v-chip>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="bg-shades-transparent">
|
||||||
|
<v-responsive max-width="140" min-width="140" class="mb-2">
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.pageSize"
|
||||||
|
density="compact"
|
||||||
|
:items="pageSizeOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="changePageNum(1)"
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2">
|
||||||
|
<v-btn color="info" @click="openCreateModal"
|
||||||
|
>Create Script
|
||||||
|
</v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<v-card class="rounded-lg pa-8">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table
|
||||||
|
density="comfortable"
|
||||||
|
fixed-header
|
||||||
|
height="625"
|
||||||
|
col-md-12
|
||||||
|
col-12
|
||||||
|
overflow-x-auto
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="`${item.style}`"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="(item, i) in data.results"
|
||||||
|
:key="i"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<td>{{ item.title }}</td>
|
||||||
|
<td>{{ item.fileName }}</td>
|
||||||
|
<td>{{ item.filePath }}</td>
|
||||||
|
<td>{{ item.description }}</td>
|
||||||
|
<td>{{ item.createdData }}</td>
|
||||||
|
<td>{{ item.modifiedData }}</td>
|
||||||
|
<td style="white-space: nowrap">
|
||||||
|
<IconInfoBtn @on-click="openSettingModal(item)" />
|
||||||
|
<IconModifyBtn @on-click="openModifyModal()" />
|
||||||
|
<IconDeleteBtn
|
||||||
|
@on-click="
|
||||||
|
removeData([{ deviceKey: item.deviceKey }])
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
<v-card-actions class="text-center mt-8 justify-center">
|
||||||
|
<v-pagination
|
||||||
|
v-model="data.params.pageNum"
|
||||||
|
:length="data.pageLength"
|
||||||
|
:total-visible="10"
|
||||||
|
color="primary"
|
||||||
|
rounded="circle"
|
||||||
|
@update:model-value="getData"
|
||||||
|
></v-pagination>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
<v-dialog v-model="data.isCreateVisible" max-width="600" persistent>
|
||||||
|
<TrainingScriptBaseDoalog
|
||||||
|
:edit-data="data.selectedData"
|
||||||
|
:mode="data.modalMode"
|
||||||
|
@close-modal="closeCreateModal"
|
||||||
|
@handle-data="saveData"
|
||||||
|
:user-option="data.userOption"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
<v-dialog v-model="data.isUploadVisible" max-width="600" persistent>
|
||||||
|
<TrainingScriptBaseDoalog
|
||||||
|
:edit-data="data.selectedData"
|
||||||
|
:mode="data.modalMode"
|
||||||
|
@close-modal="closeModifyModal"
|
||||||
|
@handle-data="saveData"
|
||||||
|
:user-option="data.userOption"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-100" v-else>
|
||||||
|
<ViewComponent @close="closeDetail" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,291 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
|
import * as monaco from "monaco-editor";
|
||||||
|
import "monaco-editor/min/vs/editor/editor.main.css";
|
||||||
|
// const store = commonStore();
|
||||||
|
const editorRef = ref<HTMLDivElement | null>(null);
|
||||||
|
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
|
||||||
|
const experimentInfo = ref({
|
||||||
|
modelName: "ImageClassifier",
|
||||||
|
projectName: "배터리 상태 예측 모델 프로젝트",
|
||||||
|
experimentName: "Baseline Model Training",
|
||||||
|
executionName: "run-batch32-lr0.001",
|
||||||
|
deployDate: "2025-02-06",
|
||||||
|
createdId: "ADMIN_001",
|
||||||
|
description: "기본 모델 구조로 학습 성능 측정",
|
||||||
|
});
|
||||||
|
|
||||||
|
const yamlContent = `import argparse
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import torch.optim as optim
|
||||||
|
from torch.utils.data import DataLoader, TensorDataset
|
||||||
|
import os
|
||||||
|
class SimpleNet(nn.Module):
|
||||||
|
def __init__(self, input_dim, hidden_dim, output_dim):
|
||||||
|
super(SimpleNet, self).__init__()
|
||||||
|
self.fc1 = nn.Linear(input_dim, hidden_dim)
|
||||||
|
self.relu = nn.ReLU()
|
||||||
|
`;
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
||||||
|
data.value.pageLength =
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize;
|
||||||
|
} else {
|
||||||
|
data.value.pageLength = Math.ceil(
|
||||||
|
data.value.totalDataLength / data.value.params.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
let removeList = value ? value : data.value.selected;
|
||||||
|
const remove = (code) => {
|
||||||
|
// return DeviceService.delete(code).then((d) => {
|
||||||
|
// if (d.status !== 200) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (removeList.length === 1) {
|
||||||
|
remove(removeList[0].deviceKey).then(() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
||||||
|
() => {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "모두 삭제되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getCodeList();
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorInstance = monaco.editor.create(editorRef.value, {
|
||||||
|
value: yamlContent,
|
||||||
|
language: "yaml",
|
||||||
|
theme: "vs-dark",
|
||||||
|
readOnly: true,
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
lineNumbers: "on",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (editorInstance) {
|
||||||
|
editorInstance.dispose();
|
||||||
|
editorInstance = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">View Details</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Training Script Information </span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<!-- Experiment Name -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Training Script Title
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{ experimentInfo.modelName }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Project Name -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">File Name </v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.projectName
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">File Path </v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.experimentName
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
<!-- Created Date / ID -->
|
||||||
|
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Created Date
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.deployDate }}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Modified Date
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" class="pa-2">{{ experimentInfo.createdId }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Description</v-col>
|
||||||
|
<v-col cols="9" class="pa-2">{{
|
||||||
|
experimentInfo.description
|
||||||
|
}}</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Training Script Preview </span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<div ref="editorRef" class="editor-container"></div
|
||||||
|
></v-card-text>
|
||||||
|
<v-sheet class="d-flex justify-end mb-2">
|
||||||
|
<v-btn color="primary" @click="emit('close')"> Back to List </v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px; /* 원하시는 높이로 설정하세요 */
|
||||||
|
}
|
||||||
|
.v-card-text {
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 전체 테이블 1px 테두리 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card-text th {
|
||||||
|
font-size: 20px;
|
||||||
|
min-width: 400px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.v-card-text td {
|
||||||
|
font-size: 16px;
|
||||||
|
min-width: 600px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
.v-card-text tr:nth-child(odd) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,578 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
||||||
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
||||||
|
import IconSettingBtn from "@/components/atoms/button/IconSettingBtn.vue";
|
||||||
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
||||||
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
|
||||||
|
import WorkflowsCreateDialog from "@/components/atoms/organisms/WorkflowsCreateDialog.vue";
|
||||||
|
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
|
||||||
|
import { AutoflowService } from "@/components/service/management/AutoflowService";
|
||||||
|
import { commonStore } from "@/stores/commonStore";
|
||||||
|
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
|
||||||
|
const store = commonStore();
|
||||||
|
|
||||||
|
const openView = ref(false);
|
||||||
|
const openModify = ref(false);
|
||||||
|
const tableHeader = [
|
||||||
|
{
|
||||||
|
label: "No",
|
||||||
|
width: "5%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Workflow Name",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: "Step Count",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Config Progress",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Kubeflow Status",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Created DateTime",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Action",
|
||||||
|
width: "7%",
|
||||||
|
style: "word-break: keep-all;",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchOptions = [
|
||||||
|
{
|
||||||
|
searchType: "전체",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 별칭",
|
||||||
|
searchText: "deviceAlias",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 키",
|
||||||
|
searchText: "deviceKey",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "사용자",
|
||||||
|
searchText: "userId",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 이름",
|
||||||
|
searchText: "deviceName",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 모델",
|
||||||
|
searchText: "deviceModel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
searchType: "디바이스 OS",
|
||||||
|
searchText: "deviceOs",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageSizeOptions = [
|
||||||
|
{ text: "10 페이지", value: 10 },
|
||||||
|
{ text: "50 페이지", value: 50 },
|
||||||
|
{ text: "100 페이지", value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
searchType: "",
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
results: [],
|
||||||
|
totalDataLength: 0,
|
||||||
|
pageLength: 0,
|
||||||
|
modalMode: "",
|
||||||
|
selectedData: null,
|
||||||
|
allSelected: false,
|
||||||
|
selected: [],
|
||||||
|
isCreateVisible: false,
|
||||||
|
isUploadVisible: false,
|
||||||
|
isModalVisible: false,
|
||||||
|
isConfirmDialogVisible: false,
|
||||||
|
userOption: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCodeList = () => {
|
||||||
|
// UserService.search(data.value.params).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.userOption = d.data.userList;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const toRow = (workflow: any, index: number, offset: number) => ({
|
||||||
|
no: offset + index + 1,
|
||||||
|
name: workflow.workflowName,
|
||||||
|
version: workflow.version,
|
||||||
|
stepCount: workflow.stepCount,
|
||||||
|
configProgress: workflow.configProgress,
|
||||||
|
kubeflowStatus: workflow.kubeflowStatus,
|
||||||
|
registDt: workflow.regDt,
|
||||||
|
deviceKey: workflow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
const params = data.value.params;
|
||||||
|
const pageNum = params.pageNum;
|
||||||
|
const pageSize = params.pageSize;
|
||||||
|
const startIndex = (pageNum - 1) * pageSize;
|
||||||
|
|
||||||
|
AutoflowService.getAll()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error("워크플로우 조회 실패:", res);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = res.data;
|
||||||
|
console.log(result);
|
||||||
|
const rawList = Array.isArray(result) ? result : (result.content ?? []);
|
||||||
|
const totalCount = Array.isArray(result)
|
||||||
|
? rawList.length
|
||||||
|
: (result.totalElements ?? rawList.length);
|
||||||
|
const currentPageList = Array.isArray(result)
|
||||||
|
? rawList.slice(startIndex, startIndex + pageSize)
|
||||||
|
: rawList;
|
||||||
|
|
||||||
|
data.value.results = currentPageList.map((w: any, i: number) =>
|
||||||
|
toRow(w, i, startIndex),
|
||||||
|
);
|
||||||
|
data.value.totalDataLength = totalCount;
|
||||||
|
|
||||||
|
setPaginationLength();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("워크플로우 조회 에러:", err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const setPaginationLength = () => {
|
||||||
|
const total = data.value.totalDataLength || 0;
|
||||||
|
const pageSize = data.value.params.pageSize || 10;
|
||||||
|
data.value.pageLength =
|
||||||
|
total % pageSize === 0 ? total / pageSize : Math.ceil(total / pageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = (formData) => {
|
||||||
|
if (data.value.modalMode === "create") {
|
||||||
|
// DeviceService.add(formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "등록 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum(1);
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
||||||
|
// if (d.status === 200) {
|
||||||
|
// data.value.isModalVisible = false;
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "수정 되었습니다.",
|
||||||
|
// result: 200,
|
||||||
|
// });
|
||||||
|
// changePageNum();
|
||||||
|
// } else {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: d,
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeData = (value) => {
|
||||||
|
const removeList = value ?? data.value.selected;
|
||||||
|
if (!removeList || removeList.length === 0) return;
|
||||||
|
|
||||||
|
const ids = removeList.map((x) => x.deviceKey);
|
||||||
|
|
||||||
|
const remove = (id) =>
|
||||||
|
AutoflowService.delete(id).then((res) => {
|
||||||
|
if (res.status < 200 || res.status >= 300) {
|
||||||
|
return Promise.reject(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const after = () => {
|
||||||
|
if (
|
||||||
|
ids.length >= data.value.results.length &&
|
||||||
|
data.value.params.pageNum > 1
|
||||||
|
) {
|
||||||
|
data.value.params.pageNum -= 1;
|
||||||
|
}
|
||||||
|
getData();
|
||||||
|
|
||||||
|
data.value.isConfirmDialogVisible = false;
|
||||||
|
data.value.selected = [];
|
||||||
|
data.value.allSelected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ids.length === 1) {
|
||||||
|
remove(ids[0])
|
||||||
|
.then(() => {
|
||||||
|
store.setSnackbarMsg({
|
||||||
|
color: "success",
|
||||||
|
text: "삭제되었습니다.",
|
||||||
|
result: 200,
|
||||||
|
});
|
||||||
|
after();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
store.setSnackbarMsg({
|
||||||
|
color: "warning",
|
||||||
|
text: "삭제 실패",
|
||||||
|
result: 500,
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.all(ids.map(remove))
|
||||||
|
.then(() => {
|
||||||
|
store.setSnackbarMsg({
|
||||||
|
color: "success",
|
||||||
|
text: "모두 삭제되었습니다.",
|
||||||
|
result: 200,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
store.setSnackbarMsg({
|
||||||
|
color: "warning",
|
||||||
|
text: "일부 삭제 실패",
|
||||||
|
result: 500,
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
})
|
||||||
|
.finally(after);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveData = () => {
|
||||||
|
if (data.value.selected.length === 0) {
|
||||||
|
// store.setSnackbarMsg({
|
||||||
|
// text: "삭제 할 데이터를 선택해주세요. ",
|
||||||
|
// result: 500,
|
||||||
|
// });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.value.allSelected || data.value.selected.length !== 1) {
|
||||||
|
data.value.isConfirmDialogVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//리스트로 삭제 할때
|
||||||
|
removeData(undefined);
|
||||||
|
};
|
||||||
|
const closeDetail = () => {
|
||||||
|
openView.value = false;
|
||||||
|
};
|
||||||
|
const changePageNum = (page) => {
|
||||||
|
data.value.params.pageNum = page;
|
||||||
|
getData();
|
||||||
|
};
|
||||||
|
const openDetailModal = (selectedItem) => {
|
||||||
|
data.value.selectedData = selectedItem;
|
||||||
|
openView.value = true;
|
||||||
|
};
|
||||||
|
const openModifyModal = (item: { workflow: string; stepName: string }) => {
|
||||||
|
data.value.selectedData = {
|
||||||
|
workflow: item.workflow,
|
||||||
|
stepName: item.stepName,
|
||||||
|
};
|
||||||
|
openModify.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "create";
|
||||||
|
data.value.isCreateVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openUploadModal = () => {
|
||||||
|
data.value.selectedData = null;
|
||||||
|
data.value.modalMode = "upload";
|
||||||
|
data.value.isUploadVisible = true;
|
||||||
|
};
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.isCreateVisible = false;
|
||||||
|
};
|
||||||
|
const closeUploadModal = () => {
|
||||||
|
data.value.isModalVisible = false;
|
||||||
|
data.value.isUploadVisible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedAllData = () => {
|
||||||
|
data.value.selected = data.value.allSelected
|
||||||
|
? data.value.results.map((item) => {
|
||||||
|
return {
|
||||||
|
deviceKey: item.deviceKey,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
watch(
|
||||||
|
() => data.value.isCreateVisible,
|
||||||
|
(now, prev) => {
|
||||||
|
if (prev && !now) getData();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => data.value.isUploadVisible,
|
||||||
|
(now, prev) => {
|
||||||
|
if (prev && !now) getData();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
getCodeList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-100" v-if="!openView">
|
||||||
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">Workflows</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card flat class="bg-shades-transparent mb-4">
|
||||||
|
<div class="d-flex justify-center flex-wrap align-center">
|
||||||
|
<v-responsive
|
||||||
|
max-width="180"
|
||||||
|
min-width="180"
|
||||||
|
class="mr-3 mt-3 mb-3"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.searchType"
|
||||||
|
label="검색조건"
|
||||||
|
density="compact"
|
||||||
|
:items="searchOptions"
|
||||||
|
item-title="searchType"
|
||||||
|
item-value="searchText"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
<v-responsive min-width="540" max-width="540">
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.params.searchText"
|
||||||
|
label="검색어"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
required
|
||||||
|
class="mt-3 mb-3"
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="changePageNum(1)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-responsive>
|
||||||
|
|
||||||
|
<div class="ml-3">
|
||||||
|
<v-btn
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
:rounded="5"
|
||||||
|
@click="changePageNum(1)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-sheet
|
||||||
|
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
||||||
|
>
|
||||||
|
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
||||||
|
<v-sheet
|
||||||
|
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
||||||
|
>
|
||||||
|
<v-chip color="primary"
|
||||||
|
>총 {{ data.totalDataLength.toLocaleString() }}개
|
||||||
|
</v-chip>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="bg-shades-transparent">
|
||||||
|
<v-responsive max-width="140" min-width="140" class="mb-2">
|
||||||
|
<v-select
|
||||||
|
v-model="data.params.pageSize"
|
||||||
|
density="compact"
|
||||||
|
:items="pageSizeOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="changePageNum(1)"
|
||||||
|
></v-select>
|
||||||
|
</v-responsive>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet class="justify-end mb-2">
|
||||||
|
<v-btn color="info" class="mr-4" @click="openUploadModal"
|
||||||
|
>Upload Workflow
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="info" @click="openCreateModal"
|
||||||
|
>Create Workflow
|
||||||
|
</v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
|
<v-card class="rounded-lg pa-8">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-sheet>
|
||||||
|
<v-table
|
||||||
|
density="comfortable"
|
||||||
|
fixed-header
|
||||||
|
height="625"
|
||||||
|
col-md-12
|
||||||
|
col-12
|
||||||
|
overflow-x-auto
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 5%" />
|
||||||
|
<col
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
:style="`width:${item.width}`"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.allSelected"
|
||||||
|
style="min-width: 36px"
|
||||||
|
:indeterminate="data.allSelected === true"
|
||||||
|
hide-details
|
||||||
|
@change="getSelectedAllData"
|
||||||
|
></v-checkbox>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-for="(item, i) in tableHeader"
|
||||||
|
:key="i"
|
||||||
|
class="text-center font-weight-bold"
|
||||||
|
:style="`${item.style}`"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-body-2">
|
||||||
|
<tr
|
||||||
|
v-for="(item, i) in data.results"
|
||||||
|
:key="i"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="data.selected"
|
||||||
|
hide-details
|
||||||
|
:value="{
|
||||||
|
deviceKey: item.deviceKey,
|
||||||
|
}"
|
||||||
|
></v-checkbox>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.no }}</td>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.stepCount }}</td>
|
||||||
|
<td>{{ item.configProgress }}</td>
|
||||||
|
<td>{{ item.kubeflowStatus }}</td>
|
||||||
|
<td>{{ item.registDt }}</td>
|
||||||
|
<td style="white-space: nowrap">
|
||||||
|
<IconInfoBtn @on-click="openDetailModal(item)" />
|
||||||
|
<IconSettingBtn />
|
||||||
|
<IconModifyBtn @on-click="openModifyModal(item)" />
|
||||||
|
<IconDeleteBtn
|
||||||
|
@on-click="
|
||||||
|
removeData([{ deviceKey: item.deviceKey }])
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-sheet>
|
||||||
|
<v-card-actions class="text-center mt-8 justify-center">
|
||||||
|
<v-pagination
|
||||||
|
v-model="data.params.pageNum"
|
||||||
|
:length="data.pageLength"
|
||||||
|
:total-visible="10"
|
||||||
|
color="primary"
|
||||||
|
rounded="circle"
|
||||||
|
@update:model-value="changePageNum"
|
||||||
|
></v-pagination>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
<v-dialog v-model="data.isCreateVisible" max-width="800" persistent>
|
||||||
|
<WorkflowsCreateDialog
|
||||||
|
:edit-data="data.selectedData"
|
||||||
|
:mode="data.modalMode"
|
||||||
|
@close-modal="closeCreateModal"
|
||||||
|
@handle-data="saveData"
|
||||||
|
:user-option="data.userOption"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
<v-dialog v-model="data.isUploadVisible" max-width="800" persistent>
|
||||||
|
<WorkflowsUploadDialog
|
||||||
|
:edit-data="data.selectedData"
|
||||||
|
:mode="data.modalMode"
|
||||||
|
@close-modal="closeUploadModal"
|
||||||
|
@handle-data="saveData"
|
||||||
|
:user-option="data.userOption"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
<v-dialog v-model="openModify" max-width="600px">
|
||||||
|
<StapComfigDialog
|
||||||
|
v-model="openModify"
|
||||||
|
:selectedData="data.selectedData"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-100" v-else>
|
||||||
|
<ViewComponent
|
||||||
|
v-if="data.selectedData"
|
||||||
|
:id="data.selectedData.deviceKey"
|
||||||
|
@close="closeDetail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,262 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch, onBeforeUnmount } from "vue";
|
||||||
|
import * as monaco from "monaco-editor";
|
||||||
|
import "monaco-editor/min/vs/editor/editor.main.css";
|
||||||
|
import { AutoflowService } from "@/components/service/management/AutoflowService";
|
||||||
|
|
||||||
|
type TabKey = "details" | "yaml";
|
||||||
|
|
||||||
|
const props = defineProps<{ id: number | string }>();
|
||||||
|
const emit = defineEmits<{ (e: "close"): void }>();
|
||||||
|
|
||||||
|
const activeTab = ref<TabKey>("details");
|
||||||
|
const editorRef = ref<HTMLDivElement | null>(null);
|
||||||
|
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
|
||||||
|
|
||||||
|
const detail = ref({
|
||||||
|
workflowName: "",
|
||||||
|
version: "",
|
||||||
|
workflowDescription: "",
|
||||||
|
createdDate: "",
|
||||||
|
createdId: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepHeaders = [
|
||||||
|
{ title: "Order", key: "order", width: "10%", align: "center" },
|
||||||
|
{ title: "Step Name", key: "name", width: "40%", align: "center" },
|
||||||
|
{
|
||||||
|
title: "Component Type",
|
||||||
|
key: "componentType",
|
||||||
|
width: "30%",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{ title: "Status", key: "status", width: "20%", align: "center" },
|
||||||
|
];
|
||||||
|
const steps = ref<
|
||||||
|
Array<{ order: number; name: string; componentType: string; status: string }>
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const defaultYaml = `# YAML not provided by server
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Workflow
|
||||||
|
metadata:
|
||||||
|
generateName: dummy-
|
||||||
|
spec:
|
||||||
|
entrypoint: main
|
||||||
|
templates:
|
||||||
|
- name: main
|
||||||
|
container:
|
||||||
|
image: alpine:latest
|
||||||
|
command: [sh, -c]
|
||||||
|
args: ["echo hello"]
|
||||||
|
`;
|
||||||
|
|
||||||
|
/** ===== 상세 조회 ===== */
|
||||||
|
async function fetchDetail(id: number | string) {
|
||||||
|
try {
|
||||||
|
const res = await AutoflowService.view(Number(id));
|
||||||
|
const d = res.data;
|
||||||
|
|
||||||
|
detail.value.workflowName = d.workflowName || "";
|
||||||
|
detail.value.version = String(d.version || 1);
|
||||||
|
detail.value.workflowDescription = d.workflowDescription || "";
|
||||||
|
detail.value.createdDate = d.regDt || d.regDate || "-";
|
||||||
|
detail.value.createdId = d.regUserId || "-";
|
||||||
|
|
||||||
|
if (Array.isArray(d.steps)) {
|
||||||
|
steps.value = d.steps.map((s: any, idx: number) => ({
|
||||||
|
order: idx + 1,
|
||||||
|
name: s.stepName || s.name || `Step ${idx + 1}`,
|
||||||
|
componentType: s.componentType || s.type || "-",
|
||||||
|
status: s.status || "Not Configured",
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
steps.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// YAML 표시 (서버 필드 이름에 맞춰 하나라도 있으면 사용)
|
||||||
|
const yamlFromServer =
|
||||||
|
d.workflowYaml ||
|
||||||
|
d.yaml ||
|
||||||
|
d.pipelineYaml ||
|
||||||
|
d.specYaml ||
|
||||||
|
d.yamlStr ||
|
||||||
|
"";
|
||||||
|
if (editorInstance) {
|
||||||
|
editorInstance.setValue(yamlFromServer || defaultYaml);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Child] view API failed:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ===== 마운트 & 변경 감지 ===== */
|
||||||
|
onMounted(() => {
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorInstance = monaco.editor.create(editorRef.value, {
|
||||||
|
value: defaultYaml,
|
||||||
|
language: "yaml",
|
||||||
|
theme: "vs-dark",
|
||||||
|
readOnly: true,
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
lineNumbers: "on",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// props.id가 바뀌면 재조회
|
||||||
|
watch(
|
||||||
|
() => props.id,
|
||||||
|
(val) => {
|
||||||
|
if (val !== null && val !== undefined && val !== "") {
|
||||||
|
fetchDetail(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (editorInstance) {
|
||||||
|
editorInstance.dispose();
|
||||||
|
editorInstance = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container class="h-100 w-100 pa-5 d-flex flex-column align-center">
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bg-shades-transparent d-flex flex-column justify-center w-100"
|
||||||
|
>
|
||||||
|
<!-- 헤더 -->
|
||||||
|
<v-card flat class="bg-shades-transparent w-100">
|
||||||
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
||||||
|
<div class="d-flex flex-row justify-start align-center">
|
||||||
|
<div class="text-primary">View Details</div>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 탭 -->
|
||||||
|
<v-tabs
|
||||||
|
v-model="activeTab"
|
||||||
|
background-color="grey lighten-4"
|
||||||
|
style="max-width: 360px"
|
||||||
|
grow
|
||||||
|
>
|
||||||
|
<v-tab value="details">Details</v-tab>
|
||||||
|
<v-tab value="yaml">YAML</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<!-- Details 탭 -->
|
||||||
|
<template v-if="activeTab === 'details'">
|
||||||
|
<v-card class="bordered-box mb-6 w-100 rounded-lg pa-8 step-card">
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Workflow Information</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="px-6 pb-6 pt-4">
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Workflow Name</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="3">{{ detail.workflowName }}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold">Version</v-col>
|
||||||
|
<v-col cols="3">{{ detail.version }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Workflow Description</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="9">{{ detail.workflowDescription }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
<v-row align="center" class="py-2">
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Created Date</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="3">{{ detail.createdDate }}</v-col>
|
||||||
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
||||||
|
>Created ID</v-col
|
||||||
|
>
|
||||||
|
<v-col cols="3">{{ detail.createdId }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Steps -->
|
||||||
|
<v-card
|
||||||
|
flat
|
||||||
|
class="bordered-box mb-6 w-100 rounded-lg pa-8"
|
||||||
|
style="min-height: 500px"
|
||||||
|
>
|
||||||
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||||
|
<span class="font-weight-bold">Step Overview</span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="stepHeaders"
|
||||||
|
:items="steps"
|
||||||
|
dense
|
||||||
|
class="text-center"
|
||||||
|
hide-default-footer
|
||||||
|
:items-per-page="5"
|
||||||
|
header-color="primary"
|
||||||
|
disable-sort
|
||||||
|
>
|
||||||
|
<template #item.order="{ index }">{{ index + 1 }}</template>
|
||||||
|
<template #item.status="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="
|
||||||
|
{ Configured: 'success', 'Not Configured': 'warning' }[
|
||||||
|
item.status
|
||||||
|
] || 'default'
|
||||||
|
"
|
||||||
|
small
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
{{ item.status }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
|
||||||
|
<v-sheet class="d-flex justify-end mt-4">
|
||||||
|
<v-btn class="back-to-list" color="primary" @click="emit('close')"
|
||||||
|
>Back to List</v-btn
|
||||||
|
>
|
||||||
|
</v-sheet>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- YAML 탭 -->
|
||||||
|
<div
|
||||||
|
v-show="activeTab === 'yaml'"
|
||||||
|
ref="editorRef"
|
||||||
|
class="editor-container"
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 900px;
|
||||||
|
max-height: 900px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.step-card {
|
||||||
|
position: relative;
|
||||||
|
min-height: 500px;
|
||||||
|
padding-bottom: 84px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-list {
|
||||||
|
position: absolute;
|
||||||
|
right: 24px;
|
||||||
|
bottom: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,532 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import IconDeleteBtn from "@/components/button/IconDeleteBtn.vue";
|
|
||||||
import IconModifyBtn from "@/components/button/IconModifyBtn.vue";
|
|
||||||
// import FormComponent from "@/components/device/FormComponent.vue";
|
|
||||||
import { onMounted, ref, watch } from "vue";
|
|
||||||
|
|
||||||
// const store = commonStore();
|
|
||||||
|
|
||||||
const tableHeader = [
|
|
||||||
{
|
|
||||||
label: "No",
|
|
||||||
width: "5%",
|
|
||||||
style: "word-break: keep-all;",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Workflow Name",
|
|
||||||
width: "7%",
|
|
||||||
style: "word-break: keep-all;",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Version",
|
|
||||||
width: "7%",
|
|
||||||
style: "word-break: keep-all;",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Step Count",
|
|
||||||
width: "7%",
|
|
||||||
style: "word-break: keep-all;",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Config Progress",
|
|
||||||
width: "7%",
|
|
||||||
style: "word-break: keep-all;",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Kubeflow Status",
|
|
||||||
width: "7%",
|
|
||||||
style: "word-break: keep-all;",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Created DateTime",
|
|
||||||
width: "7%",
|
|
||||||
style: "word-break: keep-all;",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Action",
|
|
||||||
width: "7%",
|
|
||||||
style: "word-break: keep-all;",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const searchOptions = [
|
|
||||||
{
|
|
||||||
searchType: "전체",
|
|
||||||
searchText: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
searchType: "디바이스 별칭",
|
|
||||||
searchText: "deviceAlias",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
searchType: "디바이스 키",
|
|
||||||
searchText: "deviceKey",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
searchType: "사용자",
|
|
||||||
searchText: "userId",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
searchType: "디바이스 이름",
|
|
||||||
searchText: "deviceName",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
searchType: "디바이스 모델",
|
|
||||||
searchText: "deviceModel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
searchType: "디바이스 OS",
|
|
||||||
searchText: "deviceOs",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
searchType: "디바이스 버전",
|
|
||||||
searchText: "deviceVersion",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const pageSizeOptions = [
|
|
||||||
{ text: "10 페이지", value: 10 },
|
|
||||||
{ text: "50 페이지", value: 50 },
|
|
||||||
{ text: "100 페이지", value: 100 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const data = ref({
|
|
||||||
params: {
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
searchType: "",
|
|
||||||
searchText: "",
|
|
||||||
},
|
|
||||||
results: [],
|
|
||||||
totalDataLength: 0,
|
|
||||||
pageLength: 0,
|
|
||||||
modalMode: "",
|
|
||||||
selectedData: null,
|
|
||||||
allSelected: false,
|
|
||||||
selected: [],
|
|
||||||
isModalVisible: false,
|
|
||||||
isConfirmDialogVisible: false,
|
|
||||||
userOption: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const getCodeList = () => {
|
|
||||||
// UserService.search(data.value.params).then((d) => {
|
|
||||||
// if (d.status === 200) {
|
|
||||||
// data.value.userOption = d.data.userList;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
|
|
||||||
const getData = () => {
|
|
||||||
const params = { ...data.value.params };
|
|
||||||
if (params.searchType === "" || params.searchText === "") {
|
|
||||||
delete params.searchType;
|
|
||||||
delete params.searchText;
|
|
||||||
}
|
|
||||||
data.value.results = [{
|
|
||||||
"no":5,
|
|
||||||
"name": "sentiment-analysis",
|
|
||||||
"version": "v2.0",
|
|
||||||
"stepCount": 2,
|
|
||||||
"configProgress": "0/2",
|
|
||||||
"kubeflowStatus": "Not Uploaded",
|
|
||||||
"registDt": "2025-06-10T00:00:00Z",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"no":4,
|
|
||||||
"name": "image-classfier",
|
|
||||||
"version": "v2.0",
|
|
||||||
"stepCount": 3,
|
|
||||||
"configProgress": "1/3",
|
|
||||||
"kubeflowStatus": "Not Uploaded",
|
|
||||||
"registDt": "2025-06-09T00:00:00Z",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"no":3,
|
|
||||||
"name": "user-clustering",
|
|
||||||
"version": "v1.0",
|
|
||||||
"stepCount": 3,
|
|
||||||
"configProgress": "0/3",
|
|
||||||
"kubeflowStatus": "Not Uploaded",
|
|
||||||
"registDt": "2025-06-01T00:00:00Z",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"no":2,
|
|
||||||
"name": "time-series-train",
|
|
||||||
"version": "v1.0",
|
|
||||||
"stepCount": 2,
|
|
||||||
"configProgress": "1/3",
|
|
||||||
"kubeflowStatus": "Not Uploaded",
|
|
||||||
"registDt": "2025-05-29T00:00:00Z",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"no":1,
|
|
||||||
"name": "customer-churn-pred",
|
|
||||||
"version": "v1.0",
|
|
||||||
"stepCount": 3,
|
|
||||||
"configProgress": "0/3",
|
|
||||||
"kubeflowStatus": "Not Uploaded",
|
|
||||||
"registDt": "2025-05-31T00:00:00Z",
|
|
||||||
}];
|
|
||||||
data.value.totalDataLength =5;
|
|
||||||
// DeviceService.search(params).then((d) => {
|
|
||||||
// if (d.status === 200) {
|
|
||||||
// data.value.results = d.data.deviceList;
|
|
||||||
// data.value.totalDataLength = d.data.totalCount;
|
|
||||||
// setTimeout(() => {
|
|
||||||
// setPaginationLength();
|
|
||||||
// }, 200);
|
|
||||||
// } else {
|
|
||||||
// store.setSnackbarMsg({
|
|
||||||
// text: "디바이스 조회 실패",
|
|
||||||
// color: "error",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// DeviceService.search().then((d) => {
|
|
||||||
// data.value.totalDataLength = d.data.totalCount;
|
|
||||||
// setTimeout(() => {
|
|
||||||
// setPaginationLength();
|
|
||||||
// }, 200);
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
|
|
||||||
const setPaginationLength = () => {
|
|
||||||
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
|
||||||
data.value.pageLength =
|
|
||||||
data.value.totalDataLength / data.value.params.pageSize;
|
|
||||||
} else {
|
|
||||||
data.value.pageLength = Math.ceil(
|
|
||||||
data.value.totalDataLength / data.value.params.pageSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveData = (formData) => {
|
|
||||||
if (data.value.modalMode === "create") {
|
|
||||||
// DeviceService.add(formData).then((d) => {
|
|
||||||
// if (d.status === 200) {
|
|
||||||
// data.value.isModalVisible = false;
|
|
||||||
// store.setSnackbarMsg({
|
|
||||||
// text: "등록 되었습니다.",
|
|
||||||
// result: 200,
|
|
||||||
// });
|
|
||||||
// changePageNum(1);
|
|
||||||
// } else {
|
|
||||||
// store.setSnackbarMsg({
|
|
||||||
// text: d,
|
|
||||||
// result: 500,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
} else {
|
|
||||||
// DeviceService.update(formData.deviceKey, formData).then((d) => {
|
|
||||||
// if (d.status === 200) {
|
|
||||||
// data.value.isModalVisible = false;
|
|
||||||
// store.setSnackbarMsg({
|
|
||||||
// text: "수정 되었습니다.",
|
|
||||||
// result: 200,
|
|
||||||
// });
|
|
||||||
// changePageNum();
|
|
||||||
// } else {
|
|
||||||
// store.setSnackbarMsg({
|
|
||||||
// text: d,
|
|
||||||
// result: 500,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeData = (value) => {
|
|
||||||
let removeList = value ? value : data.value.selected;
|
|
||||||
const remove = (code) => {
|
|
||||||
// return DeviceService.delete(code).then((d) => {
|
|
||||||
// if (d.status !== 200) {
|
|
||||||
// store.setSnackbarMsg({
|
|
||||||
// text: d,
|
|
||||||
// result: 500,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (removeList.length === 1) {
|
|
||||||
remove(removeList[0].deviceKey).then(() => {
|
|
||||||
// store.setSnackbarMsg({
|
|
||||||
// text: "삭제되었습니다.",
|
|
||||||
// result: 200,
|
|
||||||
// });
|
|
||||||
changePageNum();
|
|
||||||
data.value.isConfirmDialogVisible = false;
|
|
||||||
data.value.selected = [];
|
|
||||||
data.value.allSelected = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
|
|
||||||
() => {
|
|
||||||
// store.setSnackbarMsg({
|
|
||||||
// text: "모두 삭제되었습니다.",
|
|
||||||
// result: 200,
|
|
||||||
// });
|
|
||||||
changePageNum();
|
|
||||||
data.value.isConfirmDialogVisible = false;
|
|
||||||
data.value.selected = [];
|
|
||||||
data.value.allSelected = false;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveData = () => {
|
|
||||||
if (data.value.selected.length === 0) {
|
|
||||||
// store.setSnackbarMsg({
|
|
||||||
// text: "삭제 할 데이터를 선택해주세요. ",
|
|
||||||
// result: 500,
|
|
||||||
// });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (data.value.allSelected || data.value.selected.length !== 1) {
|
|
||||||
data.value.isConfirmDialogVisible = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//리스트로 삭제 할때
|
|
||||||
removeData(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePageNum = (page) => {
|
|
||||||
data.value.params.pageNum = page;
|
|
||||||
getData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModifyModal = (selectedItem) => {
|
|
||||||
data.value.selectedData = selectedItem;
|
|
||||||
data.value.modalMode = "modify";
|
|
||||||
data.value.isModalVisible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openCreateModal = () => {
|
|
||||||
data.value.selectedData = null;
|
|
||||||
data.value.modalMode = "create";
|
|
||||||
data.value.isModalVisible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
data.value.isModalVisible = false;
|
|
||||||
data.value.selectedData = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSelectedAllData = () => {
|
|
||||||
data.value.selected = data.value.allSelected
|
|
||||||
? data.value.results.map((item) => {
|
|
||||||
return {
|
|
||||||
deviceKey: item.deviceKey,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getData();
|
|
||||||
getCodeList();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<!-- <v-dialog v-model="data.isModalVisible" max-width="600" persistent>-->
|
|
||||||
<!-- <FormComponent-->
|
|
||||||
<!-- :edit-data="data.selectedData"-->
|
|
||||||
<!-- :mode="data.modalMode"-->
|
|
||||||
<!-- @close-modal="closeModal"-->
|
|
||||||
<!-- @handle-data="saveData"-->
|
|
||||||
<!-- :user-option="data.userOption"-->
|
|
||||||
<!-- />-->
|
|
||||||
<!-- </v-dialog>-->
|
|
||||||
<!-- <v-dialog v-model="data.isConfirmDialogVisible" persistent max-width="300">-->
|
|
||||||
<!-- <ConfirmDialogComponent-->
|
|
||||||
<!-- @cancel="data.isConfirmDialogVisible = false"-->
|
|
||||||
<!-- @delete="removeData(undefined)"-->
|
|
||||||
<!-- @init="(data.selected = []), (data.allSelected = false)"-->
|
|
||||||
<!-- />-->
|
|
||||||
<!-- </v-dialog>-->
|
|
||||||
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
|
||||||
<v-card
|
|
||||||
flat
|
|
||||||
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
|
||||||
>
|
|
||||||
<v-card flat class="bg-shades-transparent w-100">
|
|
||||||
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
|
||||||
<div class="d-flex flex-row justify-start align-center">
|
|
||||||
<div class="text-primary">Workflows</div>
|
|
||||||
</div>
|
|
||||||
</v-card-item>
|
|
||||||
</v-card>
|
|
||||||
<v-card flat class="bg-shades-transparent w-100">
|
|
||||||
<v-card flat class="bg-shades-transparent mb-4">
|
|
||||||
<div class="d-flex justify-center flex-wrap align-center">
|
|
||||||
<v-responsive
|
|
||||||
max-width="180"
|
|
||||||
min-width="180"
|
|
||||||
class="mr-3 mt-3 mb-3"
|
|
||||||
>
|
|
||||||
<v-select
|
|
||||||
v-model="data.params.searchType"
|
|
||||||
label="검색조건"
|
|
||||||
density="compact"
|
|
||||||
:items="searchOptions"
|
|
||||||
item-title="searchType"
|
|
||||||
item-value="searchText"
|
|
||||||
hide-details
|
|
||||||
></v-select>
|
|
||||||
</v-responsive>
|
|
||||||
<v-responsive min-width="540" max-width="540">
|
|
||||||
<v-text-field
|
|
||||||
v-model="data.params.searchText"
|
|
||||||
label="검색어"
|
|
||||||
density="compact"
|
|
||||||
clearable
|
|
||||||
required
|
|
||||||
class="mt-3 mb-3"
|
|
||||||
hide-details
|
|
||||||
@keyup.enter="changePageNum(1)"
|
|
||||||
></v-text-field>
|
|
||||||
</v-responsive>
|
|
||||||
|
|
||||||
<div class="ml-3">
|
|
||||||
<v-btn
|
|
||||||
size="large"
|
|
||||||
color="primary"
|
|
||||||
:rounded="5"
|
|
||||||
@click="changePageNum(1)"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-magnify</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<v-sheet
|
|
||||||
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
|
||||||
>
|
|
||||||
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
|
||||||
<v-sheet
|
|
||||||
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
|
||||||
>
|
|
||||||
<v-chip color="primary"
|
|
||||||
>총 {{ data.totalDataLength.toLocaleString() }}개
|
|
||||||
</v-chip>
|
|
||||||
</v-sheet>
|
|
||||||
<v-sheet class="bg-shades-transparent">
|
|
||||||
<v-responsive max-width="140" min-width="140" class="mb-2">
|
|
||||||
<v-select
|
|
||||||
v-model="data.params.pageSize"
|
|
||||||
density="compact"
|
|
||||||
:items="pageSizeOptions"
|
|
||||||
item-title="text"
|
|
||||||
item-value="value"
|
|
||||||
variant="outlined"
|
|
||||||
color="primary"
|
|
||||||
hide-details
|
|
||||||
@update:model-value="changePageNum(1)"
|
|
||||||
></v-select>
|
|
||||||
</v-responsive>
|
|
||||||
</v-sheet>
|
|
||||||
</v-sheet>
|
|
||||||
<v-sheet class="justify-end mb-2">
|
|
||||||
<v-btn color="error" class="mr-4" @click="handleRemoveData"
|
|
||||||
>선택 삭제
|
|
||||||
</v-btn>
|
|
||||||
<v-btn color="success" @click="openCreateModal">등록</v-btn>
|
|
||||||
</v-sheet>
|
|
||||||
</v-sheet>
|
|
||||||
|
|
||||||
<v-card class="rounded-lg pa-8">
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-sheet>
|
|
||||||
<v-table
|
|
||||||
density="comfortable"
|
|
||||||
fixed-header
|
|
||||||
height="625"
|
|
||||||
col-md-12
|
|
||||||
col-12
|
|
||||||
overflow-x-auto
|
|
||||||
>
|
|
||||||
<colgroup>
|
|
||||||
<col style="width: 5%" />
|
|
||||||
<col
|
|
||||||
v-for="(item, i) in tableHeader"
|
|
||||||
:key="i"
|
|
||||||
:style="`width:${item.width}`"
|
|
||||||
/>
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<v-checkbox
|
|
||||||
v-model="data.allSelected"
|
|
||||||
style="min-width: 36px"
|
|
||||||
:indeterminate="data.allSelected === true"
|
|
||||||
hide-details
|
|
||||||
@change="getSelectedAllData"
|
|
||||||
></v-checkbox>
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
v-for="(item, i) in tableHeader"
|
|
||||||
:key="i"
|
|
||||||
class="text-center font-weight-bold"
|
|
||||||
:style="`${item.style}`"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="text-body-2">
|
|
||||||
<tr
|
|
||||||
v-for="(item, i) in data.results"
|
|
||||||
:key="i"
|
|
||||||
class="text-center"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<v-checkbox
|
|
||||||
v-model="data.selected"
|
|
||||||
hide-details
|
|
||||||
:value="{
|
|
||||||
deviceKey: item.deviceKey,
|
|
||||||
}"
|
|
||||||
></v-checkbox>
|
|
||||||
</td>
|
|
||||||
<td>{{ item.no }}</td>
|
|
||||||
<td>{{ item.name }}</td>
|
|
||||||
<td>{{ item.version }}</td>
|
|
||||||
<td>{{ item.stepCount }}</td>
|
|
||||||
<td>{{ item.configProgress }}</td>
|
|
||||||
<td>{{ item.kubeflowStatus }}</td>
|
|
||||||
<td>{{ item.registDt }}</td>
|
|
||||||
<td style="white-space: nowrap">
|
|
||||||
<IconModifyBtn @on-click="openModifyModal(item)" />
|
|
||||||
<IconDeleteBtn
|
|
||||||
@on-click="removeData([{ deviceKey: item.deviceKey }])"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</v-table>
|
|
||||||
</v-sheet>
|
|
||||||
<v-card-actions class="text-center mt-8 justify-center">
|
|
||||||
<v-pagination
|
|
||||||
v-model="data.params.pageNum"
|
|
||||||
:length="data.pageLength"
|
|
||||||
:total-visible="10"
|
|
||||||
color="primary"
|
|
||||||
rounded="circle"
|
|
||||||
@update:model-value="getData"
|
|
||||||
></v-pagination>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-col>
|
|
||||||
</v-card>
|
|
||||||
</v-card>
|
|
||||||
</v-card>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@ -1,10 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import ListComponent from "@/components/templates/Datasets/ListComponent.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ListComponent />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped lang="sass"></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import ListComponent from "@/components/templates/deployment/ListComponent.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ListComponent />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped lang="sass"></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="sass">
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<script setup>
|
||||||
|
import ListComponent from "@/components/templates/run/executions/ListComponent.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ListComponent />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="sass"></style>
|
||||||
@ -1,10 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import ListComponent from "@/components/templates/run/experiment/ListComponent.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ListComponent />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped lang="sass"></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
|
import ListComponent from "@/components/home/ListComponent.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ListComponent />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
<script setup>
|
||||||
|
import ListComponent from "@/components/templates/Project/ListComponent.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ListComponent />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="sass"></style>
|
||||||
@ -0,0 +1,210 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { storage } from "@/utils/storage";
|
||||||
|
import logo from "@/assets/wordmark.png";
|
||||||
|
|
||||||
|
import logo2 from "@/assets/workflow.png";
|
||||||
|
import { UserManagerService } from "@/components/service/management/userManagerService";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
form: false,
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
role: [],
|
||||||
|
password: "",
|
||||||
|
loading: false,
|
||||||
|
snackbar: false,
|
||||||
|
snackbarText: "",
|
||||||
|
snackbarColor: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
data.value.userId = "";
|
||||||
|
data.value.userPw = "";
|
||||||
|
data.value.loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetLogin = () => {
|
||||||
|
data.value.userId = "";
|
||||||
|
data.value.userPw = "";
|
||||||
|
data.value.loading = false;
|
||||||
|
};
|
||||||
|
const resetSignup = () => {
|
||||||
|
data.value.username = "";
|
||||||
|
data.value.email = "";
|
||||||
|
data.value.role = [];
|
||||||
|
data.value.password = "";
|
||||||
|
data.value.loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const signUp = () => {
|
||||||
|
const payload = {
|
||||||
|
username: data.value.username,
|
||||||
|
email: data.value.email,
|
||||||
|
role: data.value.role,
|
||||||
|
password: data.value.password,
|
||||||
|
};
|
||||||
|
console.log("회원가입 호출 payload:", payload);
|
||||||
|
|
||||||
|
UserManagerService.signUp(payload)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.success) {
|
||||||
|
router.push("/login");
|
||||||
|
} else {
|
||||||
|
data.value.snackbarText =
|
||||||
|
res.data.message || "회원가입에 실패했습니다.";
|
||||||
|
data.value.snackbarColor = "red";
|
||||||
|
data.value.snackbar = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
data.value.snackbarText =
|
||||||
|
"비밀번호는 6자 이상, 40자 이하로 입력해야 합니다.";
|
||||||
|
data.value.snackbarColor = "red";
|
||||||
|
data.value.snackbar = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container :fluid="true" class="pa-0">
|
||||||
|
<v-sheet
|
||||||
|
class="background-image w-100 h-screen d-flex align-center justify-center flex-column"
|
||||||
|
>
|
||||||
|
<v-card flat class="bg-transparent d-flex align-center flex-column">
|
||||||
|
<v-card
|
||||||
|
class="mx-auto pa-10 mb-4 login-box rounded-lg d-flex flex-column"
|
||||||
|
style="min-width: 450px !important"
|
||||||
|
density="comfortable"
|
||||||
|
>
|
||||||
|
<div class="mb-4 w-100 d-flex justify-center">
|
||||||
|
<div
|
||||||
|
class="bg-transparent rounded-circle pa-2"
|
||||||
|
style="border: 1.8px solid #398bec"
|
||||||
|
>
|
||||||
|
<v-icon class="text-primary">mdi-shield-key-outline</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-h5 pb-2 text-center font-weight-bold text-primary">
|
||||||
|
Autoflow Web Console
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-form v-model="data.form" @submit.prevent="signUp" class="mt-3">
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.username"
|
||||||
|
:readonly="data.loading"
|
||||||
|
class="mb-2"
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
placeholder="아이디를 입력해주세요."
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.email"
|
||||||
|
:readonly="data.loading"
|
||||||
|
class="mb-2"
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
placeholder="이메일을 입력해주세요."
|
||||||
|
></v-text-field>
|
||||||
|
<v-select
|
||||||
|
v-model="data.role"
|
||||||
|
:items="['ROLE_USER', 'ROLE_MODERATOR', 'ROLE_ADMIN']"
|
||||||
|
multiple
|
||||||
|
variant="outlined"
|
||||||
|
placeholder="Role"
|
||||||
|
class="mb-2"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.password"
|
||||||
|
:readonly="data.loading"
|
||||||
|
type="password"
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
class="mb-2"
|
||||||
|
placeholder="비밀번호를 입력해주세요."
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
:disabled="!data.form"
|
||||||
|
:loading="data.loading"
|
||||||
|
block
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
type="submit"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
SignUp
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<span>계정이 있으십니까?</span>
|
||||||
|
<router-link
|
||||||
|
to="/login"
|
||||||
|
class="ml-2 font-weight-medium"
|
||||||
|
style="color: #90caf9; text-decoration: none"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-sheet
|
||||||
|
class="position-absolute w-100 bg-transparent pa-4"
|
||||||
|
style="bottom: 0; left: 0"
|
||||||
|
>
|
||||||
|
<v-sheet class="bg-shades-transparent d-flex align-end"
|
||||||
|
>Copyright © 2025 Autoflow Web Console
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
<v-sheet
|
||||||
|
class="position-absolute w-100 bg-transparent pa-4"
|
||||||
|
style="bottom: 0; right: 0"
|
||||||
|
>
|
||||||
|
<v-sheet class="bg-shades-transparent d-flex justify-end">
|
||||||
|
<img :src="logo" style="width: 5%" />
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
</v-sheet>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<v-snackbar
|
||||||
|
v-model="data.snackbar"
|
||||||
|
timeout="2000"
|
||||||
|
location="button"
|
||||||
|
:color="data.snackbarColor"
|
||||||
|
>
|
||||||
|
{{ data.snackbarText }}
|
||||||
|
<template #actions>
|
||||||
|
<v-btn variant="text" @click="data.snackbar = false"> 닫기 </v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-box {
|
||||||
|
background-color: rgba(18, 18, 18, 0.4); /* 흰색 배경에 30% 불투명도 */
|
||||||
|
backdrop-filter: blur(10px); /* 배경 블러 효과 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(19, 18, 18, 0.5),
|
||||||
|
rgba(19, 18, 18, 0.3),
|
||||||
|
rgba(19, 18, 18, 0.3),
|
||||||
|
rgba(19, 18, 18, 0.5)
|
||||||
|
),
|
||||||
|
url("@/assets/4117551.jpg"); /* 배경 이미지 경로 */
|
||||||
|
background-size: cover; /* 이미지가 화면을 꽉 채우도록 설정 */
|
||||||
|
background-position: center; /* 이미지 중앙 정렬 */
|
||||||
|
background-repeat: no-repeat; /* 이미지 반복 방지 */
|
||||||
|
min-height: 100vh; /* 화면 전체 높이 설정 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,10 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import ListComponent from "@/components/templates/trainingscript/ListComponent.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ListComponent />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped lang="sass"></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import ListComponent from "@/components/templates/stepconfig/ListComponent.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ListComponent />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped lang="sass"></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import ListComponent from "@/components/templates/workflow/ListComponent.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ListComponent/>
|
<ListComponent />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped lang="sass"></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export const useAutoflowStore = defineStore("autoflowStore", () => {
|
||||||
|
// 초기값 복원
|
||||||
|
const storedId = localStorage.getItem("projectId");
|
||||||
|
const projectId = ref<number | null>(storedId ? Number(storedId) : null);
|
||||||
|
|
||||||
|
const projectName = ref<string>(localStorage.getItem("projectName") || "");
|
||||||
|
|
||||||
|
const setProjectId = (id: number) => {
|
||||||
|
projectId.value = id;
|
||||||
|
localStorage.setItem("projectId", String(id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearProjectId = () => {
|
||||||
|
projectId.value = null;
|
||||||
|
localStorage.removeItem("projectId");
|
||||||
|
};
|
||||||
|
|
||||||
|
const setProjectName = (name: string) => {
|
||||||
|
projectName.value = name;
|
||||||
|
localStorage.setItem("projectName", name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearProjectName = () => {
|
||||||
|
projectName.value = "";
|
||||||
|
localStorage.removeItem("projectName");
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
setProjectId,
|
||||||
|
clearProjectId,
|
||||||
|
setProjectName,
|
||||||
|
clearProjectName,
|
||||||
|
};
|
||||||
|
});
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { load } from "webfontloader";
|
||||||
|
|
||||||
|
export const commonStore = defineStore("common", () => {
|
||||||
|
const snackbarMsg = ref({ text: "", color: "", result: 200 });
|
||||||
|
|
||||||
|
const setSnackbarMsg = (data: {
|
||||||
|
result: number;
|
||||||
|
color: string;
|
||||||
|
text: string;
|
||||||
|
}) => {
|
||||||
|
snackbarMsg.value = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
snackbarMsg,
|
||||||
|
setSnackbarMsg,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loadingStore = defineStore("loading", () => {
|
||||||
|
const loading = ref(false);
|
||||||
|
const setLoading = (value: boolean) => {
|
||||||
|
loading.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
setLoading,
|
||||||
|
};
|
||||||
|
});
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
export const CommonFunctions = {
|
||||||
|
stringTruncate: (str: string, len: number) => {
|
||||||
|
return str.length > len ? str.slice(0, len) + "..." : str;
|
||||||
|
},
|
||||||
|
getFileExtension: (filename: string) => {
|
||||||
|
const index = filename.lastIndexOf(".");
|
||||||
|
return index !== -1 ? filename.substring(index + 1).toLowerCase() : null;
|
||||||
|
},
|
||||||
|
getDateFormatYmd: (date: string) => {
|
||||||
|
if (date != null && date != "") {
|
||||||
|
const inputDate = new Date(date);
|
||||||
|
const year = inputDate.getFullYear();
|
||||||
|
const month = (inputDate.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = inputDate.getDate().toString().padStart(2, "0");
|
||||||
|
const formattedDate = `${year}.${month}.${day}`;
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDateFormatHis: (date: string) => {
|
||||||
|
if (date != null && date != "") {
|
||||||
|
const timeMatch = date.match(/\b\d{2}:\d{2}:\d{2}\b/);
|
||||||
|
if (timeMatch) {
|
||||||
|
return timeMatch[0];
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getQuantumDateFormat: (dateString: string) => {
|
||||||
|
if (dateString != null && dateString != "" && dateString != "-") {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
// 날짜와 시간을 각각 포맷팅
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(date.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}.${month}.${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
dateString;
|
||||||
|
},
|
||||||
|
getQuantumDateFormatToCustom: (dateString: string) => {
|
||||||
|
if (dateString != null && dateString != "" && dateString != "-") {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
// toLocaleString을 사용해 형식 지정
|
||||||
|
return date.toLocaleString("en-US", {
|
||||||
|
month: "short", // Oct
|
||||||
|
day: "2-digit", // 07
|
||||||
|
year: "numeric", // 2024
|
||||||
|
hour: "2-digit", // 10
|
||||||
|
minute: "2-digit", // 37
|
||||||
|
hour12: true, // AM/PM 형식
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getTimeDifferenceInSeconds: (dateString1: string, dateString2: string) => {
|
||||||
|
const date1 = new Date(dateString1);
|
||||||
|
const date2 = new Date(dateString2);
|
||||||
|
|
||||||
|
// 두 날짜의 밀리초 차이 계산
|
||||||
|
const differenceInMs = Math.abs(date1.getTime() - date2.getTime());
|
||||||
|
|
||||||
|
// 밀리초 차이를 초 단위로 변환
|
||||||
|
const differenceInSeconds = Math.floor(differenceInMs / 1000);
|
||||||
|
return differenceInSeconds;
|
||||||
|
},
|
||||||
|
getJobChartData: (data: string[]) => {
|
||||||
|
const binaryMapping: Record<string, string> = {
|
||||||
|
"0x0": "00",
|
||||||
|
"0x1": "01",
|
||||||
|
"0x2": "10",
|
||||||
|
"0x3": "11",
|
||||||
|
};
|
||||||
|
|
||||||
|
const frequency: Record<string, number> = {};
|
||||||
|
data.forEach((d) => {
|
||||||
|
const binary = binaryMapping[d] || "00";
|
||||||
|
frequency[binary] = (frequency[binary] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return frequency;
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,420 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onBeforeUnmount, ref, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useAutoflowStore } from "@/stores/autoflowStore";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
UiProject,
|
||||||
|
ApiProject,
|
||||||
|
Permission,
|
||||||
|
} from "@/components/models/project/Project";
|
||||||
|
import { ProjectService } from "@/components/service/project/projectService";
|
||||||
|
import { UserManagerService } from "@/components/service/management/userManagerService";
|
||||||
|
import { storage } from "@/utils/storage.js";
|
||||||
|
|
||||||
|
/** ===== 상수 & 기본 권한 ===== */
|
||||||
|
const DEFAULT_PERMISSIONS: Permission[] = [
|
||||||
|
"CREATE",
|
||||||
|
"READ",
|
||||||
|
"UPDATE",
|
||||||
|
"DELETE",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** ===== 라우터 & 스토어 ===== */
|
||||||
|
const router = useRouter();
|
||||||
|
const autoflowStore = useAutoflowStore();
|
||||||
|
|
||||||
|
/** ===== 상태 ===== */
|
||||||
|
const dialog = ref(false);
|
||||||
|
const contextMenu = ref(false);
|
||||||
|
const menuX = ref(0);
|
||||||
|
const menuY = ref(0);
|
||||||
|
const selectedIndex = ref<number | null>(null);
|
||||||
|
|
||||||
|
const projects = ref<UiProject[]>([]);
|
||||||
|
type UserOption = { id: number | string; username: string };
|
||||||
|
const userOptions = ref<UserOption[]>([]);
|
||||||
|
const modalMode = ref<"create" | "edit">("create");
|
||||||
|
const editingProjectId = ref<number | null>(null);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
prjCd: "",
|
||||||
|
prjNm: "",
|
||||||
|
prjDesc: "",
|
||||||
|
selectedUsers: [] as string[],
|
||||||
|
});
|
||||||
|
/** ===== 서버 응답 타입 ===== */
|
||||||
|
const roles = ref<string[]>([]);
|
||||||
|
const refreshRoles = () => {
|
||||||
|
const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null;
|
||||||
|
const r = auth?.userInfo?.roles ?? auth?.roles ?? [];
|
||||||
|
roles.value = Array.isArray(r) ? r : [];
|
||||||
|
};
|
||||||
|
const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN"));
|
||||||
|
|
||||||
|
/** ===== 서버 응답 타입 ===== */
|
||||||
|
interface ProjectSearchResponseItem {
|
||||||
|
id: number;
|
||||||
|
prjNm: string;
|
||||||
|
prjDesc: string;
|
||||||
|
prjStartDt?: string;
|
||||||
|
regUserId?: string; // 화면의 "생성자"에 그대로 표시(콤마 구분 username들)
|
||||||
|
}
|
||||||
|
interface UserResponseItem {
|
||||||
|
id: number | string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ===== 유틸 ===== */
|
||||||
|
const buildApiProjectPayload = (): ApiProject => {
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
const nowIso = new Date().toISOString();
|
||||||
|
const namesCsv = form.value.selectedUsers.join(",");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: modalMode.value === "edit" ? editingProjectId.value! : null,
|
||||||
|
prjCd: form.value.prjCd,
|
||||||
|
prjNm: form.value.prjNm,
|
||||||
|
prjDesc: form.value.prjDesc,
|
||||||
|
prjStartDt: today,
|
||||||
|
prjEndDt: today,
|
||||||
|
delYn: "N",
|
||||||
|
regDate: nowIso,
|
||||||
|
regUserId: namesCsv,
|
||||||
|
regUserNm: namesCsv,
|
||||||
|
modDate: nowIso,
|
||||||
|
modUserId: namesCsv,
|
||||||
|
modUserNm: namesCsv,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.value.prjCd = `PRJ${Date.now()}`;
|
||||||
|
form.value.prjNm = "";
|
||||||
|
form.value.prjDesc = "";
|
||||||
|
form.value.selectedUsers = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadProjects = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await ProjectService.search();
|
||||||
|
const rawList = data as ProjectSearchResponseItem[];
|
||||||
|
projects.value = rawList.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
title: p.prjNm,
|
||||||
|
creator: p.regUserId ?? "",
|
||||||
|
date: p.prjStartDt ?? "",
|
||||||
|
description: p.prjDesc,
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("프로젝트 조회 실패:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await UserManagerService.getAll();
|
||||||
|
const raw = data as UserResponseItem[];
|
||||||
|
userOptions.value = raw.map((u) => ({ id: u.id, username: u.username }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("사용자 조회 실패:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openContextMenu = (event: MouseEvent, index: number) => {
|
||||||
|
event.preventDefault();
|
||||||
|
selectedIndex.value = index;
|
||||||
|
menuX.value = event.pageX;
|
||||||
|
menuY.value = event.pageY;
|
||||||
|
contextMenu.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
dialog.value = false;
|
||||||
|
contextMenu.value = false;
|
||||||
|
selectedIndex.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectProject = (index: number) => {
|
||||||
|
const selected = projects.value[index];
|
||||||
|
autoflowStore.setProjectId(selected.id);
|
||||||
|
autoflowStore.setProjectName(selected.title);
|
||||||
|
router.push("/home");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** ===== 프로젝트 저장 & 권한 부여 ===== */
|
||||||
|
const grantDefaultPermissions = async (
|
||||||
|
projectId: number,
|
||||||
|
usernames: string[],
|
||||||
|
) => {
|
||||||
|
if (!usernames?.length) return;
|
||||||
|
|
||||||
|
const nameSet = new Set(usernames);
|
||||||
|
const numericIds = userOptions.value
|
||||||
|
.filter((u) => nameSet.has(u.username))
|
||||||
|
.map((u) => Number(u.id))
|
||||||
|
.filter((n) => Number.isFinite(n));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
numericIds.map((uid) =>
|
||||||
|
ProjectService.projectAuthority(projectId, {
|
||||||
|
projectId,
|
||||||
|
userId: uid, // number로 보장
|
||||||
|
permissions: DEFAULT_PERMISSIONS,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveProject = async () => {
|
||||||
|
if (!isAdmin.value) {
|
||||||
|
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = buildApiProjectPayload();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let projectId: number;
|
||||||
|
|
||||||
|
if (modalMode.value === "create") {
|
||||||
|
const createRes = await ProjectService.add(payload);
|
||||||
|
projectId = createRes.data.id;
|
||||||
|
} else {
|
||||||
|
await ProjectService.update(editingProjectId.value!, payload);
|
||||||
|
projectId = editingProjectId.value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
await grantDefaultPermissions(projectId, form.value.selectedUsers);
|
||||||
|
await loadProjects();
|
||||||
|
closeDialog();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`${modalMode.value} 실패:`, error?.response?.data || error);
|
||||||
|
alert(error?.response?.data?.message || "저장 실패");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProject = async () => {
|
||||||
|
try {
|
||||||
|
if (selectedIndex.value === null) return;
|
||||||
|
const target = projects.value[selectedIndex.value];
|
||||||
|
|
||||||
|
await ProjectService.delete(target.id);
|
||||||
|
await loadProjects();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("삭제 실패:", e?.response?.status, e?.response?.data || e);
|
||||||
|
alert("삭제 실패: " + (e?.response?.data?.message || e.message || ""));
|
||||||
|
} finally {
|
||||||
|
contextMenu.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프로젝트 권한있을 때
|
||||||
|
// const deleteProject = async (): Promise<void> => {
|
||||||
|
// try {
|
||||||
|
// if (selectedIndex.value === null) return;
|
||||||
|
// const target = projects.value[selectedIndex.value];
|
||||||
|
// const projectId = target.id;
|
||||||
|
|
||||||
|
// // 1) 프로젝트에 연결된 username들 뽑기
|
||||||
|
// const usernames = (target.creator || "")
|
||||||
|
// .split(",")
|
||||||
|
// .map((s) => s.trim())
|
||||||
|
// .filter(Boolean);
|
||||||
|
|
||||||
|
// // 2) username -> userId 매핑
|
||||||
|
// if (usernames.length) {
|
||||||
|
// const ids = userOptions.value
|
||||||
|
// .filter((u) => usernames.includes(u.username))
|
||||||
|
// .map((u) => u.id);
|
||||||
|
|
||||||
|
// // 3) 각 사용자 권한/매핑 제거
|
||||||
|
// await Promise.all(
|
||||||
|
// ids.map((uid) => ProjectService.deleteProjectAuthority(projectId, uid)),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 4) 마지막에 프로젝트 삭제
|
||||||
|
// await ProjectService.delete(projectId);
|
||||||
|
|
||||||
|
// await loadProjects();
|
||||||
|
// } catch (e: any) {
|
||||||
|
// console.error("삭제 실패:", e?.response?.status, e?.response?.data || e);
|
||||||
|
// alert("삭제 실패: " + (e?.response?.data?.message || e.message || ""));
|
||||||
|
// } finally {
|
||||||
|
// contextMenu.value = false;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
const onAddProject = () => {
|
||||||
|
if (!isAdmin.value) {
|
||||||
|
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalMode.value = "create";
|
||||||
|
editingProjectId.value = null;
|
||||||
|
resetForm();
|
||||||
|
dialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const modifyProject = () => {
|
||||||
|
contextMenu.value = false;
|
||||||
|
if (selectedIndex.value === null) return;
|
||||||
|
|
||||||
|
const selected = projects.value[selectedIndex.value];
|
||||||
|
modalMode.value = "edit";
|
||||||
|
editingProjectId.value = selected.id;
|
||||||
|
|
||||||
|
form.value.prjCd = selected.title;
|
||||||
|
form.value.prjNm = selected.title;
|
||||||
|
form.value.prjDesc = selected.description;
|
||||||
|
form.value.selectedUsers =
|
||||||
|
selected.creator
|
||||||
|
?.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean) ?? [];
|
||||||
|
|
||||||
|
dialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** ===== 라이프사이클 ===== */
|
||||||
|
const onStorage = (e: StorageEvent) => {
|
||||||
|
if (!e.key || /auth|vpp-Auth/i.test(e.key)) refreshRoles();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
refreshRoles();
|
||||||
|
await Promise.all([loadProjects(), loadUsers()]);
|
||||||
|
window.addEventListener("storage", onStorage);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("storage", onStorage);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container class="mt-12" style="max-width: 1600px">
|
||||||
|
<v-row class="mb-6" align="center" justify="space-between">
|
||||||
|
<v-col cols="auto">
|
||||||
|
<h2 class="font-weight-bold text-h5">Project Selection</h2>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="auto">
|
||||||
|
<!-- ADMIN만 노출 -->
|
||||||
|
<v-btn
|
||||||
|
v-show="isAdmin"
|
||||||
|
color="secondary"
|
||||||
|
variant="flat"
|
||||||
|
class="text-white font-weight-bold"
|
||||||
|
@click="onAddProject"
|
||||||
|
>
|
||||||
|
<v-icon left icon="mdi-plus" size="20" />
|
||||||
|
Create Project
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row dense>
|
||||||
|
<v-col
|
||||||
|
v-for="(project, index) in projects"
|
||||||
|
:key="index"
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="6"
|
||||||
|
lg="6"
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
class="pa-4 flex-grow-1 d-flex flex-column"
|
||||||
|
color="primary"
|
||||||
|
variant="elevated"
|
||||||
|
elevation="6"
|
||||||
|
rounded="lg"
|
||||||
|
@click="selectProject(index)"
|
||||||
|
@contextmenu.prevent="(e) => openContextMenu(e, index)"
|
||||||
|
>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon color="#6EC1E4" icon="mdi-file" start size="18" />
|
||||||
|
<h4>{{ project.title }}</h4>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-subtitle
|
||||||
|
class="text-white text-caption d-flex justify-space-between"
|
||||||
|
>
|
||||||
|
<span>Select Users: {{ project.creator }}</span>
|
||||||
|
<span>등록일: {{ project.date }}</span>
|
||||||
|
</v-card-subtitle>
|
||||||
|
|
||||||
|
<v-card-text
|
||||||
|
class="text-white mt-3 text-body-2 flex-grow-1"
|
||||||
|
style="white-space: normal"
|
||||||
|
>
|
||||||
|
{{ project.description }}
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-menu
|
||||||
|
v-model="contextMenu"
|
||||||
|
absolute
|
||||||
|
:style="{ top: menuY + 'px', left: menuX + 'px' }"
|
||||||
|
max-width="180"
|
||||||
|
width="130"
|
||||||
|
>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item @click="modifyProject">
|
||||||
|
<v-list-item-icon
|
||||||
|
><v-icon>mdi-square-edit-outline</v-icon></v-list-item-icon
|
||||||
|
>
|
||||||
|
<v-list-item-title>Modify</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="deleteProject">
|
||||||
|
<v-list-item-icon><v-icon>mdi-delete</v-icon></v-list-item-icon>
|
||||||
|
<v-list-item-title>Delete</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<v-dialog v-model="dialog" max-width="500">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">
|
||||||
|
{{ modalMode === "create" ? "Create Project" : "Modify Project" }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-form>
|
||||||
|
<v-text-field label="Project Name" v-model="form.prjNm" required />
|
||||||
|
<v-textarea
|
||||||
|
label="Description"
|
||||||
|
v-model="form.prjDesc"
|
||||||
|
rows="3"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
label="Select Users"
|
||||||
|
v-model="form.selectedUsers"
|
||||||
|
:items="userOptions"
|
||||||
|
item-title="username"
|
||||||
|
item-value="username"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
closable-chips
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn text @click="closeDialog">Cancel</v-btn>
|
||||||
|
<v-btn color="primary" @click="saveProject">
|
||||||
|
{{ modalMode === "create" ? "Create" : "Save" }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
Loading…
Reference in new issue