You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by wa...@apache.org on 2023/06/27 19:08:37 UTC

[echarts-examples] 02/02: feat(editor): support PR preview (apache/echarts#18803)

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

wangzx pushed a commit to branch pr-preview
in repository https://gitbox.apache.org/repos/asf/echarts-examples.git

commit aca662d2ac32e06bca1be1454d6218b247f1f6a9
Author: plainheart <yh...@all-my-life.cn>
AuthorDate: Wed Jun 28 03:06:57 2023 +0800

    feat(editor): support PR preview (apache/echarts#18803)
---
 public/en/editor.html       |   4 +-
 public/zh/editor.html       |   4 +-
 src/common/config.js        |   6 +-
 src/common/i18n.js          |  62 ++++++++
 src/common/store.js         |  12 ++
 src/editor/Editor.vue       | 354 +++++++++++++++++++++++++++++++++++++++++++-
 src/editor/Preview.vue      |  72 +++++++--
 src/editor/sandbox/index.js |  49 +++---
 8 files changed, 518 insertions(+), 45 deletions(-)

diff --git a/public/en/editor.html b/public/en/editor.html
index 43f4b7b1..b790de66 100644
--- a/public/en/editor.html
+++ b/public/en/editor.html
@@ -24,12 +24,12 @@
     />
     <link
       rel="stylesheet"
-      href="https://fastly.jsdelivr.net/npm/element-ui@2.13.2/lib/theme-chalk/index.css"
+      href="https://fastly.jsdelivr.net/npm/element-ui@2.15.13/lib/theme-chalk/index.css"
     />
     <script src="https://fastly.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js"></script>
     <script src="https://fastly.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
     <script src="https://fastly.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js"></script>
-    <script src="https://fastly.jsdelivr.net/npm/element-ui@2.13.2/lib/index.js"></script>
+    <script src="https://fastly.jsdelivr.net/npm/element-ui@2.15.13/lib/index.js"></script>
   </head>
   <body>
     <div id="main"></div>
diff --git a/public/zh/editor.html b/public/zh/editor.html
index f658bf92..3ee7a541 100644
--- a/public/zh/editor.html
+++ b/public/zh/editor.html
@@ -24,12 +24,12 @@
     />
     <link
       rel="stylesheet"
-      href="https://fastly.jsdelivr.net/npm/element-ui@2.13.2/lib/theme-chalk/index.css"
+      href="https://fastly.jsdelivr.net/npm/element-ui@2.15.13/lib/theme-chalk/index.css"
     />
     <script src="https://fastly.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js"></script>
     <script src="https://fastly.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
     <script src="https://fastly.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js"></script>
-    <script src="https://fastly.jsdelivr.net/npm/element-ui@2.13.2/lib/index.js"></script>
+    <script src="https://fastly.jsdelivr.net/npm/element-ui@2.15.13/lib/index.js"></script>
   </head>
   <body>
     <div id="main"></div>
diff --git a/src/common/config.js b/src/common/config.js
index b8f49392..40bf3776 100644
--- a/src/common/config.js
+++ b/src/common/config.js
@@ -115,7 +115,9 @@ export const SCRIPT_URLS = {
   echartsJS: '/dist/echarts.min.js',
 
   localEChartsDir: 'http://localhost/echarts',
-  localEChartsGLJS: 'http://localhost/echarts-gl/dist/echarts-gl.js',
+  localEChartsGLDir: 'http://localhost/echarts-gl',
+
+  prPreviewEChartsDir: 'https://echarts-pr-{{PR_NUMBER}}.surge.sh',
 
   echartsWorldMapJS: `${CDN_ROOT}echarts@4.9.0/map/js/world.js`,
   echartsStatJS: `${CDN_ROOT}echarts-stat@latest/dist/ecStat.min.js`,
@@ -123,8 +125,8 @@ export const SCRIPT_URLS = {
   datGUIMinJS: `${CDN_ROOT}dat.gui@0.6.5/build/dat.gui.min.js`,
   monacoDir: `${CDN_ROOT}monaco-editor@0.27.0/min/vs`,
   aceDir: `${CDN_ROOT}ace-builds@1.4.12/src-min-noconflict`,
-
   prettierDir: `${CDN_ROOT}prettier@2.3.2`,
+  highlightjsDir: `https://fastly.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build`,
 
   bmapLibJS:
     'https://api.map.baidu.com/api?v=3.0&ak=KOmVjPVUAey1G2E8zNhPiuQ6QiEmAwZu',
diff --git a/src/common/i18n.js b/src/common/i18n.js
index ec22555f..70da5415 100644
--- a/src/common/i18n.js
+++ b/src/common/i18n.js
@@ -30,6 +30,32 @@ export default {
       tabFullCodePreview: 'Full Code',
       tabOptionPreview: 'Option Preview',
       minimalBundle: 'Minimal Bundle',
+      prPreview: {
+        title: 'PR Preview',
+        author: 'Author',
+        fromBranch: 'From Branch',
+        toBranch: 'To Branch',
+        milestone: 'Milestone',
+        labels: 'Labels',
+        changes: 'Changes',
+        addedLines: 'Added Lines',
+        removedLines: 'Removed Lines',
+        changedFiles: 'Changed Files',
+        commits: 'Commits',
+        latestCommit: 'Latest Commit',
+        review: 'Review',
+        loadingReview: 'Loading review...',
+        reviewLoadFailed: 'Failed to load review.',
+        reviewedBy: 'Reviewed By',
+        reviewedAt: 'Reviewed At',
+        reviewState: 'State',
+        reviewComment: 'Comment',
+        noComment: 'NO COMMENT',
+        diff: 'Diff',
+        viewDiff: 'View Diff',
+        loadingDiff: 'Loading diff...',
+        diffLoadFailed: 'Failed to load diff.'
+      },
 
       tooltip: {
         jsMode: 'JavaScript',
@@ -49,6 +75,11 @@ export default {
         success: 'Sharable URL has been copied to your clipboard',
         hint: 'Please be aware that this chart is not an official demo of Apache ECharts but is made by user-generated code.',
         tooltip: 'Create a sharable link for your code ✨'
+      },
+
+      pr: {
+        hint: 'Please be aware that this chart is not based on the distribution files released by Apache ECharts but built from GitHub PR #{{PR}}.',
+        tooltip: 'Click to view the PR on GitHub'
       }
     },
 
@@ -125,6 +156,32 @@ export default {
       tabFullCodePreview: '完整代码',
       tabOptionPreview: '配置项',
       minimalBundle: '按需引入',
+      prPreview: {
+        title: 'PR 预览',
+        author: '作者',
+        fromBranch: '源分支',
+        toBranch: '目标分支',
+        milestone: '里程碑',
+        labels: '标签',
+        changes: '变更',
+        addedLines: '添加行数',
+        removedLines: '删除行数',
+        changedFiles: '文件变更数',
+        commits: '提交数',
+        latestCommit: '最新提交',
+        review: '评审',
+        loadingReview: '加载中...',
+        reviewLoadFailed: '加载失败',
+        reviewedBy: '评审人',
+        reviewedAt: '时间',
+        reviewState: '状态',
+        reviewComment: '评论',
+        noComment: '无评论内容',
+        diff: 'Diff',
+        viewDiff: '点击查看 Diff',
+        loadingDiff: '加载中...',
+        diffLoadFailed: '加载 diff 失败'
+      },
 
       tooltip: {
         jsMode: 'JavaScript 代码编辑',
@@ -144,6 +201,11 @@ export default {
         success: '分享链接已复制到剪贴板',
         hint: '请注意,该图表不是 Apache ECharts 官方示例,而是由用户代码生成的。请注意鉴别其内容。',
         tooltip: '为当前代码创建一个可分享的链接 ✨'
+      },
+
+      pr: {
+        hint: '请注意,该图表是基于 GitHub PR #{{PR}} 构建产物生成的,而非 Apache ECharts 官方发行产物。请注意鉴别其内容。',
+        tooltip: '点击前往 GitHub 查看该 PR 详情'
       }
     },
 
diff --git a/src/common/store.js b/src/common/store.js
index 9c42754b..59f2e3bb 100644
--- a/src/common/store.js
+++ b/src/common/store.js
@@ -12,9 +12,17 @@ import { customAlphabet } from 'nanoid';
 
 const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10);
 
+const REG_PR_VERSION = /PR-(\d+)(?:@(\w+))?/i;
+const prMatches =
+  URL_PARAMS.version && URL_PARAMS.version.match(REG_PR_VERSION);
+
 export const store = {
   echartsVersion: URL_PARAMS.version || '5',
 
+  isPR: !!prMatches,
+  prNumber: prMatches && prMatches[1],
+  prLatestCommit: prMatches && prMatches[2],
+
   cdnRoot: '',
   version: '',
   locale: '',
@@ -53,6 +61,10 @@ export const store = {
   randomSeed: URL_PARAMS.random || 'echarts'
 };
 
+export function isValidPRVersion(version) {
+  return REG_PR_VERSION.test(version);
+}
+
 function findExample(item) {
   return URL_PARAMS.c === item.id;
 }
diff --git a/src/editor/Editor.vue b/src/editor/Editor.vue
index cc0beccc..b4d1636c 100644
--- a/src/editor/Editor.vue
+++ b/src/editor/Editor.vue
@@ -37,6 +37,23 @@
                 </el-tooltip>
               </div>
               <div class="editor-controls">
+                <a
+                  v-if="shared.isPR"
+                  class="btn btn-default btn-sm pull-request"
+                  target="_blank"
+                  :href="`https://github.com/apache/echarts/pull/${shared.prNumber}`"
+                  :title="`${pr && pr.title ? pr.title + '\n' : ''}${$t(
+                    'editor.pr.tooltip'
+                  )}`"
+                >
+                  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+                    <path
+                      fill="currentColor"
+                      d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4. [...]
+                    />
+                  </svg>
+                  <span>#{{ shared.prNumber }}</span>
+                </a>
                 <a
                   class="btn btn-sm codepen"
                   @click="toExternalEditor('CodePen')"
@@ -164,6 +181,209 @@
         <el-tab-pane :label="$t('editor.tabOptionPreview')" name="full-option">
           <div id="option-outline" ref="optionOutline"></div>
         </el-tab-pane>
+
+        <el-tab-pane
+          v-if="shared.isPR"
+          v-loading="isPRLoading"
+          :label="$t('editor.prPreview.title')"
+          name="pr-preview"
+        >
+          <div v-if="pr" id="pr-preview" ref="prPreview">
+            <el-descriptions
+              class="pr-info"
+              direction="vertical"
+              size="small"
+              :column="4"
+              border
+            >
+              <a
+                slot="title"
+                class="pr-title"
+                ref="prTitle"
+                target="_blank"
+                :href="pr.html_url"
+                >(#{{ pr.number }}) {{ pr.title }}</a
+              >
+              <el-descriptions-item :label="$t('editor.prPreview.author')">
+                <a
+                  target="_blank"
+                  :href="pr.user.html_url"
+                  class="pr-author-avatar"
+                >
+                  <el-avatar :src="pr.user.avatar_url" :size="20" />
+                  <span>{{ pr.user.login }}</span>
+                </a>
+              </el-descriptions-item>
+              <el-descriptions-item :label="$t('editor.prPreview.fromBranch')">
+                <a
+                  target="_blank"
+                  :href="pr.head.repo.html_url + '/tree/' + pr.head.ref"
+                  :title="pr.head.label"
+                >
+                  {{ pr.head.ref }}
+                </a>
+              </el-descriptions-item>
+              <el-descriptions-item :label="$t('editor.prPreview.toBranch')">
+                <a
+                  target="_blank"
+                  :href="pr.head.repo.html_url + '/tree/' + pr.head.ref"
+                  :title="pr.base.label"
+                >
+                  {{ pr.base.ref }}
+                </a>
+              </el-descriptions-item>
+              <el-descriptions-item :label="$t('editor.prPreview.milestone')">
+                <a
+                  v-if="pr.milestone"
+                  target="_blank"
+                  :href="pr.milestone.html_url"
+                >
+                  {{ pr.milestone.title }}
+                </a>
+                <template v-else>-</template>
+              </el-descriptions-item>
+              <el-descriptions-item
+                :label="$t('editor.prPreview.labels')"
+                :span="4"
+              >
+                <el-tag
+                  v-for="label in pr.labels"
+                  :key="label.id"
+                  size="small"
+                  class="pr-label"
+                  :color="'#' + label.color"
+                  :title="label.description"
+                  >{{ label.name }}</el-tag
+                >
+              </el-descriptions-item>
+              <el-descriptions-item
+                :label="$t('editor.prPreview.changes')"
+                :span="4"
+              >
+                <el-descriptions
+                  size="small"
+                  direction="vertical"
+                  :column="5"
+                  :colon="false"
+                >
+                  <el-descriptions-item
+                    :label="$t('editor.prPreview.addedLines')"
+                  >
+                    <span>
+                      {{ pr.additions }}
+                    </span>
+                  </el-descriptions-item>
+                  <el-descriptions-item
+                    :label="$t('editor.prPreview.removedLines')"
+                  >
+                    <span>
+                      {{ pr.deletions }}
+                    </span>
+                  </el-descriptions-item>
+                  <el-descriptions-item
+                    :label="$t('editor.prPreview.changedFiles')"
+                  >
+                    <a :href="pr.html_url + '/files'" target="_blank">
+                      {{ pr.changed_files }}
+                    </a>
+                  </el-descriptions-item>
+                  <el-descriptions-item :label="$t('editor.prPreview.commits')">
+                    <a :href="pr.html_url + '/commits'" target="_blank">
+                      {{ pr.commits }}
+                    </a>
+                  </el-descriptions-item>
+                  <el-descriptions-item
+                    :label="$t('editor.prPreview.latestCommit')"
+                  >
+                    <a
+                      :href="pr.html_url + '/commits/' + pr.head.sha"
+                      target="_blank"
+                    >
+                      {{ pr.head.sha.slice(0, 7) }}
+                    </a>
+                  </el-descriptions-item>
+                </el-descriptions>
+              </el-descriptions-item>
+              <el-descriptions-item :span="4">
+                <span slot="label">
+                  {{ $t('editor.prPreview.review') }}
+                  <i
+                    v-if="isPRReviewLoading"
+                    class="el-icon-loading"
+                    style="margin-left: 5px"
+                  ></i>
+                </span>
+                <span v-if="isPRReviewLoading">{{
+                  $t('editor.prPreview.loadingReview')
+                }}</span>
+                <span v-else-if="isPRReviewLoading === false">{{
+                  $t('editor.prPreview.reviewLoadFailed')
+                }}</span>
+                <el-descriptions
+                  v-else-if="prLatestReview"
+                  size="small"
+                  direction="vertical"
+                  :column="3"
+                  :colon="false"
+                >
+                  <el-descriptions-item
+                    :label="$t('editor.prPreview.reviewedBy')"
+                  >
+                    <a
+                      target="_blank"
+                      :href="prLatestReview.user.html_url"
+                      class="pr-author-avatar"
+                    >
+                      <el-avatar
+                        :src="prLatestReview.user.avatar_url"
+                        :size="20"
+                      />
+                      <span>{{ prLatestReview.user.login }}</span>
+                    </a>
+                  </el-descriptions-item>
+                  <el-descriptions-item
+                    :label="$t('editor.prPreview.reviewedAt')"
+                  >
+                    {{ new Date(prLatestReview.submitted_at).toLocaleString() }}
+                  </el-descriptions-item>
+                  <el-descriptions-item
+                    :label="$t('editor.prPreview.reviewState')"
+                  >
+                    {{ prLatestReview.state }}
+                  </el-descriptions-item>
+                  <el-descriptions-item
+                    :label="$t('editor.prPreview.reviewComment')"
+                    :span="3"
+                  >
+                    <a :href="prLatestReview.html_url" target="_blank">
+                      {{
+                        prLatestReview.body || $t('editor.prPreview.noComment')
+                      }}
+                    </a>
+                  </el-descriptions-item>
+                </el-descriptions>
+              </el-descriptions-item>
+              <el-descriptions-item
+                :label="$t('editor.prPreview.diff')"
+                :span="4"
+              >
+                <details @toggle="$event.target.open && loadPRDiff()">
+                  <summary style="display: revert; cursor: pointer">
+                    {{ $t('editor.prPreview.viewDiff') }}
+                    <i
+                      v-if="isPRDiffLoading"
+                      class="el-icon-loading"
+                      style="margin-left: 5px"
+                    ></i>
+                  </summary>
+                  <pre
+                    class="pr-diff"
+                  ><span v-if="isPRDiffLoading">{{ $t('editor.prPreview.loadingDiff') }}</span><span v-if="isPRDiffLoading === false">{{ $t('editor.prPreview.diffLoadFailed') }}</span><div ref="prDiff"></div></pre>
+                </details>
+              </el-descriptions-item>
+            </el-descriptions>
+          </div>
+        </el-tab-pane>
       </el-tabs>
     </div>
     <div
@@ -203,8 +423,8 @@ import { gotoURL } from '../common/route';
 import { mount } from '@lang/object-visualizer';
 
 import './object-visualizer.css';
-import { URL_PARAMS } from '../common/config';
-import { formatCode } from '../common/helper';
+import { SCRIPT_URLS, URL_PARAMS } from '../common/config';
+import { formatCode, loadScriptsAsync } from '../common/helper';
 import openWithCodePen from './sandbox/openwith/codepen';
 import openWithCodeSandbox from './sandbox/openwith/codesandbox';
 
@@ -234,7 +454,13 @@ export default {
         node: false // If is in node
       },
 
-      formatterReady: false
+      formatterReady: false,
+
+      pr: null,
+      isPRLoading: false,
+      prLatestReview: null,
+      isPRReviewLoading: false,
+      isPRDiffLoading: false
     };
   },
 
@@ -412,6 +638,8 @@ export default {
         this.updateFullCode();
       } else if (tab === 'full-option') {
         this.updateOptionOutline();
+      } else if (tab === 'pr-preview') {
+        this.preparePRPreview();
       }
     },
     changeLang(lang) {
@@ -459,6 +687,82 @@ export default {
     },
     onPreviewReady() {
       this.updateTabContent(this.currentTab);
+    },
+    preparePRPreview() {
+      if (!store.isPR || this.isPRLoading || this.pr) {
+        return;
+      }
+      this.isPRLoading = true;
+      const prURL = `https://api.github.com/repos/apache/echarts/pulls/${store.prNumber}`;
+      $.ajax({
+        url: prURL,
+        headers: {
+          Accept: 'application/json'
+        },
+        dataType: 'json',
+        success: (pr) => {
+          this.pr = pr;
+          this.isPRReviewLoading = true;
+          $.ajax({
+            url: prURL + '/reviews?per_page=100',
+            headers: {
+              Accept: 'application/json'
+            },
+            dataType: 'json',
+            success: (reviews) => {
+              this.prLatestReview = reviews[reviews.length - 1];
+              this.isPRReviewLoading = 0;
+            },
+            error: (xhr, status, err) => {
+              this.isPRReviewLoading = false;
+              console.error('failed to fetch PR reviews', err);
+            }
+          });
+        },
+        error(xhr, status, err) {
+          console.error('failed to fetch PR info', err);
+        },
+        complete: () => {
+          this.isPRLoading = false;
+        }
+      });
+    },
+    loadPRDiff() {
+      if (this.isPRDiffLoading || this.isPRDiffLoading === 0) {
+        return;
+      }
+      this.isPRDiffLoading = true;
+      $.ajax({
+        url: `https://api.github.com/repos/apache/echarts/pulls/${store.prNumber}`,
+        headers: {
+          Accept: 'application/vnd.github.v3.diff'
+        },
+        dataType: 'text',
+        success: (diff) => {
+          const highlightjsDir = SCRIPT_URLS.highlightjsDir;
+          loadScriptsAsync([
+            highlightjsDir + '/styles/github.min.css',
+            highlightjsDir + '/highlight.min.js',
+            highlightjsDir + '/languages/diff.min.js'
+          ])
+            .then(() => {
+              return hljs.highlight(diff, {
+                language: 'diff'
+              }).value;
+            })
+            .catch((err) => {
+              console.error('failed to load PR diff', err);
+            })
+            .then((diff) => {
+              this.isPRDiffLoading = diff ? 0 : false;
+              diff && (this.$refs.prDiff.innerHTML = diff);
+            });
+        },
+        error: (xhr, status, err) => {
+          this.isPRDiffLoading = false;
+          console.error('failed to fetch PR diff', err);
+        }
+      });
     }
   },
 
@@ -628,6 +932,12 @@ $handler-width: 5px;
 }
 
 #editor-control-panel {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  white-space: nowrap;
+  overflow-x: auto;
+
   .setting-panel {
     display: inline-block;
 
@@ -683,8 +993,6 @@ $handler-width: 5px;
   }
 
   .editor-controls {
-    float: right;
-
     .el-switch__label {
       margin-top: -3px;
     }
@@ -732,6 +1040,42 @@ $handler-width: 5px;
   }
 }
 
