This is an automated email from the ASF dual-hosted git repository.
benjobs pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/incubator-streampark.git
The following commit(s) were added to refs/heads/dev by this push:
new 1c6d8fd2b [Improve]: add spark application create page (#3992)
1c6d8fd2b is described below
commit 1c6d8fd2b534b681702e6da429a0a267a407aa43
Author: Kriszu <[email protected]>
AuthorDate: Sat Aug 24 20:10:58 2024 +0800
[Improve]: add spark application create page (#3992)
---
.../streampark-console-webapp/src/api/spark/app.ts | 182 +++++++++
.../src/api/spark/app.type.ts | 181 +++++++++
.../src/api/spark/conf.ts | 33 ++
.../streampark-console-webapp/src/api/spark/sql.ts | 30 ++
.../components/SimpleMenu/src/SimpleSubMenu.vue | 10 +-
.../src/enums/sparkEnum.ts | 33 ++
.../flink/app/hooks/useCreateAndEditSchema.ts | 1 +
.../views/spark/app/components/AppDashboard.vue | 101 +++++
.../views/spark/app/components/AppStartModal.vue | 244 ++++++++++++
.../src/views/spark/app/components/Mergely.vue | 202 ++++++++++
.../src/views/spark/app/components/ProgramArgs.vue | 128 +++++++
.../src/views/spark/app/components/SparkSql.vue | 301 +++++++++++++++
.../views/spark/app/components/StatisticCard.vue | 54 +++
.../views/spark/app/components/VariableReview.vue | 65 ++++
.../src/views/spark/app/create.vue | 201 ++++++++++
.../src/views/spark/app/data/index.ts | 81 ++++
.../src/views/spark/app/hooks/useAppFormSchema.tsx | 264 +++++++++++++
.../src/views/spark/app/hooks/useFlinkRender.tsx | 326 ++++++++++++++++
.../src/views/spark/app/hooks/useSparkAction.tsx | 192 ++++++++++
.../src/views/spark/app/hooks/useSparkColumns.ts | 101 +++++
.../views/spark/app/hooks/useSparkTableAction.ts | 234 ++++++++++++
.../src/views/spark/app/index.vue | 317 ++++++++++++++++
.../src/views/spark/app/sqlFormatter.js | 408 +++++++++++++++++++++
.../src/views/spark/app/styles/View.less | 176 +++++++++
24 files changed, 3860 insertions(+), 5 deletions(-)
diff --git a/streampark-console/streampark-console-webapp/src/api/spark/app.ts
b/streampark-console/streampark-console-webapp/src/api/spark/app.ts
new file mode 100644
index 000000000..76b57a746
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/api/spark/app.ts
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { AppListResponse, SparkApplication, DashboardResponse } from
'./app.type';
+import type { Result } from '/#/axios';
+import { ContentTypeEnum } from '/@/enums/httpEnum';
+import type { AppExistsStateEnum } from '/@/enums/sparkEnum';
+import { defHttp } from '/@/utils/http/axios';
+
+const apiPrefix = `/spark/app`;
+
+/**
+ * Get application information by id
+ * @param params get parameters
+ */
+export function fetchGetSparkApp(data: { id: string }) {
+ return defHttp.post<SparkApplication>({ url: `${apiPrefix}/get`, data });
+}
+/**
+ * create spark application information
+ * @param params get parameters
+ */
+export function fetchCreateSparkApp(data: SparkApplication) {
+ return defHttp.post<boolean>({ url: `${apiPrefix}/create`, data });
+}
+/**
+ * copy spark application information
+ * @param params get parameters
+ */
+export function fetchCopySparkApp(data: SparkApplication) {
+ return defHttp.post({ url: `${apiPrefix}/copy`, data });
+}
+/**
+ * update spark application information
+ * @param params get parameters
+ */
+export function fetchUpdateSparkApp(data: SparkApplication) {
+ return defHttp.post<boolean>({ url: `${apiPrefix}/update`, data });
+}
+
+/**
+ * Dashboard data
+ * @returns Promise<DashboardResponse>
+ */
+export function fetchSparkDashboard() {
+ return defHttp.post<DashboardResponse>({
+ url: `${apiPrefix}/dashboard`,
+ });
+}
+
+/**
+ * Get app list data
+ */
+export function fetchSparkAppRecord(data: Recordable) {
+ return defHttp.post<AppListResponse>({ url: `${apiPrefix}/list`, data });
+}
+
+/**
+ * mapping
+ * @param params {id:string,appId:string,jobId:string}
+ */
+export function fetchSparkMapping(data: SparkApplication) {
+ return defHttp.post<boolean>({ url: `${apiPrefix}/mapping`, data });
+}
+
+export function fetchSparkAppStart(data: SparkApplication) {
+ return defHttp.post<Result<boolean>>(
+ { url: `${apiPrefix}/start`, data },
+ { isTransformResponse: false },
+ );
+}
+
+export function fetchCheckSparkAppStart(data: SparkApplication) {
+ return defHttp.post<AppExistsStateEnum>({ url: `${apiPrefix}/check/start`,
data });
+}
+
+/**
+ * Cancel
+ * @param {CancelParam} data
+ */
+export function fetchSparkAppCancel(data: SparkApplication) {
+ return defHttp.post({ url: `${apiPrefix}/cancel`, data });
+}
+
+/**
+ * clean
+ * @param {CancelParam} data
+ */
+export function fetchSparkAppClean(data: SparkApplication) {
+ return defHttp.post({ url: `${apiPrefix}/clean`, data });
+}
+/**
+ * forcedStop
+ */
+export function fetchSparkAppForcedStop(data: SparkApplication) {
+ return defHttp.post({ url: `${apiPrefix}/forcedStop`, data });
+}
+
+/**
+ * get yarn address
+ */
+export function fetchSparkYarn() {
+ return defHttp.post<string>({ url: `${apiPrefix}/yarn` });
+}
+
+/**
+ * check spark name
+ */
+export function fetchCheckSparkName(data: { id?: string; jobName: string }) {
+ return defHttp.post<AppExistsStateEnum>({ url: `${apiPrefix}/check/name`,
data });
+}
+
+/**
+ * read configuration file
+ */
+export function fetchSparkAppConf(params?: { config: any }) {
+ return defHttp.post<string>({
+ url: `${apiPrefix}/read_conf`,
+ params,
+ });
+}
+
+/**
+ * main
+ */
+export function fetchSparkMain(data: SparkApplication) {
+ return defHttp.post<string>({
+ url: `${apiPrefix}/main`,
+ data,
+ });
+}
+
+export function fetchSparkBackUps(data: SparkApplication) {
+ return defHttp.post({ url: `${apiPrefix}/backups`, data });
+}
+
+export function fetchSparkOptionLog(data: SparkApplication) {
+ return defHttp.post({ url: `${apiPrefix}/opt_log`, data });
+}
+
+export function fetchSparkDeleteOptLog(id: string) {
+ return defHttp.post({ url: `${apiPrefix}/delete/opt_log`, data: { id } });
+}
+
+/**
+ * remove the app
+ */
+export function fetchSparkAppRemove(id: string) {
+ return defHttp.post({ url: `${apiPrefix}/delete`, data: { id } });
+}
+
+export function fetchSparkRemoveBackup(id: string) {
+ return defHttp.post({ url: `${apiPrefix}/delete/bak`, data: { id } });
+}
+
+/**
+ * upload
+ * @param params
+ */
+export function fetchSparkUpload(params: any) {
+ return defHttp.post<string>({
+ url: `${apiPrefix}/upload`,
+ params,
+ headers: {
+ 'Content-Type': ContentTypeEnum.FORM_DATA,
+ },
+ timeout: 1000 * 60 * 10, // Uploading files timed out for 10 minutes
+ });
+}
diff --git
a/streampark-console/streampark-console-webapp/src/api/spark/app.type.ts
b/streampark-console/streampark-console-webapp/src/api/spark/app.type.ts
new file mode 100644
index 000000000..1bdd20e23
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/api/spark/app.type.ts
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// dashboard
+export interface DashboardResponse {
+ totalTM: number;
+ task: Task;
+ availableSlot: number;
+ totalSlot: number;
+ runningJob: number;
+ tmMemory: number;
+ jmMemory: number;
+}
+
+interface Task {
+ total: number;
+ created: number;
+ scheduled: number;
+ deploying: number;
+ running: number;
+ finished: number;
+ canceling: number;
+ canceled: number;
+ failed: number;
+ reconciling: number;
+}
+// The list of data
+export interface AppListResponse {
+ total: string;
+ records: SparkApplication[];
+}
+export interface SparkApplication {
+ id?: string;
+ teamId?: string;
+ jobType?: number;
+ appType?: number;
+ versionId?: number;
+ appName?: string;
+ executionMode?: number;
+ resourceFrom?: number;
+ module?: any;
+ mainClass?: string;
+ jar?: string;
+ jarCheckSum?: string;
+ appProperties?: string;
+ appArgs?: string;
+ appId?: string;
+ yarnQueue?: any;
+
+ projectId?: any;
+ tags?: any;
+ userId?: string;
+ jobName?: string;
+ jobId?: string;
+ clusterId?: string;
+ flinkImage?: string;
+ k8sNamespace?: string;
+ state?: number;
+ release?: number;
+ build?: boolean;
+ restartSize?: number;
+ restartCount?: number;
+ optionState?: number;
+ alertId?: any;
+ args?: string;
+ options?: string;
+ hotParams?: string;
+ resolveOrder?: number;
+ dynamicProperties?: string;
+ tracking?: number;
+
+ startTime?: string;
+ endTime?: string;
+ duration?: string;
+ cpMaxFailureInterval?: any;
+ cpFailureRateInterval?: any;
+ cpFailureAction?: any;
+ totalTM?: any;
+ totalSlot?: any;
+ availableSlot?: any;
+ jmMemory?: number;
+ tmMemory?: number;
+ totalTask?: number;
+ flinkClusterId?: any;
+ description?: string;
+ createTime?: string;
+ optionTime?: string;
+ modifyTime?: string;
+ k8sRestExposedType?: any;
+ k8sPodTemplate?: any;
+ k8sJmPodTemplate?: any;
+ k8sTmPodTemplate?: any;
+ ingressTemplate?: any;
+ defaultModeIngress?: any;
+ k8sHadoopIntegration?: boolean;
+ overview?: any;
+ teamResource?: any;
+ dependency?: any;
+ sqlId?: any;
+ flinkSql?: any;
+ stateArray?: any;
+ jobTypeArray?: any;
+ backUp?: boolean;
+ restart?: boolean;
+ userName?: string;
+ nickName?: string;
+ config?: any;
+ configId?: any;
+ flinkVersion?: string;
+ confPath?: any;
+ format?: any;
+ savepointPath?: any;
+ restoreOrTriggerSavepoint?: boolean;
+ drain?: boolean;
+ nativeFormat?: boolean;
+ allowNonRestored?: boolean;
+ socketId?: any;
+ projectName?: any;
+ createTimeFrom?: any;
+ createTimeTo?: any;
+ backUpDescription?: any;
+ teamIdList?: any;
+ teamName?: string;
+ flinkRestUrl?: any;
+ buildStatus?: number;
+ appControl?: AppControl;
+ fsOperator?: any;
+ workspace?: any;
+ k8sPodTemplates?: {
+ empty?: boolean;
+ };
+ streamParkJob?: boolean;
+ hadoopUser?: string;
+}
+
+interface AppControl {
+ allowStart: boolean;
+ allowStop: boolean;
+ allowBuild: boolean;
+}
+
+// create Params
+export interface CreateParams {
+ jobType: number;
+ executionMode: number;
+ versionId: string;
+ flinkSql: string;
+ appType: number;
+ config?: any;
+ format?: any;
+ jobName: string;
+ tags: string;
+ args?: any;
+ dependency: string;
+ options: string;
+ cpMaxFailureInterval: number;
+ cpFailureRateInterval: number;
+ cpFailureAction: number;
+ dynamicProperties: string;
+ resolveOrder: number;
+ restartSize: number;
+ alertId: string;
+ description: string;
+ k8sNamespace?: any;
+ clusterId: string;
+ flinkClusterId: string;
+ flinkImage?: any;
+}
diff --git a/streampark-console/streampark-console-webapp/src/api/spark/conf.ts
b/streampark-console/streampark-console-webapp/src/api/spark/conf.ts
new file mode 100644
index 000000000..e4e0ccb59
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/api/spark/conf.ts
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { defHttp } from '/@/utils/http/axios';
+
+const apiPrefix = '/flink/conf';
+
+export function fetchGetSparkConf(data: { id: string }) {
+ return defHttp.post({ url: `${apiPrefix}/get`, data });
+}
+export function handleSparkConfTemplate() {
+ return defHttp.post<string>({
+ url: `${apiPrefix}/template`,
+ });
+}
+export function fetchSysHadoopConf() {
+ return defHttp.post({
+ url: `${apiPrefix}/sysHadoopConf`,
+ });
+}
diff --git a/streampark-console/streampark-console-webapp/src/api/spark/sql.ts
b/streampark-console/streampark-console-webapp/src/api/spark/sql.ts
new file mode 100644
index 000000000..8d3ada344
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/api/spark/sql.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { defHttp } from '/@/utils/http/axios';
+const apiPrefix = '/flink/conf';
+
+export function fetchSparkSqlVerify(data: Recordable) {
+ return defHttp.post({ url: `${apiPrefix}/verify`, data }, {
isTransformResponse: false });
+}
+
+export function fetchSparkSql(data: Recordable) {
+ return defHttp.post({
+ url: `${apiPrefix}/get`,
+ data,
+ });
+}
diff --git
a/streampark-console/streampark-console-webapp/src/components/SimpleMenu/src/SimpleSubMenu.vue
b/streampark-console/streampark-console-webapp/src/components/SimpleMenu/src/SimpleSubMenu.vue
index e0af2a31e..cfdd795a6 100644
---
a/streampark-console/streampark-console-webapp/src/components/SimpleMenu/src/SimpleSubMenu.vue
+++
b/streampark-console/streampark-console-webapp/src/components/SimpleMenu/src/SimpleSubMenu.vue
@@ -23,11 +23,11 @@
>
<template #title>
<span class="menu-down-svg">
- <SvgIcon v-if="item.path === '/system'" name="management" size="25" />
- <SvgIcon v-if="item.path === '/flink'" name="flink3" size="25" />
- <SvgIcon v-if="item.path === '/spark'" name="spark" size="25"/>
- <SvgIcon v-if="item.path === '/setting'" name="settings" size="25" />
- <SvgIcon v-if="item.path === '/resource'" name="resource" size="25" />
+ <SvgIcon v-if="item.path === '/system'" name="management" size="20" />
+ <SvgIcon v-if="item.path === '/flink'" name="flink3" size="20" />
+ <SvgIcon v-if="item.path === '/spark'" name="spark" size="20" />
+ <SvgIcon v-if="item.path === '/setting'" name="settings" size="20" />
+ <SvgIcon v-if="item.path === '/resource'" name="resource" size="20" />
</span>
<div v-if="collapsedShowTitle && getIsCollapseParent" class="mt-2
collapse-title">
{{ getI18nName }}
diff --git
a/streampark-console/streampark-console-webapp/src/enums/sparkEnum.ts
b/streampark-console/streampark-console-webapp/src/enums/sparkEnum.ts
index 96e1d180a..a1d4fbd20 100644
--- a/streampark-console/streampark-console-webapp/src/enums/sparkEnum.ts
+++ b/streampark-console/streampark-console-webapp/src/enums/sparkEnum.ts
@@ -5,3 +5,36 @@ export enum SparkEnvCheckEnum {
SPARK_DIST_NOT_FOUND = 2,
SPARK_DIST_REPEATED = 3,
}
+
+export enum JobTypeEnum {
+ JAR = 1,
+ SQL = 2,
+ PYSPARK = 3,
+}
+
+/* ExecutionMode */
+export enum ExecModeEnum {
+ /** remote (standalone) */
+ REMOTE = 1,
+ /** yarn per-job (deprecated, please use yarn-application mode) */
+ YARN_CLUSTER = 2,
+ /** yarn session */
+ YARN_CLIENT = 3,
+}
+
+export enum AppExistsStateEnum {
+ /** no exists */
+ NO,
+
+ /** exists in database */
+ IN_DB,
+
+ /** exists in yarn */
+ IN_YARN,
+
+ /** exists in remote kubernetes cluster. */
+ IN_KUBERNETES,
+
+ /** job name invalid because of special utf-8 character */
+ INVALID,
+}
diff --git
a/streampark-console/streampark-console-webapp/src/views/flink/app/hooks/useCreateAndEditSchema.ts
b/streampark-console/streampark-console-webapp/src/views/flink/app/hooks/useCreateAndEditSchema.ts
index d3263d2a1..de0fa14cd 100644
---
a/streampark-console/streampark-console-webapp/src/views/flink/app/hooks/useCreateAndEditSchema.ts
+++
b/streampark-console/streampark-console-webapp/src/views/flink/app/hooks/useCreateAndEditSchema.ts
@@ -534,6 +534,7 @@ export const useCreateAndEditSchema = (
} else if (model.appType == AppTypeEnum.STREAMPARK_SPARK) {
return getAlertSvgIcon('spark', 'StreamPark Spark');
}
+ return '';
},
},
];
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppDashboard.vue
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppDashboard.vue
new file mode 100644
index 000000000..bd97f7cb3
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppDashboard.vue
@@ -0,0 +1,101 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<script lang="ts" setup>
+ import { onMounted, reactive, ref } from 'vue';
+ import StatisticCard from './StatisticCard.vue';
+ import { Row, Col } from 'ant-design-vue';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { fetchSparkDashboard } from '/@/api/spark/app';
+ const dashBigScreenMap = reactive<Recordable>({});
+ const dashboardLoading = ref(true);
+ const { t } = useI18n();
+
+ // Get Dashboard Metrics Data
+ async function handleDashboard(showLoading: boolean) {
+ try {
+ dashboardLoading.value = showLoading;
+ const res = await fetchSparkDashboard();
+ if (res) {
+ Object.assign(dashBigScreenMap, {
+ runningJob: {
+ staticstics: { title: t('flink.app.dashboard.runningJobs'), value:
res.runningJob },
+ footer: [
+ { title: t('flink.app.dashboard.totalTask'), value:
res?.task?.total || 0 },
+ { title: t('flink.app.dashboard.runningTask'), value:
res?.task?.running || 0 },
+ ],
+ },
+ availiableTask: {
+ staticstics: {
+ title: t('flink.app.dashboard.availableTaskSlots'),
+ value: res.availableSlot,
+ },
+ footer: [
+ { title: t('flink.app.dashboard.taskSlots'), value:
res.totalSlot },
+ { title: t('flink.app.dashboard.taskManagers'), value:
res.totalTM },
+ ],
+ },
+ jobManager: {
+ staticstics: { title: t('flink.app.dashboard.jobManagerMemory'),
value: res.jmMemory },
+ footer: [
+ {
+ title: t('flink.app.dashboard.totalJobManagerMemory'),
+ value: `${res.jmMemory} MB`,
+ },
+ ],
+ },
+ taskManager: {
+ staticstics: { title: t('flink.app.dashboard.taskManagerMemory'),
value: res.tmMemory },
+ footer: [
+ {
+ title: t('flink.app.dashboard.totalTaskManagerMemory'),
+ value: `${res.tmMemory} MB`,
+ },
+ ],
+ },
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ dashboardLoading.value = false;
+ }
+ }
+
+ onMounted(() => {
+ handleDashboard(true);
+ });
+
+ defineExpose({ handleDashboard });
+</script>
+<template>
+ <Row :gutter="24" class="dashboard">
+ <Col
+ class="gutter-row mt-10px"
+ :md="6"
+ :xs="24"
+ v-for="(value, key) in dashBigScreenMap"
+ :key="key"
+ >
+ <StatisticCard
+ :statisticProps="value.staticstics"
+ :footerList="value.footer"
+ :loading="dashboardLoading"
+ />
+ </Col>
+ </Row>
+</template>
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppStartModal.vue
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppStartModal.vue
new file mode 100644
index 000000000..3fd5976ef
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppStartModal.vue
@@ -0,0 +1,244 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<script setup lang="ts" name="StartApplicationModal">
+ import { reactive } from 'vue';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { exceptionPropWidth } from '/@/utils';
+ import { h, ref } from 'vue';
+ import { Select, Input, Tag } from 'ant-design-vue';
+ import { BasicForm, useForm } from '/@/components/Form';
+ import { SvgIcon, Icon } from '/@/components/Icon';
+ import { BasicModal, useModalInner } from '/@/components/Modal';
+ import { useMessage } from '/@/hooks/web/useMessage';
+ import { useRouter } from 'vue-router';
+ import {
+ fetchCheckSparkAppStart,
+ fetchSparkAppForcedStop,
+ fetchSparkAppStart,
+ } from '/@/api/spark/app';
+ import { AppExistsStateEnum } from '/@/enums/sparkEnum';
+ defineOptions({
+ name: 'StartApplicationModal',
+ });
+ const SelectOption = Select.Option;
+
+ const { t } = useI18n();
+ const { Swal } = useMessage();
+ const router = useRouter();
+ const selectInput = ref<boolean>(false);
+ const selectValue = ref<string | null>(null);
+
+ const emits = defineEmits(['register', 'updateOption']);
+ const receiveData = reactive<Recordable>({});
+
+ const [registerModal, { closeModal }] = useModalInner((data) => {
+ if (data) {
+ Object.assign(receiveData, data);
+ resetFields();
+ setFieldsValue({
+ savepointPath: receiveData.selected?.path,
+ });
+ }
+ });
+
+ function handleSavePointTip(list) {
+ if (list != null && list.length > 0) {
+ return t('flink.app.view.savepointSwitch');
+ }
+ return t('flink.app.view.savepointInput');
+ }
+
+ const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
+ name: 'startApplicationModal',
+ labelWidth: 120,
+ schemas: [
+ {
+ field: 'restoreSavepoint',
+ label: t('flink.app.view.fromSavepoint'),
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: 'ON',
+ unCheckedChildren: 'OFF',
+ },
+ defaultValue: true,
+ afterItem: () => h('span', { class: 'conf-switch' },
t('flink.app.view.savepointTip')),
+ },
+ {
+ field: 'savepointPath',
+ label: 'Savepoint',
+ component:
+ receiveData.historySavePoint && receiveData.historySavePoint.length
> 0
+ ? 'Select'
+ : 'Input',
+ afterItem: () =>
+ h('span', { class: 'conf-switch' },
handleSavePointTip(receiveData.historySavePoint)),
+ slot: 'savepoint',
+ ifShow: ({ values }) => values.restoreSavepoint,
+ required: true,
+ },
+ {
+ field: 'allowNonRestoredState',
+ label: t('flink.app.view.ignoreRestored'),
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: 'ON',
+ unCheckedChildren: 'OFF',
+ },
+ afterItem: () => h('span', { class: 'conf-switch' },
t('flink.app.view.ignoreRestoredTip')),
+ defaultValue: false,
+ ifShow: ({ values }) => values.restoreSavepoint,
+ },
+ ],
+ colon: true,
+ showActionButtonGroup: false,
+ labelCol: { lg: { span: 7, offset: 0 }, sm: { span: 7, offset: 0 } },
+ wrapperCol: { lg: { span: 16, offset: 0 }, sm: { span: 4, offset: 0 } },
+ baseColProps: { span: 24 },
+ });
+
+ async function handleSubmit() {
+ // when then app is building, show forced starting modal
+ const resp = await fetchCheckSparkAppStart({
+ id: receiveData.application.id,
+ });
+ if (+resp === AppExistsStateEnum.IN_YARN) {
+ await fetchSparkAppForcedStop({
+ id: receiveData.application.id,
+ });
+ }
+ await handleDoSubmit();
+ }
+
+ async function handleReset() {
+ selectInput.value = false;
+ selectValue.value = null;
+ }
+
+ /* submit */
+ async function handleDoSubmit() {
+ try {
+ const formValue = (await validate()) as Recordable;
+ const restoreOrTriggerSavepoint = formValue.restoreSavepoint;
+ const savepointPath = restoreOrTriggerSavepoint ?
formValue['savepointPath'] : null;
+ handleReset();
+ const res = await fetchSparkAppStart({
+ id: receiveData.application.id,
+ restoreOrTriggerSavepoint,
+ savepointPath: savepointPath,
+ allowNonRestored: formValue.allowNonRestoredState || false,
+ });
+ if (res.data) {
+ Swal.fire({
+ icon: 'success',
+ title: t('flink.app.operation.starting'),
+ showConfirmButton: false,
+ timer: 2000,
+ });
+ emits('updateOption', {
+ type: 'starting',
+ key: receiveData.application.id,
+ value: new Date().getTime(),
+ });
+ closeModal();
+ } else {
+ closeModal();
+ Swal.fire({
+ title: 'Failed',
+ icon: 'error',
+ width: exceptionPropWidth(),
+ html:
+ '<pre class="api-exception"> startup failed, ' +
+ res.message.replaceAll(/\[StreamPark]/g, '') +
+ '</pre>',
+ showCancelButton: true,
+ confirmButtonColor: '#55BDDDFF',
+ confirmButtonText: 'Detail',
+ cancelButtonText: 'Close',
+ }).then((isConfirm: Recordable) => {
+ if (isConfirm.value) {
+ router.push({
+ path: '/flink/app/detail',
+ query: { appId: receiveData.application.id },
+ });
+ }
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ function handleSavepoint(model, field, input) {
+ selectInput.value = input;
+ if (input) {
+ selectValue.value = model[field];
+ model[field] = null;
+ } else {
+ model[field] = selectValue.value;
+ }
+ }
+</script>
+<template>
+ <BasicModal
+ @register="registerModal"
+ :minHeight="100"
+ @ok="handleSubmit"
+ @cancel="handleReset"
+ :okText="t('common.apply')"
+ :cancelText="t('common.cancelText')"
+ >
+ <template #title>
+ <SvgIcon name="play" />
+ {{ t('flink.app.view.start') }}
+ </template>
+
+ <BasicForm @register="registerForm" class="!pt-40px">
+ <template #savepoint="{ model, field }">
+ <template
+ v-if="
+ !selectInput && receiveData.historySavePoint &&
receiveData.historySavePoint.length > 0
+ "
+ >
+ <Select v-model:value="model[field]"
@dblclick="handleSavepoint(model, field, true)">
+ <SelectOption v-for="(k, i) in receiveData.historySavePoint"
:key="i" :value="k.path">
+ <span style="color: darkgrey">
+ <Icon icon="ant-design:clock-circle-outlined" />
+ {{ k.createTime }}
+ </span>
+ <span style="float: left" v-if="k.type === 1">
+ <tag color="cyan">CP</tag>
+ </span>
+ <span style="float: right" v-else>
+ <tag color="blue">SP</tag>
+ </span>
+ <span style="float: right" v-if="k.latest">
+ <tag color="#2db7f5">latest</tag>
+ </span>
+ </SelectOption>
+ </Select>
+ </template>
+ <Input
+ v-else
+ @dblclick="handleSavepoint(model, field, false)"
+ type="text"
+ :placeholder="t('flink.app.view.savepointInput')"
+ v-model:value="model[field]"
+ />
+ </template>
+ </BasicForm>
+ </BasicModal>
+</template>
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/components/Mergely.vue
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/Mergely.vue
new file mode 100644
index 000000000..999871bea
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/Mergely.vue
@@ -0,0 +1,202 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<script setup lang="ts">
+ import { useDiffMonaco } from '/@/views/flink/app/hooks/useDiffMonaco';
+ import { useMonaco } from '/@/hooks/web/useMonaco';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { nextTick, Ref, ref, unref } from 'vue';
+ import { getMonacoOptions } from '../data';
+ import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+ import { SvgIcon, Icon } from '/@/components/Icon';
+ const emit = defineEmits(['ok', 'close', 'register']);
+ const props = defineProps({
+ readOnly: {
+ type: Boolean as PropType<boolean>,
+ default: false,
+ },
+ });
+
+ const { t } = useI18n();
+ const title = ref('edit configuration');
+ const compareMode = ref(false);
+ const visibleDiff = ref(false);
+ const changed = ref(false);
+ const targetValue = ref<Nullable<string>>(null);
+ const originalValue = ref<Nullable<string>>(null);
+
+ const monacoMergely = ref();
+ const monacoConfig = ref();
+ const loading = ref(false);
+
+ /* Click Next */
+ function handleNext() {
+ visibleDiff.value = true;
+ title.value = 'Compare configuration';
+ nextTick(() => {
+ handleHeight(monacoMergely, 100);
+ });
+ // handleDifferent(unref(originalValue), unref(targetValue));
+ }
+ useDiffMonaco(
+ monacoMergely,
+ 'yaml',
+ () => unref(originalValue),
+ () => unref(targetValue),
+ getMonacoOptions(props.readOnly) as any,
+ );
+
+ const { setContent, onChange } = useMonaco(monacoConfig, {
+ language: 'yaml',
+ options: getMonacoOptions(props.readOnly) as any,
+ });
+ onChange((value) => {
+ // the first time
+ if (targetValue.value) {
+ changed.value = true;
+ }
+ targetValue.value = value;
+ });
+
+ /* Change editor height */
+ function handleHeight(ele: Ref, h: number) {
+ const height = document.documentElement.offsetHeight ||
document.body.offsetHeight;
+ unref(ele).style.height = height - h + 'px';
+ }
+
+ /* close match */
+ function handleCloseDiff() {
+ title.value = 'Edit configuration';
+ visibleDiff.value = false;
+ }
+
+ /* Click OK */
+ function handleOk() {
+ const value = unref(targetValue);
+ if (value == null || !value.replace(/^\s+|\s+$/gm, '')) {
+ emit('ok', { isSetConfig: false, configOverride: null });
+ } else {
+ emit('ok', { isSetConfig: true, configOverride: value });
+ }
+
+ handleCancel();
+ }
+
+ /* Click to cancel */
+ function handleCancel() {
+ changed.value = false;
+ targetValue.value = null;
+ originalValue.value = null;
+ visibleDiff.value = false;
+ loading.value = false;
+ emit('close');
+ closeDrawer();
+ }
+ const [registerMergelyDrawer, { closeDrawer }] = useDrawerInner(
+ (data: { configOverride: string }) => {
+ data && onReceiveDrawerData(data);
+ },
+ );
+ /* data reception */
+ function onReceiveDrawerData(data) {
+ compareMode.value = false;
+ changed.value = false;
+ targetValue.value = null;
+ originalValue.value = data.configOverride;
+ if (props.readOnly) {
+ title.value = 'Configuration detail';
+ }
+ nextTick(() => {
+ handleHeight(unref(monacoConfig), 130);
+ visibleDiff.value = false;
+ setContent(unref(originalValue) || '');
+ });
+ }
+</script>
+
+<template>
+ <BasicDrawer
+ @register="registerMergelyDrawer"
+ :keyboard="false"
+ :closable="false"
+ :mask-closable="false"
+ width="80%"
+ class="drawer-conf"
+ >
+ <template #title>
+ <SvgIcon v-if="props.readOnly" name="see" />
+ <SvgIcon v-else name="edit" />
+ {{ title }}
+ </template>
+ <div v-show="!visibleDiff">
+ <div ref="monacoConfig"></div>
+ <div class="drawer-bottom-button">
+ <div style="float: right">
+ <a-button type="primary" class="drwaer-button-item"
@click="handleCancel">
+ {{ t('common.cancelText') }}
+ </a-button>
+ <a-button v-if="changed" type="primary" class="drwaer-button-item"
@click="handleNext()">
+ <Icon icon="clarity:note-edit-line" />
+
+ {{ t('common.next') }}
+ </a-button>
+ </div>
+ </div>
+ </div>
+
+ <div v-if="visibleDiff">
+ <div ref="monacoMergely"></div>
+ <div class="drawer-bottom-button" style="position: absolute">
+ <div style="float: right">
+ <a-button
+ v-if="changed"
+ type="primary"
+ class="drwaer-button-item"
+ @click="handleCloseDiff"
+ >
+ <Icon icon="ant-design:left-outlined" />
+
+ {{ t('common.previous') }}
+ </a-button>
+ <a-button v-if="!compareMode" class="drwaer-button-item"
type="primary" @click="handleOk">
+ <Icon icon="ant-design:cloud-outlined" />
+
+ {{ t('common.apply') }}
+ </a-button>
+ </div>
+ </div>
+ </div>
+ </BasicDrawer>
+</template>
+<style scoped>
+ .drawer-conf :deep(.ant-drawer-body) {
+ padding: 5px !important;
+ padding-bottom: 0px !important;
+ }
+
+ .drawer-bottom-button {
+ position: absolute;
+ padding-top: 10px;
+ padding-right: 50px;
+ width: 100%;
+ bottom: 10px;
+ z-index: 9;
+ }
+
+ .drwaer-button-item {
+ margin-right: 20px;
+ }
+</style>
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/components/ProgramArgs.vue
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/ProgramArgs.vue
new file mode 100644
index 000000000..0da0d6366
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/ProgramArgs.vue
@@ -0,0 +1,128 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<template>
+ <div style="height: 340px" :class="fullContentClass">
+ <div
+ class="full-content-tool flex justify-between px-20px border-solid
border-b pb-10px mb-10px"
+ v-if="fullScreenStatus"
+ >
+ <div class="basic-title">
+ <Icon icon="material-symbols:energy-program-saving" color="#477de9" />
+ Program args
+ </div>
+ <Tooltip :title="t('component.modal.restore')" placement="bottom">
+ <FullscreenExitOutlined role="full" @click="toggle" style="font-size:
18px" />
+ </Tooltip>
+ </div>
+ <div ref="programArgRef" :class="fullEditorClass" class="w-full
program-box mt-5px"> </div>
+ <ButtonGroup class="flinksql-tool" v-if="!fullScreenStatus">
+ <a-button
+ class="flinksql-tool-item"
+ v-if="canReview"
+ type="primary"
+ @click="emit('preview', value)"
+ size="small"
+ >
+ <Icon icon="ant-design:eye-outlined" />
+ {{ t('flink.app.flinkSql.preview') }}
+ </a-button>
+ <a-button
+ class="flinksql-tool-item"
+ size="small"
+ :type="canReview ? 'default' : 'primary'"
+ @click="toggle"
+ >
+ <Icon icon="ant-design:fullscreen-outlined" />
+ {{ t('layout.header.tooltipEntryFull') }}
+ </a-button>
+ </ButtonGroup>
+ <ButtonGroup v-else class="flinksql-tool">
+ <a-button
+ type="primary"
+ class="flinksql-tool-item"
+ v-if="canReview"
+ @click="emit('preview', value)"
+ >
+ <Icon icon="ant-design:eye-outlined" />
+ {{ t('flink.app.flinkSql.preview') }}
+ </a-button>
+ <a-button
+ class="flinksql-tool-item"
+ size="small"
+ :type="canReview ? 'default' : 'primary'"
+ @click="toggle"
+ >
+ <Icon icon="ant-design:fullscreen-exit-outlined" />
+ {{ t('layout.header.tooltipExitFull') }}
+ </a-button>
+ </ButtonGroup>
+ </div>
+</template>
+<script lang="ts" setup>
+ import { Button, Tooltip } from 'ant-design-vue';
+ import { FullscreenExitOutlined } from '@ant-design/icons-vue';
+ import { computed, ref, toRefs, watchEffect } from 'vue';
+ import { getMonacoOptions } from '../data';
+ import Icon from '/@/components/Icon';
+ import { useFullContent } from '/@/hooks/event/useFullscreen';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { useMonaco } from '/@/hooks/web/useMonaco';
+ const { t } = useI18n();
+ const ButtonGroup = Button.Group;
+
+ const props = defineProps({
+ value: {
+ type: String,
+ required: true,
+ },
+ suggestions: {
+ type: Array as PropType<Array<{ text: string; description: string;
value: string }>>,
+ default: () => [],
+ },
+ });
+ const { value, suggestions } = toRefs(props);
+ const emit = defineEmits(['update:value', 'preview']);
+ const programArgRef = ref();
+
+ const { toggle, fullContentClass, fullEditorClass, fullScreenStatus } =
useFullContent();
+ const { onChange, setContent, setMonacoSuggest } = useMonaco(programArgRef, {
+ language: 'plaintext',
+ code: '',
+ options: {
+ ...(getMonacoOptions(false) as any),
+ autoClosingBrackets: 'never',
+ },
+ });
+ watchEffect(() => {
+ if (suggestions.value.length > 0) {
+ setMonacoSuggest(suggestions.value);
+ }
+ });
+ const canReview = computed(() => {
+ return /\${.+}/.test(value.value);
+ });
+
+ onChange((data) => {
+ emit('update:value', data);
+ });
+ defineExpose({ setContent });
+</script>
+<style lang="less">
+ .program-box {
+ border: 1px solid @border-color-base;
+ }
+</style>
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSql.vue
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSql.vue
new file mode 100644
index 000000000..3a6bc71b4
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSql.vue
@@ -0,0 +1,301 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<script setup lang="ts">
+ import { computed, reactive, ref, watchEffect } from 'vue';
+ import { Tooltip } from 'ant-design-vue';
+ import { FullscreenExitOutlined } from '@ant-design/icons-vue';
+ import { getMonacoOptions } from '../data';
+ import { Icon, SvgIcon } from '/@/components/Icon';
+ import { useMonaco } from '/@/hooks/web/useMonaco';
+ import { Button } from 'ant-design-vue';
+ import { isEmpty } from '/@/utils/is';
+ import { useMessage } from '/@/hooks/web/useMessage';
+ import { format } from '../sqlFormatter';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { useFullContent } from '/@/hooks/event/useFullscreen';
+ import { fetchSparkSqlVerify } from '/@/api/spark/sql';
+ defineOptions({
+ name: 'SparkSQL',
+ });
+ const ButtonGroup = Button.Group;
+ const { t } = useI18n();
+
+ const flinkSql = ref();
+ const verifyRes = reactive({
+ errorMsg: '',
+ verified: false,
+ errorStart: 0,
+ errorEnd: 0,
+ });
+
+ const { toggle, fullContentClass, fullEditorClass, fullScreenStatus } =
useFullContent();
+ const emit = defineEmits(['update:value', 'preview']);
+ const { createMessage } = useMessage();
+
+ const props = defineProps({
+ value: {
+ type: String,
+ default: '',
+ },
+ appId: {
+ type: String as PropType<Nullable<string>>,
+ },
+ versionId: {
+ type: String as PropType<Nullable<string>>,
+ },
+ suggestions: {
+ type: Array as PropType<Array<{ text: string; description: string }>>,
+ default: () => [],
+ },
+ });
+ const defaultValue = '';
+
+ /* verify */
+ async function handleVerifySql() {
+ if (isEmpty(props.value)) {
+ verifyRes.errorMsg = 'empty sql';
+ return false;
+ }
+
+ if (!props.versionId) {
+ createMessage.error(t('flink.app.dependencyError'));
+ return false;
+ } else {
+ try {
+ const { data } = await fetchSparkSqlVerify({
+ sql: props.value,
+ versionId: props.versionId,
+ });
+ const success = data.data === true || data.data === 'true';
+ if (success) {
+ verifyRes.verified = true;
+ verifyRes.errorMsg = '';
+ syntaxError();
+ return true;
+ } else {
+ verifyRes.errorStart = parseInt(data.start);
+ verifyRes.errorEnd = parseInt(data.end);
+ switch (data.type) {
+ case 4:
+ verifyRes.errorMsg = 'Unsupported sql';
+ break;
+ case 5:
+ verifyRes.errorMsg = "SQL is not endWith ';'";
+ break;
+ default:
+ verifyRes.errorMsg = data.message;
+ break;
+ }
+ syntaxError();
+ return false;
+ }
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ }
+ }
+
+ async function syntaxError() {
+ const editor = await getInstance();
+ if (editor) {
+ const model = editor.getModel();
+ const monaco = await getMonacoInstance();
+ if (verifyRes.errorMsg) {
+ try {
+ monaco.editor.setModelMarkers(model, 'sql', [
+ {
+ startLineNumber: verifyRes.errorStart,
+ endLineNumber: verifyRes.errorEnd,
+ severity: monaco.MarkerSeverity.Error,
+ message: verifyRes.errorMsg,
+ },
+ ]);
+ } catch (e) {
+ console.log(e);
+ }
+ } else {
+ monaco.editor.setModelMarkers(model, 'sql', []);
+ }
+ }
+ }
+ /* format */
+ function handleFormatSql() {
+ if (isEmpty(props.value)) return;
+ const formatSql = format(props.value);
+ setContent(formatSql);
+ }
+ /* full screen */
+ // function handleBigScreen() {
+ // toggle();
+ // unref(flinkSql).style.width = '0';
+ // setTimeout(() => {
+ // unref(flinkSql).style.width = '100%';
+ // unref(flinkSql).style.height = isFullscreen.value ? 'calc(100vh -
50px)' : '550px';
+ // }, 500);
+ // }
+ const { onChange, setContent, getInstance, getMonacoInstance,
setMonacoSuggest } = useMonaco(
+ flinkSql,
+ {
+ language: 'sql',
+ code: props.value || defaultValue,
+ options: {
+ minimap: { enabled: true },
+ ...(getMonacoOptions(false) as any),
+ autoClosingBrackets: 'never',
+ },
+ },
+ );
+
+ watchEffect(() => {
+ if (props.suggestions.length > 0) {
+ setMonacoSuggest(props.suggestions);
+ }
+ });
+ const canPreview = computed(() => {
+ return /\${.+}/.test(props.value);
+ });
+ const flinkEditorClass = computed(() => {
+ return {
+ ...fullEditorClass.value,
+ ['syntax-' + (verifyRes.errorMsg ? 'false' : 'true')]: true,
+ };
+ });
+
+ onChange((data) => {
+ emit('update:value', data);
+ });
+
+ defineExpose({ handleVerifySql, setContent });
+</script>
+
+<template>
+ <div class="w-full h-550px" :class="fullContentClass">
+ <div
+ class="full-content-tool flex justify-between px-20px pb-10px mb-10px"
+ v-if="fullScreenStatus"
+ >
+ <div class="flex items-center">
+ <SvgIcon name="fql" />
+ <div class="basic-title ml-10px">Spark Sql</div>
+ </div>
+ <Tooltip :title="t('component.modal.restore')" placement="bottom">
+ <FullscreenExitOutlined role="full" @click="toggle" style="font-size:
18px" />
+ </Tooltip>
+ </div>
+
+ <div
+ ref="flinkSql"
+ class="overflow-hidden w-full mt-5px sql-bordered"
+ :class="flinkEditorClass"
+ ></div>
+ <ButtonGroup class="sql-tool" v-if="!fullScreenStatus">
+ <a-button size="small" class="sql-tool-item" type="primary"
@click="handleVerifySql">
+ <Icon icon="ant-design:check-outlined" />
+ {{ t('flink.app.flinkSql.verify') }}
+ </a-button>
+ <a-button
+ class="sql-tool-item"
+ size="small"
+ type="default"
+ v-if="canPreview"
+ @click="emit('preview', value)"
+ >
+ <Icon icon="ant-design:eye-outlined" />
+ {{ t('flink.app.flinkSql.preview') }}
+ </a-button>
+ <a-button class="sql-tool-item" size="small" type="default"
@click="handleFormatSql">
+ <Icon icon="ant-design:thunderbolt-outlined" />
+ {{ t('flink.app.flinkSql.format') }}
+ </a-button>
+ <a-button class="sql-tool-item" size="small" type="default"
@click="toggle">
+ <Icon icon="ant-design:fullscreen-outlined" />
+ {{ t('flink.app.flinkSql.fullScreen') }}
+ </a-button>
+ </ButtonGroup>
+ <div class="flex items-center justify-between" v-else>
+ <div class="mt-10px flex-1 mr-10px overflow-hidden whitespace-nowrap">
+ <div class="text-red-600 overflow-ellipsis overflow-hidden"
v-if="verifyRes.errorMsg">
+ {{ verifyRes.errorMsg }}
+ </div>
+ <div v-else class="text-green-700">
+ <span v-if="verifyRes.verified"> {{
t('flink.app.flinkSql.successful') }} </span>
+ </div>
+ </div>
+ <div class="sql-tool">
+ <a-button type="primary" @click="handleVerifySql">
+ <div class="flex items-center">
+ <Icon icon="ant-design:check-outlined" />
+ {{ t('flink.app.flinkSql.verify') }}
+ </div>
+ </a-button>
+ <a-button v-if="canPreview" @click="emit('preview', value)"
class="ml-10px">
+ <div class="flex items-center">
+ <Icon icon="ant-design:eye-outlined" />
+ {{ t('flink.app.flinkSql.preview') }}
+ </div>
+ </a-button>
+ <a-button type="default" @click="handleFormatSql" class="ml-10px">
+ <div class="flex items-center">
+ <Icon icon="ant-design:thunderbolt-outlined" />
+ {{ t('flink.app.flinkSql.format') }}
+ </div>
+ </a-button>
+ <a-button type="default" @click="toggle" class="ml-10px">
+ <div class="flex items-center">
+ <Icon icon="ant-design:fullscreen-exit-outlined" />
+ {{ t('layout.header.tooltipExitFull') }}
+ </div>
+ </a-button>
+ </div>
+ </div>
+ </div>
+ <p class="conf-desc mt-10px" v-if="!fullScreenStatus">
+ <span class="text-red-600" v-if="verifyRes.errorMsg"> {{
verifyRes.errorMsg }} </span>
+ <span v-else class="text-green-700">
+ <span v-if="verifyRes.verified"> {{ t('flink.app.flinkSql.successful')
}} </span>
+ </span>
+ </p>
+</template>
+<style lang="less" scoped>
+ .full-content-tool {
+ border-bottom: 1px solid @border-color-base;
+ }
+ .sql-bordered {
+ border: 1px solid @border-color-base;
+ border-radius: 4px;
+ }
+
+ .sql-tool {
+ z-index: 99;
+ float: right;
+ margin-right: 5px;
+ cursor: pointer;
+ margin-top: 5px;
+ }
+
+ .sql-tool-item {
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ }
+ .conf-desc {
+ color: darkgrey;
+ margin-bottom: 0;
+ }
+</style>
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/components/StatisticCard.vue
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/StatisticCard.vue
new file mode 100644
index 000000000..2c2247401
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/StatisticCard.vue
@@ -0,0 +1,54 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<script setup lang="ts" name="StatisticCard">
+ import { Card, Statistic, Divider, Skeleton } from 'ant-design-vue';
+ defineProps({
+ loading: { type: Boolean, default: false },
+ statisticProps: {
+ type: Object as PropType<Recordable>,
+ default: () => ({ title: '', value: 0 }),
+ },
+ footerList: {
+ type: Array as PropType<Array<{ title: string; value: string | number
}>>,
+ default: () => [],
+ },
+ });
+</script>
+<template>
+ <div class="gutter-box">
+ <Skeleton :loading="loading" active>
+ <Card :bordered="false" class="dash-statistic">
+ <Statistic
+ v-bind="statisticProps"
+ :value-style="{
+ color: '#3f8600',
+ fontSize: '45px',
+ fontWeight: 500,
+ textShadow: '1px 1px 0 rgba(0,0,0,0.2)',
+ }"
+ />
+ </Card>
+ <Divider class="def-margin-bottom" />
+ <template v-for="(item, index) in footerList" :key="index">
+ <span> {{ item.title }} </span>
+ <strong class="pl-10px">{{ item.value }}</strong>
+ <Divider type="vertical" v-if="index !== footerList.length - 1" />
+ </template>
+ </Skeleton>
+ </div>
+</template>
+<style scoped></style>
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/components/VariableReview.vue
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/VariableReview.vue
new file mode 100644
index 000000000..3022a4361
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/components/VariableReview.vue
@@ -0,0 +1,65 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<template>
+ <BasicDrawer
+ @register="registerDrawer"
+ :width="800"
+ placement="right"
+ :showOkBtn="false"
+ showFooter
+ >
+ <template #title>
+ <EyeOutlined style="color: green" />
+ Flink SQL preview
+ </template>
+ <div ref="flinkReviewRef" class="h-[calc(100vh-150px)]
flink-preview"></div>
+ </BasicDrawer>
+</template>
+
+<script lang="ts" setup>
+ import { EyeOutlined } from '@ant-design/icons-vue';
+ import { ref } from 'vue';
+ import { getMonacoOptions } from '../data';
+ import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+ import { useMonaco } from '/@/hooks/web/useMonaco';
+ const flinkReviewRef = ref();
+ const { setContent } = useMonaco(flinkReviewRef, {
+ language: 'sql',
+ code: '',
+ options: getMonacoOptions(true) as any,
+ });
+ const [registerDrawer] = useDrawerInner((data) => {
+ if (data) {
+ const suggestions = data.suggestions.reduce((pre: Recordable, cur:
Recordable) => {
+ pre[cur.text] = cur.value;
+ return pre;
+ }, {});
+ console.log('suggestions', suggestions);
+ const content = data.value.replace(
+ /\${(.*?)}/g,
+ (node: string, $1: string) => suggestions[$1] || node,
+ );
+
+ setContent(content || '');
+ }
+ });
+</script>
+<style lang="less">
+ .flink-preview {
+ border: 1px solid @border-color-base;
+ }
+</style>
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/create.vue
b/streampark-console/streampark-console-webapp/src/views/spark/app/create.vue
new file mode 100644
index 000000000..8c714de26
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/create.vue
@@ -0,0 +1,201 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<script setup lang="ts">
+ import { useGo } from '/@/hooks/web/usePage';
+ import ProgramArgs from './components/ProgramArgs.vue';
+ import { onMounted, ref } from 'vue';
+ import { PageWrapper } from '/@/components/Page';
+ import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+
+ import { BasicForm, useForm } from '/@/components/Form';
+ import { useDrawer } from '/@/components/Drawer';
+ import { useMessage } from '/@/hooks/web/useMessage';
+ import { createLocalStorage } from '/@/utils/cache';
+ import { buildUUID } from '/@/utils/uuid';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import VariableReview from './components/VariableReview.vue';
+ import { encryptByBase64 } from '/@/utils/cipher';
+ import { AppTypeEnum, JobTypeEnum, ResourceFromEnum } from
'/@/enums/flinkEnum';
+ import { useSparkSchema } from './hooks/useAppFormSchema';
+ import { fetchCreateSparkApp } from '/@/api/spark/app';
+ import { SparkApplication } from '/@/api/spark/app.type';
+
+ const SparkSqlEditor = createAsyncComponent(() =>
import('./components/SparkSql.vue'), {
+ loading: true,
+ });
+
+ defineOptions({
+ name: 'SparkApplicationAction',
+ });
+ const go = useGo();
+ const sparkSql = ref();
+ const submitLoading = ref(false);
+
+ const { t } = useI18n();
+ const { createMessage } = useMessage();
+ const ls = createLocalStorage();
+
+ const { formSchema, sparkEnvs, suggestions } = useSparkSchema();
+
+ const [registerAppForm, { setFieldsValue, submit }] = useForm({
+ labelCol: { lg: { span: 5, offset: 0 }, sm: { span: 7, offset: 0 } },
+ wrapperCol: { lg: { span: 16, offset: 0 }, sm: { span: 17, offset: 0 } },
+ baseColProps: { span: 24 },
+ colon: true,
+ showActionButtonGroup: false,
+ });
+
+ const [registerReviewDrawer, { openDrawer: openReviewDrawer }] = useDrawer();
+
+ /* Initialize the form */
+ async function handleInitForm() {
+ const defaultValue = {};
+ const v = sparkEnvs.value.filter((v) => v.isDefault)[0];
+ if (v) {
+ Object.assign(defaultValue, { versionId: v.id });
+ }
+ await setFieldsValue(defaultValue);
+ }
+
+ /* custom mode */
+ async function handleCustomJobMode(values: Recordable) {
+ const params = {
+ jobType: JobTypeEnum.SQL,
+ executionMode: values.executionMode,
+ appType: AppTypeEnum.APACHE_SPARK,
+ versionId: values.versionId,
+ sparkSql: null,
+ jar: values.teamResource,
+ mainClass: values.mainClass,
+ appName: values.jobName,
+ tags: values.tags,
+ yarnQueue: values.yarnQueue,
+ resourceFrom: ResourceFromEnum.UPLOAD,
+ config: null,
+ appProperties: values.appProperties,
+ appArgs: values.args,
+ hadoopUser: values.hadoopUser,
+ description: values.description,
+ };
+ handleCreateAction(params);
+ }
+ /* spark sql mode */
+ async function handleSQLMode(values: Recordable) {
+ let config = values.configOverride;
+ if (config != null && config !== undefined && config.trim() != '') {
+ config = encryptByBase64(config);
+ } else {
+ config = null;
+ }
+ handleCreateAction({
+ jobType: JobTypeEnum.SQL,
+ executionMode: values.executionMode,
+ appType: AppTypeEnum.APACHE_SPARK,
+ versionId: values.versionId,
+ sparkSql: values.sparkSql,
+ jar: null,
+ mainClass: null,
+ appName: values.jobName,
+ tags: values.tags,
+ yarnQueue: values.yarnQueue,
+ resourceFrom: ResourceFromEnum.UPLOAD,
+ config,
+ appProperties: values.appProperties,
+ appArgs: values.args,
+ hadoopUser: values.hadoopUser,
+ description: values.description,
+ });
+ }
+ /* Submit to create */
+ async function handleAppSubmit(formValue: Recordable) {
+ try {
+ submitLoading.value = true;
+ if (formValue.jobType == JobTypeEnum.SQL) {
+ if (formValue.sparkSql == null || formValue.sparkSql.trim() === '') {
+
createMessage.warning(t('flink.app.editStreamPark.sparkSqlRequired'));
+ } else {
+ const access = await sparkSql?.value?.handleVerifySql();
+ if (!access) {
+ createMessage.warning(t('flink.app.editStreamPark.sqlCheck'));
+ throw new Error(access);
+ }
+ }
+ handleSQLMode(formValue);
+ } else {
+ handleCustomJobMode(formValue);
+ }
+ } catch (error) {
+ submitLoading.value = false;
+ }
+ }
+ /* send create request */
+ async function handleCreateAction(params: Recordable) {
+ const param: SparkApplication = {};
+ for (const k in params) {
+ const v = params[k];
+ if (v != null && v !== undefined) {
+ param[k] = v;
+ }
+ }
+ const socketId = buildUUID();
+ ls.set('DOWN_SOCKET_ID', socketId);
+ Object.assign(param, { socketId });
+ await fetchCreateSparkApp(param);
+ submitLoading.value = false;
+ go('/spark/app');
+ }
+
+ onMounted(async () => {
+ handleInitForm();
+ });
+</script>
+
+<template>
+ <PageWrapper contentFullHeight contentBackground contentClass="p-26px
app_controller">
+ <BasicForm @register="registerAppForm" @submit="handleAppSubmit"
:schemas="formSchema">
+ <template #sparkSql="{ model, field }">
+ <SparkSqlEditor
+ ref="sparkSql"
+ v-model:value="model[field]"
+ :versionId="model['versionId']"
+ :suggestions="suggestions"
+ @preview="(value) => openReviewDrawer(true, { value, suggestions })"
+ />
+ </template>
+ <template #args="{ model }">
+ <template v-if="model.args !== undefined">
+ <ProgramArgs
+ v-model:value="model.args"
+ :suggestions="suggestions"
+ @preview="(value) => openReviewDrawer(true, { value, suggestions
})"
+ />
+ </template>
+ </template>
+ <template #formFooter>
+ <div class="flex items-center w-full justify-center">
+ <a-button @click="go('/spark/app')">
+ {{ t('common.cancelText') }}
+ </a-button>
+ <a-button class="ml-4" :loading="submitLoading" type="primary"
@click="submit()">
+ {{ t('common.submitText') }}
+ </a-button>
+ </div>
+ </template>
+ </BasicForm>
+ <VariableReview @register="registerReviewDrawer" />
+ </PageWrapper>
+</template>
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/data/index.ts
b/streampark-console/streampark-console-webapp/src/views/spark/app/data/index.ts
new file mode 100644
index 000000000..ba5f4d2af
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/data/index.ts
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { FailoverStrategyEnum, ReleaseStateEnum } from '/@/enums/flinkEnum';
+import { ExecModeEnum } from '/@/enums/sparkEnum';
+
+/* Get diff editor configuration */
+export const getMonacoOptions = (readOnly: boolean) => {
+ return {
+ selectOnLineNumbers: false,
+ foldingStrategy: 'indentation', // code fragmentation
+ overviewRulerBorder: false, // Don't scroll bar borders
+ autoClosingBrackets: 'always',
+ autoClosingDelete: 'always',
+ tabSize: 2, // tab indent length
+ readOnly,
+ inherit: true,
+ scrollBeyondLastLine: false,
+ lineNumbersMinChars: 5,
+ lineHeight: 24,
+ automaticLayout: true,
+ cursorBlinking: 'line',
+ cursorStyle: 'line',
+ cursorWidth: 3,
+ renderFinalNewline: true,
+ renderLineHighlight: 'all',
+ quickSuggestionsDelay: 100, // Code prompt delay
+ scrollbar: {
+ useShadows: false,
+ vertical: 'visible',
+ horizontal: 'visible',
+ horizontalSliderSize: 5,
+ verticalSliderSize: 5,
+ horizontalScrollbarSize: 15,
+ verticalScrollbarSize: 15,
+ },
+ };
+};
+
+export const resolveOrder = [
+ { label: 'parent-first', value: 0 },
+ { label: 'child-first', value: 1 },
+];
+
+export const k8sRestExposedType = [
+ { label: 'LoadBalancer', value: 0 },
+ { label: 'ClusterIP', value: 1 },
+ { label: 'NodePort', value: 2 },
+];
+
+export const executionModes = [
+ { label: 'Standalone', value: ExecModeEnum.REMOTE, disabled: false },
+ { label: 'Yarn-Cluster', value: ExecModeEnum.YARN_CLUSTER, disabled: false },
+ { label: 'Yarn-Client', value: ExecModeEnum.YARN_CLIENT, disabled: false },
+];
+
+export const cpTriggerAction = [
+ { label: 'alert', value: FailoverStrategyEnum.ALERT },
+ { label: 'restart', value: FailoverStrategyEnum.RESTART },
+];
+
+export const releaseTitleMap = {
+ [ReleaseStateEnum.FAILED]: 'release failed',
+ [ReleaseStateEnum.NEED_RELEASE]: 'current job need release',
+ [ReleaseStateEnum.RELEASING]: 'releasing',
+ [ReleaseStateEnum.NEED_RESTART]: 'release finished,need restart',
+ [ReleaseStateEnum.NEED_ROLLBACK]: 'application is rollbacked,need release',
+};
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useAppFormSchema.tsx
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useAppFormSchema.tsx
new file mode 100644
index 000000000..b4ca02eb6
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useAppFormSchema.tsx
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { computed, h, onMounted, ref, unref } from 'vue';
+import type { FormSchema } from '/@/components/Form';
+import { useI18n } from '/@/hooks/web/useI18n';
+import { AppExistsStateEnum, JobTypeEnum } from '/@/enums/sparkEnum';
+import { ResourceFromEnum } from '/@/enums/flinkEnum';
+import { SvgIcon } from '/@/components/Icon';
+import type { SparkEnv } from '/@/api/spark/home.type';
+import type { RuleObject } from 'ant-design-vue/lib/form';
+import type { StoreValue } from 'ant-design-vue/lib/form/interface';
+import { renderIsSetConfig, renderStreamParkResource, renderYarnQueue } from
'./useFlinkRender';
+import { executionModes } from '../data';
+import { useDrawer } from '/@/components/Drawer';
+import { fetchSparkEnvList } from '/@/api/spark/home';
+import { fetchVariableAll } from '/@/api/resource/variable';
+import { fetchTeamResource } from '/@/api/resource/upload';
+import { fetchCheckSparkName } from '/@/api/spark/app';
+export function useSparkSchema() {
+ const { t } = useI18n();
+ const sparkEnvs = ref<SparkEnv[]>([]);
+ const teamResource = ref<Array<any>>([]);
+ const suggestions = ref<Array<{ text: string; description: string; value:
string }>>([]);
+
+ const [registerConfDrawer, { openDrawer: openConfDrawer }] = useDrawer();
+ /* Detect job name field */
+ async function getJobNameCheck(_rule: RuleObject, value: StoreValue, _model:
Recordable) {
+ if (value === null || value === undefined || value === '') {
+ return
Promise.reject(t('flink.app.addAppTips.appNameIsRequiredMessage'));
+ }
+ const params = { jobName: value };
+ // if (edit?.appId) {
+ // Object.assign(params, { id: edit.appId });
+ // }
+ const res = await fetchCheckSparkName(params);
+ switch (parseInt(res)) {
+ case AppExistsStateEnum.NO:
+ return Promise.resolve();
+ case AppExistsStateEnum.IN_DB:
+ return
Promise.reject(t('flink.app.addAppTips.appNameNotUniqueMessage'));
+ case AppExistsStateEnum.IN_YARN:
+ return
Promise.reject(t('flink.app.addAppTips.appNameExistsInYarnMessage'));
+ case AppExistsStateEnum.IN_KUBERNETES:
+ return
Promise.reject(t('flink.app.addAppTips.appNameExistsInK8sMessage'));
+ default:
+ return Promise.reject(t('flink.app.addAppTips.appNameValid'));
+ }
+ }
+ const getJobTypeOptions = () => {
+ return [
+ {
+ label: h('div', {}, [
+ h(SvgIcon, { name: 'code', color: '#108ee9' }, ''),
+ h('span', { class: 'pl-10px' }, 'Custom Code'),
+ ]),
+ value: String(JobTypeEnum.JAR),
+ },
+ {
+ label: h('div', {}, [
+ h(SvgIcon, { name: 'fql', color: '#108ee9' }, ''),
+ h('span', { class: 'pl-10px' }, 'Flink SQL'),
+ ]),
+ value: String(JobTypeEnum.SQL),
+ },
+ {
+ label: h('div', {}, [
+ h(SvgIcon, { name: 'py', color: '#108ee9' }, ''),
+ h('span', { class: 'pl-10px' }, 'Python Flink'),
+ ]),
+ value: String(JobTypeEnum.PYSPARK),
+ },
+ ];
+ };
+
+ const formSchema = computed((): FormSchema[] => {
+ return [
+ {
+ field: 'jobType',
+ label: t('flink.app.developmentMode'),
+ component: 'Select',
+ componentProps: ({ formModel }) => {
+ return {
+ placeholder: t('flink.app.addAppTips.developmentModePlaceholder'),
+ options: getJobTypeOptions(),
+ onChange: (value) => {
+ if (value != JobTypeEnum.SQL) {
+ formModel.resourceFrom = String(ResourceFromEnum.PROJECT);
+ }
+ },
+ };
+ },
+ defaultValue: String(JobTypeEnum.SQL),
+ rules: [
+ { required: true, message:
t('flink.app.addAppTips.developmentModeIsRequiredMessage') },
+ ],
+ },
+ {
+ field: 'executionMode',
+ label: t('flink.app.executionMode'),
+ component: 'Select',
+ itemProps: {
+ autoLink: false, //Resolve multiple trigger validators with null
value ·
+ },
+ componentProps: {
+ placeholder: t('flink.app.addAppTips.executionModePlaceholder'),
+ options: executionModes,
+ },
+ rules: [
+ {
+ required: true,
+ validator: async (_rule, value) => {
+ if (value === null || value === undefined || value === '') {
+ return
Promise.reject(t('flink.app.addAppTips.executionModeIsRequiredMessage'));
+ } else {
+ return Promise.resolve();
+ }
+ },
+ },
+ ],
+ },
+ {
+ field: 'versionId',
+ label: 'Spark Version',
+ component: 'Select',
+ componentProps: {
+ placeholder: 'Spark Version',
+ options: unref(sparkEnvs),
+ fieldNames: { label: 'sparkName', value: 'id', options: 'options' },
+ },
+ rules: [
+ { required: true, message:
t('flink.app.addAppTips.flinkVersionIsRequiredMessage') },
+ ],
+ },
+ {
+ field: 'sparkSql',
+ label: 'Spark SQL',
+ component: 'Input',
+ slot: 'sparkSql',
+ ifShow: ({ values }) => values?.jobType == JobTypeEnum.SQL,
+ rules: [{ required: true, message:
t('flink.app.addAppTips.flinkSqlIsRequiredMessage') }],
+ },
+ {
+ field: 'teamResource',
+ label: t('flink.app.resource'),
+ component: 'Select',
+ render: ({ model }) => renderStreamParkResource({ model, resources:
unref(teamResource) }),
+ ifShow: ({ values }) => values.jobType == JobTypeEnum.JAR,
+ },
+ {
+ field: 'mainClass',
+ label: t('flink.app.mainClass'),
+ component: 'Input',
+ componentProps: { placeholder:
t('flink.app.addAppTips.mainClassPlaceholder') },
+ ifShow: ({ values }) => values?.jobType == JobTypeEnum.JAR,
+ rules: [{ required: true, message:
t('flink.app.addAppTips.mainClassIsRequiredMessage') }],
+ },
+ {
+ field: 'jobName',
+ label: t('flink.app.appName'),
+ component: 'Input',
+ componentProps: { placeholder:
t('flink.app.addAppTips.appNamePlaceholder') },
+ dynamicRules: ({ model }) => {
+ return [
+ {
+ required: true,
+ trigger: 'blur',
+ validator: (rule: RuleObject, value: StoreValue) =>
+ getJobNameCheck(rule, value, model),
+ },
+ ];
+ },
+ },
+ {
+ field: 'tags',
+ label: t('flink.app.tags'),
+ component: 'Input',
+ componentProps: {
+ placeholder: t('flink.app.addAppTips.tagsPlaceholder'),
+ },
+ },
+ {
+ field: 'yarnQueue',
+ label: t('flink.app.yarnQueue'),
+ component: 'Input',
+ render: (renderCallbackParams) =>
renderYarnQueue(renderCallbackParams),
+ },
+ {
+ field: 'isSetConfig',
+ label: t('flink.app.appConf'),
+ component: 'Switch',
+ render({ model, field }) {
+ return renderIsSetConfig(model, field, registerConfDrawer,
openConfDrawer);
+ },
+ },
+ {
+ field: 'appProperties',
+ label: 'Application Properties',
+ component: 'InputTextArea',
+ componentProps: {
+ rows: 4,
+ placeholder:
+ '$key=$value,If there are multiple parameters,you can new line
enter them (-D <arg>)',
+ },
+ },
+ {
+ field: 'args',
+ label: t('flink.app.programArgs'),
+ component: 'InputTextArea',
+ defaultValue: '',
+ slot: 'args',
+ ifShow: ({ values }) => [JobTypeEnum.JAR,
JobTypeEnum.PYSPARK].includes(values?.jobType),
+ },
+ {
+ field: 'hadoopUser',
+ label: t('flink.app.hadoopUser'),
+ component: 'Input',
+ },
+ {
+ field: 'description',
+ label: t('common.description'),
+ component: 'InputTextArea',
+ componentProps: { rows: 4, placeholder:
t('flink.app.addAppTips.descriptionPlaceholder') },
+ },
+ ];
+ });
+ onMounted(async () => {
+ //get flinkEnv
+ fetchSparkEnvList().then((res) => {
+ sparkEnvs.value = res;
+ });
+ /* Get team dependencies */
+ fetchTeamResource({}).then((res) => {
+ teamResource.value = res;
+ });
+ fetchVariableAll().then((res) => {
+ suggestions.value = res.map((v) => {
+ return {
+ text: v.variableCode,
+ description: v.description,
+ value: v.variableValue,
+ };
+ });
+ });
+ });
+ return {
+ formSchema,
+ suggestions,
+ sparkEnvs,
+ };
+}
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useFlinkRender.tsx
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useFlinkRender.tsx
new file mode 100644
index 000000000..b25dfede2
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useFlinkRender.tsx
@@ -0,0 +1,326 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { RenderCallbackParams } from '/@/components/Form/src/types/form';
+import { Icon, SvgIcon } from '/@/components/Icon';
+
+import Mergely from '../components/Mergely.vue';
+import { Alert, Dropdown, Input, Menu, Select, Switch, Tag } from
'ant-design-vue';
+import { SettingTwoTone } from '@ant-design/icons-vue';
+import { unref } from 'vue';
+import { decodeByBase64 } from '/@/utils/cipher';
+import { SelectValue } from 'ant-design-vue/lib/select';
+import { useI18n } from '/@/hooks/web/useI18n';
+import { fetchYarnQueueList } from '/@/api/setting/yarnQueue';
+import { ApiSelect } from '/@/components/Form';
+import { ResourceTypeEnum } from '/@/views/resource/upload/upload.data';
+import { handleSparkConfTemplate } from '/@/api/spark/conf';
+
+const { t } = useI18n();
+/* render input dropdown component */
+export const renderInputDropdown = (
+ formModel: Recordable,
+ field: string,
+ componentProps: { placeholder: string; options: Array<string> },
+) => {
+ return (
+ <Input
+ type="text"
+ placeholder={componentProps.placeholder}
+ allowClear
+ value={formModel[field]}
+ onInput={(e) => (formModel[field] = e.target.value)}
+ >
+ {{
+ addonAfter: () => (
+ <Dropdown placement="bottomRight">
+ {{
+ overlay: () => (
+ <div>
+ <Menu trigger="['click', 'hover']">
+ {componentProps.options.map((item) => {
+ return (
+ <Menu.Item
+ key={item}
+ onClick={() => (formModel[field] = item)}
+ class="pr-60px"
+ >
+ <Icon icon="ant-design:plus-circle-outlined" />
+ {{ item }}
+ </Menu.Item>
+ );
+ })}
+ </Menu>
+ <Icon icon="ant-design:history-outlined" />
+ </div>
+ ),
+ }}
+ </Dropdown>
+ ),
+ }}
+ </Input>
+ );
+};
+
+/* render Yarn Queue */
+export const renderYarnQueue = ({ model, field }: RenderCallbackParams) => {
+ return (
+ <div>
+ <ApiSelect
+ name="yarnQueue"
+
placeholder={t('setting.yarnQueue.placeholder.yarnQueueLabelExpression')}
+ api={fetchYarnQueueList}
+ params={{ page: 1, pageSize: 9999 }}
+ resultField={'records'}
+ labelField={'queueLabel'}
+ valueField={'queueLabel'}
+ showSearch={true}
+ value={model[field]}
+ onChange={(value: string) => (model[field] = value)}
+ />
+ <p class="conf-desc mt-10px">
+ <span class="note-info">
+ <Tag color="#2db7f5" class="tag-note">
+ {t('flink.app.noteInfo.note')}
+ </Tag>
+ {t('setting.yarnQueue.selectionHint')}
+ </span>
+ </p>
+ </div>
+ );
+};
+
+/* render memory option */
+export const renderDynamicProperties = ({ model, field }:
RenderCallbackParams) => {
+ return (
+ <div>
+ <Input.TextArea
+ rows={8}
+ name="dynamicProperties"
+ placeholder="$key=$value,If there are multiple parameters,you can new
line enter them (-D <arg>)"
+ value={model[field]}
+ onInput={(e: ChangeEvent) => (model[field] = e?.target?.value)}
+ />
+ <p class="conf-desc mt-10px">
+ <span class="note-info">
+ <Tag color="#2db7f5" class="tag-note">
+ {t('flink.app.noteInfo.note')}
+ </Tag>
+ <a
+
href="https://ci.apache.org/projects/flink/flink-docs-stable/ops/config.html"
+ target="_blank"
+ class="pl-5px"
+ >
+ Flink {t('flink.app.noteInfo.officialDoc')}
+ </a>
+ </span>
+ </p>
+ </div>
+ );
+};
+
+export const getAlertSvgIcon = (name: string, text: string) => {
+ return (
+ <Alert type="info">
+ {{
+ message: () => (
+ <div>
+ <SvgIcon class="mr-8px" name={name} style={{ color: '#108ee9' }} />
+ <span>{text}</span>
+ </div>
+ ),
+ }}
+ </Alert>
+ );
+};
+/* render application conf */
+export const renderIsSetConfig = (
+ model: Recordable,
+ field: string,
+ registerConfDrawer: Fn,
+ openConfDrawer: Fn,
+) => {
+ /* Open the sqlConf drawer */
+ async function handleSQLConf(checked: boolean) {
+ if (checked) {
+ if (unref(model.configOverride)) {
+ openConfDrawer(true, {
+ configOverride: unref(model.configOverride),
+ });
+ } else {
+ const res = await handleSparkConfTemplate();
+ openConfDrawer(true, {
+ configOverride: decodeByBase64(res),
+ });
+ }
+ } else {
+ openConfDrawer(false);
+ Object.assign(model, {
+ configOverride: null,
+ isSetConfig: false,
+ });
+ }
+ }
+ function handleEditConfClose() {
+ if (!model.configOverride) {
+ model.isSetConfig = false;
+ }
+ }
+ function handleMergeSubmit(data: { configOverride: string; isSetConfig:
boolean }) {
+ if (data.configOverride == null ||
!data.configOverride.replace(/^\s+|\s+$/gm, '')) {
+ Object.assign(model, {
+ configOverride: null,
+ isSetConfig: false,
+ });
+ } else {
+ Object.assign(model, {
+ configOverride: data.configOverride,
+ isSetConfig: true,
+ });
+ }
+ }
+ function handleConfChange(checked: boolean) {
+ model[field] = checked;
+ if (checked) {
+ handleSQLConf(true);
+ }
+ }
+ return (
+ <div>
+ <Switch
+ checked-children="ON"
+ un-checked-children="OFF"
+ checked={model[field]}
+ onChange={handleConfChange}
+ />
+ {model[field] && (
+ <SettingTwoTone
+ class="ml-10px"
+ theme="twoTone"
+ two-tone-color="#4a9ff5"
+ onClick={() => handleSQLConf(true)}
+ />
+ )}
+
+ <Mergely
+ onOk={handleMergeSubmit}
+ onClose={() => handleEditConfClose()}
+ onRegister={registerConfDrawer}
+ />
+ </div>
+ );
+};
+
+export const renderResourceFrom = (model: Recordable) => {
+ return (
+ <Select
+ onChange={(value: string) => (model.resourceFrom = value)}
+ value={model.resourceFrom}
+ placeholder="Please select resource from"
+ >
+ <Select.Option value="1">
+ <SvgIcon name="github" />
+ <span class="pl-10px">Project</span>
+ <span class="gray"> (build from Project)</span>
+ </Select.Option>
+ <Select.Option value="2">
+ <SvgIcon name="upload" />
+ <span class="pl-10px">Upload</span>
+ <span class="gray"> (upload local job)</span>
+ </Select.Option>
+ </Select>
+ );
+};
+
+export const renderStreamParkResource = ({ model, resources }) => {
+ const renderOptions = () => {
+ return (resources || [])
+ .filter((item) => item.resourceType !== ResourceTypeEnum.FLINK_APP)
+ .map((resource) => {
+ return (
+ <Select.Option
+ key={resource.id}
+ label={resource.resourceType + '-' + resource.resourceName}
+ >
+ <div>
+ <Tag color="green" class="ml5px" size="small">
+ {resource.resourceType}
+ </Tag>
+ <span class="color-[darkgrey]">{resource.resourceName}</span>
+ </div>
+ </Select.Option>
+ );
+ });
+ };
+
+ return (
+ <div>
+ <Select
+ show-search
+ allow-clear
+ optionFilterProp="label"
+ mode="multiple"
+ max-tag-count={3}
+ onChange={(value) => (model.teamResource = value)}
+ value={model.teamResource}
+ placeholder={t('flink.app.resourcePlaceHolder')}
+ >
+ {renderOptions()}
+ </Select>
+ </div>
+ );
+};
+
+export const renderStreamParkJarApp = ({ model, resources }) => {
+ function handleAppChange(value: SelectValue) {
+ const res = resources.filter((item) => item.id == value)[0];
+ model.mainClass = res.mainClass;
+ model.uploadJobJar = res.resourceName;
+ }
+
+ const renderOptions = () => {
+ console.log('resources', resources);
+ return (resources || [])
+ .filter((item) => item.resourceType == ResourceTypeEnum.FLINK_APP)
+ .map((resource) => {
+ return (
+ <Select.Option key={resource.id} label={resource.resourceName}>
+ <div>
+ <Tag color="green" style=";margin-left: 5px;" size="small">
+ {resource.resourceType}
+ </Tag>
+ <span style="color: darkgrey">{resource.resourceName}</span>
+ </div>
+ </Select.Option>
+ );
+ });
+ };
+
+ return (
+ <div>
+ <Select
+ show-search
+ allow-clear
+ optionFilterProp="label"
+ onChange={handleAppChange}
+ value={model.uploadJobJar}
+ placeholder={t('flink.app.selectAppPlaceHolder')}
+ >
+ {renderOptions()}
+ </Select>
+ </div>
+ );
+};
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkAction.tsx
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkAction.tsx
new file mode 100644
index 000000000..6d7beb588
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkAction.tsx
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Alert, Form, Input } from 'ant-design-vue';
+import { h, onMounted, reactive, ref, unref } from 'vue';
+import { fetchAppOwners } from '/@/api/system/user';
+import { SvgIcon } from '/@/components/Icon';
+import { AppExistsStateEnum } from '/@/enums/sparkEnum';
+import { useI18n } from '/@/hooks/web/useI18n';
+import { useMessage } from '/@/hooks/web/useMessage';
+import { fetchCheckSparkName, fetchCopySparkApp, fetchSparkMapping } from
'/@/api/spark/app';
+
+export const useSparkAction = () => {
+ const { t } = useI18n();
+ const { Swal, createConfirm, createMessage } = useMessage();
+ const users = ref<Recordable>([]);
+
+ /* copy application */
+ function handleCopy(item: Recordable) {
+ const validateStatus = ref<'' | 'error' | 'validating' | 'success' |
'warning'>('');
+ let help = '';
+ let copyAppName: string | undefined = '';
+ createConfirm({
+ width: '600px',
+ title: () => [
+ h(SvgIcon, {
+ name: 'copy',
+ style: { color: 'red', display: 'inline-block', marginRight: '10px'
},
+ }),
+ 'Copy Application',
+ ],
+ content: () => {
+ return (
+ <Form class="!pt-50px">
+ <Form.Item
+ label="Application Name"
+ labelCol={{ lg: { span: 7 }, sm: { span: 7 } }}
+ wrapperCol={{ lg: { span: 16 }, sm: { span: 4 } }}
+ validateStatus={unref(validateStatus)}
+ help={help}
+ rules={[{ required: true }]}
+ >
+ <Input
+ type="text"
+ placeholder="New Application Name"
+ onInput={(e) => {
+ copyAppName = e.target.value;
+ }}
+ ></Input>
+ </Form.Item>
+ </Form>
+ );
+ },
+ okText: t('common.apply'),
+ cancelText: t('common.closeText'),
+ onOk: async () => {
+ //1) check empty
+ if (copyAppName == null) {
+ validateStatus.value = 'error';
+ help = 'Sorry, Application Name cannot be empty';
+ return Promise.reject('copy application error');
+ }
+ //2) check name
+ const params = { jobName: copyAppName };
+ const resp = await fetchCheckSparkName(params);
+ const code = parseInt(resp);
+ if (code === AppExistsStateEnum.NO) {
+ try {
+ const { data } = await fetchCopySparkApp({
+ id: item.id,
+ jobName: copyAppName,
+ });
+ const status = data.status || 'error';
+ if (status === 'success') {
+ Swal.fire({
+ icon: 'success',
+ title: 'copy successful',
+ timer: 1500,
+ });
+ }
+ } catch (error: any) {
+ if (error?.response?.data?.message) {
+ createMessage.error(
+ error.response.data.message
+ .replaceAll(/\[StreamPark\]/g, '')
+ .replaceAll(/\[StreamPark\]/g, '') || 'copy failed',
+ );
+ }
+ return Promise.reject('copy application error');
+ }
+ } else {
+ validateStatus.value = 'error';
+ if (code === AppExistsStateEnum.IN_DB) {
+ help = t('flink.app.addAppTips.appNameNotUniqueMessage');
+ } else if (code === AppExistsStateEnum.IN_YARN) {
+ help = t('flink.app.addAppTips.appNameExistsInYarnMessage');
+ } else if (code === AppExistsStateEnum.IN_KUBERNETES) {
+ help = t('flink.app.addAppTips.appNameExistsInK8sMessage');
+ } else {
+ help = t('flink.app.addAppTips.appNameNotValid');
+ }
+ return Promise.reject('copy application error');
+ }
+ },
+ });
+ }
+
+ /* mapping */
+ function handleMapping(app: Recordable) {
+ const mappingRef = ref();
+ const formValue = reactive<any>({});
+ createConfirm({
+ width: '600px',
+ title: () => [
+ h(SvgIcon, {
+ name: 'mapping',
+ style: { color: 'green', display: 'inline-block', marginRight:
'10px' },
+ }),
+ 'Mapping Application',
+ ],
+ content: () => {
+ return (
+ <Form
+ class="!pt-40px"
+ ref={mappingRef}
+ name="mappingForm"
+ labelCol={{ lg: { span: 7 }, sm: { span: 7 } }}
+ wrapperCol={{ lg: { span: 16 }, sm: { span: 4 } }}
+ v-model:model={formValue}
+ >
+ <Form.Item label="Application Name">
+ <Alert message={app.jobName} type="info" />
+ </Form.Item>
+ <Form.Item
+ label="JobId"
+ name="jobId"
+ rules={[{ required: true, message: 'jobId is required' }]}
+ >
+ <Input type="text" placeholder="JobId"
v-model:value={formValue.jobId} />
+ </Form.Item>
+ </Form>
+ );
+ },
+ okText: t('common.apply'),
+ cancelText: t('common.closeText'),
+ onOk: async () => {
+ try {
+ await mappingRef.value.validate();
+ await fetchSparkMapping({
+ id: app.id,
+ appId: formValue.appId,
+ jobId: formValue.jobId,
+ });
+ Swal.fire({
+ icon: 'success',
+ title: 'The current job is mapping',
+ showConfirmButton: false,
+ timer: 2000,
+ });
+ return Promise.resolve();
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ },
+ });
+ }
+
+ onMounted(() => {
+ fetchAppOwners({}).then((res) => {
+ users.value = res;
+ });
+ });
+
+ return {
+ handleCopy,
+ handleMapping,
+ users,
+ };
+};
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkColumns.ts
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkColumns.ts
new file mode 100644
index 000000000..da31b3280
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkColumns.ts
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ColumnType } from 'ant-design-vue/lib/table';
+import { useI18n } from '/@/hooks/web/useI18n';
+import { computed, ref, unref } from 'vue';
+import { BasicColumn } from '/@/components/Table';
+import { AppStateEnum } from '/@/enums/flinkEnum';
+import { dateToDuration } from '/@/utils/dateUtil';
+const { t } = useI18n();
+
+export const useSparkColumns = () => {
+ // app table column width
+ const tableColumnWidth = ref({
+ jobName: 250,
+ flinkVersion: 110,
+ tags: 150,
+ state: 120,
+ release: 190,
+ duration: 150,
+ modifyTime: 165,
+ nickName: 100,
+ });
+
+ function onTableColumnResize(width: number, columns: ColumnType) {
+ if (!columns?.dataIndex) return;
+ const dataIndexStr = columns?.dataIndex.toString() ?? '';
+ if (Reflect.has(tableColumnWidth.value, dataIndexStr)) {
+ // when table column width changed, save it to table column width ref
+ tableColumnWidth.value[dataIndexStr] = width < 100 ? 100 : width;
+ }
+ }
+
+ const getAppColumns = computed((): BasicColumn[] => [
+ {
+ title: t('flink.app.appName'),
+ dataIndex: 'jobName',
+ align: 'left',
+ fixed: 'left',
+ resizable: true,
+ width: unref(tableColumnWidth).jobName,
+ },
+ { title: t('flink.app.flinkVersion'), dataIndex: 'flinkVersion' },
+ { title: t('flink.app.tags'), ellipsis: true, dataIndex: 'tags', width:
150 },
+ {
+ title: t('flink.app.runStatus'),
+ dataIndex: 'state',
+ fixed: 'right',
+ width: unref(tableColumnWidth).state,
+ filters: [
+ { text: t('flink.app.runStatusOptions.added'), value:
String(AppStateEnum.ADDED) },
+ { text: t('flink.app.runStatusOptions.starting'), value:
String(AppStateEnum.STARTING) },
+ { text: t('flink.app.runStatusOptions.running'), value:
String(AppStateEnum.RUNNING) },
+ { text: t('flink.app.runStatusOptions.failed'), value:
String(AppStateEnum.FAILED) },
+ { text: t('flink.app.runStatusOptions.canceled'), value:
String(AppStateEnum.CANCELED) },
+ { text: t('flink.app.runStatusOptions.finished'), value:
String(AppStateEnum.FINISHED) },
+ { text: t('flink.app.runStatusOptions.suspended'), value:
String(AppStateEnum.SUSPENDED) },
+ { text: t('flink.app.runStatusOptions.lost'), value:
String(AppStateEnum.LOST) },
+ { text: t('flink.app.runStatusOptions.silent'), value:
String(AppStateEnum.SILENT) },
+ {
+ text: t('flink.app.runStatusOptions.terminated'),
+ value: String(AppStateEnum.TERMINATED),
+ },
+ ],
+ },
+ {
+ title: t('flink.app.releaseBuild'),
+ dataIndex: 'release',
+ width: unref(tableColumnWidth).release,
+ fixed: 'right',
+ },
+ {
+ title: t('flink.app.duration'),
+ dataIndex: 'duration',
+ sorter: true,
+ width: unref(tableColumnWidth).duration,
+ customRender: ({ value }) => dateToDuration(value),
+ },
+ {
+ title: t('flink.app.modifiedTime'),
+ dataIndex: 'modifyTime',
+ sorter: true,
+ width: unref(tableColumnWidth).modifyTime,
+ },
+ { title: t('flink.app.owner'), dataIndex: 'nickName', width:
unref(tableColumnWidth).nickName },
+ ]);
+ return { getAppColumns, onTableColumnResize, tableColumnWidth };
+};
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkTableAction.ts
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkTableAction.ts
new file mode 100644
index 000000000..f5488ffff
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkTableAction.ts
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { computed, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { ActionItem, FormProps } from '/@/components/Table';
+import { useMessage } from '/@/hooks/web/useMessage';
+import { AppStateEnum } from '/@/enums/flinkEnum';
+import { JobTypeEnum } from '/@/enums/sparkEnum';
+import { usePermission } from '/@/hooks/web/usePermission';
+import { useI18n } from '/@/hooks/web/useI18n';
+import { isFunction, isObject } from '/@/utils/is';
+import { fetchSparkAppRecord, fetchSparkAppRemove } from '/@/api/spark/app';
+import type { SparkApplication } from '/@/api/spark/app.type';
+import { useSparkAction } from './useSparkAction';
+
+// Create form configurations and operation functions in the application table
+export const useSparkTableAction = (handlePageDataReload: Fn) => {
+ const { t } = useI18n();
+ const tagsOptions = ref<Recordable>([]);
+
+ const router = useRouter();
+ const { createMessage } = useMessage();
+ const { hasPermission } = usePermission();
+ const { handleCopy, handleMapping, users } = useSparkAction();
+
+ /* Operation button list */
+ function getActionList(record: SparkApplication, _currentPageNo: number):
ActionItem[] {
+ return [
+ {
+ label: t('flink.app.operation.copy'),
+ auth: 'app:copy',
+ icon: 'ant-design:copy-outlined',
+ onClick: handleCopy.bind(null, record),
+ },
+ {
+ label: t('flink.app.operation.remapping'),
+ ifShow: [
+ AppStateEnum.ADDED,
+ AppStateEnum.FAILED,
+ AppStateEnum.CANCELED,
+ AppStateEnum.KILLED,
+ AppStateEnum.SUCCEEDED,
+ AppStateEnum.TERMINATED,
+ AppStateEnum.POS_TERMINATED,
+ AppStateEnum.FINISHED,
+ AppStateEnum.SUSPENDED,
+ AppStateEnum.LOST,
+ ].includes(record.state as AppStateEnum),
+ auth: 'app:mapping',
+ icon: 'ant-design:deployment-unit-outlined',
+ onClick: handleMapping.bind(null, record),
+ },
+ {
+ popConfirm: {
+ title: t('flink.app.operation.deleteTip'),
+ confirm: handleDelete.bind(null, record),
+ },
+ label: t('common.delText'),
+ ifShow: [
+ AppStateEnum.ADDED,
+ AppStateEnum.FAILED,
+ AppStateEnum.CANCELED,
+ AppStateEnum.FINISHED,
+ AppStateEnum.LOST,
+ AppStateEnum.TERMINATED,
+ AppStateEnum.POS_TERMINATED,
+ AppStateEnum.SUCCEEDED,
+ AppStateEnum.KILLED,
+ ].includes(record.state as AppStateEnum),
+ auth: 'app:delete',
+ icon: 'ant-design:delete-outlined',
+ color: 'error',
+ },
+ ];
+ }
+ /** action button is show */
+ function actionIsShow(tableActionItem: ActionItem): boolean | undefined {
+ const { auth, ifShow } = tableActionItem;
+ let flag = isFunction(ifShow) ? ifShow(tableActionItem) : ifShow;
+ /** Judgment auth when not set or allowed to display */
+ if ((flag || flag === undefined) && auth) flag = hasPermission(auth);
+ return flag;
+ }
+
+ function getTableActions(
+ record: SparkApplication,
+ currentPageNo: any,
+ ): { actions: ActionItem[]; dropDownActions: ActionItem[] } {
+ const tableAction = getActionList(record, currentPageNo).filter((item:
ActionItem) =>
+ actionIsShow(item),
+ );
+ const actions = tableAction.splice(0, 3).map((item: ActionItem) => {
+ if (item.label) {
+ item.tooltip = {
+ title: item.label,
+ };
+ delete item.label;
+ }
+ return item;
+ });
+ return {
+ actions,
+ dropDownActions: tableAction.map((item: ActionItem) => {
+ if (!item.label && isObject(item.tooltip)) item.label =
item.tooltip?.title || '';
+ return item;
+ }),
+ };
+ }
+
+ /* Click to delete */
+ async function handleDelete(app: SparkApplication) {
+ const hide = createMessage.loading('deleting', 0);
+ try {
+ await fetchSparkAppRemove(app.id!);
+ createMessage.success('delete successful');
+ handlePageDataReload(false);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ hide();
+ }
+ }
+
+ const formConfig = computed((): Partial<FormProps> => {
+ const tableFormConfig: FormProps = {
+ baseColProps: { span: 5, style: { paddingRight: '20px' } },
+ actionColOptions: { span: 4 },
+ showSubmitButton: false,
+ showResetButton: false,
+ async resetFunc() {
+ router.push({ path: '/spark/app/create' });
+ },
+ schemas: [
+ {
+ label: t('flink.app.tags'),
+ field: 'tags',
+ component: 'Select',
+ componentProps: {
+ placeholder: t('flink.app.tags'),
+ showSearch: true,
+ options: tagsOptions.value.map((t: Recordable) => ({ label: t,
value: t })),
+ onChange: handlePageDataReload.bind(null, false),
+ },
+ },
+ {
+ label: t('flink.app.owner'),
+ field: 'userId',
+ component: 'Select',
+ componentProps: {
+ placeholder: t('flink.app.owner'),
+ showSearch: true,
+ options: users.value.map((u: Recordable) => {
+ return { label: u.nickName || u.username, value: u.userId };
+ }),
+ onChange: handlePageDataReload.bind(null, false),
+ },
+ },
+ {
+ label: t('flink.app.jobType'),
+ field: 'jobType',
+ component: 'Select',
+ componentProps: {
+ placeholder: t('flink.app.jobType'),
+ showSearch: true,
+ options: [
+ { label: 'JAR', value: JobTypeEnum.JAR },
+ { label: 'SQL', value: JobTypeEnum.SQL },
+ { label: 'PySpark', value: JobTypeEnum.PYSPARK },
+ ],
+ onChange: handlePageDataReload.bind(null, false),
+ },
+ },
+ {
+ label: t('flink.app.searchName'),
+ field: 'jobName',
+ component: 'Input',
+ componentProps: {
+ placeholder: t('flink.app.searchName'),
+ onChange: handlePageDataReload.bind(null, false),
+ onSearch: handlePageDataReload.bind(null, false),
+ },
+ },
+ ],
+ };
+ if (hasPermission('app:create')) {
+ Object.assign(tableFormConfig, {
+ showResetButton: true,
+ resetButtonOptions: {
+ text: t('common.add'),
+ color: 'primary',
+ preIcon: 'ant-design:plus-outlined',
+ },
+ });
+ }
+ return tableFormConfig;
+ });
+
+ /* tag */
+ function handleInitTagsOptions() {
+ const params = Object.assign({}, { pageSize: 999999999, pageNum: 1 });
+ fetchSparkAppRecord(params).then((res) => {
+ const dataSource = res?.records || [];
+ dataSource.forEach((record) => {
+ if (record.tags !== null && record.tags !== undefined && record.tags
!== '') {
+ const tagsArray = record.tags.split(',') as string[];
+ tagsArray.forEach((x: string) => {
+ if (x.length > 0 && tagsOptions.value.indexOf(x) == -1) {
+ tagsOptions.value.push(x);
+ }
+ });
+ }
+ });
+ });
+ }
+ onMounted(() => {
+ handleInitTagsOptions();
+ });
+ return { getTableActions, formConfig };
+};
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/index.vue
b/streampark-console/streampark-console-webapp/src/views/spark/app/index.vue
new file mode 100644
index 000000000..c660c040a
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/index.vue
@@ -0,0 +1,317 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<script lang="ts" setup>
+ import { nextTick, ref, onUnmounted, onMounted } from 'vue';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { AppStateEnum, OptionStateEnum, ReleaseStateEnum } from
'/@/enums/flinkEnum';
+ import { JobTypeEnum } from '/@/enums/sparkEnum';
+ import { useTimeoutFn } from '@vueuse/core';
+ import { Tooltip, Badge, Tag, Popover } from 'ant-design-vue';
+ import { useTable } from '/@/components/Table';
+ import { PageWrapper } from '/@/components/Page';
+ import { BasicTable, TableAction } from '/@/components/Table';
+ import { releaseTitleMap } from './data';
+
+ import AppDashboard from './components/AppDashboard.vue';
+ import State, {
+ buildStatusMap,
+ optionStateMap,
+ releaseStateMap,
+ stateMap,
+ } from '/@/views/flink/app/components/State';
+ import { useSparkColumns } from './hooks/useSparkColumns';
+ import AppTableResize from '/@/views/flink/app/components/AppResize.vue';
+ import { fetchSparkAppRecord } from '/@/api/spark/app';
+ import { useSparkTableAction } from './hooks/useSparkTableAction';
+ defineOptions({
+ name: 'SparkApplication',
+ });
+ const { t } = useI18n();
+ const optionApps = {
+ starting: new Map(),
+ stopping: new Map(),
+ release: new Map(),
+ savepointing: new Map(),
+ };
+ const errorCount = ref(0);
+ const appDashboardRef = ref<any>();
+
+ const currentTablePage = ref(1);
+ const { onTableColumnResize, tableColumnWidth, getAppColumns } =
useSparkColumns();
+ const titleLenRef = ref({
+ maxState: '',
+ maxRelease: '',
+ maxBuild: '',
+ });
+
+ const getSparkAppList = async (params: Recordable) => {
+ try {
+ if (Reflect.has(params, 'state')) {
+ if (params.state && params.state.length > 0) {
+ params['stateArray'] = [...params.state];
+ }
+ delete params.state;
+ }
+ currentTablePage.value = params.pageNum;
+ // sessionStorage.setItem('appPageNo', params.pageNum);
+ const res = await fetchSparkAppRecord(params);
+
+ const timestamp = new Date().getTime();
+ res.records.forEach((x) => {
+ Object.assign(x, {
+ expanded: [
+ {
+ appId: x.appId,
+ jmMemory: x.jmMemory,
+ tmMemory: x.tmMemory,
+ totalTM: x.totalTM,
+ totalSlot: x.totalSlot,
+ availableSlot: x.availableSlot,
+ },
+ ],
+ });
+ if (x['optionState'] === OptionStateEnum.NONE) {
+ if (optionApps.starting.get(x.id)) {
+ if (timestamp - optionApps.starting.get(x.id) > 2000 * 2) {
+ optionApps.starting.delete(x.id);
+ }
+ }
+ if (optionApps.stopping.get(x.id)) {
+ if (timestamp - optionApps.stopping.get(x.id) > 2000) {
+ optionApps.stopping.delete(x.id);
+ }
+ }
+ if (optionApps.release.get(x.id)) {
+ if (timestamp - optionApps.release.get(x.id) > 2000) {
+ optionApps.release.delete(x.id);
+ }
+ }
+ if (optionApps.savepointing.get(x.id)) {
+ if (timestamp - optionApps.savepointing.get(x.id) > 2000) {
+ optionApps.savepointing.delete(x.id);
+ }
+ }
+ }
+ });
+ const stateLenMap = res.records.reduce(
+ (
+ prev: {
+ maxState: string;
+ maxRelease: string;
+ maxBuild: string;
+ },
+ cur: any,
+ ) => {
+ const { state, optionState, release, buildStatus } = cur;
+ // state title len
+ if (optionState === OptionStateEnum.NONE) {
+ const stateStr = stateMap[state]?.title;
+ if (stateStr && stateStr.length > prev.maxState.length) {
+ prev.maxState = stateStr;
+ }
+ } else {
+ const stateStr = optionStateMap[optionState]?.title;
+ if (stateStr && stateStr.length > prev.maxState.length) {
+ prev.maxState = stateStr;
+ }
+ }
+
+ //release title len
+ const releaseStr = releaseStateMap[release]?.title;
+ if (releaseStr && releaseStr.length > prev.maxRelease.length) {
+ prev.maxRelease = releaseStr;
+ }
+
+ //build title len
+ const buildStr = buildStatusMap[buildStatus]?.title;
+ if (buildStr && buildStr.length > prev.maxBuild.length) {
+ prev.maxBuild = buildStr;
+ }
+ return prev;
+ },
+ {
+ maxState: '',
+ maxRelease: '',
+ maxBuild: '',
+ },
+ );
+ Object.assign(titleLenRef.value, stateLenMap);
+
+ return {
+ list: res.records,
+ total: res.total,
+ };
+ } catch (error) {
+ errorCount.value += 1;
+ console.error(error);
+ }
+ };
+ const [registerTable, { reload, getLoading, setPagination }] = useTable({
+ rowKey: 'id',
+ api: getSparkAppList,
+ immediate: true,
+ canResize: false,
+ showIndexColumn: false,
+ showTableSetting: true,
+ useSearchForm: true,
+ tableSetting: { fullScreen: true, redo: false },
+ actionColumn: {
+ dataIndex: 'operation',
+ title: t('component.table.operation'),
+ width: 180,
+ },
+ });
+
+ const { getTableActions, formConfig } =
useSparkTableAction(handlePageDataReload);
+
+ function handlePageDataReload(polling = false) {
+ nextTick(() => {
+ appDashboardRef.value?.handleDashboard(false);
+ reload({ polling });
+ });
+ }
+ const { start, stop } = useTimeoutFn(() => {
+ if (errorCount.value <= 3) {
+ start();
+ } else {
+ return;
+ }
+ if (!getLoading()) {
+ handlePageDataReload(true);
+ }
+ }, 2000);
+
+ onMounted(() => {
+ // If there is a page, jump to the page number of the record
+ const currentPage = sessionStorage.getItem('appPageNo');
+ if (currentPage) {
+ setPagination({
+ current: Number(currentPage) || 1,
+ });
+ sessionStorage.removeItem('appPageNo');
+ }
+ });
+
+ onUnmounted(() => {
+ stop();
+ });
+</script>
+<template>
+ <PageWrapper contentFullHeight>
+ <AppDashboard ref="appDashboardRef" />
+ <BasicTable
+ @register="registerTable"
+ :columns="getAppColumns"
+ @resize-column="onTableColumnResize"
+ class="app_list !px-0 pt-20px"
+ :formConfig="formConfig"
+ >
+ <template #bodyCell="{ column, record }">
+ <template v-if="column.dataIndex === 'jobName'">
+ <span class="app_type app_jar" v-if="record['jobType'] ==
JobTypeEnum.JAR"> JAR </span>
+ <span class="app_type app_sql" v-if="record['jobType'] ==
JobTypeEnum.SQL"> SQL </span>
+ <span class="app_type app_py" v-if="record['jobType'] ==
JobTypeEnum.PYSPARK">
+ PySpark
+ </span>
+ <span
+ class="link"
+ :class="{
+ 'cursor-pointer':
+ [AppStateEnum.RESTARTING,
AppStateEnum.RUNNING].includes(record.state) ||
+ record['optionState'] === OptionStateEnum.SAVEPOINTING,
+ }"
+ >
+ <Popover :title="t('common.detailText')">
+ <template #content>
+ <div class="flex">
+ <span class="pr-6px font-bold">{{ t('flink.app.appName')
}}:</span>
+ <div class="max-w-300px break-words">{{ record.jobName
}}</div>
+ </div>
+ <div class="pt-2px">
+ <span class="pr-6px font-bold">{{ t('flink.app.jobType')
}}:</span>
+ <Tag color="blue">
+ <span v-if="record['jobType'] == JobTypeEnum.JAR"> JAR
</span>
+ <span v-if="record['jobType'] == JobTypeEnum.SQL"> SQL
</span>
+ <span v-if="record['jobType'] == JobTypeEnum.PYSPARK">
PySpark </span>
+ </Tag>
+ </div>
+ <div class="pt-2px flex">
+ <span class="pr-6px font-bold">{{ t('common.description')
}}:</span>
+ <div class="max-w-300px break-words">{{ record.description
}}</div>
+ </div>
+ </template>
+ {{ record.jobName }}
+ </Popover>
+ </span>
+
+ <template v-if="record['jobType'] == JobTypeEnum.JAR">
+ <Badge
+ v-if="record.release === ReleaseStateEnum.NEED_CHECK"
+ class="build-badge"
+ count="NEW"
+ :title="t('flink.app.view.recheck')"
+ />
+ <Badge
+ v-else-if="record.release >= ReleaseStateEnum.RELEASING"
+ class="build-badge"
+ count="NEW"
+ :title="t('flink.app.view.changed')"
+ />
+ </template>
+ </template>
+ <template v-if="column.dataIndex === 'tags'">
+ <Tooltip v-if="record.tags" :title="record.tags">
+ <span
+ v-for="(tag, index) in record.tags.split(',')"
+ :key="'tag-'.concat(index.toString())"
+ class="pl-4px"
+ >
+ <Tag color="blue">{{ tag }}</Tag>
+ </span>
+ </Tooltip>
+ </template>
+ <template v-if="column.dataIndex === 'task'">
+ <State option="task" :data="record" />
+ </template>
+ <template v-if="column.dataIndex === 'state'">
+ <State option="state" :data="record"
:maxTitle="titleLenRef.maxState" />
+ </template>
+ <template v-if="column.dataIndex === 'release'">
+ <State
+ option="release"
+ :maxTitle="titleLenRef.maxRelease"
+ :title="releaseTitleMap[record.release] || ''"
+ :data="record"
+ />
+ </template>
+ <template v-if="column.dataIndex === 'operation'">
+ <TableAction v-bind="getTableActions(record, currentTablePage)" />
+ </template>
+ </template>
+ <template #insertTable="{ tableContainer }">
+ <AppTableResize
+ :table-container="tableContainer"
+ :resize-min="100"
+ v-model:left="tableColumnWidth.jobName"
+ />
+ </template>
+ </BasicTable>
+ </PageWrapper>
+</template>
+<style lang="less">
+ @import url('./styles/View.less');
+</style>
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/sqlFormatter.js
b/streampark-console/streampark-console-webapp/src/views/spark/app/sqlFormatter.js
new file mode 100644
index 000000000..9c00b6f6a
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/sqlFormatter.js
@@ -0,0 +1,408 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Formatter from 'sql-formatter/lib/core/Formatter';
+import Tokenizer from 'sql-formatter/lib/core/Tokenizer';
+
+//
https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#reserved-word
+const reservedWords = [
+ 'ABS',
+ 'ALL',
+ 'ALLOCATE',
+ 'ALTER',
+ 'AND',
+ 'ANY',
+ 'ARE',
+ 'ARRAY',
+ 'AS',
+ 'ASENSITIVE',
+ 'ASYMMETRIC',
+ 'AT',
+ 'ATOMIC',
+ 'AUTHORIZATION',
+ 'AVG',
+ 'BEGIN',
+ 'BETWEEN',
+ 'BIGINT',
+ 'BINARY',
+ 'BLOB',
+ 'BOOLEAN',
+ 'BOTH',
+ 'BY',
+ 'CALL',
+ 'CALLED',
+ 'CARDINALITY',
+ 'CASCADED',
+ 'CASE',
+ 'CAST',
+ 'CEIL',
+ 'CEILING',
+ 'CHAR',
+ 'CHAR_LENGTH',
+ 'CHARACTER',
+ 'CHARACTER_LENGTH',
+ 'CHECK',
+ 'CLOB',
+ 'CLOSE',
+ 'COALESCE',
+ 'COLLATE',
+ 'COLLECT',
+ 'COLUMN',
+ 'COMMIT',
+ 'CONDITION',
+ 'CONNECT',
+ 'CONSTRAINT',
+ 'CONVERT',
+ 'CORR',
+ 'CORRESPONDING',
+ 'COUNT',
+ 'COVAR_POP',
+ 'COVAR_SAMP',
+ 'CREATE',
+ 'CROSS',
+ 'CUBE',
+ 'CUME_DIST',
+ 'CURRENT',
+ 'CURRENT_CATALOG',
+ 'CURRENT_DATE',
+ 'CURRENT_DEFAULT_TRANSFORM_GROUP',
+ 'CURRENT_PATH',
+ 'CURRENT_ROLE',
+ 'CURRENT_SCHEMA',
+ 'CURRENT_TIME',
+ 'CURRENT_TIMESTAMP',
+ 'CURRENT_TRANSFORM_GROUP_FOR_TYPE',
+ 'CURRENT_USER',
+ 'CURSOR',
+ 'CYCLE',
+ 'DATE',
+ 'DAY',
+ 'DEALLOCATE',
+ 'DEC',
+ 'DECIMAL',
+ 'DECLARE',
+ 'DEFAULT',
+ 'DELETE',
+ 'DENSE_RANK',
+ 'DEREF',
+ 'DESCRIBE',
+ 'DETERMINISTIC',
+ 'DISCONNECT',
+ 'DISTINCT',
+ 'DOUBLE',
+ 'DROP',
+ 'DYNAMIC',
+ 'EACH',
+ 'ELEMENT',
+ 'ELSE',
+ 'END',
+ 'END-EXEC',
+ 'ESCAPE',
+ 'EVERY',
+ 'EXCEPT',
+ 'EXEC',
+ 'EXECUTE',
+ 'EXISTS',
+ 'EXP',
+ 'EXTERNAL',
+ 'EXTRACT',
+ 'FALSE',
+ 'FETCH',
+ 'FILTER',
+ 'FLOAT',
+ 'FLOOR',
+ 'FOR',
+ 'FOREIGN',
+ 'FREE',
+ 'FROM',
+ 'FULL',
+ 'FUNCTION',
+ 'FUSION',
+ 'GET',
+ 'GLOBAL',
+ 'GRANT',
+ 'GROUP',
+ 'GROUPING',
+ 'HAVING',
+ 'HOLD',
+ 'HOUR',
+ 'IDENTITY',
+ 'IN',
+ 'INDICATOR',
+ 'INNER',
+ 'INOUT',
+ 'INSENSITIVE',
+ 'INSERT',
+ 'INT',
+ 'INTEGER',
+ 'INTERSECT',
+ 'INTERSECTION',
+ 'INTERVAL',
+ 'INTO',
+ 'IS',
+ 'JOIN',
+ 'LANGUAGE',
+ 'LARGE',
+ 'LATERAL',
+ 'LEADING',
+ 'LEFT',
+ 'LIKE',
+ 'LIKE_REGEX',
+ 'LN',
+ 'LOCAL',
+ 'LOCALTIME',
+ 'LOCALTIMESTAMP',
+ 'LOWER',
+ 'MATCH',
+ 'MAX',
+ 'MEMBER',
+ 'MERGE',
+ 'METHOD',
+ 'MIN',
+ 'MINUTE',
+ 'MOD',
+ 'MODIFIES',
+ 'MODULE',
+ 'MONTH',
+ 'MULTISET',
+ 'NATIONAL',
+ 'NATURAL',
+ 'NCHAR',
+ 'NCLOB',
+ 'NEW',
+ 'NO',
+ 'NONE',
+ 'NORMALIZE',
+ 'NOT',
+ 'NULL',
+ 'NULLIF',
+ 'NUMERIC',
+ 'OCTET_LENGTH',
+ 'OCCURRENCES_REGEX',
+ 'OF',
+ 'OLD',
+ 'ON',
+ 'ONLY',
+ 'OPEN',
+ 'OR',
+ 'ORDER',
+ 'OUT',
+ 'OUTER',
+ 'OVER',
+ 'OVERLAPS',
+ 'OVERLAY',
+ 'PARAMETER',
+ 'PARTITION',
+ 'PERCENT_RANK',
+ 'PERCENTILE_CONT',
+ 'PERCENTILE_DISC',
+ 'POSITION',
+ 'POSITION_REGEX',
+ 'POWER',
+ 'PRECISION',
+ 'PREPARE',
+ 'PRIMARY',
+ 'PROCEDURE',
+ 'RANGE',
+ 'RANK',
+ 'READS',
+ 'REAL',
+ 'RECURSIVE',
+ 'REF',
+ 'REFERENCES',
+ 'REFERENCING',
+ 'REGR_AVGX',
+ 'REGR_AVGY',
+ 'REGR_COUNT',
+ 'REGR_INTERCEPT',
+ 'REGR_R2',
+ 'REGR_SLOPE',
+ 'REGR_SXX',
+ 'REGR_SXY',
+ 'REGR_SYY',
+ 'RELEASE',
+ 'RESULT',
+ 'RETURN',
+ 'RETURNS',
+ 'REVOKE',
+ 'RIGHT',
+ 'ROLLBACK',
+ 'ROLLUP',
+ 'ROW',
+ 'ROW_NUMBER',
+ 'ROWS',
+ 'SAVEPOINT',
+ 'SCOPE',
+ 'SCROLL',
+ 'SEARCH',
+ 'SECOND',
+ 'SELECT',
+ 'SENSITIVE',
+ 'SESSION_USER',
+ 'SET',
+ 'SIMILAR',
+ 'SMALLINT',
+ 'SOME',
+ 'SPECIFIC',
+ 'SPECIFICTYPE',
+ 'SQL',
+ 'SQLEXCEPTION',
+ 'SQLSTATE',
+ 'SQLWARNING',
+ 'SQRT',
+ 'START',
+ 'STATIC',
+ 'STDDEV_POP',
+ 'STDDEV_SAMP',
+ 'SUBMULTISET',
+ 'SUBSTRING',
+ 'SUBSTRING_REGEX',
+ 'SUM',
+ 'SYMMETRIC',
+ 'SYSTEM',
+ 'SYSTEM_USER',
+ 'TABLE',
+ 'TABLESAMPLE',
+ 'THEN',
+ 'TIME',
+ 'TIMESTAMP',
+ 'TIMEZONE_HOUR',
+ 'TIMEZONE_MINUTE',
+ 'TO',
+ 'TRAILING',
+ 'TRANSLATE',
+ 'TRANSLATE_REGEX',
+ 'TRANSLATION',
+ 'TREAT',
+ 'TRIGGER',
+ 'TRIM',
+ 'TRUE',
+ 'UESCAPE',
+ 'UNION',
+ 'UNIQUE',
+ 'UNKNOWN',
+ 'UNNEST',
+ 'UPDATE',
+ 'UPPER',
+ 'USER',
+ 'USING',
+ 'VALUE',
+ 'VALUES',
+ 'VAR_POP',
+ 'VAR_SAMP',
+ 'VARBINARY',
+ 'VARCHAR',
+ 'VARYING',
+ 'WHEN',
+ 'WHENEVER',
+ 'WHERE',
+ 'WIDTH_BUCKET',
+ 'WINDOW',
+ 'WITH',
+ 'WITHIN',
+ 'WITHOUT',
+ 'YEAR',
+];
+
+const reservedTopLevelWords = [
+ 'ADD',
+ 'ALTER COLUMN',
+ 'ALTER TABLE',
+ 'CASE',
+ 'DELETE FROM',
+ 'END',
+ 'FETCH FIRST',
+ 'FETCH NEXT',
+ 'FETCH PRIOR',
+ 'FETCH LAST',
+ 'FETCH ABSOLUTE',
+ 'FETCH RELATIVE',
+ 'FROM',
+ 'GROUP BY',
+ 'HAVING',
+ 'INSERT INTO',
+ 'LIMIT',
+ 'ORDER BY',
+ 'SELECT',
+ 'SET SCHEMA',
+ 'SET',
+ 'UPDATE',
+ 'VALUES',
+ 'WHERE',
+];
+
+const reservedTopLevelWordsNoIndent = [
+ 'INTERSECT',
+ 'INTERSECT ALL',
+ 'INTERSECT DISTINCT',
+ 'UNION',
+ 'UNION ALL',
+ 'UNION DISTINCT',
+ 'EXCEPT',
+ 'EXCEPT ALL',
+ 'EXCEPT DISTINCT',
+];
+
+const reservedNewlineWords = [
+ 'AND',
+ 'ELSE',
+ 'OR',
+ 'WHEN',
+ // joins
+ 'JOIN',
+ 'INNER JOIN',
+ 'LEFT JOIN',
+ 'LEFT OUTER JOIN',
+ 'RIGHT JOIN',
+ 'RIGHT OUTER JOIN',
+ 'FULL JOIN',
+ 'FULL OUTER JOIN',
+ 'CROSS JOIN',
+ 'NATURAL JOIN',
+];
+
+class FlinkSqlFormatter extends Formatter {
+ tokenizer() {
+ return new Tokenizer({
+ reservedWords,
+ reservedTopLevelWords,
+ reservedNewlineWords,
+ reservedTopLevelWordsNoIndent,
+ stringTypes: [`""`, "''", '``'],
+ openParens: ['(', 'CASE'],
+ closeParens: [')', 'END'],
+ indexedPlaceholderTypes: ['?'],
+ namedPlaceholderTypes: [],
+ lineCommentTypes: ['--'],
+ });
+ }
+}
+
+/**
+ * Format whitespace in a query to make it easier to read.
+ *
+ * @param {String} query
+ * @param {Object} cfg
+ * @param {String} cfg.indent Characters used for indentation, default is "
" (2 spaces)
+ * @param {Boolean} cfg.uppercase Converts keywords to uppercase
+ * @param {Integer} cfg.linesBetweenQueries How many line breaks between
queries
+ * @param {Object} cfg.params Collection of params for placeholder replacement
+ * @return {String}
+ */
+export const format = (query, config = {}) => {
+ return new FlinkSqlFormatter(config).format(query);
+};
diff --git
a/streampark-console/streampark-console-webapp/src/views/spark/app/styles/View.less
b/streampark-console/streampark-console-webapp/src/views/spark/app/styles/View.less
new file mode 100644
index 000000000..98852052f
--- /dev/null
+++
b/streampark-console/streampark-console-webapp/src/views/spark/app/styles/View.less
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.link {
+ color: #1890ff;
+ text-decoration: none;
+ background-color: transparent;
+ outline: none;
+ transition: color 0.3s;
+}
+
+.ant-upload.ant-upload-drag p.ant-upload-drag-icon .anticon {
+ font-size: 100px;
+}
+
+.ant-alert.ant-alert-no-icon {
+ padding: 6px 15px;
+}
+
+.ant-input-number {
+ width: 100%;
+}
+
+.ant-statistic {
+ line-height: 1.8;
+}
+
+.gutter-box {
+ padding: 10px 15px;
+ background: @component-background;
+ font-size: 14px;
+ font-variant: tabular-nums;
+ line-height: 1.8;
+ list-style: none;
+ font-feature-settings: 'tnum';
+ position: relative;
+ border-radius: 4px;
+ transition: all 0.3s;
+
+ .ant-divider-horizontal {
+ margin: 10px 0;
+ }
+
+ .dash-statistic {
+ .ant-card-body {
+ padding: 8px !important;
+ }
+ }
+
+ .stat-right {
+ float: right;
+
+ .ant-card-body {
+ float: right;
+ }
+ }
+}
+
+.build-badge {
+ float: right;
+ margin-right: 5px;
+ font-size: 12px;
+ transform: scale(0.7, 0.7);
+
+ .ant-badge-count {
+ padding: 0 5px 1px;
+ }
+}
+
+.app_list {
+ .app_type {
+ display: inline-block;
+ text-align: center;
+ vertical-align: middle;
+ margin-right: 5px;
+ line-height: 12px;
+ overflow: hidden;
+ }
+
+ .app_sql {
+ padding: 2px 4px;
+ border: 1px solid #0070cc;
+ /* stylelint-disable-next-line color-function-notation */
+ background: rgba(0, 112, 204, 20%);
+ /* stylelint-disable-next-line color-function-notation */
+ color: rgba(0, 0, 0, 85%);
+ font-size: 14px;
+ transform: scale(0.7);
+ }
+
+ .app_jar {
+ border: 1px solid #06f;
+ padding: 3px 5px;
+ background: #1890ff;
+ color: #f5f5f5;
+ font-size: 14px;
+ transform: scale(0.7);
+ }
+
+ .expanded-table {
+ .ant-table-tbody {
+ tr {
+ border-bottom: none !important;
+ padding: 11px 9px !important;
+
+ td {
+ border-bottom: none !important;
+ padding: 11px 9px !important;
+ }
+
+ th {
+ font-size: 13px;
+ }
+ }
+ }
+ }
+
+ .expand-icon-open {
+ font-size: 10px;
+ }
+
+ .expand-icon-close {
+ font-size: 10px;
+ color: darkgray;
+ }
+
+ .close-deploy {
+ left: 15px;
+ font-size: 8px;
+ font-weight: bold;
+ top: -8px;
+ }
+}
+
+.def-margin-bottom {
+ margin-bottom: 10px;
+}
+
+.running-tag {
+ animation: running-tag-color 800ms ease-out infinite alternate;
+}
+
+@keyframes running-tag-color {
+ 0% {
+ border-color: #3da9f2;
+ box-shadow: 0 0 1px #3da9f2, inset 0 0 2px #3da9f2;
+ }
+
+ 100% {
+ border-color: #3da9f2;
+ box-shadow: 0 0 10px #3da9f2, inset 0 0 5px #3da9f2;
+ }
+}
+
+[data-theme='dark'] {
+ .app_list .app_sql {
+ color: #f4f5f6;
+ }
+
+ .gutter-box {
+ box-shadow: 0 1px 6px #000;
+ }
+}