You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampark.apache.org by kr...@apache.org on 2023/03/07 00:45:17 UTC

[incubator-streampark] branch dev updated: [Feature][ISSUE-2401] show more info of the sql history (#2405)

This is an automated email from the ASF dual-hosted git repository.

kriszu 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 f3aefcecb [Feature][ISSUE-2401] show more info of the sql history (#2405)
f3aefcecb is described below

commit f3aefcecb3012e32003e215dde7f7cb25bbbac2c
Author: totoro <zh...@foxmail.com>
AuthorDate: Tue Mar 7 08:45:09 2023 +0800

    [Feature][ISSUE-2401] show more info of the sql history (#2405)
---
 .../src/api/flink/app/flinkSql.ts                  |  16 ++
 .../src/locales/lang/en/flink/app.ts               |   3 +
 .../src/locales/lang/zh-CN/flink/app.ts            |   3 +
 .../flink/app/components/AppDetail/DetailTab.vue   | 101 ++++++++++++-
 .../components/AppDetail/FlinkSqlCompareModal.vue  | 166 +++++++++++++++++++++
 .../app/components/AppDetail/FlinkSqlDifferent.vue | 142 ++++++++++++++++++
 .../app/components/AppDetail/FlinkSqlReview.vue    |  55 +++++++
 .../src/views/flink/app/data/detail.data.ts        |   7 +
 8 files changed, 492 insertions(+), 1 deletion(-)

diff --git a/streampark-console/streampark-console-webapp/src/api/flink/app/flinkSql.ts b/streampark-console/streampark-console-webapp/src/api/flink/app/flinkSql.ts
index 4f123390d..1ec71f79e 100644
--- a/streampark-console/streampark-console-webapp/src/api/flink/app/flinkSql.ts
+++ b/streampark-console/streampark-console-webapp/src/api/flink/app/flinkSql.ts
@@ -20,6 +20,8 @@ import { defHttp } from '/@/utils/http/axios';
 enum FLINK_SQL_API {
   VERIFY = '/flink/sql/verify',
   GET = '/flink/sql/get',
+  LIST = '/flink/sql/list',
+  DELETE = '/flink/sql/delete',
   HISTORY = '/flink/sql/history',
 }
 
@@ -34,6 +36,20 @@ export function fetchFlinkSql(data) {
   });
 }
 
+export function fetchFlinkSqlList(data) {
+  return defHttp.post({
+    url: FLINK_SQL_API.LIST,
+    data,
+  });
+}
+
+export function fetchRemoveFlinkSql(data: { id: string }): Promise<boolean> {
+  return defHttp.post({
+    url: FLINK_SQL_API.DELETE,
+    data,
+  });
+}
+
 export function fetchFlinkHistory(data) {
   return defHttp.post({
     url: FLINK_SQL_API.HISTORY,
diff --git a/streampark-console/streampark-console-webapp/src/locales/lang/en/flink/app.ts b/streampark-console/streampark-console-webapp/src/locales/lang/en/flink/app.ts
index 2fe53af06..2fd8a4b79 100644
--- a/streampark-console/streampark-console-webapp/src/locales/lang/en/flink/app.ts
+++ b/streampark-console/streampark-console-webapp/src/locales/lang/en/flink/app.ts
@@ -75,6 +75,7 @@ export default {
     detailTitle: 'Application Info',
     flinkWebUi: 'Flink Web UI',
     compareConfig: 'Compare Config',
+    compareFlinkSql: 'Compare Flink SQL',
     candidate: 'Candidate',
     compare: 'Compare',
     compareSelectTips: 'Please select the target version',
@@ -88,7 +89,9 @@ export default {
     invalidAccessToken: 'access token is invalid,please contact the administrator.',
     detailTab: {
       configDetail: 'View Config Detail',
+      sqlDetail: 'View SQL Detail',
       confDeleteTitle: 'Are you sure delete this record',
+      sqlDeleteTitle: 'Are you sure delete this sql',
       copyPath: 'Copy Path',
       pointDeleteTitle: 'Are you sure delete?',
       copySuccess: 'copied to clipboard successfully',
diff --git a/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/flink/app.ts b/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/flink/app.ts
index b84e05e44..439a2978b 100644
--- a/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/flink/app.ts
+++ b/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/flink/app.ts
@@ -74,6 +74,7 @@ export default {
     detailTitle: '作业详情',
     flinkWebUi: 'Flink原生引擎界面',
     compareConfig: '比较配置',
+    compareFlinkSql: '比较 Flink SQL',
     candidate: '侯选',
     compare: '比较',
     compareSelectTips: '请选择目标板本',
@@ -86,7 +87,9 @@ export default {
     invalidAccessToken: '访问令牌无效,请联系管理员。',
     detailTab: {
       configDetail: '查看日志详情',
+      sqlDetail: '查看 SQL 详情',
       confDeleteTitle: '您确定要删除此记录吗',
+      sqlDeleteTitle: '您确定要删除此 SQL 吗',
       copyPath: '复制路径',
       pointDeleteTitle: '您确定要删除?',
       copySuccess: '已成功复制到剪贴板',
diff --git a/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/DetailTab.vue b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/DetailTab.vue
index 87476b3cf..f85c60f77 100644
--- a/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/DetailTab.vue
+++ b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/DetailTab.vue
@@ -32,6 +32,7 @@
   import {
     getBackupColumns,
     getConfColumns,
+    getFlinkSqlColumns,
     getOptionLogColumns,
     getSavePointColumns,
   } from '../../data/detail.data';
@@ -49,6 +50,9 @@
   import { useMessage } from '/@/hooks/web/useMessage';
   import { useClipboard } from '@vueuse/core';
   import { AppTypeEnum, JobTypeEnum, SavePointEnum } from '/@/enums/flinkEnum';
+  import { fetchFlinkSql, fetchFlinkSqlList, fetchRemoveFlinkSql } from '/@/api/flink/app/flinkSql';
+  import FlinkSqlReview from './FlinkSqlReview.vue';
+  import FlinkSqlCompareModal from './FlinkSqlCompareModal.vue';
 
   const DescriptionItem = Descriptions.Item;
   const TabPane = Tabs.TabPane;
@@ -101,8 +105,10 @@
   };
 
   const [registerCompare, { openModal: openCompareModal }] = useModal();
+  const [registerFlinkSqlCompare, { openModal: openFlinkSqlCompareModal }] = useModal();
   const [registerExecOption, { openModal: openExecOptionModal }] = useModal();
   const [registerDetailDrawer, { openDrawer: openDetailDrawer }] = useDrawer();
+  const [registerFlinkSqlDrawer, { openDrawer: openFlinkDrawer }] = useDrawer();
 
   const [registerConfigTable, { getDataSource, reload: reloadConf }] = useTable({
     api: fetchListVer,
@@ -110,6 +116,14 @@
     ...tableCommonConf,
   });
 
+  const [registerFlinkSqlTable, { getDataSource: getFlinkSqls, reload: reloadFlinkSql }] = useTable(
+    {
+      api: fetchFlinkSqlList,
+      columns: getFlinkSqlColumns(),
+      ...tableCommonConf,
+    },
+  );
+
   const [registerSavePointTable, { reload: reloadSavePoint }] = useTable({
     api: fetchSavePonitHistory,
     columns: getSavePointColumns(),
@@ -156,6 +170,36 @@
       },
     ];
   }
+
+  function getFlinkSqlAction(record: Recordable): ActionItem[] {
+    return [
+      {
+        tooltip: { title: t('flink.app.detail.detailTab.sqlDetail') },
+        type: 'link',
+        icon: 'ant-design:eye-outlined',
+        onClick: handleSqlDetail.bind(null, record),
+      },
+      {
+        tooltip: { title: t('flink.app.detail.compareFlinkSql') },
+        type: 'link',
+        icon: 'ant-design:swap-outlined',
+        onClick: handleCompareFlinkSql.bind(null, record),
+        ifShow: getFlinkSqls().length > 1,
+      },
+      {
+        popConfirm: {
+          title: t('flink.app.detail.detailTab.sqlDeleteTitle'),
+          confirm: handleDeleteFlinkSql.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 {
@@ -172,12 +216,34 @@
     }
   }
 
+  async function handleSqlDetail(record: Recordable) {
+    const hide = createMessage.loading('loading');
+    try {
+      const res = await fetchFlinkSql({
+        id: record.id,
+      });
+      openFlinkDrawer(true, {
+        sql: decodeByBase64(res.sql),
+      });
+    } catch (error: unknown) {
+      console.error(error);
+    } finally {
+      hide();
+    }
+  }
+
   /* delete configuration */
   async function handleDeleteConf(record: Recordable) {
     await fetchRemoveConf({ id: record.id });
     reloadConf();
   }
 
+  /* delete flink sql */
+  async function handleDeleteFlinkSql(record: Recordable) {
+    await fetchRemoveFlinkSql({ id: record.id });
+    reloadFlinkSql();
+  }
+
   function handleCompare(record: Recordable) {
     openCompareModal(true, {
       id: record.id,
@@ -186,6 +252,14 @@
     });
   }
 
+  function handleCompareFlinkSql(record: Recordable) {
+    openFlinkSqlCompareModal(true, {
+      id: record.id,
+      version: record.version,
+      createTime: record.createTime,
+    });
+  }
+
   function getSavePointAction(record: Recordable): ActionItem[] {
     return [
       {
@@ -269,7 +343,30 @@
         </BasicTable>
       </TabPane>
       <TabPane key="3" tab="Flink SQL" v-if="app.jobType === JobTypeEnum.SQL">
-        <div class="sql-box syntax-true" ref="flinkSql" style="height: 600px"></div>
+        <BasicTable @register="registerFlinkSqlTable">
+          <template #bodyCell="{ column, record }">
+            <template v-if="column.dataIndex == 'candidate'">
+              <Tag color="#52c41a" v-if="record.candidate == 0"> None </Tag>
+              <Tag color="#2db7f5" v-if="record.candidate == 1"> New </Tag>
+              <Tag color="#108ee9" v-if="record.candidate == 2"> 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="getFlinkSqlAction(record)" />
+            </template>
+          </template>
+        </BasicTable>
       </TabPane>
       <TabPane key="4" tab="Savepoints" v-if="tabConf.showSaveOption">
         <BasicTable @register="registerSavePointTable">
@@ -350,7 +447,9 @@
     </Tabs>
 
     <CompareModal @register="registerCompare" />
+    <FlinkSqlCompareModal @register="registerFlinkSqlCompare" />
     <ExecOptionModal @register="registerExecOption" />
     <Mergely :read-only="true" @register="registerDetailDrawer" />
+    <FlinkSqlReview @register="registerFlinkSqlDrawer" />
   </div>
 </template>
diff --git a/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/FlinkSqlCompareModal.vue b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/FlinkSqlCompareModal.vue
new file mode 100644
index 000000000..04f185e6e
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/FlinkSqlCompareModal.vue
@@ -0,0 +1,166 @@
+<!--
+  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">
+  import { computed, defineComponent, reactive, ref, toRaw } from 'vue';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  export default defineComponent({
+    name: 'FlinkSqlCompareModal',
+  });
+</script>
+<script setup lang="ts" name="FlinkSqlCompareModal">
+  import { Select, Tag } from 'ant-design-vue';
+  import { Icon, SvgIcon } from '/@/components/Icon';
+  import { useDetailProviderContext } from '../../hooks/useDetailContext';
+  import { fetchFlinkSql, fetchFlinkSqlList } from '/@/api/flink/app/flinkSql';
+  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 FlinkSqlDifferent from '/@/views/flink/app/components/AppDetail/FlinkSqlDifferent.vue';
+
+  const SelectOption = Select.Option;
+  const { t } = useI18n();
+  defineEmits(['register']);
+  const allFlinkSqlVersions = ref<any[]>([]);
+  const submitLoading = ref<boolean>(false);
+  const compareRecord = reactive<Recordable>({});
+  const values = useDetailProviderContext();
+
+  const [registerFlinkSqlDifferentDrawer, { openDrawer: openFlinkSqlDiffDrawer }] = useDrawer();
+  const [registerModal, { closeModal }] = useModalInner((data) => {
+    data && onReceiveModalData(data);
+  });
+
+  async function onReceiveModalData(data) {
+    const res = await fetchFlinkSqlList({
+      appId: toRaw(values).app.id,
+      pageNo: 1,
+      pageSize: 999999,
+    });
+    Object.assign(compareRecord, data);
+    allFlinkSqlVersions.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 fetchFlinkSql({
+        id: compareRecord.id,
+      });
+      const sourceSql = decodeByBase64(source.sql);
+      const sourceVersion = source.version;
+      const target = await fetchFlinkSql({
+        id: values.target,
+      });
+      const targetSql = decodeByBase64(target.sql);
+      const targetVersion = target.version;
+      closeModal();
+      openFlinkSqlDiffDrawer(true, {
+        immediate: true,
+        param: [
+          {
+            name: 'Flink SQL',
+            format: 'sql',
+            original: sourceSql,
+            modified: targetSql,
+          },
+        ],
+        original: sourceVersion,
+        modified: targetVersion,
+      });
+    } catch (error) {
+      console.error(error);
+    } finally {
+      submitLoading.value = false;
+    }
+  }
+
+  const filterNotCurrSql = computed(() => {
+    return allFlinkSqlVersions.value.filter((x) => x.version !== compareRecord.version);
+  });
+</script>
+<template>
+  <BasicModal @register="registerModal" :afterClose="resetFields">
+    <template #title>
+      <SvgIcon name="swap" />
+      {{ t('flink.app.detail.compareFlinkSql') }}
+    </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('flink.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('flink.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('flink.app.detail.compare') }}
+      </a-button>
+    </template>
+  </BasicModal>
+  <FlinkSqlDifferent @register="registerFlinkSqlDifferentDrawer" />
+</template>
diff --git a/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/FlinkSqlDifferent.vue b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/FlinkSqlDifferent.vue
new file mode 100644
index 000000000..ea5bf06e1
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/FlinkSqlDifferent.vue
@@ -0,0 +1,142 @@
+<!--
+  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">
+  import { defineComponent } from 'vue';
+  export default defineComponent({
+    name: 'FlinkSqlDifferent',
+  });
+</script>
+<script setup lang="ts" name="FlinkSqlDifferent">
+  import { nextTick, reactive, ref } from 'vue';
+  import { getMonacoOptions } from '../../data';
+  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+  import { useDiffMonaco } from '../../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('flink.app.detail.compareFlinkSql') }}</span>
+      <span> ( </span>
+      <span class="pr-5px">{{ t('flink.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('flink.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/flink/app/components/AppDetail/FlinkSqlReview.vue b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/FlinkSqlReview.vue
new file mode 100644
index 000000000..7b1c184d5
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/views/flink/app/components/AppDetail/FlinkSqlReview.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" />
+      Flink SQL preview
+    </template>
+    <div ref="flinkReviewRef" class="h-[calc(100vh-150px)] flink-preview"></div>
+  </BasicDrawer>
+</template>
+
+<script lang="ts" setup>
+  import { EyeOutlined } from '@ant-design/icons-vue';
+  import { ref } from 'vue';
+  import { getMonacoOptions } from '../../data';
+  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+  import { useMonaco } from '/@/hooks/web/useMonaco';
+  const flinkReviewRef = ref();
+  const { setContent } = useMonaco(flinkReviewRef, {
+    language: 'sql',
+    code: '',
+    options: getMonacoOptions(true) as any,
+  });
+  const [registerDrawer] = useDrawerInner((data) => {
+    if (data) {
+      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/flink/app/data/detail.data.ts b/streampark-console/streampark-console-webapp/src/views/flink/app/data/detail.data.ts
index 510e2e5b3..66fcda6c1 100644
--- a/streampark-console/streampark-console-webapp/src/views/flink/app/data/detail.data.ts
+++ b/streampark-console/streampark-console-webapp/src/views/flink/app/data/detail.data.ts
@@ -111,6 +111,13 @@ export const getConfColumns = (): BasicColumn[] => [
   { title: 'Modify Time', dataIndex: 'createTime' },
 ];
 
+export const getFlinkSqlColumns = (): 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 },