+#pr-preview {
+  height: 100%;
+  padding: 10px;
+  box-sizing: border-box;
+  overflow: auto;
+
+  .pr-info {
+    width: 100%;
+  }
+
+  .pr-author-avatar {
+    display: flex;
+    align-items: center;
+
+    .el-avatar {
+      margin-right: 6px;
+    }
+  }
+
+  .pr-label {
+    margin-right: 5px;
+    margin-bottom: 5px;
+    color: #fff;
+    border: none;
+    border-radius: 2em;
+  }
+
+  .pr-diff {
+    width: 100%;
+    margin-top: 5px;
+    overflow: auto;
+    box-sizing: border-box;
+    white-space: pre-wrap;
+  }
+}
+
 .right-container {
   position: absolute;
   right: 0;
diff --git a/src/editor/Preview.vue b/src/editor/Preview.vue
index 5fe02318..3b3873f1 100644
--- a/src/editor/Preview.vue
+++ b/src/editor/Preview.vue
@@ -67,7 +67,10 @@
         <el-select
           v-if="shared.echartsVersion && !shared.isMobile"
           class="version-select"
-          :class="{ nightly }"
+          :class="{
+            'is-nightly': nightly,
+            'is-pr': shared.isPR || hasPRVersion
+          }"
           size="mini"
           id="choose-echarts-version"
           v-model="shared.echartsVersion"
