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'
|
||||
|
||||
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>
|
||||
|
||||
import ListComponent from "@/components/templates/Datasets/ListComponent.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListComponent />
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
||||
<style scoped lang="sass"></style>
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
<script setup>
|
||||
|
||||
import ListComponent from "@/components/templates/deployment/ListComponent.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListComponent />
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
||||
<style scoped lang="sass"></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>
|
||||
|
||||
import ListComponent from "@/components/templates/run/experiment/ListComponent.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListComponent />
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
||||
<style scoped lang="sass"></style>
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
<script setup>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListComponent from "@/components/home/ListComponent.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListComponent />
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
||||
<style scoped></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>
|
||||
|
||||
import ListComponent from "@/components/templates/trainingscript/ListComponent.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListComponent />
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
||||
<style scoped lang="sass"></style>
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
<script setup>
|
||||
|
||||
import ListComponent from "@/components/templates/stepconfig/ListComponent.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListComponent />
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
||||
<style scoped lang="sass"></style>
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
<script setup>
|
||||
|
||||
import ListComponent from "@/components/templates/workflow/ListComponent.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListComponent />
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
||||
<style scoped lang="sass"></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