You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampark.apache.org by be...@apache.org on 2024/09/03 08:35:13 UTC
(incubator-streampark) branch dev updated: [Improve]: spark app update & detail page (#4025)
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 e85e67c5f [Improve]: spark app update & detail page (#4025)
e85e67c5f is described below
commit e85e67c5ffe86b9fb2f8d9fa17c060bb852470a4
Author: Kriszu <39...@users.noreply.github.com>
AuthorDate: Tue Sep 3 16:35:08 2024 +0800
[Improve]: spark app update & detail page (#4025)
---
.../src/main/resources/db/data-h2.sql | 3 +
.../streampark-console-webapp/src/api/spark/app.ts | 2 +-
.../src/api/spark/app.type.ts | 155 +++----
.../src/api/spark/{sql.ts => build.ts} | 36 +-
.../src/api/spark/conf.ts | 31 +-
.../src/api/spark/{sql.ts => conf.type.ts} | 27 +-
.../streampark-console-webapp/src/api/spark/sql.ts | 32 +-
.../src/api/spark/{sql.ts => sql.type.ts} | 30 +-
.../src/enums/sparkEnum.ts | 60 +++
.../src/locales/lang/en/menu.ts | 5 +-
.../src/locales/lang/en/spark/app.ts | 315 +++++++++++++
.../src/locales/lang/zh-CN/menu.ts | 5 +-
.../src/locales/lang/zh-CN/spark/app.ts | 307 +++++++++++++
.../views/spark/app/components/AppDashboard.vue | 50 +--
.../src/views/spark/app/components/AppForm.vue | 121 +++++
.../src/views/spark/app/components/AppLog.vue | 124 ++++++
.../views/spark/app/components/AppStartModal.vue | 244 -----------
.../src/views/spark/app/components/BuildDrawer.vue | 239 ++++++++++
.../views/spark/app/components/CompareModal.vue | 153 +++++++
.../src/views/spark/app/components/DetailTab.vue | 485 +++++++++++++++++++++
.../src/views/spark/app/components/SparkSql.vue | 28 +-
.../spark/app/components/SparkSqlCompareModal.vue | 165 +++++++
.../spark/app/components/SparkSqlDifferent.vue | 136 ++++++
.../views/spark/app/components/SparkSqlReview.vue | 55 +++
.../src/views/spark/app/components/State.tsx | 240 ++++++++++
.../views/spark/app/components/StatisticCard.vue | 3 +
.../src/views/spark/app/create.vue | 97 +----
.../src/views/spark/app/data/detail.data.ts | 126 ++++++
.../src/views/spark/app/detail.vue | 226 ++++++++++
.../src/views/spark/app/{create.vue => edit.vue} | 130 ++----
.../src/views/spark/app/hooks/useAppFormSchema.tsx | 68 ++-
.../spark/app/hooks/useDetailContext.ts} | 26 +-
.../src/views/spark/app/hooks/useSparkAction.tsx | 251 ++++++++++-
.../src/views/spark/app/hooks/useSparkColumns.ts | 53 ++-
.../{useFlinkRender.tsx => useSparkRender.tsx} | 4 +-
.../views/spark/app/hooks/useSparkTableAction.ts | 197 +++++++--
.../src/views/spark/app/index.vue | 170 ++++++--
.../src/views/spark/app/sqlFormatter.js | 408 -----------------
.../src/views/spark/app/styles/View.less | 176 --------
.../src/views/spark/app/utils/index.ts | 205 +++++++++
40 files changed, 3866 insertions(+), 1322 deletions(-)
diff --git a/streampark-console/streampark-console-service/src/main/resources/db/data-h2.sql b/streampark-console/streampark-console-service/src/main/resources/db/data-h2.sql
index 446daec1c..b909b5df5 100644
--- a/streampark-console/streampark-console-service/src/main/resources/db/data-h2.sql
+++ b/streampark-console/streampark-console-service/src/main/resources/db/data-h2.sql
@@ -82,6 +82,9 @@ insert into `t_menu` values (110302, 110300, 'cluster edit', '/flink/edit_cluste
insert into `t_menu` values (120100, 120000, 'spark.application', '/spark/app', 'spark/app/index', null, null, '0', 1, 2, now(), now());
insert into `t_menu` values (120200, 120000, 'spark.sparkHome', '/spark/home', 'spark/home/index', null, null, '0', 1, 3, now(), now());
+insert into `t_menu` values (120300, 120000, 'spark.createApplication', '/spark/app/create', 'spark/app/create', 'app:create', '', '0', 0, null, now(), now());
+insert into `t_menu` values (120400, 120000, 'spark.updateApplication', '/spark/app/edit', 'spark/app/edit', 'app:update', '', '0', 0, null, now(), now());
+insert into `t_menu` values (120500, 120000, 'spark.applicationDetail', '/spark/app/detail', 'spark/app/detail', 'app:detail', '', '0', 0, null, now(), now());
insert into `t_menu` values (130100, 130000, 'resource.project', '/resource/project', 'resource/project/View', null, 'github', '0', 1, 2, now(), now());
insert into `t_menu` values (130200, 130000, 'resource.variable', '/resource/variable', 'resource/variable/View', null, null, '0', 1, 3, now(), now());
diff --git a/streampark-console/streampark-console-webapp/src/api/spark/app.ts b/streampark-console/streampark-console-webapp/src/api/spark/app.ts
index 76b57a746..f4f639eff 100644
--- a/streampark-console/streampark-console-webapp/src/api/spark/app.ts
+++ b/streampark-console/streampark-console-webapp/src/api/spark/app.ts
@@ -119,7 +119,7 @@ export function fetchSparkYarn() {
/**
* check spark name
*/
-export function fetchCheckSparkName(data: { id?: string; jobName: string }) {
+export function fetchCheckSparkName(data: { id?: string; appName: string }) {
return defHttp.post<AppExistsStateEnum>({ url: `${apiPrefix}/check/name`, data });
}
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
index 1bdd20e23..71ca0ac47 100644
--- a/streampark-console/streampark-console-webapp/src/api/spark/app.type.ts
+++ b/streampark-console/streampark-console-webapp/src/api/spark/app.type.ts
@@ -14,138 +14,99 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+import type { AppStateEnum, OptionStateEnum } from '/@/enums/sparkEnum';
+
// dashboard
export interface DashboardResponse {
- totalTM: number;
- task: Task;
- availableSlot: number;
- totalSlot: number;
- runningJob: number;
- tmMemory: number;
- jmMemory: number;
+ numTasks: number;
+ numStages: number;
+ usedMemory: number;
+ usedVCores: number;
+ numCompletedTasks: number;
+ runningApplication: number;
+ numCompletedStages: 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 {
+ createTime?: string;
+ modifyTime?: string;
id?: string;
teamId?: string;
jobType?: number;
appType?: number;
- versionId?: number;
+ versionId?: string;
appName?: string;
executionMode?: number;
resourceFrom?: number;
- module?: any;
+ projectId?: number;
+ module?: string;
mainClass?: string;
jar?: string;
- jarCheckSum?: string;
+ jarCheckSum?: number;
appProperties?: string;
appArgs?: string;
appId?: string;
- yarnQueue?: any;
-
- projectId?: any;
- tags?: any;
- userId?: string;
- jobName?: string;
- jobId?: string;
- clusterId?: string;
- flinkImage?: string;
+ yarnQueue?: string;
+ yarnQueueName?: string;
+ yarnQueueLabel?: string;
+ k8sMasterUrl?: string;
+ k8sContainerImage?: string;
+ k8sImagePullPolicy?: number;
+ k8sServiceAccount?: number;
k8sNamespace?: string;
- state?: number;
- release?: number;
- build?: boolean;
+ hadoopUser?: string;
restartSize?: number;
restartCount?: number;
- optionState?: number;
- alertId?: any;
- args?: string;
+ state?: AppStateEnum;
options?: string;
- hotParams?: string;
- resolveOrder?: number;
- dynamicProperties?: string;
+ optionState?: OptionStateEnum;
+ optionTime?: string;
+ userId?: string;
+ description?: string;
tracking?: number;
-
+ release?: number;
+ build?: boolean;
+ alertId?: 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;
+ duration?: number;
+ tags?: string;
+ driverCores?: string;
+ driverMemory?: string;
+ executorCores?: string;
+ executorMemory?: string;
+ executorMaxNums?: string;
+ numTasks?: number;
+ numCompletedTasks?: number;
+ numStages?: number;
+ numCompletedStages?: number;
+ usedMemory?: number;
+ usedVCores?: number;
+ teamResource?: number;
+ dependency?: string;
+ sqlId?: number;
+ sparkSql?: string;
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;
+ config?: string;
+ configId?: number;
+ sparkVersion?: string;
+ confPath?: string;
+ format?: string;
+ backUpDescription?: string;
+ sparkRestUrl?: string;
buildStatus?: number;
appControl?: AppControl;
- fsOperator?: any;
- workspace?: any;
- k8sPodTemplates?: {
- empty?: boolean;
- };
+ canBeStart?: boolean;
streamParkJob?: boolean;
- hadoopUser?: string;
}
-
interface AppControl {
allowStart: boolean;
allowStop: boolean;
diff --git a/streampark-console/streampark-console-webapp/src/api/spark/sql.ts b/streampark-console/streampark-console-webapp/src/api/spark/build.ts
similarity index 51%
copy from streampark-console/streampark-console-webapp/src/api/spark/sql.ts
copy to streampark-console/streampark-console-webapp/src/api/spark/build.ts
index 8d3ada344..ff0889b0b 100644
--- a/streampark-console/streampark-console-webapp/src/api/spark/sql.ts
+++ b/streampark-console/streampark-console-webapp/src/api/spark/build.ts
@@ -14,17 +14,35 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import type { Result } from '/#/axios';
import { defHttp } from '/@/utils/http/axios';
-const apiPrefix = '/flink/conf';
-export function fetchSparkSqlVerify(data: Recordable) {
- return defHttp.post({ url: `${apiPrefix}/verify`, data }, { isTransformResponse: false });
+const apiPrefix = `/spark/pipe`;
+
+/**
+ * Release application building pipeline.
+ *
+ * @param appId application id
+ * @param forceBuild forced start pipeline or not
+ * @return Whether the pipeline was successfully started
+ */
+export function fetchBuildSparkApp(data: { appId: string; forceBuild: boolean }) {
+ return defHttp.post<Result<boolean>>(
+ { url: `${apiPrefix}/build`, data },
+ { isTransformResponse: false },
+ );
}
-export function fetchSparkSql(data: Recordable) {
- return defHttp.post({
- url: `${apiPrefix}/get`,
- data,
- });
+/**
+ * Get application building pipeline progress detail.
+ *
+ * @param appId application id
+ * @return "pipeline" -> pipeline details, "docker" -> docker resolved snapshot
+ */
+
+export function fetchBuildProgressDetail(appId: number) {
+ return defHttp.post<{
+ pipeline: any;
+ docker: any;
+ }>({ url: `${apiPrefix}/detail`, data: { appId } });
}
diff --git a/streampark-console/streampark-console-webapp/src/api/spark/conf.ts b/streampark-console/streampark-console-webapp/src/api/spark/conf.ts
index e4e0ccb59..6df880b3c 100644
--- a/streampark-console/streampark-console-webapp/src/api/spark/conf.ts
+++ b/streampark-console/streampark-console-webapp/src/api/spark/conf.ts
@@ -14,18 +14,45 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import type { SparkApplication } from './app.type';
+import type { SparkApplicationConfig } from './conf.type';
import { defHttp } from '/@/utils/http/axios';
const apiPrefix = '/flink/conf';
export function fetchGetSparkConf(data: { id: string }) {
- return defHttp.post({ url: `${apiPrefix}/get`, data });
+ return defHttp.post<SparkApplicationConfig>({ url: `${apiPrefix}/get`, data });
}
-export function handleSparkConfTemplate() {
+
+export function fetchSparkConfTemplate() {
return defHttp.post<string>({
url: `${apiPrefix}/template`,
});
}
+
+export function fetchSparkConfList(data: Recordable) {
+ return defHttp.post<{
+ total: number;
+ records: SparkApplicationConfig[];
+ }>({
+ url: `${apiPrefix}/list`,
+ data,
+ });
+}
+
+export function fetchSparkConfHistory(data: SparkApplication) {
+ return defHttp.post<SparkApplicationConfig[]>({
+ url: `${apiPrefix}/history`,
+ data,
+ });
+}
+export function fetchSparkConfRemove(data: { id: number }) {
+ return defHttp.post<boolean>({
+ url: `${apiPrefix}/delete`,
+ data,
+ });
+}
+
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/conf.type.ts
similarity index 69%
copy from streampark-console/streampark-console-webapp/src/api/spark/sql.ts
copy to streampark-console/streampark-console-webapp/src/api/spark/conf.type.ts
index 8d3ada344..542f8cbf7 100644
--- a/streampark-console/streampark-console-webapp/src/api/spark/sql.ts
+++ b/streampark-console/streampark-console-webapp/src/api/spark/conf.type.ts
@@ -14,17 +14,18 @@
* 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,
- });
+export interface SparkApplicationConfig {
+ id?: number;
+ appId?: string;
+ /**
+ * 1)yaml <br>
+ * 2)prop <br>
+ * 3)hocon
+ */
+ format?: 1 | 2 | 3;
+ content?: string;
+ version?: number;
+ latest?: boolean;
+ createTime: string;
+ effective: boolean;
}
diff --git a/streampark-console/streampark-console-webapp/src/api/spark/sql.ts b/streampark-console/streampark-console-webapp/src/api/spark/sql.ts
index 8d3ada344..cee95f2b2 100644
--- a/streampark-console/streampark-console-webapp/src/api/spark/sql.ts
+++ b/streampark-console/streampark-console-webapp/src/api/spark/sql.ts
@@ -15,16 +15,46 @@
* limitations under the License.
*/
+import type { SparkSql } from './sql.type';
import { defHttp } from '/@/utils/http/axios';
-const apiPrefix = '/flink/conf';
+const apiPrefix = '/spark/sql';
export function fetchSparkSqlVerify(data: Recordable) {
return defHttp.post({ url: `${apiPrefix}/verify`, data }, { isTransformResponse: false });
}
+export function fetchSparkSqlList(data: Recordable) {
+ return defHttp.post<{
+ total: number;
+ records: SparkSql[];
+ }>({
+ url: `${apiPrefix}/list`,
+ data,
+ });
+}
+
+export function fetchSparkSqlRemove(data: SparkSql) {
+ return defHttp.post<boolean>({
+ url: `${apiPrefix}/delete`,
+ data,
+ });
+}
+
export function fetchSparkSql(data: Recordable) {
return defHttp.post({
url: `${apiPrefix}/get`,
data,
});
}
+export function fetchSparkSqlHistory(data: Recordable) {
+ return defHttp.post<SparkSql[]>({
+ url: `${apiPrefix}/history`,
+ data,
+ });
+}
+export function fetchSparkSqlComplete(data: Recordable) {
+ return defHttp.post({
+ url: `${apiPrefix}/sqlComplete`,
+ data,
+ });
+}
diff --git a/streampark-console/streampark-console-webapp/src/api/spark/sql.ts b/streampark-console/streampark-console-webapp/src/api/spark/sql.type.ts
similarity index 64%
copy from streampark-console/streampark-console-webapp/src/api/spark/sql.ts
copy to streampark-console/streampark-console-webapp/src/api/spark/sql.type.ts
index 8d3ada344..0a45780bb 100644
--- a/streampark-console/streampark-console-webapp/src/api/spark/sql.ts
+++ b/streampark-console/streampark-console-webapp/src/api/spark/sql.type.ts
@@ -14,17 +14,21 @@
* 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,
- });
+export interface SparkSql {
+ id?: number;
+ appId?: string;
+ sql?: string;
+ teamResource?: string;
+ dependency?: string;
+ version?: number;
+ /**
+ * candidate number: 0: none candidate <br>
+ * 1: newly added record becomes a candidate <br>
+ * 2: specific history becomes a candidate <br>
+ */
+ candidate?: number;
+ createTime?: string;
+ effective?: boolean;
+ sqlDifference?: boolean;
+ dependencyDifference?: boolean;
}
diff --git a/streampark-console/streampark-console-webapp/src/enums/sparkEnum.ts b/streampark-console/streampark-console-webapp/src/enums/sparkEnum.ts
index a1d4fbd20..cf40d222e 100644
--- a/streampark-console/streampark-console-webapp/src/enums/sparkEnum.ts
+++ b/streampark-console/streampark-console-webapp/src/enums/sparkEnum.ts
@@ -11,6 +11,16 @@ export enum JobTypeEnum {
SQL = 2,
PYSPARK = 3,
}
+export enum OptionStateEnum {
+ /** Application which is currently action: none. */
+ NONE = 0,
+ /** Application which is currently action: deploying. */
+ RELEASING = 1,
+ /** Application which is currently action: starting. */
+ STARTING = 2,
+ /** Application which is currently action: stopping. */
+ STOPPING = 3,
+}
/* ExecutionMode */
export enum ExecModeEnum {
@@ -38,3 +48,53 @@ export enum AppExistsStateEnum {
/** job name invalid because of special utf-8 character */
INVALID,
}
+
+export enum AppStateEnum {
+ /** Added new job to database. */
+ ADDED = 0,
+
+ /** (From Yarn)Application which was just created. */
+ NEW = 1,
+
+ /** (From Yarn)Application which is being saved. */
+ NEW_SAVING = 2,
+
+ /** Application which is currently running. */
+ STARTING = 3,
+
+ /** (From Yarn)Application which has been submitted. */
+ SUBMITTE = 4,
+
+ /** (From Yarn)Application has been accepted by the scheduler. */
+ ACCEPTE = 5,
+
+ /** The job has failed and is currently waiting for the cleanup to complete. */
+ RUNNIN = 6,
+
+ /** (From Yarn)Application which finished successfully. */
+ FINISHE = 7,
+
+ /** (From Yarn)Application which failed. */
+ FAILE = 8,
+
+ /** Loss of mapping. */
+ LOS = 9,
+
+ /** Mapping. */
+ MAPPING = 10,
+
+ /** Other statuses. */
+ OTHER = 11,
+
+ /** Has rollback. */
+ REVOKED = 12,
+
+ /** Spark job has being cancelling(killing) by streampark */
+ STOPPING = 13,
+
+ /** Job SUCCEEDED on yarn. */
+ SUCCEEDED = 14,
+
+ /** Has killed in Yarn. */
+ KILLED = -9,
+}
diff --git a/streampark-console/streampark-console-webapp/src/locales/lang/en/menu.ts b/streampark-console/streampark-console-webapp/src/locales/lang/en/menu.ts
index ee876c06d..0238263aa 100644
--- a/streampark-console/streampark-console-webapp/src/locales/lang/en/menu.ts
+++ b/streampark-console/streampark-console-webapp/src/locales/lang/en/menu.ts
@@ -36,7 +36,10 @@ export default {
},
spark: {
application: 'Applications',
- sparkHome: 'Spark Home'
+ sparkHome: 'Spark Home',
+ createApplication: 'Create Application',
+ updateApplication: 'Update Application',
+ applicationDetail: 'Application Detail',
},
setting: {
menu: 'Settings',
diff --git a/streampark-console/streampark-console-webapp/src/locales/lang/en/spark/app.ts b/streampark-console/streampark-console-webapp/src/locales/lang/en/spark/app.ts
new file mode 100644
index 000000000..762d9d0f2
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/locales/lang/en/spark/app.ts
@@ -0,0 +1,315 @@
+/*
+ * 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.
+ */
+export default {
+ dashboard: {
+ runningTasks: 'Number of Running Applications',
+ totalTask: 'Total Tasks',
+ totalStage: 'Total Stages',
+ completedTask: 'Total Completed Tasks',
+ completedStage: 'Total Completed Stages',
+ memory: 'Total Memory Used',
+ VCore: 'Total VCores Used',
+ },
+ runState: {
+ added: 'New Applications',
+ saving: 'Saving',
+ created: 'Creating',
+ starting: 'Starting',
+ submitted: 'Submitted',
+ accept: 'Accepted',
+ running: 'Running',
+ finished: 'Completed',
+ failed: 'Job Failed',
+ lost: 'Job Lost',
+ mapping: 'Mapping',
+ other: 'Other',
+ revoked: 'Revoked',
+ stopping: 'Stopping',
+ killed: 'Terminated',
+ },
+ releaseState: {
+ releasing: 'Releasing',
+ failed: 'Release Failed',
+ success: 'Release Successful',
+ waiting: 'Waiting to Release',
+ pending: 'Pending Rollback',
+ },
+ id: 'Applications ID',
+ appName: 'Applications Name',
+ searchName: 'Applications Name',
+ tags: 'Applications Tags',
+ owner: 'Creator',
+ sparkVersion: 'Spark Version',
+ duration: 'Runtime',
+ modifiedTime: 'Modification Time',
+ runStatus: 'Run Status',
+ releaseBuild: 'Release Status',
+ jobType: 'Job Type',
+ developmentMode: 'development Mode',
+ executionMode: 'Execution Mode',
+ historyVersion: 'History Version',
+ resource: 'Resource',
+ resourcePlaceHolder: 'Select Resource',
+ selectAppPlaceHolder: 'Select Applications',
+ dependency: 'Job Dependency',
+ appConf: 'Applications Configuration',
+ resolveOrder: 'Class Loading Order',
+ parallelism: 'Parallelism',
+ restartSize: '(On Failure) Restart Count',
+ faultAlertTemplate: 'Alert Template',
+ checkPointFailureOptions: 'Checkpoint Alert Strategy',
+ totalMemoryOptions: 'Total Memory',
+ jmMemoryOptions: 'JM Memory',
+ tmMemoryOptions: 'TM Memory',
+ podTemplate: 'Kubernetes Pod Template',
+ flinkCluster: 'Flink Cluster',
+ yarnQueue: 'Yarn Queue',
+ mavenPom: 'Maven POM',
+ uploadJar: 'Upload Dependency Jar File',
+ kubernetesNamespace: 'Kubernetes Namespace',
+ kubernetesClusterId: 'Kubernetes Cluster ID',
+ flinkBaseDockerImage: 'Flink Base Docker Image',
+ restServiceExposedType: 'Rest-Service Exposed Type',
+ resourceFrom: 'Resource Source',
+ uploadJobJar: 'Upload Jar File',
+ selectJobJar: 'Select Jar File',
+ mainClass: 'Main Entry Class',
+ project: 'Project',
+ module: 'Module',
+ appType: 'Job Type',
+ programArgs: 'Program Arguments',
+ programJar: 'Program Jar File',
+ dynamicProperties: 'Dynamic Parameters',
+ hadoopConfigTitle: 'System Hadoop',
+ dragUploadTitle: 'Click or Drag jar or py to this area to upload',
+ dragUploadTip: 'Supports single upload. You can upload local jar here to support the current job',
+ dependencyError: 'Please check the Flink version first.',
+ status: 'Run Status',
+ startTime: 'Start Time',
+ endTime: 'End Time',
+ hadoopUser: 'Hadoop User',
+ restoreModeTip:
+ 'Flink 1.15 and later supports restore mode; generally, this parameter does not need to be set',
+ success: 'Submission Successful',
+ appidCheck: 'appId cannot be empty!',
+ release: {
+ releaseTitle: 'This job is currently starting.',
+ releaseDesc: 'Are you sure you want to force another build?',
+ releaseFail: 'Job release failed',
+ releasing: 'Current job is being released',
+ },
+
+ clusterState: {
+ created: 'New',
+ started: 'Running',
+ canceled: 'Stopped',
+ lost: 'Lost',
+ },
+ detail: {
+ detailTitle: 'Job Details',
+ flinkWebUi: 'Flink Web UI',
+ compareConfig: 'Compare Configuration',
+ compareSparkSql: 'Compare Spark SQL',
+ candidate: 'Candidate',
+ compare: 'Compare',
+ compareSelectTips: 'Please select the target version',
+ resetApi: 'Open API',
+ copyCurl: 'Copy CURL',
+ apiTitle: 'API Details',
+ resetApiToolTip: 'OPEN API, third-party systems can easily integrate with StreamPark',
+ copyStartcURL: 'Job Start',
+ copyCancelcURL: 'Job Stop',
+ apiDocCenter: 'API Documentation',
+ nullAccessToken: 'Access token does not exist, please contact the administrator to add',
+ invalidAccessToken: 'Access token is invalid, please contact the administrator',
+ invalidTokenUser: 'Current user has been locked, please contact the administrator',
+ detailTab: {
+ detailTabName: {
+ option: 'Options',
+ configuration: 'Configuration',
+ sparkSql: 'Spark SQL',
+ backup: 'Backup',
+ operationLog: 'Operation Log',
+ },
+ configDetail: 'View Configuration Details',
+ sqlDetail: 'View SQL Details',
+ confDeleteTitle: 'Are you sure you want to delete this record?',
+ sqlDeleteTitle: 'Are you sure you want to delete this SQL?',
+ confBackupTitle: 'Are you sure you want to delete this backup?',
+ operationLogDeleteTitle: 'Are you sure you want to delete this operation record?',
+ copyPath: 'Copy Path',
+ pointDeleteTitle: 'Are you sure you want to delete?',
+ copySuccess: 'Successfully copied to clipboard',
+ copyFail: 'Copy failed',
+ check: 'CheckPoint',
+ save: 'SavePoint',
+ exception: 'View Exception',
+ },
+ different: {
+ original: 'Original Version',
+ target: 'Target Version',
+ },
+ exceptionModal: {
+ title: 'Exception Information',
+ },
+ },
+ view: {
+ buildTitle: 'Job Start Progress',
+ stepTitle: 'Step Details',
+ errorLog: 'Error Log',
+ errorSummary: 'Error Summary',
+ errorStack: 'Error Stack',
+ logTitle: 'Start Log: Job Name [ {0} ]',
+ refreshTime: 'Last Refresh Time',
+ refresh: 'Refresh',
+ start: 'Start Job',
+ stop: 'Stop Job',
+ recheck: 'The associated project has been updated and needs to re-release this job',
+ changed: 'Job has been updated',
+ ignoreRestored: 'Ignore Failure',
+ ignoreRestoredTip:
+ 'Skip errors when the state restore fails, allowing the job to continue running, same parameter: -allowNonRestoredState(-n)',
+ },
+ pod: {
+ choice: 'Select',
+ init: 'Initialization Content',
+ host: 'Host Alias',
+ },
+ sparkSql: {
+ preview: 'Content Preview',
+ verify: 'Verify',
+ format: 'Format',
+ fullScreen: 'Full Screen',
+ exit: 'Exit',
+ successful: 'Verification Successful',
+ compare: 'Compare',
+ version: 'Version',
+ compareSparkSQL: 'Select Compare Version',
+ compareVersionPlaceholder: 'Please select the SQL version to compare',
+ effectiveVersion: 'Current Effective Version',
+ candidateVersion: 'Candidate Compare Version',
+ },
+ operation: {
+ edit: 'Edit Job',
+ release: 'Release Job',
+ releaseDetail: 'Release Details',
+ start: 'Start Job',
+ cancel: 'Cancel Job',
+ detail: 'Job Details',
+ startLog: 'View Flink Start Log',
+ abort: 'Terminate Job',
+ copy: 'Copy Job',
+ remapping: 'Remap Job',
+ deleteTip: 'Are you sure you want to delete this job?',
+ canceling: 'Current job is stopping',
+ starting: 'Current job is starting',
+ },
+
+ runStatusOptions: {
+ added: 'New',
+ starting: 'Starting',
+ running: 'Running',
+ failed: 'Failed',
+ canceled: 'Canceled',
+ finished: 'Completed',
+ suspended: 'Suspended',
+ lost: 'Lost',
+ silent: 'Silent',
+ terminated: 'Terminated',
+ },
+
+ addAppTips: {
+ developmentModePlaceholder: 'Please select development mode',
+ developmentModeIsRequiredMessage: 'Development mode is required',
+ executionModePlaceholder: 'Please select execution mode',
+ executionModeIsRequiredMessage: 'Execution mode is required',
+ hadoopEnvInitMessage: 'Hadoop environment check failed, please check configuration',
+ resourceFromMessage: 'Resource source is required',
+ mainClassPlaceholder: 'Please enter the main entry class',
+ mainClassIsRequiredMessage: 'Main entry class is required',
+ projectPlaceholder: 'Please select a project',
+ projectIsRequiredMessage: 'Project is required',
+ projectModulePlaceholder: 'Please select the project module',
+ appTypePlaceholder: 'Please select job type',
+ appTypeIsRequiredMessage: 'Job type is required',
+ programJarIsRequiredMessage: 'Program jar file is required',
+ useSysHadoopConf: 'Use system Hadoop configuration',
+ flinkVersionIsRequiredMessage: 'Flink version is required',
+ appNamePlaceholder: 'Please enter job name',
+ appNameIsRequiredMessage: 'Job name is required',
+ appNameNotUniqueMessage: 'Job name must be unique; the entered job name already exists',
+ appNameExistsInYarnMessage:
+ 'Job name already exists in the YARN cluster and cannot be duplicated. Please check',
+ appNameExistsInK8sMessage:
+ 'The job name already exists in the Kubernetes cluster and cannot be duplicated. Please check',
+ appNameValid: 'Job name is invalid',
+ appNameRole: 'Job name must follow the following rules:',
+ K8sSessionClusterIdRole: 'Kubernetes Cluster ID must follow the following rules:',
+ appNameK8sClusterIdRole:
+ 'When the deployment mode is Kubernetes Application mode, the job name will be used as the Kubernetes cluster ID, so the job name must follow the following rules:',
+ appNameK8sClusterIdRoleLength: 'Should not exceed 45 characters',
+ appNameK8sClusterIdRoleRegexp:
+ 'Can only consist of lowercase letters, numbers, characters, and "-" and must satisfy the regex format [a-z]([-a-z0-9]*[a-z0-9])',
+ appNameRoleContent:
+ 'Characters must be (Chinese or English or "-" or "_") and cannot have two consecutive spaces',
+ appNameNotValid:
+ 'Job name is invalid. Characters must be (Chinese or English or "-" or "_") and cannot have two consecutive spaces. Please check',
+ flinkClusterIsRequiredMessage: 'Flink cluster is required',
+ sparkSqlIsRequiredMessage: 'Spark SQL is required',
+ tagsPlaceholder: 'Please enter tags, if more than one, separate with commas (,)',
+ parallelismPlaceholder: 'Parallelism for running the program',
+ slotsOfPerTaskManagerPlaceholder: 'Number of slots per TaskManager',
+ restartSizePlaceholder: 'Maximum restart count',
+ alertTemplatePlaceholder: 'Alert Template',
+ totalMemoryOptionsPlaceholder: 'Please select the resource parameters to set',
+ tmPlaceholder: 'Please select the resource parameters to set',
+ yarnQueuePlaceholder: 'Please enter the YARN queue label name',
+ descriptionPlaceholder: 'Please enter a description for this job',
+ kubernetesNamespacePlaceholder: 'Please enter Kubernetes namespace, e.g., default',
+ kubernetesClusterIdPlaceholder: 'Please select Kubernetes Cluster ID',
+ kubernetesClusterIdRequire:
+ 'Lowercase letters, numbers, "-", and must start and end with alphanumeric characters, and not exceed 45 characters',
+ kubernetesClusterIdIsRequiredMessage: 'Kubernetes Cluster ID is required',
+ flinkImagePlaceholder:
+ 'Please enter the tag of the Flink base Docker image, e.g., flink:1.13.0-scala_2.11-java8',
+ flinkImageIsRequiredMessage: 'Flink base Docker image is required',
+ k8sRestExposedTypePlaceholder: 'Kubernetes Rest-Service Exposed Type',
+ hadoopXmlConfigFileTips:
+ 'Automatically copy configuration file from system environment parameters',
+ dynamicPropertiesPlaceholder:
+ '$key=$value; if there are multiple parameters, you can input them on separate lines (-D <arg>)',
+ sqlCheck: 'SQL Check Error',
+ },
+
+ noteInfo: {
+ note: 'Note',
+ minute: 'Minute',
+ count: 'Count',
+ officialDoc: 'Official Documentation',
+ checkPointFailureOptions: 'Checkpoint Failure Strategy',
+ checkpointFailureRateInterval: 'Checkpoint Failure Interval',
+ maxFailuresPerInterval: 'Maximum Failure Count',
+ checkPointFailureNote:
+ 'Checkpoint failure handling strategy, e.g., within 5 minutes (checkpoint failure interval), if the number of checkpoint failures exceeds 10 (maximum checkpoint failure count), it will trigger an operation (send an alert or restart the job)',
+ totalMemoryNote:
+ 'It is not recommended to configure both "total process memory" and "total Flink memory" at the same time. Due to potential memory configuration conflicts, it may lead to deployment failures. Configuring other memory components also requires caution, as it may produce further configuration conflicts; the simplest approach is to set "total process memory"',
+ dynamicProperties:
+ 'Dynamic Properties: Works the same as -D$property=$value in Flink run command line mode, allowing multiple parameters to be specified. For more configurable parameters, please refer to the documentation',
+ yarnQueue:
+ 'This input not only supports quickly setting "yarn.application.name" but also supports setting "yarn.application.node-label". For example, entering "queue1" sets the value of "yarn.application.name" to "queue1", while entering "queue1{\'@\'}label1,label2" sets "yarn.application.name" to "queue1" and "yarn.application.node-label" to "label1,label2". Queue and label are separated by {\'@\'}.',
+ },
+};
diff --git a/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/menu.ts b/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/menu.ts
index fec0d8ac2..50e933e5f 100644
--- a/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/menu.ts
+++ b/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/menu.ts
@@ -36,7 +36,10 @@ export default {
},
spark: {
application: '作业管理',
- flinkHome: 'Spark 版本'
+ flinkHome: 'Spark 版本',
+ createApplication: '创建应用',
+ updateApplication: '更新应用',
+ applicationDetail: '应用详情',
},
setting: {
menu: '设置中心',
diff --git a/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/spark/app.ts b/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/spark/app.ts
new file mode 100644
index 000000000..6d69f8176
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/spark/app.ts
@@ -0,0 +1,307 @@
+/*
+ * 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.
+ */
+export default {
+ dashboard: {
+ runningTasks: '运行中的应用数',
+ totalTask: 'Task 总数',
+ totalStage: 'Stage 总数',
+ completedTask: '完成的 Task 总数',
+ completedStage: '完成的 Stage 总数',
+ memory: '占用的总内存',
+ VCore: '占用的总VCores',
+ },
+ runState: {
+ added: '新增应用',
+ saving: '保存中',
+ created: '创建中',
+ starting: '启动中',
+ submitted: '已提交',
+ accept: '已接受',
+ running: '运行中',
+ finished: '已完成',
+ failed: '作业失败',
+ lost: '作业失联',
+ mapping: '映射中',
+ other: '其他',
+ revoked: '回滚',
+ stopping: '停止中',
+ killed: '终止',
+ },
+ releaseState: {
+ releasing: '发布中',
+ failed: '发布失败',
+ success: '发布成功',
+ waiting: '待发布',
+ pending: '待回滚',
+ },
+ id: '应用ID',
+ appName: '应用名称',
+ searchName: '应用名称',
+ tags: '应用标签',
+ owner: '创建者',
+ sparkVersion: 'Spark版本',
+ duration: '运行时长',
+ modifiedTime: '修改时间',
+ runStatus: '运行状态',
+ releaseBuild: '发布状态',
+ jobType: '作业类型',
+ developmentMode: '开发模式',
+ executionMode: '执行模式',
+ historyVersion: '历史版本',
+ resource: '资源',
+ resourcePlaceHolder: '从选择资源',
+ selectAppPlaceHolder: '选择应用',
+ dependency: '作业依赖',
+ appConf: '应用配置',
+ resolveOrder: '类加载顺序',
+ parallelism: '并行度',
+ restartSize: '(失败后)重启次数',
+ faultAlertTemplate: '告警模板',
+ checkPointFailureOptions: 'Checkpoint告警策略',
+ totalMemoryOptions: '总内存',
+ jmMemoryOptions: 'JM内存',
+ tmMemoryOptions: 'TM内存',
+ podTemplate: 'Kubernetes Pod 模板',
+ flinkCluster: 'Flink集群',
+ yarnQueue: 'Yarn队列',
+ mavenPom: 'maven pom',
+ uploadJar: '上传依赖Jar文件',
+ kubernetesNamespace: 'Kubernetes 命名空间',
+ kubernetesClusterId: 'Kubernetes ClusterId',
+ flinkBaseDockerImage: 'Flink 基础docker镜像',
+ restServiceExposedType: 'Rest-Service Exposed Type',
+ resourceFrom: '资源来源',
+ uploadJobJar: '上传jar文件',
+ selectJobJar: '选择jar文件',
+ mainClass: '程序入口类',
+ project: '项目',
+ module: '模块',
+ appType: '作业类型',
+ programArgs: '程序参数',
+ programJar: '程序Jar文件',
+ dynamicProperties: '动态参数',
+ hadoopConfigTitle: '系统 Hadoop',
+ dragUploadTitle: '单击或拖动 jar或py 到此区域以上传',
+ dragUploadTip: '支持单次上传。您可以在此处上传本地 jar 以支持当前作业',
+ dependencyError: '请先检查flink 版本.',
+ status: '运行状态',
+ startTime: '启动时间',
+ endTime: '结束时间',
+ hadoopUser: 'Hadoop User',
+ restoreModeTip: 'flink 1.15开始支持restore模式,一般情况下不用设置该参数',
+ success: '提交成功',
+ appidCheck: 'appId 不能为空!',
+ release: {
+ releaseTitle: '该作业正在启动中.',
+ releaseDesc: '您确定要强制进行另一次构建吗',
+ releaseFail: '发布作业失败',
+ releasing: '当前作业正在发布中',
+ },
+
+ clusterState: {
+ created: '新增',
+ started: '运行中',
+ canceled: '停止',
+ lost: '失联',
+ },
+ detail: {
+ detailTitle: '应用详情',
+ flinkWebUi: 'Flink Web UI',
+ compareConfig: '比较配置',
+ compareSparkSql: '比较 Spark SQL',
+ candidate: '侯选',
+ compare: '比较',
+ compareSelectTips: '请选择目标板本',
+ resetApi: 'Open API',
+ copyCurl: '复制 CURL',
+ apiTitle: 'Api 详情',
+ resetApiToolTip: 'OPEN API,第三方系统可轻松对接 StreamPark',
+ copyStartcURL: '作业启动',
+ copyCancelcURL: '作业停止',
+ apiDocCenter: 'Api文档',
+ nullAccessToken: '访问令牌不存在,请联系管理员添加',
+ invalidAccessToken: '访问令牌无效,请联系管理员',
+ invalidTokenUser: '当前用户已被锁定,请联系管理员',
+ detailTab: {
+ detailTabName: {
+ option: '选项',
+ configuration: '配置',
+ sparkSql: 'Spark SQL',
+ backup: '备份',
+ operationLog: '操作日志',
+ },
+ configDetail: '查看配置详情',
+ sqlDetail: '查看 SQL 详情',
+ confDeleteTitle: '您确定要删除此记录吗?',
+ sqlDeleteTitle: '您确定要删除此 SQL 吗?',
+ confBackupTitle: '您确定要删除该备份吗?',
+ operationLogDeleteTitle: '您确定要删除该操作记录吗?',
+ copyPath: '复制路径',
+ pointDeleteTitle: '您确定要删除?',
+ copySuccess: '已成功复制到剪贴板',
+ copyFail: '复制失败',
+ check: 'CheckPoint',
+ save: 'SavePoint',
+ exception: '查看异常',
+ },
+ different: {
+ original: '原始版本',
+ target: '目标版本',
+ },
+ exceptionModal: {
+ title: '异常信息',
+ },
+ },
+ view: {
+ buildTitle: '作业启动进度',
+ stepTitle: '步骤详情',
+ errorLog: '错误日志',
+ errorSummary: '错误摘要',
+ errorStack: '错误堆栈',
+ logTitle: '启动日志 : 作业名称 [ {0} ]',
+ refreshTime: '上次刷新时间',
+ refresh: '刷新',
+ start: '启动作业',
+ stop: '停止作业',
+ recheck: '关联的项目已更新,需要重新发布此作业',
+ changed: '作业已更新',
+ ignoreRestored: '忽略失败',
+ ignoreRestoredTip: '当状态恢复失败时跳过错误,作业继续运行, 同参数:-allowNonRestoredState(-n)',
+ },
+ pod: {
+ choice: '选择',
+ init: '初始化内容',
+ host: 'Host别名',
+ },
+ sparkSql: {
+ preview: '内容预览',
+ verify: '验证',
+ format: '格式化',
+ fullScreen: '全屏',
+ exit: '退出',
+ successful: '验证成功',
+ compare: '比较',
+ version: '版本',
+ compareSparkSQL: '选择比对版本',
+ compareVersionPlaceholder: '请选择要比较的sql版本',
+ effectiveVersion: '当前生效版本',
+ candidateVersion: '候选比对版本',
+ },
+ operation: {
+ edit: '编辑作业',
+ release: '发布作业',
+ releaseDetail: '发布详情',
+ start: '启动作业',
+ cancel: '取消作业',
+ detail: '作业详情',
+ startLog: '查看 Flink 启动日志',
+ abort: '终止作业',
+ copy: '复制作业',
+ remapping: '重新映射作业',
+ deleteTip: '你确定要删除这个作业?',
+ canceling: '当前作业正在停止中',
+ starting: '当前作业正在启动中',
+ },
+
+ runStatusOptions: {
+ added: '新增',
+ starting: '启动中',
+ running: '运行中',
+ failed: '失败',
+ canceled: '已取消',
+ finished: '已完成',
+ suspended: '暂停',
+ lost: '丢失',
+ silent: '静默',
+ terminated: '终止',
+ },
+
+ addAppTips: {
+ developmentModePlaceholder: '请选择开发模式',
+ developmentModeIsRequiredMessage: '开发模式必填',
+ executionModePlaceholder: '请选择执行模式',
+ executionModeIsRequiredMessage: '执行模式必填',
+ hadoopEnvInitMessage: 'hadoop环境检查失败, 请检查配置',
+ resourceFromMessage: '资源来源必填',
+ mainClassPlaceholder: '请输入程序入口类',
+ mainClassIsRequiredMessage: '入程序入口类必填',
+ projectPlaceholder: '请选择项目',
+ projectIsRequiredMessage: '项目必填',
+ projectModulePlaceholder: '请选择项目的模块',
+ appTypePlaceholder: '请选择作业类型',
+ appTypeIsRequiredMessage: '作业类型必填',
+ programJarIsRequiredMessage: '程序jar文件必填',
+ useSysHadoopConf: '使用系统hadoop配置',
+ flinkVersionIsRequiredMessage: 'Flink版本必填',
+ appNamePlaceholder: '请输入作业名称',
+ appNameIsRequiredMessage: '作业名称必填',
+ appNameNotUniqueMessage: '作业名称必须唯一, 输入的作业名称已经存在',
+ appNameExistsInYarnMessage: '作业名称已经在YARN集群中存在,不能重复。请检查',
+ appNameExistsInK8sMessage: '该作业名称已经在 Kubernetes 集群中存在,不能重复。请检查',
+ appNameValid: '作业名称不合法',
+ appNameRole: '作业必须遵循以下规则:',
+ K8sSessionClusterIdRole: 'Kubernetes 集群ID必要遵循以下规则:',
+ appNameK8sClusterIdRole:
+ '当前部署模式是 Kubernetes Application 模式,会将作业名称作为 Kubernetes 的 clusterId,因此作业名称要遵循以下规则:',
+ appNameK8sClusterIdRoleLength: '不应超过 45 个字符',
+ appNameK8sClusterIdRoleRegexp:
+ '只能由小写字母、数字、字符、和"-" 组成,必须满足正则格式 [a-z]([-a-z0-9]*[a-z0-9])',
+ appNameRoleContent: '字符必须是(中文 或 英文 或 "-" 或 "_"),不能出现两个连续的空格',
+ appNameNotValid:
+ '作业名称无效。字符必须是(中文 或 英文 或 "-" 或 "_"),不能出现两个连续的空格,请检查',
+ flinkClusterIsRequiredMessage: 'Flink集群必填',
+ sparkSqlIsRequiredMessage: 'Spark SQL必填',
+ tagsPlaceholder: '请输入标签,如果超过一个,用逗号(,)分隔',
+ parallelismPlaceholder: '运行程序的并行度',
+ slotsOfPerTaskManagerPlaceholder: '每个TaskManager的插槽数',
+ restartSizePlaceholder: '最大重启次数',
+ alertTemplatePlaceholder: '告警模板',
+ totalMemoryOptionsPlaceholder: '请选择要设置的资源参数',
+ tmPlaceholder: '请选择要设置的资源参数',
+ yarnQueuePlaceholder: '请输入yarn队列标签名称',
+ descriptionPlaceholder: '请输入此作业的描述',
+ kubernetesNamespacePlaceholder: '请输入 Kubernetes 命名空间, 如: default',
+ kubernetesClusterIdPlaceholder: '请选择 Kubernetes ClusterId',
+ kubernetesClusterIdRequire:
+ '小写字母、数字、“-”,并且必须以字母数字字符开头和结尾,并且不超过45个字符',
+ kubernetesClusterIdIsRequiredMessage: 'Kubernetes ClusterId 必填',
+ flinkImagePlaceholder: '请输入Flink基础docker镜像的标签,如:flink:1.13.0-scala_2.11-java8',
+ flinkImageIsRequiredMessage: 'Flink基础docker镜像是必填的',
+ k8sRestExposedTypePlaceholder: 'Kubernetes Rest-Service Exposed Type',
+ hadoopXmlConfigFileTips: '从系统环境参数自动复制配置文件',
+ dynamicPropertiesPlaceholder: '$key=$value,如果有多个参数,可以换行输入(-D <arg>)',
+ sqlCheck: 'SQL 检查错误',
+ },
+
+ noteInfo: {
+ note: '提示',
+ minute: '分钟',
+ count: '次数',
+ officialDoc: '官网文档',
+ checkPointFailureOptions: 'CheckPoint失败策略',
+ checkpointFailureRateInterval: 'checkpoint失败间隔',
+ maxFailuresPerInterval: '最大失败次数',
+ checkPointFailureNote:
+ 'checkpoint 失败处理策略, 例如: 在 5 分钟内 (checkpoint的失败间隔), 如果 checkpoint 失败次数超过 10 次 (checkpoint最大失败次数),会触发操作(发送告警或者重启作业)',
+ totalMemoryNote:
+ '不建议同时配置 "total process memory" 和 "total Flink memory"。 由于潜在的内存配置冲突,它可能会导致部署失败。 配置其他内存组件也需要谨慎,因为它会产生进一步的配置冲突,最简单的方法是设置"total process memory"',
+ dynamicProperties:
+ '动态Properties: 与flink run命令行模式下的 -D$property=$value 的作用相同,允许指定多个参数。 更多可以设置的参数请查阅',
+ yarnQueue:
+ '此输入不仅支持快速设置 "yarn.application.name" 还支持设置 "yarn.application.node-label"。例如,输入 "queue1" 表示 "yarn.application.name" 的值为 "queue1",而输入 "queue1{\'@\'}label1,label2" 则表示 "yarn.application.name" 设置为 "queue1" 且 "yarn.application.node-label" 设置为 "label1,label2"。Queue 和 label 之间使用 {\'@\'} 分隔。',
+ },
+};
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
index bd97f7cb3..6c9be6b40 100644
--- 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
@@ -32,40 +32,38 @@
const res = await fetchSparkDashboard();
if (res) {
Object.assign(dashBigScreenMap, {
- runningJob: {
- staticstics: { title: t('flink.app.dashboard.runningJobs'), value: res.runningJob },
+ runningTask: {
+ statistics: {
+ title: t('spark.app.dashboard.runningTasks'),
+ value: res.runningApplication,
+ },
footer: [
- { title: t('flink.app.dashboard.totalTask'), value: res?.task?.total || 0 },
- { title: t('flink.app.dashboard.runningTask'), value: res?.task?.running || 0 },
+ { title: t('spark.app.dashboard.totalTask'), value: res?.numTasks || 0 },
+ { title: t('spark.app.dashboard.totalStage'), value: res?.numStages || 0 },
],
},
- availiableTask: {
- staticstics: {
- title: t('flink.app.dashboard.availableTaskSlots'),
- value: res.availableSlot,
+ completedTask: {
+ statistics: {
+ title: t('spark.app.dashboard.completedTask'),
+ value: res.numCompletedTasks,
},
footer: [
- { title: t('flink.app.dashboard.taskSlots'), value: res.totalSlot },
- { title: t('flink.app.dashboard.taskManagers'), value: res.totalTM },
+ { title: t('spark.app.dashboard.completedStage'), value: res.numCompletedStages },
],
},
- jobManager: {
- staticstics: { title: t('flink.app.dashboard.jobManagerMemory'), value: res.jmMemory },
- footer: [
- {
- title: t('flink.app.dashboard.totalJobManagerMemory'),
- value: `${res.jmMemory} MB`,
- },
- ],
+ memory: {
+ statistics: {
+ title: t('spark.app.dashboard.memory'),
+ value: `${res.usedMemory} MB`,
+ },
+ footer: [],
},
taskManager: {
- staticstics: { title: t('flink.app.dashboard.taskManagerMemory'), value: res.tmMemory },
- footer: [
- {
- title: t('flink.app.dashboard.totalTaskManagerMemory'),
- value: `${res.tmMemory} MB`,
- },
- ],
+ statistics: {
+ title: t('spark.app.dashboard.VCore'),
+ value: res.usedVCores,
+ },
+ footer: [],
},
});
}
@@ -92,7 +90,7 @@
:key="key"
>
<StatisticCard
- :statisticProps="value.staticstics"
+ :statisticProps="value.statistics"
:footerList="value.footer"
:loading="dashboardLoading"
/>
diff --git a/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppForm.vue b/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppForm.vue
new file mode 100644
index 000000000..a8af51c75
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppForm.vue
@@ -0,0 +1,121 @@
+<!--
+ 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 { computed, onMounted, ref } from 'vue';
+ import { useGo } from '/@/hooks/web/usePage';
+ import ProgramArgs from './ProgramArgs.vue';
+ import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+
+ import { BasicForm, useForm } from '/@/components/Form';
+ import { useDrawer } from '/@/components/Drawer';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import VariableReview from './VariableReview.vue';
+ import { useSparkSchema } from '../hooks/useAppFormSchema';
+ import { SparkEnv } from '/@/api/spark/home.type';
+
+ const SparkSqlEditor = createAsyncComponent(() => import('./SparkSql.vue'), {
+ loading: true,
+ });
+
+ defineOptions({ name: 'AppForm' });
+ const props = defineProps({
+ sparkEnvs: {
+ type: Array as PropType<SparkEnv[]>,
+ default: () => [],
+ },
+ initFormFn: {
+ type: Function as PropType<() => Promise<Recordable>>,
+ },
+ submit: {
+ type: Function as PropType<(params: Recordable) => Promise<void>>,
+ required: true,
+ },
+ });
+ const go = useGo();
+ const sparkSql = ref();
+ const submitLoading = ref(false);
+
+ const { t } = useI18n();
+
+ const getSparkEnvs = computed<SparkEnv[]>(() => props.sparkEnvs);
+ const { formSchema, suggestions } = useSparkSchema(getSparkEnvs);
+
+ const [registerAppForm, { setFieldsValue, submit: submitForm }] = 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() {
+ if (props.initFormFn) {
+ const fieldsValue = await props.initFormFn();
+ await setFieldsValue(fieldsValue);
+ }
+ }
+ /* Submit to create */
+ async function handleAppSubmit(formValue: Recordable) {
+ try {
+ submitLoading.value = true;
+ await props.submit(formValue);
+ } catch (error) {
+ submitLoading.value = false;
+ }
+ }
+
+ onMounted(async () => {
+ handleInitForm();
+ });
+</script>
+
+<template>
+ <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="submitForm()">
+ {{ t('common.submitText') }}
+ </a-button>
+ </div>
+ </template>
+ </BasicForm>
+ <VariableReview @register="registerReviewDrawer" />
+</template>
diff --git a/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppLog.vue b/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppLog.vue
new file mode 100644
index 000000000..df698fb5c
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppLog.vue
@@ -0,0 +1,124 @@
+<!--
+ 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="LogModal">
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { useLog } from '/@/hooks/web/useLog';
+ import { reactive, ref, unref } from 'vue';
+ import { fetchK8sStartLog } from '/@/api/flink/app';
+ import { BasicModal, useModalInner } from '/@/components/Modal';
+ import { Icon } from '/@/components/Icon';
+ import { formatToDateTime } from '/@/utils/dateUtil';
+ import { useTimeoutFn } from '@vueuse/shared';
+ defineOptions({
+ name: 'LogModal',
+ });
+ const { t } = useI18n();
+ const logTime = ref<string>('');
+ const getLogLoading = ref<boolean>(false);
+ const app = reactive<Recordable>({});
+ let offset = 0;
+ let logContent = '';
+
+ const [registerModal, { changeLoading, closeModal }] = useModalInner((data) => {
+ data && onReceiveModalData(data);
+ });
+
+ const { setContent, logRef, handleRevealLine, getLineCount } = useLog();
+
+ function onReceiveModalData(data) {
+ offset = 0;
+ logContent = '';
+ Object.assign(app, unref(data.app));
+ changeLoading(true);
+ refreshLog();
+ start();
+ }
+ const { start, stop } = useTimeoutFn(
+ () => {
+ refreshLog();
+ start();
+ },
+ 3000,
+ { immediate: false },
+ );
+
+ async function refreshLog() {
+ try {
+ getLogLoading.value = true;
+ const { data } = await fetchK8sStartLog({
+ id: app.id,
+ offset: offset,
+ limit: 100,
+ });
+ const status = data.status || 'error';
+ if (status === 'success') {
+ if (data.data) {
+ logContent += data.data;
+ setContent(logContent);
+ handleRevealLine();
+ setTimeout(() => {
+ const currentLineCount = getLineCount() - 1;
+ if (currentLineCount < offset + 100) {
+ offset = currentLineCount;
+ } else {
+ offset += 100;
+ }
+ }, 500);
+ }
+ logTime.value = formatToDateTime(new Date());
+ }
+ } catch (error) {
+ closeModal();
+ console.error('logModal error', error);
+ } finally {
+ getLogLoading.value = false;
+ changeLoading(false);
+ }
+ }
+
+ async function handleClose() {
+ stop();
+ }
+</script>
+<template>
+ <BasicModal
+ canFullscreen
+ :scrollTop="false"
+ @register="registerModal"
+ width="80%"
+ :after-close="handleClose"
+ >
+ <template #title>
+ <Icon icon="ant-design:code-outlined" style="color: #477de9" />
+ <span> {{ t('spark.app.view.logTitle', [app.jobName]) }}</span>
+ </template>
+ <div ref="logRef" class="h-full min-h-500px"></div>
+ <template #footer>
+ <div class="flex align-items-center">
+ <div class="flex-1 text-left">{{ t('spark.app.view.refreshTime') }}:{{ logTime }}</div>
+ <div class="button-group">
+ <a-button key="refresh" type="primary" @click="refreshLog" :loading="getLogLoading">
+ {{ t('spark.app.view.refresh') }}
+ </a-button>
+ <a-button key="close" type="primary" @click="closeModal()">
+ {{ t('common.closeText') }}
+ </a-button>
+ </div>
+ </div>
+ </template>
+ </BasicModal>
+</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
deleted file mode 100644
index 3fd5976ef..000000000
--- a/streampark-console/streampark-console-webapp/src/views/spark/app/components/AppStartModal.vue
+++ /dev/null
@@ -1,244 +0,0 @@
-<!--
- 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/BuildDrawer.vue b/streampark-console/streampark-console-webapp/src/views/spark/app/components/BuildDrawer.vue
new file mode 100644
index 000000000..3023a24c6
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/components/BuildDrawer.vue
@@ -0,0 +1,239 @@
+<!--
+ 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 { reactive, ref, unref } from 'vue';
+ import { Timeline, Empty, Divider, Tag, Progress, List, Space } from 'ant-design-vue';
+ import { BasicDrawer, useDrawer, useDrawerInner } from '/@/components/Drawer';
+ import { Icon } from '/@/components/Icon';
+ import {
+ handleAppBuildStatusColor,
+ handleAppBuildStatueText,
+ handleAppBuildStepTimelineColor,
+ handleAppBuildStepText,
+ } from '../utils';
+ import BuildLayer from '/@/views/flink/app/components/AppView/BuildLayer.vue';
+ import { useTimeoutFn } from '@vueuse/core';
+ import { fetchBuildProgressDetail } from '/@/api/spark/build';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { SparkApplication } from '/@/api/spark/app.type';
+ defineOptions({
+ name: 'SparkBuildDrawer',
+ });
+ const TimelineItem = Timeline.Item;
+ const ListItem = List.Item;
+
+ const { t } = useI18n();
+ const appId = ref<Nullable<string>>(null);
+ const appBuildDetail = reactive<{ pipeline: Nullable<any>; docker: Nullable<any> }>({
+ pipeline: null,
+ docker: null,
+ });
+
+ const { isPending, start, stop } = useTimeoutFn(
+ () => {
+ handleFetchBuildDetail();
+ },
+ 1000,
+ { immediate: false },
+ );
+ /* Build Detail */
+ async function handleFetchBuildDetail() {
+ try {
+ if (unref(appId) == null) {
+ console.warn('appId is null, skipping fetching build detail');
+ return;
+ }
+ const res = await fetchBuildProgressDetail(+unref(appId)!);
+ Object.assign(appBuildDetail, res);
+ start();
+ } catch (error) {
+ console.error('error', error);
+ stop();
+ }
+ }
+
+ const [registerBuild] = useDrawerInner((data: { app: SparkApplication }) => {
+ data && onReceiveData(data);
+ });
+ /* data reception */
+ function onReceiveData(data: Recordable) {
+ appId.value = data.appId;
+ if (!isPending.value) start();
+ }
+
+ const [registerErrorLog, { openDrawer: openErrorDrawer }] = useDrawer();
+</script>
+<template>
+ <BasicDrawer
+ @register="registerBuild"
+ :title="t('spark.app.view.buildTitle')"
+ width="500"
+ :closable="true"
+ @close="stop()"
+ >
+ <!-- status and cost time -->
+ <h3>
+ <Icon icon="ant-design:dashboard-outlined" />
+ Summary
+ </h3>
+ <template v-if="appBuildDetail.pipeline">
+ <div>
+ <Progress
+ v-if="appBuildDetail.pipeline.hasError"
+ :percent="appBuildDetail.pipeline.percent"
+ status="exception"
+ />
+ <Progress
+ v-else-if="appBuildDetail.pipeline.percent < 100"
+ :percent="appBuildDetail.pipeline.percent"
+ status="active"
+ />
+ <Progress v-else :percent="appBuildDetail.pipeline.percent" />
+ </div>
+ <div class="mt-10px">
+ <template v-if="appBuildDetail.pipeline.pipeStatus == 2">
+ <Tag
+ :color="handleAppBuildStatusColor(appBuildDetail.pipeline.pipeStatus)"
+ class="running-tag"
+ >
+ {{ handleAppBuildStatueText(appBuildDetail.pipeline.pipeStatus) }}
+ </Tag>
+ </template>
+ <template v-else>
+ <Tag :color="handleAppBuildStatusColor(appBuildDetail.pipeline.pipeStatus)">
+ {{ handleAppBuildStatueText(appBuildDetail.pipeline.pipeStatus) }}
+ </Tag>
+ </template>
+ cost {{ appBuildDetail.pipeline.costSec }} seconds
+ </div>
+ </template>
+ <Empty v-else />
+ <Divider />
+
+ <!-- step detail -->
+ <h3>
+ <Icon icon="ant-design:project-outlined" />
+
+ {{ t('spark.app.view.stepTitle') }}
+ </h3>
+ <template v-if="appBuildDetail.pipeline">
+ <Timeline style="margin-top: 20px">
+ <TimelineItem
+ v-for="stepItem in appBuildDetail.pipeline.steps"
+ :key="stepItem.seq"
+ :color="handleAppBuildStepTimelineColor(stepItem)"
+ >
+ <!-- step status, desc -->
+ <p>
+ <template v-if="stepItem.status == 2">
+ <Tag :color="handleAppBuildStepTimelineColor(stepItem)" class="running-tag">
+ {{ handleAppBuildStepText(stepItem.status) }}
+ </Tag>
+ <b>Step-{{ stepItem.seq }}</b> {{ stepItem.desc }}
+ </template>
+ <template v-else>
+ <Tag :color="handleAppBuildStepTimelineColor(stepItem)">
+ {{ handleAppBuildStepText(stepItem.status) }}
+ </Tag>
+ <b>Step-{{ stepItem.seq }}</b> {{ stepItem.desc }}
+ </template>
+ </p>
+ <!-- step info update time --->
+ <template v-if="stepItem.status !== 0 && stepItem.status !== 1">
+ <p style="color: gray; font-size: 12px">{{ stepItem.ts }}</p>
+ </template>
+ <!-- docker resolved detail --->
+ <template v-if="appBuildDetail.pipeline.pipeType === 2 && appBuildDetail.docker !== null">
+ <template
+ v-if="
+ stepItem.seq === 5 &&
+ appBuildDetail.docker.pull !== null &&
+ appBuildDetail.docker.pull.layers !== null
+ "
+ >
+ <template v-for="layer in appBuildDetail.docker.pull.layers" :key="layer.layerId">
+ <BuildLayer :layer="layer" />
+ </template>
+ </template>
+
+ <template
+ v-else-if="
+ stepItem.seq === 6 &&
+ appBuildDetail.docker.build !== null &&
+ appBuildDetail.docker.build.steps != null
+ "
+ >
+ <List bordered :data-source="appBuildDetail.docker.build.steps" size="small">
+ <template #renderItem="{ item }">
+ <ListItem>
+ <Space>
+ <Icon icon="ant-design:arrow-right-outlined" />
+ <span style="font-size: 12px">{{ item }}</span>
+ </Space>
+ </ListItem>
+ </template>
+ </List>
+ </template>
+
+ <template
+ v-else-if="
+ stepItem.seq === 7 &&
+ appBuildDetail.docker.push !== null &&
+ appBuildDetail.docker.push.layers !== null
+ "
+ >
+ <template v-for="layer in appBuildDetail.docker.push.layers" :key="layer.layerId">
+ <BuildLayer :layer="layer" />
+ </template>
+ </template>
+ </template>
+ </TimelineItem>
+ </Timeline>
+ </template>
+ <Empty v-else />
+
+ <!-- bottom tools -->
+ <template v-if="appBuildDetail.pipeline != null && appBuildDetail.pipeline.hasError">
+ <div
+ class="absolute -bottom-0 -left-0 w-full px-16px py-10px text-right bg-white rounded-bl-4px"
+ style="border-top: 1px solid #e8e8e8"
+ >
+ <a-button type="primary" @click="openErrorDrawer(true)">
+ <Icon icon="ant-design:warning-outlined" />
+ Error Log
+ {{ t('spark.app.view.errorLog') }}
+ </a-button>
+ </div>
+ <BasicDrawer
+ @register="registerErrorLog"
+ :title="t('spark.app.view.errorLog')"
+ width="800"
+ :closable="true"
+ >
+ <div>
+ <h3>{{ t('spark.app.view.errorSummary') }}</h3>
+ <br />
+ <p>{{ appBuildDetail.pipeline.errorSummary }}</p>
+ <Divider />
+ <h3>{{ t('spark.app.view.errorStack') }}</h3>
+ <br />
+ <pre style="font-size: 12px">{{ appBuildDetail.pipeline.errorStack }}</pre>
+ </div>
+ </BasicDrawer>
+ </template>
+ </BasicDrawer>
+</template>
diff --git a/streampark-console/streampark-console-webapp/src/views/spark/app/components/CompareModal.vue b/streampark-console/streampark-console-webapp/src/views/spark/app/components/CompareModal.vue
new file mode 100644
index 000000000..8fa941576
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/components/CompareModal.vue
@@ -0,0 +1,153 @@
+<!--
+ 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, toRaw } from 'vue';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { Select, Tag } from 'ant-design-vue';
+ import { Icon, SvgIcon } from '/@/components/Icon';
+ import { useDetailProviderContext } from '../hooks/useDetailContext';
+ import { fetchGetSparkConf, fetchSparkConfList } from '/@/api/spark/conf';
+ import { useDrawer } from '/@/components/Drawer';
+ import { BasicForm, useForm } from '/@/components/Form';
+ import { BasicModal, useModalInner } from '/@/components/Modal';
+ import { decodeByBase64 } from '/@/utils/cipher';
+ import Different from '/@/views/flink/app/components/AppDetail/Different.vue';
+ import { SparkApplicationConfig } from '/@/api/spark/conf.type';
+
+ const SelectOption = Select.Option;
+ const { t } = useI18n();
+ defineEmits(['register']);
+ const allConfigVersions = ref<SparkApplicationConfig[]>([]);
+ const submitLoading = ref<boolean>(false);
+ const compareRecord = reactive<Recordable>({});
+ const values = useDetailProviderContext();
+
+ const [registerDifferentDrawer, { openDrawer: openDiffDrawer }] = useDrawer();
+ const [registerModal, { closeModal }] = useModalInner((data) => {
+ data && onReceiveModalData(data);
+ });
+
+ async function onReceiveModalData(data) {
+ const res = await fetchSparkConfList({
+ appId: toRaw(values).app.id,
+ pageNo: 1,
+ pageSize: 999999,
+ });
+ Object.assign(compareRecord, data);
+ allConfigVersions.value = res.records;
+ }
+
+ const [registerForm, { resetFields, submit }] = useForm({
+ labelWidth: 180,
+ colon: true,
+ name: 'compareForm',
+ 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 },
+ showActionButtonGroup: false,
+ schemas: [
+ { label: 'source version', field: 'source', component: 'Input', slot: 'source' },
+ {
+ label: 'target version',
+ field: 'target',
+ defaultValue: undefined,
+ component: 'Select',
+ slot: 'target',
+ required: true,
+ },
+ ],
+ });
+
+ // submit form
+ async function handleCompareOk(values: Recordable) {
+ submitLoading.value = true;
+ try {
+ const source = await fetchGetSparkConf({
+ id: compareRecord.id,
+ });
+ const sourceConf = decodeByBase64(source.content || '');
+ const sourceVersion = source.version;
+ const target = await fetchGetSparkConf({
+ id: values.target,
+ });
+ const targetConf = decodeByBase64(target.content || '');
+ const targetVersion = target.version;
+ closeModal();
+ openDiffDrawer(true, {
+ immediate: true,
+ param: [
+ {
+ name: 'Configuration',
+ format: 'yaml',
+ original: sourceConf,
+ modified: targetConf,
+ },
+ ],
+ original: sourceVersion,
+ modified: targetVersion,
+ });
+ } catch (error) {
+ console.error(error);
+ } finally {
+ submitLoading.value = false;
+ }
+ }
+
+ const filterNotCurrConfig = computed(() => {
+ return allConfigVersions.value.filter((x) => x.version !== compareRecord.version);
+ });
+</script>
+<template>
+ <BasicModal @register="registerModal" :afterClose="resetFields">
+ <template #title>
+ <SvgIcon name="swap" />
+ {{ t('spark.app.detail.compareConfig') }}
+ </template>
+ <BasicForm @register="registerForm" @submit="handleCompareOk">
+ <template #source>
+ <a-button type="primary" shape="circle" size="small" style="margin-right: 10px">
+ {{ compareRecord.version }}
+ </a-button>
+ <Icon icon="ant-design:clock-circle-outlined" style="color: darkgrey" />
+ <span style="color: darkgrey">{{ compareRecord.createTime }}</span>
+ </template>
+ <template #target="{ model }">
+ <Select v-model:value="model.target" :placeholder="t('spark.app.detail.compareSelectTips')">
+ <SelectOption v-for="(ver, index) in filterNotCurrConfig" :value="ver.id" :key="index">
+ <div style="padding-left: 5px">
+ <a-button type="primary" shape="circle" size="small" style="margin-right: 10px">
+ {{ ver.version }}
+ </a-button>
+ <Tag color="green" style="margin-left: 10px" size="small" v-if="ver.effective">
+ Effective
+ </Tag>
+ </div>
+ </SelectOption>
+ </Select>
+ </template>
+ </BasicForm>
+ <template #footer>
+ <a-button key="back" @click="closeModal">
+ {{ t('common.closeText') }}
+ </a-button>
+ <a-button key="submit" type="primary" @click="submit" :loading="submitLoading">
+ {{ t('spark.app.detail.compare') }}
+ </a-button>
+ </template>
+ </BasicModal>
+ <Different @register="registerDifferentDrawer" />
+</template>
diff --git a/streampark-console/streampark-console-webapp/src/views/spark/app/components/DetailTab.vue b/streampark-console/streampark-console-webapp/src/views/spark/app/components/DetailTab.vue
new file mode 100644
index 000000000..1fbee7f63
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/components/DetailTab.vue
@@ -0,0 +1,485 @@
+<!--
+ 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="DetailTab">
+ import { ref, toRefs, watch } from 'vue';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { useDrawer } from '/@/components/Drawer';
+ import Mergely from './Mergely.vue';
+ import { Tabs, Descriptions, Tag } from 'ant-design-vue';
+
+ import { useMonaco } from '/@/hooks/web/useMonaco';
+ import { BasicTable, useTable, TableAction, ActionItem } from '/@/components/Table';
+ import Icon from '/@/components/Icon';
+ import {
+ getBackupColumns,
+ getConfColumns,
+ getSparkSqlColumns,
+ getOptionLogColumns,
+ } from '../data/detail.data';
+ import { getMonacoOptions } from '../data';
+ import { useRoute } from 'vue-router';
+ import { fetchGetSparkConf, fetchSparkConfList, fetchSparkConfRemove } from '/@/api/spark/conf';
+ import {
+ fetchSparkBackUps,
+ fetchSparkDeleteOptLog,
+ fetchSparkOptionLog,
+ fetchSparkRemoveBackup,
+ fetchSparkYarn,
+ } from '/@/api/spark/app';
+ import { decodeByBase64 } from '/@/utils/cipher';
+ import { useModal } from '/@/components/Modal';
+ import CompareModal from './CompareModal.vue';
+ import ExecOptionModal from '/@/views/flink/app/components/AppDetail/ExecOptionModal.vue';
+ import { useMessage } from '/@/hooks/web/useMessage';
+ import { useClipboard } from '@vueuse/core';
+ import { OperationEnum, CandidateTypeEnum, ConfigTypeEnum } from '/@/enums/flinkEnum';
+ import { JobTypeEnum } from '/@/enums/sparkEnum';
+ import SparkSqlReview from './SparkSqlReview.vue';
+ import SparkSqlCompareModal from './SparkSqlCompareModal.vue';
+ import { fetchSparkSql, fetchSparkSqlList, fetchSparkSqlRemove } from '/@/api/spark/sql';
+ import { SparkApplication } from '/@/api/spark/app.type';
+ const DescriptionItem = Descriptions.Item;
+ const TabPane = Tabs.TabPane;
+
+ const { createMessage } = useMessage();
+ const { t } = useI18n();
+ const { copy } = useClipboard({
+ legacy: true,
+ });
+ const route = useRoute();
+ const props = defineProps({
+ app: {
+ type: Object as PropType<SparkApplication>,
+ default: () => ({}),
+ },
+ tabConf: {
+ type: Object as PropType<Recordable<boolean>>,
+ default: () => ({}),
+ },
+ });
+ const { app, tabConf } = toRefs(props);
+ const sparkSql = ref();
+
+ const { setContent } = useMonaco(sparkSql, {
+ language: 'sql',
+ options: { minimap: { enabled: true }, ...(getMonacoOptions(true) as any) },
+ });
+
+ watch(
+ () => props.app.sparkSql,
+ (val) => {
+ if (!val) return;
+ setContent(decodeByBase64(props.app.sparkSql || ''));
+ },
+ );
+
+ const tableCommonConf = {
+ beforeFetch: (params) => Object.assign(params, { appId: route.query.appId }),
+ fetchSetting: { listField: 'records', pageField: 'pageNum', sizeField: 'pageSize' },
+ rowKey: 'id',
+ canResize: false,
+ showIndexColumn: false,
+ showTableSetting: false,
+ actionColumn: {
+ title: t('component.table.operation'),
+ dataIndex: 'operation',
+ key: 'operation',
+ width: 150,
+ },
+ };
+
+ const [registerCompare, { openModal: openCompareModal }] = useModal();
+ const [registerSparkSqlCompare, { openModal: openSparkSqlCompareModal }] = useModal();
+ const [registerExecOption, { openModal: openExecOptionModal }] = useModal();
+ const [registerDetailDrawer, { openDrawer: openDetailDrawer }] = useDrawer();
+ const [registerSparkSqlDrawer, { openDrawer: openSparkDrawer }] = useDrawer();
+
+ const [registerConfigTable, { getDataSource, reload: reloadConf }] = useTable({
+ api: fetchSparkConfList,
+ columns: getConfColumns(),
+ ...tableCommonConf,
+ });
+
+ const [registerSparkSqlTable, { getDataSource: getSparkSql, reload: reloadSparkSql }] = useTable({
+ api: fetchSparkSqlList,
+ columns: getSparkSqlColumns(),
+ ...tableCommonConf,
+ });
+
+ const [registerBackupTable, { reload: reloadBackup }] = useTable({
+ api: fetchSparkBackUps,
+ columns: getBackupColumns(),
+ ...tableCommonConf,
+ });
+
+ const [registerLogsTable, { reload: reloadOperationLog }] = useTable({
+ api: fetchSparkOptionLog,
+ columns: getOptionLogColumns(),
+ ...tableCommonConf,
+ });
+
+ function getConfAction(record: Recordable): ActionItem[] {
+ return [
+ {
+ tooltip: { title: t('spark.app.detail.detailTab.configDetail') },
+ type: 'link',
+ icon: 'ant-design:eye-outlined',
+ onClick: handleConfDetail.bind(null, record),
+ },
+ {
+ tooltip: { title: t('spark.app.detail.compareConfig') },
+ type: 'link',
+ icon: 'ant-design:swap-outlined',
+ onClick: handleCompare.bind(null, record),
+ ifShow: getDataSource().length > 1,
+ },
+ {
+ popConfirm: {
+ title: t('spark.app.detail.detailTab.confDeleteTitle'),
+ confirm: handleDeleteConf.bind(null, record),
+ },
+ auth: 'conf:delete',
+ type: 'link',
+ icon: 'ant-design:delete-outlined',
+ color: 'error',
+ ifShow: !record.effective,
+ },
+ ];
+ }
+
+ function getSparkSqlAction(record: Recordable): ActionItem[] {
+ return [
+ {
+ tooltip: { title: t('spark.app.detail.detailTab.sqlDetail') },
+ type: 'link',
+ icon: 'ant-design:eye-outlined',
+ onClick: handleSqlDetail.bind(null, record),
+ },
+ {
+ tooltip: { title: t('spark.app.detail.compareSparkSql') },
+ type: 'link',
+ icon: 'ant-design:swap-outlined',
+ onClick: handleCompareSparkSql.bind(null, record),
+ ifShow: getSparkSql().length > 1,
+ },
+ {
+ popConfirm: {
+ title: t('spark.app.detail.detailTab.sqlDeleteTitle'),
+ confirm: handleDeleteSparkSql.bind(null, record),
+ },
+ auth: 'conf:delete',
+ type: 'link',
+ icon: 'ant-design:delete-outlined',
+ color: 'error',
+ ifShow: !record.effective,
+ },
+ ];
+ }
+
+ async function handleConfDetail(record: Recordable) {
+ const hide = createMessage.loading('loading');
+ try {
+ const res = await fetchGetSparkConf({
+ id: record.id,
+ });
+ openDetailDrawer(true, {
+ configOverride: decodeByBase64(res.content || ''),
+ });
+ } catch (error: unknown) {
+ console.error(error);
+ } finally {
+ hide();
+ }
+ }
+
+ async function handleSqlDetail(record: Recordable) {
+ const hide = createMessage.loading('loading');
+ try {
+ const res = await fetchSparkSql({
+ id: record.id,
+ appId: record.appId,
+ });
+ openSparkDrawer(true, {
+ sql: decodeByBase64(res.sql),
+ });
+ } catch (error: unknown) {
+ console.error(error);
+ } finally {
+ hide();
+ }
+ }
+
+ /* delete configuration */
+ async function handleDeleteConf(record: Recordable) {
+ await fetchSparkConfRemove({ id: record.id });
+ await reloadConf();
+ }
+
+ /* delete spark sql */
+ async function handleDeleteSparkSql(record: Recordable) {
+ await fetchSparkSqlRemove({ id: record.id, appId: record.appId });
+ await reloadSparkSql();
+ }
+
+ function handleCompare(record: Recordable) {
+ openCompareModal(true, {
+ id: record.id,
+ version: record.version,
+ createTime: record.createTime,
+ });
+ }
+
+ function handleCompareSparkSql(record: Recordable) {
+ openSparkSqlCompareModal(true, {
+ id: record.id,
+ version: record.version,
+ createTime: record.createTime,
+ });
+ }
+
+ async function handleYarnUrl(yarnAppId: string) {
+ const res = await fetchSparkYarn();
+ window.open(res + '/proxy/' + yarnAppId + '/');
+ }
+
+ function getBackupAction(record: Recordable): ActionItem[] {
+ return [
+ {
+ tooltip: { title: t('spark.app.detail.detailTab.copyPath') },
+ shape: 'circle',
+ icon: 'ant-design:copy-outlined',
+ onClick: handleCopy.bind(null, record),
+ },
+ {
+ popConfirm: {
+ title: t('spark.app.detail.detailTab.confBackupTitle'),
+ confirm: handleDeleteBackup.bind(null, record),
+ },
+ auth: 'app:delete',
+ type: 'link',
+ icon: 'ant-design:delete-outlined',
+ color: 'error',
+ },
+ ];
+ }
+
+ function getOperationLogAction(record: Recordable): ActionItem[] {
+ return [
+ {
+ tooltip: { title: t('spark.app.detail.detailTab.exception') },
+ auth: 'app:detail',
+ shape: 'circle',
+ type: 'default',
+ ifShow: !record.success,
+ icon: 'ant-design:eye-outlined',
+ onClick: handleException.bind(null, record),
+ },
+ {
+ popConfirm: {
+ title: t('spark.app.detail.detailTab.operationLogDeleteTitle'),
+ confirm: handleDeleteOperationLog.bind(null, record),
+ },
+ auth: 'app:delete',
+ type: 'link',
+ icon: 'ant-design:delete-outlined',
+ color: 'error',
+ },
+ ];
+ }
+
+ async function handleDeleteBackup(record: Recordable) {
+ await fetchSparkRemoveBackup(record.id);
+ await reloadBackup();
+ }
+
+ async function handleDeleteOperationLog(record: Recordable) {
+ await fetchSparkDeleteOptLog(record.id);
+ await reloadOperationLog();
+ }
+
+ /* copy path */
+ function handleCopy(record: Recordable) {
+ try {
+ copy(record.path);
+ createMessage.success(t('spark.app.detail.detailTab.copySuccess'));
+ } catch (error) {
+ console.error(error);
+ createMessage.error(t('spark.app.detail.detailTab.copyFail'));
+ }
+ }
+
+ function handleException(record: Recordable) {
+ openExecOptionModal(true, {
+ content: record.exception,
+ });
+ }
+</script>
+<template>
+ <div>
+ <Tabs :defaultActiveKey="1" class="mt-15px" :tab-bar-gutter="0">
+ <TabPane key="1" :tab="t('spark.app.detail.detailTab.detailTabName.operationLog')">
+ <BasicTable @register="registerLogsTable">
+ <template #bodyCell="{ column, record }">
+ <template v-if="column.dataIndex === 'optionName'">
+ <Tag color="blue" v-if="record.optionName === OperationEnum.RELEASE"> Release </Tag>
+ <Tag color="green" v-if="record.optionName === OperationEnum.START"> Start </Tag>
+ <Tag color="orange" v-if="record.optionName === OperationEnum.CANCEL"> Cancel </Tag>
+ </template>
+ <template v-if="column.dataIndex === 'yarnAppId'">
+ <a type="link" @click="handleYarnUrl(record.yarnAppId)" target="_blank">
+ {{ record.yarnAppId }}
+ </a>
+ </template>
+ <template v-if="column.dataIndex === 'jobManagerUrl'">
+ <a type="link" :href="record.jobManagerUrl" target="_blank">
+ {{ record.jobManagerUrl }}
+ </a>
+ </template>
+ <template v-if="column.dataIndex === 'optionTime'">
+ <Icon icon="ant-design:clock-circle-outlined" />
+ {{ record.optionTime }}
+ </template>
+ <template v-if="column.dataIndex === 'success'">
+ <Tag class="bold-tag" color="#52c41a" v-if="record.success"> SUCCESS </Tag>
+ <Tag class="bold-tag" color="#f5222d" v-else> FAILED </Tag>
+ </template>
+ <template v-if="column.dataIndex === 'operation'">
+ <TableAction :actions="getOperationLogAction(record)" />
+ </template>
+ </template>
+ </BasicTable>
+ </TabPane>
+ <TabPane key="2" :tab="t('spark.app.detail.detailTab.detailTabName.option')">
+ <Descriptions bordered size="middle" layout="vertical">
+ <DescriptionItem v-for="(v, k) in JSON.parse(app.options || '{}')" :key="k" :label="k">
+ {{ v }}
+ </DescriptionItem>
+ </Descriptions>
+ </TabPane>
+ <TabPane
+ key="3"
+ :tab="t('spark.app.detail.detailTab.detailTabName.configuration')"
+ v-if="app && tabConf.showConf"
+ >
+ <BasicTable @register="registerConfigTable">
+ <template #bodyCell="{ column, record }">
+ <template v-if="column.dataIndex === 'format'">
+ <Tag class="bold-tag" color="#2db7f5" v-if="record.format === ConfigTypeEnum.YAML">
+ yaml
+ </Tag>
+ <Tag
+ class="bold-tag"
+ color="#108ee9"
+ v-if="record.format === ConfigTypeEnum.PROPERTIES"
+ >
+ properties
+ </Tag>
+ </template>
+ <template v-if="column.dataIndex === 'version'">
+ <a-button type="primary" shape="circle" size="small" class="mr-10px">
+ {{ record.version }}
+ </a-button>
+ </template>
+ <template v-if="column.dataIndex === 'effective'">
+ <Tag color="green" v-if="record.effective"> Effective </Tag>
+ <Tag color="cyan" v-if="record.latest"> Latest </Tag>
+ </template>
+ <template v-if="column.dataIndex === 'createTime'">
+ <Icon icon="ant-design:clock-circle-outlined" />
+ {{ record.createTime }}
+ </template>
+ <template v-if="column.dataIndex === 'operation'">
+ <TableAction :actions="getConfAction(record)" />
+ </template>
+ </template>
+ </BasicTable>
+ </TabPane>
+ <TabPane
+ key="4"
+ :tab="t('spark.app.detail.detailTab.detailTabName.sparkSql')"
+ v-if="app.jobType === JobTypeEnum.SQL"
+ >
+ <BasicTable @register="registerSparkSqlTable">
+ <template #bodyCell="{ column, record }">
+ <template v-if="column.dataIndex === 'candidate'">
+ <Tag
+ class="bold-tag"
+ color="#52c41a"
+ v-if="record.candidate === CandidateTypeEnum.NONE"
+ >
+ None
+ </Tag>
+ <Tag
+ class="bold-tag"
+ color="#2db7f5"
+ v-if="record.candidate === CandidateTypeEnum.NEW"
+ >
+ New
+ </Tag>
+ <Tag
+ class="bold-tag"
+ color="#108ee9"
+ v-if="record.candidate === CandidateTypeEnum.HISTORY"
+ >
+ History
+ </Tag>
+ </template>
+ <template v-if="column.dataIndex === 'version'">
+ <a-button type="primary" shape="circle" size="small" class="mr-10px">
+ {{ record.version }}
+ </a-button>
+ </template>
+ <template v-if="column.dataIndex === 'effective'">
+ <Tag color="green" v-if="record.effective"> Effective </Tag>
+ </template>
+ <template v-if="column.dataIndex === 'createTime'">
+ <Icon icon="ant-design:clock-circle-outlined" />
+ {{ record.createTime }}
+ </template>
+ <template v-if="column.dataIndex === 'operation'">
+ <TableAction :actions="getSparkSqlAction(record)" />
+ </template>
+ </template>
+ </BasicTable>
+ </TabPane>
+ <TabPane
+ key="6"
+ :tab="t('spark.app.detail.detailTab.detailTabName.backup')"
+ v-if="tabConf.showBackup"
+ >
+ <BasicTable @register="registerBackupTable">
+ <template #bodyCell="{ column, record }">
+ <template v-if="column.dataIndex === 'version'">
+ <a-button type="primary" shape="circle" size="small">
+ {{ record.version }}
+ </a-button>
+ </template>
+ <template v-if="column.dataIndex === 'operation'">
+ <TableAction :actions="getBackupAction(record)" />
+ </template>
+ </template>
+ </BasicTable>
+ </TabPane>
+ </Tabs>
+
+ <CompareModal @register="registerCompare" />
+ <SparkSqlCompareModal @register="registerSparkSqlCompare" />
+ <ExecOptionModal @register="registerExecOption" />
+ <Mergely :read-only="true" @register="registerDetailDrawer" />
+ <SparkSqlReview @register="registerSparkSqlDrawer" />
+ </div>
+</template>
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
index 3a6bc71b4..f3a5c97e9 100644
--- 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
@@ -25,7 +25,7 @@
import { Button } from 'ant-design-vue';
import { isEmpty } from '/@/utils/is';
import { useMessage } from '/@/hooks/web/useMessage';
- import { format } from '../sqlFormatter';
+ import { format } from '/@/views/flink/app/FlinkSqlFormatter';
import { useI18n } from '/@/hooks/web/useI18n';
import { useFullContent } from '/@/hooks/event/useFullscreen';
import { fetchSparkSqlVerify } from '/@/api/spark/sql';
@@ -35,7 +35,7 @@
const ButtonGroup = Button.Group;
const { t } = useI18n();
- const flinkSql = ref();
+ const sparkSql = ref();
const verifyRes = reactive({
errorMsg: '',
verified: false,
@@ -73,7 +73,7 @@
}
if (!props.versionId) {
- createMessage.error(t('flink.app.dependencyError'));
+ createMessage.error(t('spark.app.dependencyError'));
return false;
} else {
try {
@@ -150,7 +150,7 @@
// }, 500);
// }
const { onChange, setContent, getInstance, getMonacoInstance, setMonacoSuggest } = useMonaco(
- flinkSql,
+ sparkSql,
{
language: 'sql',
code: props.value || defaultValue,
@@ -200,14 +200,14 @@
</div>
<div
- ref="flinkSql"
+ ref="sparkSql"
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') }}
+ {{ t('spark.app.sparkSql.verify') }}
</a-button>
<a-button
class="sql-tool-item"
@@ -217,15 +217,15 @@
@click="emit('preview', value)"
>
<Icon icon="ant-design:eye-outlined" />
- {{ t('flink.app.flinkSql.preview') }}
+ {{ t('spark.app.sparkSql.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') }}
+ {{ t('spark.app.sparkSql.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') }}
+ {{ t('spark.app.sparkSql.fullScreen') }}
</a-button>
</ButtonGroup>
<div class="flex items-center justify-between" v-else>
@@ -234,26 +234,26 @@
{{ verifyRes.errorMsg }}
</div>
<div v-else class="text-green-700">
- <span v-if="verifyRes.verified"> {{ t('flink.app.flinkSql.successful') }} </span>
+ <span v-if="verifyRes.verified"> {{ t('spark.app.sparkSql.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') }}
+ {{ t('spark.app.sparkSql.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') }}
+ {{ t('spark.app.sparkSql.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') }}
+ {{ t('spark.app.sparkSql.format') }}
</div>
</a-button>
<a-button type="default" @click="toggle" class="ml-10px">
@@ -268,7 +268,7 @@
<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 v-if="verifyRes.verified"> {{ t('spark.app.sparkSql.successful') }} </span>
</span>
</p>
</template>
diff --git a/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSqlCompareModal.vue b/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSqlCompareModal.vue
new file mode 100644
index 000000000..c03498c3f
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSqlCompareModal.vue
@@ -0,0 +1,165 @@
+<!--
+ 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, toRaw } from 'vue';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { Select, Tag } from 'ant-design-vue';
+ import { Icon, SvgIcon } from '/@/components/Icon';
+ import { useDetailProviderContext } from '../hooks/useDetailContext';
+ import { fetchSparkSql, fetchSparkSqlList } from '/@/api/spark/sql';
+ import { useDrawer } from '/@/components/Drawer';
+ import { BasicForm, useForm } from '/@/components/Form';
+ import { BasicModal, useModalInner } from '/@/components/Modal';
+ import { decodeByBase64 } from '/@/utils/cipher';
+ import { CandidateTypeEnum } from '/@/enums/flinkEnum';
+ import SparkSqlDifferent from './SparkSqlDifferent.vue';
+
+ const SelectOption = Select.Option;
+ const { t } = useI18n();
+ defineEmits(['register']);
+ const allSparkSqlVersions = ref<any[]>([]);
+ const submitLoading = ref<boolean>(false);
+ const compareRecord = reactive<Recordable>({});
+ const values = useDetailProviderContext();
+ const appId = ref<any>();
+
+ const [registerSparkSqlDifferentDrawer, { openDrawer: openSparkSqlDiffDrawer }] = useDrawer();
+ const [registerModal, { closeModal }] = useModalInner((data) => {
+ data && onReceiveModalData(data);
+ });
+
+ async function onReceiveModalData(data) {
+ appId.value = toRaw(values).app.id;
+ const res = await fetchSparkSqlList({
+ appId: appId.value,
+ pageNo: 1,
+ pageSize: 999999,
+ });
+ Object.assign(compareRecord, data);
+ allSparkSqlVersions.value = res.records;
+ }
+
+ const [registerForm, { resetFields, submit }] = useForm({
+ labelWidth: 180,
+ colon: true,
+ name: 'compareForm',
+ 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 },
+ showActionButtonGroup: false,
+ schemas: [
+ { label: 'source version', field: 'source', component: 'Input', slot: 'source' },
+ {
+ label: 'target version',
+ field: 'target',
+ defaultValue: undefined,
+ component: 'Select',
+ slot: 'target',
+ required: true,
+ },
+ ],
+ });
+
+ // submit form
+ async function handleCompareOk(values: Recordable) {
+ submitLoading.value = true;
+ try {
+ const source = await fetchSparkSql({
+ id: compareRecord.id,
+ appId: appId.value,
+ });
+ const sourceSql = decodeByBase64(source.sql);
+ const sourceVersion = source.version;
+ const target = await fetchSparkSql({
+ id: values.target,
+ appId: appId.value,
+ });
+ const targetSql = decodeByBase64(target.sql);
+ const targetVersion = target.version;
+ closeModal();
+ openSparkSqlDiffDrawer(true, {
+ immediate: true,
+ param: [
+ {
+ name: 'Spark SQL',
+ format: 'sql',
+ original: sourceSql,
+ modified: targetSql,
+ },
+ ],
+ original: sourceVersion,
+ modified: targetVersion,
+ });
+ } catch (error) {
+ console.error(error);
+ } finally {
+ submitLoading.value = false;
+ }
+ }
+
+ const filterNotCurrSql = computed(() => {
+ return allSparkSqlVersions.value.filter((x) => x.version !== compareRecord.version);
+ });
+</script>
+<template>
+ <BasicModal @register="registerModal" :afterClose="resetFields">
+ <template #title>
+ <SvgIcon name="swap" />
+ {{ t('spark.app.detail.compareSparkSql') }}
+ </template>
+ <BasicForm @register="registerForm" @submit="handleCompareOk">
+ <template #source>
+ <a-button type="primary" shape="circle" size="small" style="margin-right: 10px">
+ {{ compareRecord.version }}
+ </a-button>
+ <Icon icon="ant-design:clock-circle-outlined" style="color: darkgrey" />
+ <span style="color: darkgrey">{{ compareRecord.createTime }}</span>
+ </template>
+ <template #target="{ model }">
+ <Select v-model:value="model.target" :placeholder="t('spark.app.detail.compareSelectTips')">
+ <SelectOption v-for="(ver, index) in filterNotCurrSql" :value="ver.id" :key="index">
+ <div style="padding-left: 5px">
+ <a-button type="primary" shape="circle" size="small" style="margin-right: 10px">
+ {{ ver.version }}
+ </a-button>
+ <Tag color="green" style="margin-left: 10px" size="small" v-if="ver.effective">
+ Effective
+ </Tag>
+ <Tag
+ color="cyan"
+ class="ml-5px"
+ size="small"
+ v-if="[CandidateTypeEnum.NEW, CandidateTypeEnum.HISTORY].includes(ver.candidate)"
+ >
+ {{ t('spark.app.detail.candidate') }}
+ </Tag>
+ </div>
+ </SelectOption>
+ </Select>
+ </template>
+ </BasicForm>
+ <template #footer>
+ <a-button key="back" @click="closeModal">
+ {{ t('common.closeText') }}
+ </a-button>
+ <a-button key="submit" type="primary" @click="submit" :loading="submitLoading">
+ {{ t('spark.app.detail.compare') }}
+ </a-button>
+ </template>
+ </BasicModal>
+ <SparkSqlDifferent @register="registerSparkSqlDifferentDrawer" />
+</template>
diff --git a/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSqlDifferent.vue b/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSqlDifferent.vue
new file mode 100644
index 000000000..90f92d1c3
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSqlDifferent.vue
@@ -0,0 +1,136 @@
+<!--
+ 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="FlinkSqlDifferent">
+ import { nextTick, reactive, ref } from 'vue';
+ import { getMonacoOptions } from '/@/views/flink/app/data/index';
+ import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+ import { useDiffMonaco } from '/@/views/flink/app/hooks/useDiffMonaco';
+ import { Tabs } from 'ant-design-vue';
+ import { SvgIcon } from '/@/components/Icon';
+ import { useI18n } from '/@/hooks/web/useI18n';
+
+ const TabPane = Tabs.TabPane;
+
+ const { t } = useI18n();
+
+ const items = ref<any[]>([]);
+ const renderEleMap = new Map<string, boolean>();
+ const version = reactive({
+ original: null,
+ modified: null,
+ });
+ const original = ref<Nullable<string>>(null);
+ const modified = ref<Nullable<string>>(null);
+ const [registerMergelyDrawer, { closeDrawer }] = useDrawerInner((data) => {
+ if (data && data.immediate) {
+ different(data);
+ }
+ });
+ const { setupEditor, disposeEditor } = useDiffMonaco(
+ null,
+ 'sql',
+ () => original.value,
+ () => modified.value,
+ getMonacoOptions(true) as any,
+ false,
+ );
+
+ function different(data: { param: Array<any>; original: string; modified: string }) {
+ Object.assign(version, { original: data.original, modified: data.modified });
+ items.value = data.param;
+ handleRenderTab(0);
+ }
+
+ function handleRenderTab(index: number) {
+ const x = items.value[index];
+ original.value = x.original;
+ modified.value = x.modified;
+ const ele = '#mergely' + index;
+ nextTick(() => {
+ const elem = document.querySelector(ele);
+ handleHeight(elem, 140);
+ // no render
+ if (!renderEleMap.get(ele)) {
+ setupEditor(elem);
+ renderEleMap.set(ele, true);
+ }
+ });
+ }
+ /* Change editor height */
+ function handleHeight(ele: any, h: number) {
+ const height = document.documentElement.offsetHeight || document.body.offsetHeight;
+ ele.style.height = height - h + 'px';
+ }
+
+ async function handleCancel() {
+ await disposeEditor();
+ renderEleMap.clear();
+ closeDrawer();
+ }
+
+ defineExpose({ different });
+</script>
+<template>
+ <BasicDrawer
+ @register="registerMergelyDrawer"
+ width="80%"
+ class="drawer-conf"
+ keyboard
+ :destroyOnClose="true"
+ @close="handleCancel"
+ >
+ <template #title>
+ <SvgIcon name="swap" />
+ <span class="px-10px">{{ t('spark.app.detail.compareSparkSql') }}</span>
+ <span> ( </span>
+ <span class="pr-5px">{{ t('spark.app.detail.different.original') }}</span>
+ <a-button type="primary" shape="circle" size="small">
+ {{ version.original }}
+ </a-button>
+ <span class="px-10px">VS</span>
+ <span class="pr-5px"> {{ t('spark.app.detail.different.target') }} </span>
+ <a-button type="primary" shape="circle" size="small">
+ {{ version.modified }}
+ </a-button>
+ <span> ) </span>
+ </template>
+ <Tabs type="card" @change="handleRenderTab">
+ <TabPane v-for="(item, index) in items" :key="index" :tab="item.name">
+ <div :id="'mergely'.concat(index.toString())"></div>
+ </TabPane>
+ </Tabs>
+ </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/SparkSqlReview.vue b/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSqlReview.vue
new file mode 100644
index 000000000..f7b3edb49
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/components/SparkSqlReview.vue
@@ -0,0 +1,55 @@
+<!--
+ 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" />
+ Spark SQL preview
+ </template>
+ <div ref="sparkReviewRef" 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 sparkReviewRef = ref();
+ const { setContent } = useMonaco(sparkReviewRef, {
+ language: 'sql',
+ code: '',
+ options: getMonacoOptions(true) as any,
+ });
+ const [registerDrawer] = useDrawerInner((data) => {
+ if (data) {
+ setContent(data.sql || '');
+ }
+ });
+</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/components/State.tsx b/streampark-console/streampark-console-webapp/src/views/spark/app/components/State.tsx
new file mode 100644
index 000000000..5a56faafc
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/components/State.tsx
@@ -0,0 +1,240 @@
+/*
+ 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, defineComponent, toRefs, unref } from 'vue';
+import { Tag, Tooltip } from 'ant-design-vue';
+import '../../../flink/app/components/State.less';
+import { AppStateEnum, OptionStateEnum } from '/@/enums/sparkEnum';
+import { ReleaseStateEnum } from '/@/enums/flinkEnum';
+import { useI18n } from '/@/hooks/web/useI18n';
+const { t } = useI18n();
+
+/* state map*/
+export const stateMap = {
+ [AppStateEnum.ADDED]: { color: '#2f54eb', title: t('flink.app.runState.added') },
+ [AppStateEnum.NEW_SAVING]: {
+ color: '#738df8',
+ title: t('flink.app.runState.saving'),
+ class: 'status-processing-initializing',
+ },
+ [AppStateEnum.NEW]: { color: '#2f54eb', title: t('flink.app.runState.created') },
+ [AppStateEnum.STARTING]: {
+ color: '#1AB58E',
+ title: t('flink.app.runState.starting'),
+ class: 'status-processing-starting',
+ },
+ [AppStateEnum.SUBMITTE]: {
+ color: '#13c2c2',
+ title: t('flink.app.runState.submitted'),
+ class: 'status-processing-restarting',
+ },
+ [AppStateEnum.ACCEPTE]: {
+ color: '#52c41a',
+ title: t('flink.app.runState.accept'),
+ class: 'status-processing-running',
+ },
+ [AppStateEnum.RUNNIN]: {
+ color: '#fa541c',
+ title: t('flink.app.runState.running'),
+ class: 'status-processing-failing',
+ },
+ [AppStateEnum.FINISHE]: { color: '#1890ff', title: t('flink.app.runState.finished') },
+ [AppStateEnum.FAILE]: { color: '#f5222d', title: t('flink.app.runState.failed') },
+ [AppStateEnum.LOS]: { color: '#333333', title: t('flink.app.runState.lost') },
+ [AppStateEnum.MAPPING]: {
+ color: '#13c2c2',
+ title: t('flink.app.runState.mapping'),
+ class: 'status-processing-restarting',
+ },
+ [AppStateEnum.OTHER]: { color: '#722ed1', title: t('flink.app.runState.other') },
+ [AppStateEnum.REVOKED]: {
+ color: '#eb2f96',
+ title: t('flink.app.runState.revoked'),
+ class: 'status-processing-reconciling',
+ },
+
+ [AppStateEnum.STOPPING]: {
+ color: '#738df8',
+ title: t('flink.app.runState.stopping'),
+ class: 'status-processing-initializing',
+ },
+ [AppStateEnum.KILLED]: { color: '#8E50FF', title: t('flink.app.runState.killed') },
+};
+/* option state map*/
+export const optionStateMap = {
+ [OptionStateEnum.RELEASING]: {
+ color: '#1ABBDC',
+ title: t('flink.app.releaseState.releasing'),
+ class: 'status-processing-deploying',
+ },
+ [OptionStateEnum.STOPPING]: {
+ color: '#faad14',
+ title: t('flink.app.runState.cancelling'),
+ class: 'status-processing-cancelling',
+ },
+ [OptionStateEnum.STARTING]: {
+ color: '#1AB58E',
+ title: t('flink.app.runState.starting'),
+ class: 'status-processing-starting',
+ },
+};
+
+/* release state map*/
+export const releaseStateMap = {
+ [ReleaseStateEnum.FAILED]: { color: '#f5222d', title: t('flink.app.releaseState.failed') },
+ [ReleaseStateEnum.DONE]: { color: '#52c41a', title: t('flink.app.releaseState.success') },
+ [ReleaseStateEnum.NEED_RELEASE]: { color: '#fa8c16', title: t('flink.app.releaseState.waiting') },
+ [ReleaseStateEnum.RELEASING]: {
+ color: '#52c41a',
+ title: t('flink.app.releaseState.releasing'),
+ class: 'status-processing-deploying',
+ },
+ [ReleaseStateEnum.NEED_RESTART]: { color: '#fa8c16', title: t('flink.app.releaseState.pending') },
+ [ReleaseStateEnum.NEED_ROLLBACK]: {
+ color: '#fa8c16',
+ title: t('flink.app.releaseState.waiting'),
+ },
+};
+
+/* build state map*/
+export const buildStatusMap = {
+ 0: { color: '#99A3A4', title: 'UNKNOWN' },
+ 1: { color: '#F5B041', title: 'PENDING' },
+ 2: {
+ color: '#3498DB',
+ title: 'BUILDING',
+ class: 'status-processing-deploying',
+ },
+ 3: { color: '#2ECC71', title: 'SUCCESS' },
+ 4: { color: '#E74C3C', title: 'FAILURE' },
+};
+
+const overviewMap = {
+ running: { color: '#52c41a', title: 'RUNNING' },
+ canceled: { color: '#fa8c16', title: 'CANCELED' },
+ canceling: { color: '#faad14', title: 'CANCELING' },
+ created: { color: '#2f54eb', title: 'CREATED' },
+ deploying: { color: '#eb2f96', title: 'RECONCILING' },
+ reconciling: { color: '#13c2c2', title: 'RELEASING' },
+ scheduled: { color: '#722ed1', title: 'SCHEDULED' },
+};
+
+export default defineComponent({
+ name: 'State',
+ props: {
+ option: {
+ type: String,
+ default: 'state',
+ },
+ data: {
+ type: Object as PropType<Recordable>,
+ default: () => ({}),
+ },
+ maxTitle: String,
+ },
+ setup(props) {
+ const { data, option } = toRefs(props);
+
+ const tagWidth = computed(() => {
+ if (props.maxTitle === undefined) return 0;
+ // create a dom to calculate the width of the tag
+ const dom = document.createElement('span');
+ dom.style.display = 'inline-block';
+ dom.style.fontSize = '10px';
+ dom.style.padding = '0 2px';
+ dom.style.borderRadius = '2px';
+ dom.textContent = props.maxTitle;
+ document.body.appendChild(dom);
+ const width = dom.clientWidth + 2;
+ document.body.removeChild(dom);
+ return width;
+ });
+
+ const renderTag = (map: Recordable, key: number) => {
+ if (!Reflect.has(map, key)) {
+ return;
+ }
+ return (
+ <Tag {...map[key]} style={getStyle.value}>
+ {map[key].title}
+ </Tag>
+ );
+ };
+
+ const getStyle = computed(() => {
+ if (tagWidth.value > 0) {
+ return { width: `${tagWidth.value}px`, textAlign: 'center' };
+ }
+ return {};
+ });
+ const renderState = () => {
+ if (unref(data).optionState === OptionStateEnum.NONE) {
+ return <div class="bold-tag">{renderTag(stateMap, unref(data).state)}</div>;
+ } else {
+ return <div class="bold-tag">{renderTag(optionStateMap, unref(data).optionState)}</div>;
+ }
+ };
+ function renderOverview() {
+ if (!unref(data).overview) {
+ return;
+ }
+ return Object.keys(overviewMap).map((k) => {
+ if (unref(data)?.overview[k]) {
+ const item = overviewMap[k];
+ return (
+ <Tooltip title={item.title}>
+ <Tag color={item.color}>{unref(data)?.overview[k]}</Tag>
+ </Tooltip>
+ );
+ } else {
+ return;
+ }
+ });
+ }
+ const renderOtherOption = () => {
+ if ([AppStateEnum.RUNNIN, AppStateEnum.FAILE].includes(unref(data)?.state)) {
+ return (
+ <div class="bold-tag">
+ {unref(data).totalTask && (
+ <Tooltip title="TOTAL">
+ <Tag color="#102541">{unref(data)?.totalTask}</Tag>
+ </Tooltip>
+ )}
+ {renderOverview()}
+ </div>
+ );
+ }
+ return <div>-</div>;
+ };
+
+ return () => {
+ if (unref(option) === 'state') {
+ return <span>{renderState()}</span>;
+ }
+
+ if (unref(option) === 'release') {
+ return <span class="bold-tag">{renderTag(releaseStateMap, unref(data).release)}</span>;
+ }
+
+ if (unref(option) === 'build') {
+ return <span class="bold-tag">{renderTag(buildStatusMap, unref(data).buildStatus)}</span>;
+ }
+
+ return <span>{renderOtherOption()}</span>;
+ };
+ },
+});
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
index 2c2247401..60fd0a244 100644
--- 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
@@ -48,6 +48,9 @@
<strong class="pl-10px">{{ item.value }}</strong>
<Divider type="vertical" v-if="index !== footerList.length - 1" />
</template>
+ <template v-if="footerList.length == 0">
+ <div class="inline-block h-18px"></div>
+ </template>
</Skeleton>
</div>
</template>
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
index 8c714de26..06f3d2b61 100644
--- a/streampark-console/streampark-console-webapp/src/views/spark/app/create.vue
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/create.vue
@@ -16,27 +16,20 @@
-->
<script setup lang="ts">
import { useGo } from '/@/hooks/web/usePage';
- import ProgramArgs from './components/ProgramArgs.vue';
+ import AppForm from './components/AppForm.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,
- });
+ import { fetchSparkEnvList } from '/@/api/spark/home';
+ import { SparkEnv } from '/@/api/spark/home.type';
defineOptions({
name: 'SparkApplicationAction',
@@ -49,26 +42,15 @@
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();
+ const sparkEnvs = ref<SparkEnv[]>([]);
- /* Initialize the form */
- async function handleInitForm() {
+ async function handleAppFieldValue() {
const defaultValue = {};
const v = sparkEnvs.value.filter((v) => v.isDefault)[0];
if (v) {
Object.assign(defaultValue, { versionId: v.id });
}
- await setFieldsValue(defaultValue);
+ return defaultValue;
}
/* custom mode */
@@ -122,24 +104,19 @@
}
/* 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);
+ if (formValue.jobType == JobTypeEnum.SQL) {
+ if (formValue.sparkSql == null || formValue.sparkSql.trim() === '') {
+ createMessage.warning(t('spark.app.addAppTips.sparkSqlIsRequiredMessage'));
} else {
- handleCustomJobMode(formValue);
+ const access = await sparkSql?.value?.handleVerifySql();
+ if (!access) {
+ createMessage.warning(t('spark.app.addAppTips.sqlCheck'));
+ throw new Error(access);
+ }
}
- } catch (error) {
- submitLoading.value = false;
+ handleSQLMode(formValue);
+ } else {
+ handleCustomJobMode(formValue);
}
}
/* send create request */
@@ -158,44 +135,16 @@
submitLoading.value = false;
go('/spark/app');
}
-
- onMounted(async () => {
- handleInitForm();
+ onMounted(() => {
+ //get flinkEnv
+ fetchSparkEnvList().then((res) => {
+ sparkEnvs.value = res;
+ });
});
</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" />
+ <AppForm :initFormFn="handleAppFieldValue" :submit="handleAppSubmit" :spark-envs="sparkEnvs" />
</PageWrapper>
</template>
diff --git a/streampark-console/streampark-console-webapp/src/views/spark/app/data/detail.data.ts b/streampark-console/streampark-console-webapp/src/views/spark/app/data/detail.data.ts
new file mode 100644
index 000000000..d6874e540
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/data/detail.data.ts
@@ -0,0 +1,126 @@
+/*
+ * 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 { BasicColumn } from '/@/components/Table/src/types/table';
+import { Tag, TypographyParagraph } from 'ant-design-vue';
+import { DescItem } from '/@/components/Description';
+import { h } from 'vue';
+import State from '../components/State';
+import Icon from '/@/components/Icon';
+import { dateToDuration } from '/@/utils/dateUtil';
+import { JobTypeEnum } from '/@/enums/sparkEnum';
+import { useI18n } from '/@/hooks/web/useI18n';
+
+const { t } = useI18n();
+export const getDescSchema = (): DescItem[] => {
+ return [
+ {
+ field: 'id',
+ label: t('spark.app.id'),
+ render: (curVal) =>
+ h(TypographyParagraph, { copyable: true, style: { color: '#477de9' } }, () => curVal),
+ },
+ { field: 'appName', label: t('spark.app.appName') },
+ {
+ field: 'jobType',
+ label: t('spark.app.developmentMode'),
+ render: (curVal) =>
+ h(
+ 'div',
+ { class: 'bold-tag' },
+ h(Tag, { color: curVal === 1 ? '#545454' : '#0C7EF2', class: 'mr-8px' }, () =>
+ curVal === 1 ? 'Custom Code' : 'Spark SQL',
+ ),
+ ),
+ },
+ {
+ field: 'module',
+ label: t('spark.app.module'),
+ show: (data) => data.jobType != JobTypeEnum.SQL,
+ },
+ {
+ field: 'state',
+ label: t('spark.app.status'),
+ render: (_curVal, data) => h(State, { option: 'state', data }),
+ },
+ {
+ field: 'startTime',
+ label: t('spark.app.startTime'),
+ render: (curVal) =>
+ h(
+ 'div',
+ null,
+ curVal
+ ? [h(Icon, { icon: 'ant-design:clock-circle-outlined' }), h('span', null, curVal)]
+ : '-',
+ ),
+ },
+ {
+ field: 'endTime',
+ label: t('spark.app.endTime'),
+ render: (curVal) =>
+ h(
+ 'div',
+ null,
+ curVal
+ ? [h(Icon, { icon: 'ant-design:clock-circle-outlined' }), h('span', null, curVal)]
+ : '-',
+ ),
+ },
+ {
+ field: 'duration',
+ label: t('spark.app.duration'),
+ render: (curVal) => dateToDuration(curVal),
+ show: (data) => data.duration,
+ },
+ { field: 'description', label: t('common.description'), span: 2 },
+ ];
+};
+/* setting */
+export const getConfColumns = (): BasicColumn[] => [
+ { title: 'Version', dataIndex: 'version' },
+ { title: 'Conf Format', dataIndex: 'format' },
+ { title: 'Effective', dataIndex: 'effective' },
+ { title: 'Modify Time', dataIndex: 'createTime' },
+];
+
+export const getSparkSqlColumns = (): BasicColumn[] => [
+ { title: 'Version', dataIndex: 'version' },
+ { title: 'Effective', dataIndex: 'effective' },
+ { title: 'Candidate', dataIndex: 'candidate' },
+ { title: 'Modify Time', dataIndex: 'createTime' },
+];
+
+export const getSavePointColumns = (): BasicColumn[] => [
+ { title: 'Path', dataIndex: 'path' },
+ { title: 'Trigger Time', dataIndex: 'triggerTime', width: 250 },
+ { title: 'Type', dataIndex: 'type', width: 170 },
+ { title: 'Latest', dataIndex: 'latest', width: 200 },
+];
+export const getBackupColumns = (): BasicColumn[] => [
+ { title: 'Save Path', dataIndex: 'path', align: 'left' },
+ { title: 'Description', dataIndex: 'description' },
+ { title: 'Version', dataIndex: 'version', width: 100, align: 'center' },
+ { title: 'Backup Time', dataIndex: 'createTime', width: 200 },
+];
+
+export const getOptionLogColumns = (): BasicColumn[] => [
+ { title: 'Operation Name', dataIndex: 'optionName', width: 150 },
+ { title: 'Cluster Id', dataIndex: 'yarnAppId' },
+ { title: 'JobManager URL', dataIndex: 'jobManagerUrl' },
+ { title: 'Start Status', dataIndex: 'success', width: 120 },
+ { title: 'Option Time', dataIndex: 'optionTime', width: 200 },
+];
diff --git a/streampark-console/streampark-console-webapp/src/views/spark/app/detail.vue b/streampark-console/streampark-console-webapp/src/views/spark/app/detail.vue
new file mode 100644
index 000000000..004bf65c5
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/detail.vue
@@ -0,0 +1,226 @@
+<!--
+ 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 { onUnmounted, reactive, h, unref, ref, onMounted, computed } from 'vue';
+ import { AppStateEnum, ExecModeEnum } from '/@/enums/sparkEnum';
+ import { useI18n } from '/@/hooks/web/useI18n';
+ import { fetchAppExternalLink } from '/@/api/setting/externalLink';
+ import { ExternalLink } from '/@/api/setting/types/externalLink.type';
+ import { useModal } from '/@/components/Modal';
+ import { PageWrapper } from '/@/components/Page';
+ import { Description, useDescription } from '/@/components/Description';
+ import { Icon } from '/@/components/Icon';
+ import { useRoute, useRouter } from 'vue-router';
+ import { useIntervalFn } from '@vueuse/core';
+ import { Tooltip, Divider, Space } from 'ant-design-vue';
+ import { handleView } from './utils';
+ import { Button } from '/@/components/Button';
+ import { getDescSchema } from './data/detail.data';
+ import { fetchSparkConfList } from '/@/api/spark/conf';
+ import Mergely from './components/Mergely.vue';
+ import DetailTab from './components/DetailTab.vue';
+ import RequestModal from '/@/views/flink/app/components/RequestModal';
+ import { createDetailProviderContext } from './hooks/useDetailContext';
+ import { useDrawer } from '/@/components/Drawer';
+ import { LinkBadge } from '/@/components/LinkBadge';
+ import { SparkApplication } from '/@/api/spark/app.type';
+ import {
+ fetchGetSparkApp,
+ fetchSparkBackUps,
+ fetchSparkOptionLog,
+ fetchSparkYarn,
+ } from '/@/api/spark/app';
+ import { isNullOrUnDef } from '/@/utils/is';
+
+ defineOptions({
+ name: 'ApplicationDetail',
+ });
+ const route = useRoute();
+ const router = useRouter();
+
+ const { t } = useI18n();
+
+ const yarn = ref('');
+ const externalLinks = ref<ExternalLink[]>([]);
+ const app = reactive<Partial<SparkApplication>>({});
+ const detailTabs = reactive({
+ showConf: false,
+ showSaveOption: false,
+ showBackup: false,
+ showOptionLog: false,
+ });
+
+ createDetailProviderContext({ app });
+
+ const [registerDescription] = useDescription({
+ schema: [
+ ...(getDescSchema() as any),
+ {
+ field: 'resetApi',
+ span: 2,
+ label: h('div', null, [
+ t('spark.app.detail.resetApi'),
+ h(Tooltip, { title: t('spark.app.detail.resetApiToolTip'), placement: 'top' }, () =>
+ h(Icon, { icon: 'ant-design:question-circle-outlined', class: 'pl-5px', color: 'red' }),
+ ),
+ ]),
+ render: () => [
+ h(
+ Button,
+ {
+ type: 'primary',
+ size: 'small',
+ class: 'mx-3px px-5px',
+ onClick: () =>
+ openApiModal(true, {
+ name: 'flinkStart',
+ app,
+ }),
+ },
+ () => [t('spark.app.detail.copyStartcURL')],
+ ),
+ h(
+ Button,
+ {
+ type: 'primary',
+ size: 'small',
+ class: 'mx-3px px-5px',
+ onClick: () => {
+ openApiModal(true, {
+ name: 'flinkCancel',
+ app,
+ });
+ },
+ },
+ () => [t('spark.app.detail.copyCancelcURL')],
+ ),
+ ],
+ },
+ ],
+ data: app,
+ layout: 'vertical',
+ column: 3,
+ });
+
+ const [registerConfDrawer] = useDrawer();
+ const [registerOpenApi, { openModal: openApiModal }] = useModal();
+
+ /* Flink Web UI */
+ function handleFlinkView() {
+ handleView(app as any, unref(yarn));
+ }
+
+ const { pause } = useIntervalFn(
+ () => {
+ handleGetAppInfo();
+ },
+ 5000,
+ { immediateCallback: true },
+ );
+
+ async function handleGetAppInfo() {
+ if (!route.query.appId) router.back();
+ const res = await fetchGetSparkApp({
+ id: route.query.appId as string,
+ });
+ // Get data for the first time
+ if (Object.keys(app).length == 0) {
+ if (
+ !isNullOrUnDef(res.executionMode) &&
+ [ExecModeEnum.YARN_CLIENT, ExecModeEnum.YARN_CLUSTER].includes(res.executionMode)
+ ) {
+ await handleYarn();
+ }
+ await handleDetailTabs();
+ }
+ Object.assign(app, res);
+ }
+
+ async function handleDetailTabs() {
+ const commonParams = {
+ appId: route.query.appId as string,
+ pageNum: 1,
+ pageSize: 10,
+ };
+
+ const confList = await fetchSparkConfList(commonParams);
+ const backupList = await fetchSparkBackUps(commonParams);
+ const optionList = await fetchSparkOptionLog(commonParams);
+
+ if (confList.records.length > 0) detailTabs.showConf = true;
+ if (backupList.records.length > 0) detailTabs.showBackup = true;
+ if (optionList.records.length > 0) detailTabs.showOptionLog = true;
+ }
+
+ /* Get yarn data */
+ async function handleYarn() {
+ yarn.value = await fetchSparkYarn();
+ }
+
+ async function getExternalLinks() {
+ const { data: links } = await fetchAppExternalLink({ appId: route.query.appId as string });
+ externalLinks.value = links.data;
+ }
+
+ onMounted(() => {
+ getExternalLinks();
+ });
+
+ onUnmounted(() => {
+ pause();
+ });
+
+ const appNotRunning = computed(() => app.state !== AppStateEnum.RUNNIN || yarn.value === null);
+</script>
+<template>
+ <PageWrapper content-full-height content-background contentClass="p-24px">
+ <div class="mb-15px">
+ <span class="app-bar">{{ t('spark.app.detail.detailTitle') }}</span>
+ <Space class="-mt-8px">
+ <div v-for="link in externalLinks" :key="link.id">
+ <LinkBadge
+ :label="link.badgeLabel"
+ :redirect="link.renderedLinkUrl"
+ :color="link.badgeColor"
+ :message="link.badgeName"
+ :disabled="appNotRunning"
+ />
+ </div>
+ </Space>
+ <a-button type="primary" shape="circle" @click="router.back()" class="float-right -mt-8px">
+ <Icon icon="ant-design:arrow-left-outlined" />
+ </a-button>
+ <a-button
+ type="primary"
+ @click="handleFlinkView"
+ :disabled="appNotRunning"
+ class="float-right -mt-8px mr-20px"
+ >
+ <Icon icon="ant-design:cloud-outlined" />
+ {{ t('spark.app.detail.flinkWebUi') }}
+ </a-button>
+ </div>
+ <Description @register="registerDescription" />
+ <Divider class="mt-20px -mb-17px" />
+ <DetailTab :app="app" :tabConf="detailTabs" />
+ <Mergely @register="registerConfDrawer" :readOnly="true" />
+ <RequestModal @register="registerOpenApi" />
+ </PageWrapper>
+</template>
+<style lang="less">
+ @import url('../../flink/app/styles/Detail.less');
+</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/edit.vue
similarity index 51%
copy from streampark-console/streampark-console-webapp/src/views/spark/app/create.vue
copy to streampark-console/streampark-console-webapp/src/views/spark/app/edit.vue
index 8c714de26..334af6151 100644
--- a/streampark-console/streampark-console-webapp/src/views/spark/app/create.vue
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/edit.vue
@@ -16,59 +16,41 @@
-->
<script setup lang="ts">
import { useGo } from '/@/hooks/web/usePage';
- import ProgramArgs from './components/ProgramArgs.vue';
+ import AppForm from './components/AppForm.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 { fetchGetSparkApp, fetchUpdateSparkApp } from '/@/api/spark/app';
import { SparkApplication } from '/@/api/spark/app.type';
-
- const SparkSqlEditor = createAsyncComponent(() => import('./components/SparkSql.vue'), {
- loading: true,
- });
+ import { fetchSparkEnvList } from '/@/api/spark/home';
+ import { SparkEnv } from '/@/api/spark/home.type';
+ import { useRoute } from 'vue-router';
defineOptions({
name: 'SparkApplicationAction',
});
const go = useGo();
const sparkSql = ref();
- const submitLoading = ref(false);
const { t } = useI18n();
+ const sparkApp = ref<SparkApplication>({});
+ const route = useRoute();
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 sparkEnvs = ref<SparkEnv[]>([]);
- 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);
+ async function handleAppFieldValue() {
+ const appId = route.query.appId;
+ const res = await fetchGetSparkApp({ id: appId as string });
+ sparkApp.value = res;
+ return res;
}
/* custom mode */
@@ -91,7 +73,7 @@
hadoopUser: values.hadoopUser,
description: values.description,
};
- handleCreateAction(params);
+ handleUpdateAction(params);
}
/* spark sql mode */
async function handleSQLMode(values: Recordable) {
@@ -101,7 +83,7 @@
} else {
config = null;
}
- handleCreateAction({
+ handleUpdateAction({
jobType: JobTypeEnum.SQL,
executionMode: values.executionMode,
appType: AppTypeEnum.APACHE_SPARK,
@@ -122,28 +104,23 @@
}
/* 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);
+ if (formValue.jobType == JobTypeEnum.SQL) {
+ if (formValue.sparkSql == null || formValue.sparkSql.trim() === '') {
+ createMessage.warning(t('spark.app.addAppTips.sparkSqlIsRequiredMessage'));
} else {
- handleCustomJobMode(formValue);
+ const access = await sparkSql?.value?.handleVerifySql();
+ if (!access) {
+ createMessage.warning(t('spark.app.addAppTips.sqlCheck'));
+ throw new Error(access);
+ }
}
- } catch (error) {
- submitLoading.value = false;
+ handleSQLMode(formValue);
+ } else {
+ handleCustomJobMode(formValue);
}
}
/* send create request */
- async function handleCreateAction(params: Recordable) {
+ async function handleUpdateAction(params: Recordable) {
const param: SparkApplication = {};
for (const k in params) {
const v = params[k];
@@ -154,48 +131,27 @@
const socketId = buildUUID();
ls.set('DOWN_SOCKET_ID', socketId);
Object.assign(param, { socketId });
- await fetchCreateSparkApp(param);
- submitLoading.value = false;
- go('/spark/app');
+ const updated = await fetchUpdateSparkApp(params);
+ if (updated) {
+ createMessage.success(t('spark.app.success'));
+ go('/spark/app');
+ }
}
-
- onMounted(async () => {
- handleInitForm();
+ onMounted(() => {
+ if (!route?.query?.appId) {
+ go('/spark/app');
+ createMessage.warning(t('spark.app.appidCheck'));
+ return;
+ }
+ //get flinkEnv
+ fetchSparkEnvList().then((res) => {
+ sparkEnvs.value = res;
+ });
});
</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" />
+ <AppForm :initFormFn="handleAppFieldValue" :submit="handleAppSubmit" :spark-envs="sparkEnvs" />
</PageWrapper>
</template>
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
index b4ca02eb6..3141a66d0 100644
--- 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
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { computed, h, onMounted, ref, unref } from 'vue';
+import { computed, h, onMounted, ref, unref, type Ref } from 'vue';
import type { FormSchema } from '/@/components/Form';
import { useI18n } from '/@/hooks/web/useI18n';
import { AppExistsStateEnum, JobTypeEnum } from '/@/enums/sparkEnum';
@@ -23,16 +23,15 @@ 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 { renderIsSetConfig, renderStreamParkResource, renderYarnQueue } from './useSparkRender';
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() {
+
+export function useSparkSchema(sparkEnvs: Ref<SparkEnv[]>) {
const { t } = useI18n();
- const sparkEnvs = ref<SparkEnv[]>([]);
const teamResource = ref<Array<any>>([]);
const suggestions = ref<Array<{ text: string; description: string; value: string }>>([]);
@@ -40,9 +39,9 @@ export function useSparkSchema() {
/* 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'));
+ return Promise.reject(t('spark.app.addAppTips.appNameIsRequiredMessage'));
}
- const params = { jobName: value };
+ const params = { appName: value };
// if (edit?.appId) {
// Object.assign(params, { id: edit.appId });
// }
@@ -51,13 +50,13 @@ export function useSparkSchema() {
case AppExistsStateEnum.NO:
return Promise.resolve();
case AppExistsStateEnum.IN_DB:
- return Promise.reject(t('flink.app.addAppTips.appNameNotUniqueMessage'));
+ return Promise.reject(t('spark.app.addAppTips.appNameNotUniqueMessage'));
case AppExistsStateEnum.IN_YARN:
- return Promise.reject(t('flink.app.addAppTips.appNameExistsInYarnMessage'));
+ return Promise.reject(t('spark.app.addAppTips.appNameExistsInYarnMessage'));
case AppExistsStateEnum.IN_KUBERNETES:
- return Promise.reject(t('flink.app.addAppTips.appNameExistsInK8sMessage'));
+ return Promise.reject(t('spark.app.addAppTips.appNameExistsInK8sMessage'));
default:
- return Promise.reject(t('flink.app.addAppTips.appNameValid'));
+ return Promise.reject(t('spark.app.addAppTips.appNameValid'));
}
}
const getJobTypeOptions = () => {
@@ -90,11 +89,11 @@ export function useSparkSchema() {
return [
{
field: 'jobType',
- label: t('flink.app.developmentMode'),
+ label: t('spark.app.developmentMode'),
component: 'Select',
componentProps: ({ formModel }) => {
return {
- placeholder: t('flink.app.addAppTips.developmentModePlaceholder'),
+ placeholder: t('spark.app.addAppTips.developmentModePlaceholder'),
options: getJobTypeOptions(),
onChange: (value) => {
if (value != JobTypeEnum.SQL) {
@@ -105,18 +104,18 @@ export function useSparkSchema() {
},
defaultValue: String(JobTypeEnum.SQL),
rules: [
- { required: true, message: t('flink.app.addAppTips.developmentModeIsRequiredMessage') },
+ { required: true, message: t('spark.app.addAppTips.developmentModeIsRequiredMessage') },
],
},
{
field: 'executionMode',
- label: t('flink.app.executionMode'),
+ label: t('spark.app.executionMode'),
component: 'Select',
itemProps: {
autoLink: false, //Resolve multiple trigger validators with null value ·
},
componentProps: {
- placeholder: t('flink.app.addAppTips.executionModePlaceholder'),
+ placeholder: t('spark.app.addAppTips.executionModePlaceholder'),
options: executionModes,
},
rules: [
@@ -124,7 +123,7 @@ export function useSparkSchema() {
required: true,
validator: async (_rule, value) => {
if (value === null || value === undefined || value === '') {
- return Promise.reject(t('flink.app.addAppTips.executionModeIsRequiredMessage'));
+ return Promise.reject(t('spark.app.addAppTips.executionModeIsRequiredMessage'));
} else {
return Promise.resolve();
}
@@ -142,7 +141,7 @@ export function useSparkSchema() {
fieldNames: { label: 'sparkName', value: 'id', options: 'options' },
},
rules: [
- { required: true, message: t('flink.app.addAppTips.flinkVersionIsRequiredMessage') },
+ { required: true, message: t('spark.app.addAppTips.flinkVersionIsRequiredMessage') },
],
},
{
@@ -151,28 +150,28 @@ export function useSparkSchema() {
component: 'Input',
slot: 'sparkSql',
ifShow: ({ values }) => values?.jobType == JobTypeEnum.SQL,
- rules: [{ required: true, message: t('flink.app.addAppTips.flinkSqlIsRequiredMessage') }],
+ rules: [{ required: true, message: t('spark.app.addAppTips.flinkSqlIsRequiredMessage') }],
},
{
field: 'teamResource',
- label: t('flink.app.resource'),
+ label: t('spark.app.resource'),
component: 'Select',
render: ({ model }) => renderStreamParkResource({ model, resources: unref(teamResource) }),
ifShow: ({ values }) => values.jobType == JobTypeEnum.JAR,
},
{
field: 'mainClass',
- label: t('flink.app.mainClass'),
+ label: t('spark.app.mainClass'),
component: 'Input',
- componentProps: { placeholder: t('flink.app.addAppTips.mainClassPlaceholder') },
+ componentProps: { placeholder: t('spark.app.addAppTips.mainClassPlaceholder') },
ifShow: ({ values }) => values?.jobType == JobTypeEnum.JAR,
- rules: [{ required: true, message: t('flink.app.addAppTips.mainClassIsRequiredMessage') }],
+ rules: [{ required: true, message: t('spark.app.addAppTips.mainClassIsRequiredMessage') }],
},
{
field: 'jobName',
- label: t('flink.app.appName'),
+ label: t('spark.app.appName'),
component: 'Input',
- componentProps: { placeholder: t('flink.app.addAppTips.appNamePlaceholder') },
+ componentProps: { placeholder: t('spark.app.addAppTips.appNamePlaceholder') },
dynamicRules: ({ model }) => {
return [
{
@@ -186,21 +185,21 @@ export function useSparkSchema() {
},
{
field: 'tags',
- label: t('flink.app.tags'),
+ label: t('spark.app.tags'),
component: 'Input',
componentProps: {
- placeholder: t('flink.app.addAppTips.tagsPlaceholder'),
+ placeholder: t('spark.app.addAppTips.tagsPlaceholder'),
},
},
{
field: 'yarnQueue',
- label: t('flink.app.yarnQueue'),
+ label: t('spark.app.yarnQueue'),
component: 'Input',
render: (renderCallbackParams) => renderYarnQueue(renderCallbackParams),
},
{
field: 'isSetConfig',
- label: t('flink.app.appConf'),
+ label: t('spark.app.appConf'),
component: 'Switch',
render({ model, field }) {
return renderIsSetConfig(model, field, registerConfDrawer, openConfDrawer);
@@ -218,7 +217,7 @@ export function useSparkSchema() {
},
{
field: 'args',
- label: t('flink.app.programArgs'),
+ label: t('spark.app.programArgs'),
component: 'InputTextArea',
defaultValue: '',
slot: 'args',
@@ -226,22 +225,18 @@ export function useSparkSchema() {
},
{
field: 'hadoopUser',
- label: t('flink.app.hadoopUser'),
+ label: t('spark.app.hadoopUser'),
component: 'Input',
},
{
field: 'description',
label: t('common.description'),
component: 'InputTextArea',
- componentProps: { rows: 4, placeholder: t('flink.app.addAppTips.descriptionPlaceholder') },
+ componentProps: { rows: 4, placeholder: t('spark.app.addAppTips.descriptionPlaceholder') },
},
];
});
onMounted(async () => {
- //get flinkEnv
- fetchSparkEnvList().then((res) => {
- sparkEnvs.value = res;
- });
/* Get team dependencies */
fetchTeamResource({}).then((res) => {
teamResource.value = res;
@@ -259,6 +254,5 @@ export function useSparkSchema() {
return {
formSchema,
suggestions,
- sparkEnvs,
};
}
diff --git a/streampark-console/streampark-console-webapp/src/api/spark/conf.ts b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useDetailContext.ts
similarity index 58%
copy from streampark-console/streampark-console-webapp/src/api/spark/conf.ts
copy to streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useDetailContext.ts
index e4e0ccb59..28f158b65 100644
--- a/streampark-console/streampark-console-webapp/src/api/spark/conf.ts
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useDetailContext.ts
@@ -14,20 +14,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { defHttp } from '/@/utils/http/axios';
+import { InjectionKey } from 'vue';
+import { createContext, useContext } from '/@/hooks/core/useContext';
+import type { SparkApplication } from '/@/api/spark/app.type';
-const apiPrefix = '/flink/conf';
-
-export function fetchGetSparkConf(data: { id: string }) {
- return defHttp.post({ url: `${apiPrefix}/get`, data });
+export interface DetailProviderContextProps {
+ app: Partial<SparkApplication>;
}
-export function handleSparkConfTemplate() {
- return defHttp.post<string>({
- url: `${apiPrefix}/template`,
- });
+
+const key: InjectionKey<DetailProviderContextProps> = Symbol();
+
+export function createDetailProviderContext(context: DetailProviderContextProps) {
+ return createContext<DetailProviderContextProps>(context, key);
}
-export function fetchSysHadoopConf() {
- return defHttp.post({
- url: `${apiPrefix}/sysHadoopConf`,
- });
+
+export function useDetailProviderContext() {
+ return useContext<DetailProviderContextProps>(key);
}
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
index 6d7beb588..a2c22fc87 100644
--- 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
@@ -14,22 +14,233 @@
* 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 { Alert, Form, Input, Tag } from 'ant-design-vue';
+import { h, onMounted, reactive, ref, unref, VNode } from 'vue';
+import { handleAppBuildStatueText } from '../utils';
+import {
+ fetchCheckSparkAppStart,
+ fetchCheckSparkName,
+ fetchCopySparkApp,
+ fetchSparkAppForcedStop,
+ fetchSparkAppStart,
+ fetchSparkMapping,
+} from '/@/api/spark/app';
import { fetchAppOwners } from '/@/api/system/user';
import { SvgIcon } from '/@/components/Icon';
-import { AppExistsStateEnum } from '/@/enums/sparkEnum';
+import {
+ AppExistsStateEnum,
+ AppStateEnum,
+ ExecModeEnum,
+ OptionStateEnum,
+} from '/@/enums/sparkEnum';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
-import { fetchCheckSparkName, fetchCopySparkApp, fetchSparkMapping } from '/@/api/spark/app';
+import { fetchBuildSparkApp, fetchBuildProgressDetail } from '/@/api/spark/build';
+import type { SparkApplication } from '/@/api/spark/app.type';
+import { exceptionPropWidth } from '/@/utils';
+import { useRouter } from 'vue-router';
-export const useSparkAction = () => {
+export const useSparkAction = (optionApps: Recordable) => {
const { t } = useI18n();
- const { Swal, createConfirm, createMessage } = useMessage();
+ const router = useRouter();
+ const { Swal, createConfirm, createMessage, createWarningModal } = useMessage();
const users = ref<Recordable>([]);
+ const appBuildDetail = reactive<Recordable>({});
+
+ /* check */
+ function handleCheckReleaseApp(app: Recordable) {
+ if (app['appControl']['allowBuild'] === true) {
+ handleReleaseApp(app, false);
+ } else {
+ createWarningModal({
+ title: 'WARNING',
+ content: `
+ <p class="pt-10px">${t('flink.app.release.releaseTitle')}</p>
+ <p>${t('flink.app.release.releaseDesc')}</p>
+ `,
+ okType: 'danger',
+ onOk: () => handleReleaseApp(app, true),
+ });
+ }
+ }
+
+ /* Release App */
+ async function handleReleaseApp(app: Recordable, force: boolean) {
+ const res = await fetchBuildSparkApp({
+ appId: app.id,
+ forceBuild: force,
+ });
+ if (!res.data) {
+ let message = res.message || '';
+ if (!message) {
+ message = t('flink.app.release.releaseFail') + message.replaceAll(/\[StreamPark]/g, '');
+ }
+ Swal.fire('Failed', message, 'error');
+ } else {
+ Swal.fire({
+ icon: 'success',
+ title: t('flink.app.release.releasing'),
+ showConfirmButton: false,
+ timer: 2000,
+ });
+ }
+ }
+
+ /* start application */
+ function handleAppCheckStart(app: Recordable) {
+ // when then app is building, show forced starting modal
+ if (app['appControl']['allowStart'] === false) {
+ handleFetchBuildDetail(app);
+ createWarningModal({
+ title: 'WARNING',
+ content: () => {
+ const content: Array<VNode> = [];
+ if (appBuildDetail.pipeline == null) {
+ content.push(
+ h('p', { class: 'pt-10px' }, 'No build record exists for the current application.'),
+ );
+ } else {
+ content.push(
+ h('p', { class: 'pt-10px' }, [
+ 'The current build state of the application is',
+ h(
+ Tag,
+ { color: 'orange' },
+ handleAppBuildStatueText(appBuildDetail.pipeline.pipeStatus),
+ ),
+ ]),
+ );
+ }
+ content.push(h('p', null, 'Are you sure to force the application to run?'));
+ return content;
+ },
+ okType: 'danger',
+ onOk: () => {
+ handleStart(app);
+ return Promise.resolve(true);
+ },
+ });
+ } else {
+ handleStart(app);
+ }
+ }
+
+ async function handleStart(app: SparkApplication) {
+ if (app.sparkVersion == null) {
+ Swal.fire('Failed', 'please set spark version first.', 'error');
+ } else {
+ if (!optionApps.starting.get(app.id) || app['optionState'] === OptionStateEnum.NONE) {
+ // when then app is building, show forced starting modal
+ const resp = await fetchCheckSparkAppStart({
+ id: app.id,
+ });
+ if (+resp === AppExistsStateEnum.IN_YARN) {
+ await fetchSparkAppForcedStop({
+ id: app.id,
+ });
+ }
+ await handleDoSubmit(app);
+ }
+ }
+ }
+ /* submit */
+ async function handleDoSubmit(data: SparkApplication) {
+ try {
+ const res = await fetchSparkAppStart({
+ id: data.id,
+ });
+ if (res.data) {
+ Swal.fire({
+ icon: 'success',
+ title: t('spark.app.operation.starting'),
+ showConfirmButton: false,
+ timer: 2000,
+ });
+ optionApps.starting.set(data.id, new Date().getTime());
+ } else {
+ 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: '/spark/app/detail',
+ query: { appId: data.id },
+ });
+ }
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ async function handleFetchBuildDetail(app: Recordable) {
+ const res = await fetchBuildProgressDetail(app.id);
+ appBuildDetail.pipeline = res.pipeline;
+ appBuildDetail.docker = res.docker;
+ }
+
+ function handleCanStop(app: Recordable) {
+ const optionTime = new Date(app['optionTime']).getTime();
+ const nowTime = new Date().getTime();
+ if (nowTime - optionTime >= 60 * 1000) {
+ const state = app['optionState'];
+ if (state === OptionStateEnum.NONE) {
+ return [AppStateEnum.STARTING, AppStateEnum.MAPPING].includes(app.state) || false;
+ }
+ return true;
+ }
+ return false;
+ }
+ function handleAbort(app: Recordable) {
+ let option = 'starting';
+ const optionState = app['optionState'];
+ const stateMap = {
+ [AppStateEnum.STARTING]: 'starting',
+ };
+ const optionStateMap = {
+ [OptionStateEnum.RELEASING]: 'releasing',
+ [OptionStateEnum.STARTING]: 'starting',
+ };
+ if (optionState === OptionStateEnum.NONE) {
+ option = stateMap[app.state];
+ } else {
+ option = optionStateMap[optionState];
+ }
+ Swal.fire({
+ title: 'Are you sure?',
+ text: `current job is ${option}, are you sure abort the job?`,
+ icon: 'warning',
+ showCancelButton: true,
+ confirmButtonText: 'Yes, abort job!',
+ denyButtonText: `No, cancel`,
+ confirmButtonColor: '#d33',
+ cancelButtonColor: '#3085d6',
+ }).then(async (result) => {
+ if (result.isConfirmed) {
+ Swal.fire('abort job', '', 'success');
+ const res = await fetchSparkAppForcedStop({
+ id: app.id,
+ });
+ if (res) {
+ createMessage.success('abort job starting');
+ }
+ return Promise.resolve();
+ }
+ });
+ }
/* copy application */
- function handleCopy(item: Recordable) {
+ function handleCopy(item) {
const validateStatus = ref<'' | 'error' | 'validating' | 'success' | 'warning'>('');
let help = '';
let copyAppName: string | undefined = '';
@@ -74,14 +285,14 @@ export const useSparkAction = () => {
return Promise.reject('copy application error');
}
//2) check name
- const params = { jobName: copyAppName };
+ const params = { appName: copyAppName };
const resp = await fetchCheckSparkName(params);
const code = parseInt(resp);
- if (code === AppExistsStateEnum.NO) {
+ if (code === 0) {
try {
const { data } = await fetchCopySparkApp({
id: item.id,
- jobName: copyAppName,
+ appName: copyAppName,
});
const status = data.status || 'error';
if (status === 'success') {
@@ -103,11 +314,11 @@ export const useSparkAction = () => {
}
} else {
validateStatus.value = 'error';
- if (code === AppExistsStateEnum.IN_DB) {
+ if (code === 1) {
help = t('flink.app.addAppTips.appNameNotUniqueMessage');
- } else if (code === AppExistsStateEnum.IN_YARN) {
+ } else if (code === 2) {
help = t('flink.app.addAppTips.appNameExistsInYarnMessage');
- } else if (code === AppExistsStateEnum.IN_KUBERNETES) {
+ } else if (code === 3) {
help = t('flink.app.addAppTips.appNameExistsInK8sMessage');
} else {
help = t('flink.app.addAppTips.appNameNotValid');
@@ -144,6 +355,15 @@ export const useSparkAction = () => {
<Form.Item label="Application Name">
<Alert message={app.jobName} type="info" />
</Form.Item>
+ {[ExecModeEnum.YARN_CLIENT, ExecModeEnum.YARN_CLUSTER].includes(app.executionMode) && (
+ <Form.Item
+ label="YARN Application Id"
+ name="appId"
+ rules={[{ required: true, message: 'YARN ApplicationId is required' }]}
+ >
+ <Input type="text" placeholder="ApplicationId" v-model:value={formValue.appId} />
+ </Form.Item>
+ )}
<Form.Item
label="JobId"
name="jobId"
@@ -162,7 +382,6 @@ export const useSparkAction = () => {
await fetchSparkMapping({
id: app.id,
appId: formValue.appId,
- jobId: formValue.jobId,
});
Swal.fire({
icon: 'success',
@@ -185,6 +404,10 @@ export const useSparkAction = () => {
});
return {
+ handleCheckReleaseApp,
+ handleAppCheckStart,
+ handleCanStop,
+ handleAbort,
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
index da31b3280..31f9709e7 100644
--- 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
@@ -25,8 +25,8 @@ const { t } = useI18n();
export const useSparkColumns = () => {
// app table column width
const tableColumnWidth = ref({
- jobName: 250,
- flinkVersion: 110,
+ appName: 250,
+ sparkVersion: 110,
tags: 150,
state: 120,
release: 190,
@@ -46,56 +46,65 @@ export const useSparkColumns = () => {
const getAppColumns = computed((): BasicColumn[] => [
{
- title: t('flink.app.appName'),
- dataIndex: 'jobName',
+ title: t('spark.app.appName'),
+ dataIndex: 'appName',
align: 'left',
fixed: 'left',
resizable: true,
- width: unref(tableColumnWidth).jobName,
+ width: unref(tableColumnWidth).appName,
},
- { title: t('flink.app.flinkVersion'), dataIndex: 'flinkVersion' },
- { title: t('flink.app.tags'), ellipsis: true, dataIndex: 'tags', width: 150 },
{
- title: t('flink.app.runStatus'),
+ title: t('spark.app.sparkVersion'),
+ dataIndex: 'sparkVersion',
+ width: unref(tableColumnWidth).sparkVersion,
+ },
+ {
+ title: t('spark.app.tags'),
+ ellipsis: true,
+ dataIndex: 'tags',
+ width: unref(tableColumnWidth).tags,
+ },
+ {
+ title: t('spark.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('spark.app.runStatusOptions.added'), value: String(AppStateEnum.ADDED) },
+ { text: t('spark.app.runStatusOptions.starting'), value: String(AppStateEnum.STARTING) },
+ { text: t('spark.app.runStatusOptions.running'), value: String(AppStateEnum.RUNNING) },
+ { text: t('spark.app.runStatusOptions.failed'), value: String(AppStateEnum.FAILED) },
+ { text: t('spark.app.runStatusOptions.canceled'), value: String(AppStateEnum.CANCELED) },
+ { text: t('spark.app.runStatusOptions.finished'), value: String(AppStateEnum.FINISHED) },
+ { text: t('spark.app.runStatusOptions.suspended'), value: String(AppStateEnum.SUSPENDED) },
+ { text: t('spark.app.runStatusOptions.lost'), value: String(AppStateEnum.LOST) },
+ { text: t('spark.app.runStatusOptions.silent'), value: String(AppStateEnum.SILENT) },
{
- text: t('flink.app.runStatusOptions.terminated'),
+ text: t('spark.app.runStatusOptions.terminated'),
value: String(AppStateEnum.TERMINATED),
},
],
},
{
- title: t('flink.app.releaseBuild'),
+ title: t('spark.app.releaseBuild'),
dataIndex: 'release',
width: unref(tableColumnWidth).release,
fixed: 'right',
},
{
- title: t('flink.app.duration'),
+ title: t('spark.app.duration'),
dataIndex: 'duration',
sorter: true,
width: unref(tableColumnWidth).duration,
customRender: ({ value }) => dateToDuration(value),
},
{
- title: t('flink.app.modifiedTime'),
+ title: t('spark.app.modifiedTime'),
dataIndex: 'modifyTime',
sorter: true,
width: unref(tableColumnWidth).modifyTime,
},
- { title: t('flink.app.owner'), dataIndex: 'nickName', width: unref(tableColumnWidth).nickName },
+ { title: t('spark.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/useFlinkRender.tsx b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkRender.tsx
similarity index 98%
rename from streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useFlinkRender.tsx
rename to streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkRender.tsx
index b25dfede2..b2ab593e0 100644
--- a/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useFlinkRender.tsx
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/hooks/useSparkRender.tsx
@@ -27,7 +27,7 @@ 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';
+import { fetchSparkConfTemplate } from '/@/api/spark/conf';
const { t } = useI18n();
/* render input dropdown component */
@@ -161,7 +161,7 @@ export const renderIsSetConfig = (
configOverride: unref(model.configOverride),
});
} else {
- const res = await handleSparkConfTemplate();
+ const res = await fetchSparkConfTemplate();
openConfDrawer(true, {
configOverride: decodeByBase64(res),
});
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
index f5488ffff..19a487531 100644
--- 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
@@ -16,50 +16,132 @@
*/
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
-
+import { handleIsStart } from '../utils';
import { ActionItem, FormProps } from '/@/components/Table';
import { useMessage } from '/@/hooks/web/useMessage';
-import { AppStateEnum } from '/@/enums/flinkEnum';
-import { JobTypeEnum } from '/@/enums/sparkEnum';
+import { AppStateEnum, JobTypeEnum, OptionStateEnum } 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 { isFunction, isNullAndUnDef, isObject } from '/@/utils/is';
+import {
+ fetchSparkAppForcedStop,
+ fetchSparkAppRecord,
+ fetchSparkAppRemove,
+} from '/@/api/spark/app';
import type { SparkApplication } from '/@/api/spark/app.type';
import { useSparkAction } from './useSparkAction';
+import { useDrawer } from '/@/components/Drawer';
+import { ReleaseStateEnum } from '/@/enums/flinkEnum';
// Create form configurations and operation functions in the application table
-export const useSparkTableAction = (handlePageDataReload: Fn) => {
+export const useSparkTableAction = (handlePageDataReload: Fn, optionApps: Recordable) => {
const { t } = useI18n();
const tagsOptions = ref<Recordable>([]);
const router = useRouter();
- const { createMessage } = useMessage();
+ const { createMessage, Swal } = useMessage();
const { hasPermission } = usePermission();
- const { handleCopy, handleMapping, users } = useSparkAction();
+ // const [registerStartModal, { openModal: openStartModal }] = useModal();
+ // const [registerStopModal, { openModal: openStopModal }] = useModal();
+ // const [registerLogModal, { openModal: openLogModal }] = useModal();
+ const [registerBuildDrawer, { openDrawer: openBuildDrawer }] = useDrawer();
+ const {
+ handleCheckReleaseApp,
+ handleAppCheckStart,
+ handleCanStop,
+ handleAbort,
+ handleCopy,
+ handleMapping,
+ users,
+ } = useSparkAction(optionApps);
/* Operation button list */
- function getActionList(record: SparkApplication, _currentPageNo: number): ActionItem[] {
+ function getActionList(record: SparkApplication, currentPageNo: number): ActionItem[] {
return [
{
- label: t('flink.app.operation.copy'),
+ tooltip: { title: t('spark.app.operation.edit') },
+ auth: 'app:update',
+ icon: 'clarity:note-edit-line',
+ onClick: handleEdit.bind(null, record, currentPageNo),
+ },
+ {
+ tooltip: { title: t('spark.app.operation.release') },
+ ifShow:
+ !isNullAndUnDef(record.release) &&
+ [
+ ReleaseStateEnum.FAILED,
+ ReleaseStateEnum.NEED_RELEASE,
+ ReleaseStateEnum.NEED_ROLLBACK,
+ ].includes(record.release) &&
+ record['optionState'] == OptionStateEnum.NONE,
+ auth: 'app:release',
+ icon: 'ant-design:cloud-upload-outlined',
+ onClick: handleCheckReleaseApp.bind(null, record),
+ },
+ {
+ tooltip: { title: t('spark.app.operation.releaseDetail') },
+ ifShow:
+ (!isNullAndUnDef(record.release) &&
+ [ReleaseStateEnum.FAILED, ReleaseStateEnum.RELEASING].includes(record.release)) ||
+ record['optionState'] == OptionStateEnum.RELEASING,
+ auth: 'app:release',
+ icon: 'ant-design:container-outlined',
+ onClick: () => openBuildDrawer(true, { appId: record.id }),
+ },
+ {
+ tooltip: { title: t('spark.app.operation.start') },
+ ifShow: handleIsStart(record, optionApps),
+ auth: 'app:start',
+ icon: 'ant-design:play-circle-outlined',
+ onClick: handleAppCheckStart.bind(null, record),
+ },
+ {
+ tooltip: { title: t('spark.app.operation.cancel') },
+ ifShow:
+ record.state == AppStateEnum.RUNNIN && record['optionState'] == OptionStateEnum.NONE,
+ auth: 'app:cancel',
+ icon: 'ant-design:pause-circle-outlined',
+ popConfirm: {
+ title: t('spark.app.operation.cancel'),
+ placement: 'left',
+ confirm: handleCancel.bind(null, record),
+ },
+ },
+ {
+ tooltip: { title: t('spark.app.operation.detail') },
+ auth: 'app:detail',
+ icon: 'carbon:data-view-alt',
+ onClick: handleDetail.bind(null, record),
+ },
+ // {
+ // tooltip: { title: t('spark.app.operation.startLog') },
+ // auth: 'app:detail',
+ // icon: 'ant-design:code-outlined',
+ // onClick: () => openLogModal(true, { app: record }),
+ // },
+ {
+ tooltip: { title: t('spark.app.operation.abort') },
+ ifShow: handleCanStop(record),
+ auth: 'app:cancel',
+ icon: 'ant-design:pause-circle-outlined',
+ onClick: handleAbort.bind(null, record),
+ },
+ {
+ label: t('spark.app.operation.copy'),
auth: 'app:copy',
icon: 'ant-design:copy-outlined',
onClick: handleCopy.bind(null, record),
},
{
- label: t('flink.app.operation.remapping'),
+ label: t('spark.app.operation.remapping'),
ifShow: [
AppStateEnum.ADDED,
- AppStateEnum.FAILED,
- AppStateEnum.CANCELED,
+ AppStateEnum.FAILE,
+ AppStateEnum.STOPPING,
AppStateEnum.KILLED,
AppStateEnum.SUCCEEDED,
- AppStateEnum.TERMINATED,
- AppStateEnum.POS_TERMINATED,
- AppStateEnum.FINISHED,
- AppStateEnum.SUSPENDED,
- AppStateEnum.LOST,
+ AppStateEnum.FINISHE,
+ AppStateEnum.LOS,
].includes(record.state as AppStateEnum),
auth: 'app:mapping',
icon: 'ant-design:deployment-unit-outlined',
@@ -67,21 +149,20 @@ export const useSparkTableAction = (handlePageDataReload: Fn) => {
},
{
popConfirm: {
- title: t('flink.app.operation.deleteTip'),
+ title: t('spark.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),
+ ifShow:
+ !isNullAndUnDef(record.state) &&
+ [
+ AppStateEnum.ADDED,
+ AppStateEnum.FAILE,
+ AppStateEnum.FINISHE,
+ AppStateEnum.LOS,
+ AppStateEnum.SUCCEEDED,
+ AppStateEnum.KILLED,
+ ].includes(record.state),
auth: 'app:delete',
icon: 'ant-design:delete-outlined',
color: 'error',
@@ -122,6 +203,35 @@ export const useSparkTableAction = (handlePageDataReload: Fn) => {
};
}
+ /* Click to edit */
+ function handleEdit(app: SparkApplication, currentPageNo: number) {
+ // Record the current page number
+ sessionStorage.setItem('sparkAppPageNo', String(currentPageNo || 1));
+ router.push({ path: '/spark/app/edit', query: { appId: app.id } });
+ }
+
+ /* Click for details */
+ function handleDetail(app: SparkApplication) {
+ router.push({ path: '/spark/app/detail', query: { appId: app.id } });
+ }
+
+ // click stop application
+ async function handleCancel(app: SparkApplication) {
+ if (!optionApps.stopping.get(app.id) || app['optionState'] == OptionStateEnum.NONE) {
+ await fetchSparkAppForcedStop({
+ id: app.id,
+ });
+ Swal.fire({
+ icon: 'success',
+ title: t('flink.app.operation.canceling'),
+ showConfirmButton: false,
+ timer: 2000,
+ });
+ optionApps.stopping.set(app.id, new Date().getTime());
+ // openStopModal(true, { application: app });
+ }
+ }
+
/* Click to delete */
async function handleDelete(app: SparkApplication) {
const hide = createMessage.loading('deleting', 0);
@@ -147,22 +257,22 @@ export const useSparkTableAction = (handlePageDataReload: Fn) => {
},
schemas: [
{
- label: t('flink.app.tags'),
+ label: t('spark.app.tags'),
field: 'tags',
component: 'Select',
componentProps: {
- placeholder: t('flink.app.tags'),
+ placeholder: t('spark.app.tags'),
showSearch: true,
options: tagsOptions.value.map((t: Recordable) => ({ label: t, value: t })),
onChange: handlePageDataReload.bind(null, false),
},
},
{
- label: t('flink.app.owner'),
+ label: t('spark.app.owner'),
field: 'userId',
component: 'Select',
componentProps: {
- placeholder: t('flink.app.owner'),
+ placeholder: t('spark.app.owner'),
showSearch: true,
options: users.value.map((u: Recordable) => {
return { label: u.nickName || u.username, value: u.userId };
@@ -171,11 +281,11 @@ export const useSparkTableAction = (handlePageDataReload: Fn) => {
},
},
{
- label: t('flink.app.jobType'),
+ label: t('spark.app.jobType'),
field: 'jobType',
component: 'Select',
componentProps: {
- placeholder: t('flink.app.jobType'),
+ placeholder: t('spark.app.jobType'),
showSearch: true,
options: [
{ label: 'JAR', value: JobTypeEnum.JAR },
@@ -186,11 +296,11 @@ export const useSparkTableAction = (handlePageDataReload: Fn) => {
},
},
{
- label: t('flink.app.searchName'),
+ label: t('spark.app.searchName'),
field: 'jobName',
component: 'Input',
componentProps: {
- placeholder: t('flink.app.searchName'),
+ placeholder: t('spark.app.searchName'),
onChange: handlePageDataReload.bind(null, false),
onSearch: handlePageDataReload.bind(null, false),
},
@@ -230,5 +340,14 @@ export const useSparkTableAction = (handlePageDataReload: Fn) => {
onMounted(() => {
handleInitTagsOptions();
});
- return { getTableActions, formConfig };
+ return {
+ // registerStartModal,
+ // registerStopModal,
+ // registerLogModal,
+ registerBuildDrawer,
+ getTableActions,
+ formConfig,
+ tagsOptions,
+ users,
+ };
};
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
index c660c040a..1987fac0d 100644
--- a/streampark-console/streampark-console-webapp/src/views/spark/app/index.vue
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/index.vue
@@ -15,17 +15,30 @@
limitations under the License.
-->
<script lang="ts" setup>
- import { nextTick, ref, onUnmounted, onMounted } from 'vue';
+ import { nextTick, ref, onUnmounted, onMounted, unref } 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 { JobTypeEnum, AppStateEnum, OptionStateEnum } from '/@/enums/sparkEnum';
+ import { ReleaseStateEnum } from '/@/enums/flinkEnum';
+ import { useDebounceFn, useTimeoutFn } from '@vueuse/core';
+ import {
+ Form,
+ Button,
+ Select,
+ Input,
+ Tooltip,
+ Badge,
+ Tag,
+ Popover,
+ Row,
+ Col,
+ } 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 AppStopModal from './components/AppStop.vue';
+ import AppBuildDrawer from './components/BuildDrawer.vue';
import AppDashboard from './components/AppDashboard.vue';
import State, {
buildStatusMap,
@@ -37,20 +50,30 @@
import AppTableResize from '/@/views/flink/app/components/AppResize.vue';
import { fetchSparkAppRecord } from '/@/api/spark/app';
import { useSparkTableAction } from './hooks/useSparkTableAction';
+ import { SparkApplication } from '/@/api/spark/app.type';
+ import { handleView } from './utils';
+ import { useRouter } from 'vue-router';
defineOptions({
name: 'SparkApplication',
});
const { t } = useI18n();
+ const router = useRouter();
+ const yarn = ref<Nullable<string>>(null);
+ const searchRef = ref<Recordable>({
+ tags: undefined,
+ owner: undefined,
+ jobType: undefined,
+ });
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: '',
@@ -76,11 +99,6 @@
expanded: [
{
appId: x.appId,
- jmMemory: x.jmMemory,
- tmMemory: x.tmMemory,
- totalTM: x.totalTM,
- totalSlot: x.totalSlot,
- availableSlot: x.availableSlot,
},
],
});
@@ -100,11 +118,6 @@
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(
@@ -160,11 +173,12 @@
console.error(error);
}
};
- const [registerTable, { reload, getLoading, setPagination }] = useTable({
+ const [registerTable, { reload, getLoading, setPagination, getDataSource }] = useTable({
rowKey: 'id',
api: getSparkAppList,
immediate: true,
- canResize: false,
+ isCanResizeParent: true,
+ canResize: true,
showIndexColumn: false,
showTableSetting: true,
useSearchForm: true,
@@ -176,7 +190,32 @@
},
});
- const { getTableActions, formConfig } = useSparkTableAction(handlePageDataReload);
+ const {
+ // registerStartModal,
+ // registerStopModal,
+ registerBuildDrawer,
+ getTableActions,
+ tagsOptions,
+ users,
+ } = useSparkTableAction(handlePageDataReload, optionApps);
+
+ /* view */
+ async function handleJobView(app: SparkApplication) {
+ // Task is running, restarting, in savePoint
+ if (app.state && [AppStateEnum.RUNNIN].includes(app.state)) {
+ // yarn-per-job|yarn-session|yarn-application
+ await handleView(app, unref(yarn));
+ }
+ }
+
+ /* Update options data */
+ // function handleOptionApp(data: {
+ // type: 'starting' | 'stopping' | 'release';
+ // key: any;
+ // value: any;
+ // }) {
+ // optionApps[data.type].set(data.key, data.value);
+ // }
function handlePageDataReload(polling = false) {
nextTick(() => {
@@ -184,6 +223,13 @@
reload({ polling });
});
}
+ const handleResetReload = useDebounceFn(() => {
+ setPagination({
+ current: 1,
+ });
+ reload();
+ }, 500);
+
const { start, stop } = useTimeoutFn(() => {
if (errorCount.value <= 3) {
start();
@@ -211,17 +257,82 @@
});
</script>
<template>
- <PageWrapper contentFullHeight>
+ <PageWrapper content-full-height content-class="flex flex-col">
<AppDashboard ref="appDashboardRef" />
<BasicTable
@register="registerTable"
:columns="getAppColumns"
@resize-column="onTableColumnResize"
- class="app_list !px-0 pt-20px"
- :formConfig="formConfig"
+ class="app_list table-searchbar flex-1 pt-20px !px-0 flex flex-col"
>
+ <template #tableTitle>
+ <div class="flex justify-between" style="width: calc(100% - 130px)">
+ <Form name="appTableForm" :model="searchRef" layout="inline" class="flex-1">
+ <Row :gutter="10" class="w-full">
+ <Col :span="5">
+ <Form.Item :label="t('flink.app.tags')">
+ <Select
+ :placeholder="t('flink.app.tags')"
+ show-search
+ v-model:value="searchRef.tags"
+ @change="() => handleResetReload()"
+ :options="(tagsOptions || []).map((t: Recordable) => ({ label: t, value: t }))"
+ />
+ </Form.Item>
+ </Col>
+ <Col :span="5">
+ <Form.Item :label="t('flink.app.owner')">
+ <Select
+ :placeholder="t('flink.app.owner')"
+ show-search
+ v-model:value="searchRef.userId"
+ @change="() => handleResetReload()"
+ :options="
+ (users || []).map((u: Recordable) => ({
+ label: u.nickName || u.username,
+ value: u.userId,
+ }))
+ "
+ />
+ </Form.Item>
+ </Col>
+ <Col :span="5">
+ <Form.Item :label="t('flink.app.jobType')">
+ <Select
+ :placeholder="t('flink.app.jobType')"
+ show-search
+ v-model:value="searchRef.jobType"
+ @change="() => handleResetReload()"
+ :options="[
+ { label: 'JAR', value: JobTypeEnum.JAR },
+ { label: 'SQL', value: JobTypeEnum.SQL },
+ { label: 'PySPARK', value: JobTypeEnum.PYSPARK },
+ ]"
+ />
+ </Form.Item>
+ </Col>
+ <Col :span="5">
+ <Form.Item :label="t('flink.app.searchName')">
+ <Input
+ :placeholder="t('flink.app.searchName')"
+ v-model:value="searchRef.appName"
+ @change="() => handleResetReload()"
+ @search="() => handleResetReload()"
+ />
+ </Form.Item>
+ </Col>
+ </Row>
+ </Form>
+ <div v-auth="'app:create'">
+ <Button type="primary" @click="() => router.push({ path: '/spark/app/add' })">
+ <PlusOutlined />
+ {{ t('common.add') }}
+ </Button>
+ </div>
+ </div>
+ </template>
<template #bodyCell="{ column, record }">
- <template v-if="column.dataIndex === 'jobName'">
+ <template v-if="column.dataIndex === 'appName'">
<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">
@@ -230,10 +341,9 @@
<span
class="link"
:class="{
- 'cursor-pointer':
- [AppStateEnum.RESTARTING, AppStateEnum.RUNNING].includes(record.state) ||
- record['optionState'] === OptionStateEnum.SAVEPOINTING,
+ 'cursor-pointer': [AppStateEnum.RUNNIN].includes(record.state),
}"
+ @click="handleJobView(record)"
>
<Popover :title="t('common.detailText')">
<template #content>
@@ -306,12 +416,16 @@
<AppTableResize
:table-container="tableContainer"
:resize-min="100"
- v-model:left="tableColumnWidth.jobName"
+ v-if="getDataSource()?.length > 0"
+ v-model:left="tableColumnWidth.appName"
/>
</template>
</BasicTable>
+ <!-- <AppStartModal @register="registerStartModal" @update-option="handleOptionApp" /> -->
+ <!-- <AppStopModal @register="registerStopModal" @update-option="handleOptionApp" /> -->
+ <AppBuildDrawer @register="registerBuildDrawer" />
</PageWrapper>
</template>
<style lang="less">
- @import url('./styles/View.less');
+ @import url('../../flink/app/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
deleted file mode 100644
index 9c00b6f6a..000000000
--- a/streampark-console/streampark-console-webapp/src/views/spark/app/sqlFormatter.js
+++ /dev/null
@@ -1,408 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 98852052f..000000000
--- a/streampark-console/streampark-console-webapp/src/views/spark/app/styles/View.less
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * 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;
- }
-}
diff --git a/streampark-console/streampark-console-webapp/src/views/spark/app/utils/index.ts b/streampark-console/streampark-console-webapp/src/views/spark/app/utils/index.ts
new file mode 100644
index 000000000..99eff9459
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/spark/app/utils/index.ts
@@ -0,0 +1,205 @@
+/*
+ * 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 { fetchSparkYarn } from '/@/api/spark/app';
+import { AppStateEnum, OptionStateEnum } from '/@/enums/sparkEnum';
+import { ConfigTypeEnum, ReleaseStateEnum, PipelineStepEnum } from '/@/enums/flinkEnum';
+import type { SparkApplication } from '/@/api/spark/app.type';
+
+export function handleAppBuildStatusColor(statusCode: number) {
+ switch (statusCode) {
+ case 0:
+ return '#99A3A4';
+ case 1:
+ return '#F5B041';
+ case 2:
+ return '#3498DB';
+ case 3:
+ return '#2ECC71';
+ case 4:
+ return '#E74C3C';
+ default:
+ return '#99A3A4';
+ }
+}
+
+export function handleAppBuildStatueText(statusCode) {
+ switch (statusCode) {
+ case 0:
+ return 'UNKNOWN';
+ case 1:
+ return 'PENDING';
+ case 2:
+ return 'RUNNING';
+ case 3:
+ return 'SUCCESS';
+ case 4:
+ return 'FAILURE';
+ default:
+ return 'UNKNOWN';
+ }
+}
+
+export function handleAppBuildStepTimelineColor(step) {
+ if (step == null) {
+ return 'gray';
+ }
+ switch (step.status) {
+ case 0:
+ case 1:
+ return '#99A3A4';
+ case 2:
+ return '#3498DB';
+ case 3:
+ return '#2ECC71';
+ case 4:
+ return '#E74C3C';
+ case 5:
+ return '#F5B041';
+ default:
+ return '#99A3A4';
+ }
+}
+
+export function handleAppBuildStepText(stepStatus: number) {
+ const buildStepMap = {
+ [PipelineStepEnum.UNKNOWN]: 'UNKNOWN',
+ [PipelineStepEnum.WAITING]: 'WAITING',
+ [PipelineStepEnum.RUNNING]: 'RUNNING',
+ [PipelineStepEnum.SUCCESS]: 'SUCCESS',
+ [PipelineStepEnum.FAILURE]: 'FAILURE',
+ [PipelineStepEnum.SKIPPED]: 'SKIPPED',
+ };
+ return buildStepMap[stepStatus];
+}
+
+// Field Description Filter
+export function descriptionFilter(option) {
+ if (option.unit) {
+ return option.description + ' (Unit ' + option.unit + ')';
+ } else {
+ return option.description;
+ }
+}
+
+export async function handleView(app: SparkApplication, yarn: Nullable<string>) {
+ if (!yarn) {
+ const res = await fetchSparkYarn();
+ window.open(res + '/proxy/' + app['clusterId'] + '/');
+ } else {
+ window.open(yarn + '/proxy/' + app['clusterId'] + '/');
+ }
+}
+
+export function handleIsStart(app: Recordable, optionApps: Recordable) {
+ const status = [
+ AppStateEnum.ADDED,
+ AppStateEnum.FAILE,
+ AppStateEnum.FINISHE,
+ AppStateEnum.LOS,
+ AppStateEnum.REVOKED,
+ AppStateEnum.SUCCEEDED,
+ AppStateEnum.KILLED,
+ ].includes(app.state);
+
+ /**
+ * Deployment failed FAILED(-1),
+ * Done DONE(0),
+ * After the task is modified, NEED_RELEASE(1) needs to be reissued,
+ * Online releasing(2),
+ * After going online, you need to restart NEED_RESTART(3),
+ * Need to rollback NEED_ROLLBACK(4),
+ * When the project changes, the task needs to be checked (whether the jar needs to be re-selected) NEED_CHECK(5),
+ * The posted task has been revoked REVOKED(10);
+ */
+
+ const release = [ReleaseStateEnum.DONE, ReleaseStateEnum.NEED_RESTART].includes(app.release);
+
+ const optionState =
+ !optionApps.starting.get(app.id) || app['optionState'] === OptionStateEnum.NONE || false;
+
+ return status && release && optionState;
+}
+
+export function getAppConfType(configFile: string): number {
+ const config = configFile || '';
+ if (config.endsWith('.yaml') || config.endsWith('.yml')) {
+ return ConfigTypeEnum.YAML;
+ } else if (config.endsWith('.properties')) {
+ return ConfigTypeEnum.PROPERTIES;
+ } else if (config.endsWith('.conf')) {
+ return ConfigTypeEnum.HOCON;
+ }
+ return ConfigTypeEnum.UNKNOWN;
+}
+
+export function handleDependencyJsonToPom(json, pomMap, jarMap) {
+ if (json != null && json.trim() !== '') {
+ const deps = JSON.parse(json);
+ const pom = deps.pom;
+ if (pom && pom.length > 0) {
+ pom.forEach((x) => {
+ const groupId = x.groupId;
+ const artifactId = x.artifactId;
+ const version = x.version;
+ const classifier = x.classifier;
+ const exclusions = x.exclusions || [];
+
+ const id =
+ classifier != null
+ ? groupId + '_' + artifactId + '_' + classifier
+ : groupId + '_' + artifactId;
+ const mvnPom = {
+ groupId: groupId,
+ artifactId: artifactId,
+ version: version,
+ classifier: classifier,
+ exclusions: [] as any[],
+ };
+ if (exclusions != null && exclusions.length > 0) {
+ exclusions.forEach((e) => {
+ if (e != null && e.length > 0) {
+ const e_group = e.groupId;
+ const e_artifact = e.artifactId;
+ mvnPom.exclusions.push({
+ groupId: e_group,
+ artifactId: e_artifact,
+ });
+ }
+ });
+ }
+ pomMap.set(id, mvnPom);
+ });
+ }
+ const jar = deps.jar;
+ if (jar != null && jar.length > 0) {
+ jar.forEach((x) => {
+ jarMap.set(x, x);
+ });
+ }
+ }
+}
+
+export const filterOption = (input: string, options: Recordable) => {
+ return options.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
+};
+
+export function handleTeamResource(resource: string) {
+ if (resource != null && resource !== '') {
+ return JSON.parse(resource);
+ }
+ return [];
+}