@@ -148,7 +151,8 @@ import {
   saveExampleCodeToLocal,
   store,
   updateRandomSeed,
-  updateRunHash
+  updateRunHash,
+  isValidPRVersion
 } from '../common/store';
 import { SCRIPT_URLS, URL_PARAMS } from '../common/config';
 import { compressStr } from '../common/helper';
@@ -169,17 +173,29 @@ function getScriptURL(link) {
 }
 
 function getScripts(nightly) {
-  const echartsDir = SCRIPT_URLS[
-    isLocal ? 'localEChartsDir' : nightly ? 'echartsNightlyDir' : 'echartsDir'
-  ].replace('{{version}}', store.echartsVersion);
+  const echartsDirTpl =
+    SCRIPT_URLS[
+      isLocal
+        ? 'localEChartsDir'
+        : store.isPR
+        ? 'prPreviewEChartsDir'
+        : nightly
+        ? 'echartsNightlyDir'
+        : 'echartsDir'
+    ];
+  const echartsDir = store.isPR
+    ? echartsDirTpl.replace('{{PR_NUMBER}}', store.prNumber)
+    : echartsDirTpl.replace('{{version}}', store.echartsVersion);
   const code = store.runCode;
 
   return [
-    echartsDir + getScriptURL(SCRIPT_URLS.echartsJS),
+    echartsDir +
+      getScriptURL(SCRIPT_URLS.echartsJS) +
+      (store.isPR ? '?_=' + (store.prLatestCommit || Date.now()) : ''),
     ...(isGL
       ? [
           isLocal
-            ? SCRIPT_URLS.localEChartsGLJS
+            ? SCRIPT_URLS.localEChartsGLDir + '/dist/echarts-gl.js'
             : getScriptURL(SCRIPT_URLS.echartsGLJS)
         ]
       : []),
@@ -333,7 +349,9 @@ export default {
 
       allEChartsVersions: [],
       nightlyVersions: [],
-      nightly: false
+      nightly: false,
+
+      hasPRVersion: false
     };
   },
 
@@ -378,6 +396,9 @@ export default {
           this.run();
           // show share hint on first run if code is user-shared
           store.isSharedCode && this.showShareHint();
+          // show PR hint on first run if it's based on PR
+          store.isPR &&
+            setTimeout(this.showPRHint, store.isSharedCode ? 1e3 : 0);
         } else {
           this.debouncedRun();
         }
@@ -430,12 +451,21 @@ export default {
             (store.renderer === 'svg' ? 'svg' : 'png')
         );
     },
+    showPRHint() {
+      this.$message({
+        type: 'warning',
+        message: this.$t('editor.pr.hint').replace('{{PR}}', store.prNumber),
+        customClass: 'toast-declaration',
+        duration: 8000,
+        showClose: true
+      });
+    },
     showShareHint() {
       this.$message.closeAll();
       this.$message({
         type: 'warning',
         message: this.$t('editor.share.hint'),
-        customClass: 'toast-shared-url',
+        customClass: 'toast-declaration',
         duration: 8000,
         showClose: true
       });
@@ -463,7 +493,7 @@ export default {
           this.$message({
             type: 'success',
             message: this.$t('editor.share.success'),
-            customClass: 'toast-shared-url'
+            customClass: 'toast-declaration'
           });
         })
         // PENDING
@@ -482,6 +512,7 @@ export default {
         window.__EDITOR_NO_LEAVE_CONFIRMATION__ = true;
         gotoURL({
           version: store.echartsVersion,
+          pv: store.isPR ? URL_PARAMS.version : void 0,
           ...this.toolOptions
         });
       });
@@ -505,6 +536,9 @@ export default {
         }
       };
 
+      const prVersion = URL_PARAMS.pv;
+      const hasPRVersion = (this.hasPRVersion = isValidPRVersion(prVersion));
+
       $.getJSON(`${server}/echarts`).done((data) => {
         handleData(data);
 
@@ -535,6 +569,8 @@ export default {
           versions.unshift(data.tags.rc);
           store.echartsVersion === 'rc' && (store.echartsVersion = versions[0]);
         }
+
+        hasPRVersion && versions.unshift(prVersion);
       });
 
       $.getJSON(`${server}/echarts-nightly`).done((data) => {
@@ -553,6 +589,8 @@ export default {
         this.nightlyVersions = versions
           .slice(nextIdx, nextIdx + 10)
           .concat(versions.slice(latestIdx, latestIdx + 10));
+
+        hasPRVersion && this.nightlyVersions.unshift(prVersion);
       });
 
       if (isDebug) {
@@ -633,6 +671,9 @@ export default {
   left: 15px;
   @include flex-center;
 
+  white-space: nowrap;
+  overflow-x: auto;
+
   user-select: none;
 
   * {
@@ -648,7 +689,8 @@ export default {
     width: 80px;
     margin-left: 10px;
 
-    &.nightly {
+    &.is-nightly,
+    &.is-pr {
       width: 160px;
     }
   }
@@ -693,6 +735,8 @@ export default {
   padding: 0 15px;
   font-size: 0.9rem;
   @include flex-center;
+  white-space: nowrap;
+  overflow-x: auto;
 
   .left-buttons {
     flex-shrink: 0;
@@ -725,10 +769,14 @@ export default {
 }
 
 .el-message {
-  &.toast-shared-url {
+  &.toast-declaration {
     min-width: auto;
     z-index: 9999999 !important;
 
+    .el-message__icon {
+      font-size: 20px;
+    }
+
     .el-message__content {
       padding-right: 20px;
       line-height: 1.25;
diff --git a/src/editor/sandbox/index.js b/src/editor/sandbox/index.js
index e1fdbc47..ab4fbc5e 100644
--- a/src/editor/sandbox/index.js
+++ b/src/editor/sandbox/index.js
@@ -4,6 +4,7 @@ import loopController from './loopController?raw-minify';
 import handleLoop from './handleLoop?raw-minify';
 import showDebugDirtyRect from '../../dep/showDebugDirtyRect?raw-minify';
 import setup from './setup?raw-minify';
+import { store } from '../../common/store';
 
 function prepareSetupScript(isShared) {
   const isProd = process.env.NODE_ENV === 'production';
@@ -58,28 +59,32 @@ export function createSandbox(
         'data:',
         'blob:'
       ].concat(
-        [
-          '*.apache.org',
-          '*.jsdelivr.net',
-          '*.jsdelivr.com',
-          '*.unpkg.com',
-          '*.baidu.com',
-          '*.bdimg.com',
-          '*.bdstatic.com',
-          'apache.org',
-          'apache.github.io',
-          'jsdelivr.net',
-          'jsdelivr.com',
-          'unpkg.com',
-          'baidu.com',
-          'bdimg.com',
-          'bdstatic.com',
-          'cdnjs.cloudflare.com',
-          'cdn.bootcdn.net',
-          'lib.baomitu.com',
-          'unpkg.zhimg.com',
-          'npm.elemecdn.com'
-        ].map((domain) => 'https://' + domain)
+        (() => {
+          const domains = [
+            '*.apache.org',
+            '*.jsdelivr.net',
+            '*.jsdelivr.com',
+            '*.unpkg.com',
+            '*.baidu.com',
+            '*.bdimg.com',
+            '*.bdstatic.com',
+            'apache.org',
+            'apache.github.io',
+            'jsdelivr.net',
+            'jsdelivr.com',
+            'unpkg.com',
+            'baidu.com',
+            'bdimg.com',
+            'bdstatic.com',
+            'cdnjs.cloudflare.com',
+            'cdn.bootcdn.net',
+            'lib.baomitu.com',
+            'unpkg.zhimg.com',
+            'npm.elemecdn.com'
+          ];
+          store.isPR && domains.push(`echarts-pr-${store.prNumber}.surge.sh`);
+          return domains;
+        })().map((domain) => 'https://' + domain)
       ),
       'frame-src': [`'none'`],
       'object-src': [`'none'`],


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org