Step-by-step Building an Enterprise Developer Portal with Zadig - GitLab-MR

Step-by-step Building an Enterprise Developer Portal with Zadig - GitLab-MR

This article is part of the "Zadig IDP Plugin Development in Practice" series.

When implementing an internal developer portal (IDP) in an enterprise, the plugin mechanism allows engineering teams to quickly encapsulate frequently-used collaboration abilities—offering a unified entry point and experience.

In this installment, we take the "GitLab MR Viewer" as an example, providing a complete walkthrough on how to use the Zadig plugin system, from development to release, to build a unified code collaboration interface within your organization.

Stay tuned for more practical plugins in this series, such as Monitoring Alert Viewer and Project Management Panel, to empower your engineering team from multiple angles.

# Preview of the Final Result

Current supported capabilities:

  • View Merge Requests assigned to me
  • Filter by status (opened/closed/merged/all) and scope (assigned to me/created by me/all)
  • Pagination & one-click jump to GitLab MR details

# Getting Started

Initial development of this plugin took about 2 days, but you won't need to repeat the process. By following this tutorial, you can finish in 1-2 hours and immediately experience Zadig plugin rapid development.

Basic Knowledge:

Requirements:

  • Node.js v20.0+
  • Yarn v4.0+
  • Zadig v4.0+

Others:

  • Personal Access Token for your GitLab instance (with api and read_user permissions)

First, install the Zadig IDP Plugin Development CLI and initialize your plugin scaffold:

# Install zadig-plugin-cli globally
yarn global add zadig-plugin-cli-dev
# Verify installation
zadig-plugin --version
zadig-plugin --help
# Create a plugin scaffold (default is Page type)
zadig-plugin create gitlab-mr-plugin

Enter the directory and start the development server. By default, it will display the Hello Plugin page, and you can modify the scaffold code from here.

      cd gitlab-mr-plugin
      zadig-plugin dev

# Core Code Overview

# Entry and Route Registration (index.js)

When the plugin is mounted, it registers the main route and defines manifest metadata (identifier, name, route, type, etc.). This is auto-generated by the SDK and can generally be left unchanged.

// Route registration (excerpt)
this.registerRoute({
  path: "/",
  component: WrappedComponent,
  meta: {
    title: "GitLab Merge Requests",
    icon: "el-icon-s-cooperation",
  },
});

// manifest (excerpt)
const manifest = {
  identifier: "gitlab-mr-plugin",
  name: "GitLab MR Viewer",
  version: "1.0.0",
  description: "View GitLab Merge Requests assigned to me",
  type: "page",
  route: "/gitlab-mr",
};

# Main Page Container (components/GitLabMRMain.vue)

  • Uses local storage to save gitlab_url and gitlab_token
  • fetchMergeRequests fetches the MR list using the GitLab API, supporting state/scope/pagination
  • "Test Connection" pings /api/v4/user to verify the token
// Fetch MR list (excerpt)
async fetchMergeRequests () {
  if (!this.isConfigured) {
    this.showTokenDialog = true
    return
  }
  const params = new URLSearchParams({
    state: this.filterState === 'all' ? undefined : this.filterState,
    scope: this.filterScope === 'all' ? undefined : this.filterScope,
    page: this.currentPage,
    per_page: this.pageSize,
    sort: 'desc'
  })
  Array.from(params.entries()).forEach(([k, v]) => {
    if (v === 'undefined' || v === undefined) params.delete(k)
  })
  const response = await fetch(`${this.gitlabUrl}/api/v4/merge_requests?${params}`, {
    headers: { Authorization: `Bearer ${this.gitlabToken}`, 'Content-Type': 'application/json' }
  })
  const data = await response.json()
  this.mergeRequests = data
  const totalHeader = response.headers.get('X-Total')
  this.total = totalHeader ? parseInt(totalHeader) : data.length
}

// Test connection (excerpt)
const response = await fetch(`${this.gitlabUrl}/api/v4/user`, {
  headers: { Authorization: `Bearer ${this.gitlabToken}`, 'Content-Type': 'application/json' }
})

# GitLab Config (components/ConfigDialog.vue)

  • Two-way binding for URL/Token
  • Event emissions
// Event emission (excerpt)
handleSave () {
  this.$emit('update:gitlab-url', this.form.gitlabUrl)
  this.$emit('update:gitlab-token', this.form.gitlabToken)
  this.$emit('save')
}
handleTest () {
  this.$emit('update:gitlab-url', this.form.gitlabUrl)
  this.$emit('update:gitlab-token', this.form.gitlabToken)
  this.$emit('test')
}
handleClear () {
  this.$emit('clear')
}

# Table and Interactions (components/MRTable.vue)

  • Displays MR basic info per API
  • Clicking a row jumps to MR details
<!-- MR display (excerpt) -->
<el-table
  :data="data"
  :loading="loading"
  @row-click="handleRowClick"
  style="width: 100%"
  stripe
>
  <el-table-column prop="title" label="Title" min-width="300">
    <template slot-scope="scope">
      <div class="mr-title">
        <span class="mr-iid">#{{ scope.row.iid }}</span>
        <span class="title-text">{{ scope.row.title }}</span>
      </div>
    </template>
  </el-table-column>

  <el-table-column label="Source Branch">
    <template slot-scope="scope">
      <span class="branch-name">{{ scope.row.source_branch }}</span>
    </template>
  </el-table-column>
  <el-table-column label="Target Branch">
    <template slot-scope="scope">
      <span class="branch-name">{{ scope.row.target_branch }}</span>
    </template>
  </el-table-column>

  <el-table-column label="Author">
    <template slot-scope="scope">
      <div class="author-info">
        <img
          v-if="scope.row.author.avatar_url"
          :src="scope.row.author.avatar_url"
          class="author-avatar"
          :alt="scope.row.author.name"
        />
        <span class="author-name">{{ scope.row.author.name }}</span>
      </div>
    </template>
  </el-table-column>

  <el-table-column prop="state" label="Status" width="100">
    <template slot-scope="scope">
      <el-tag :type="getStateType(scope.row.state)" size="small">
        {{ getStateText(scope.row.state) }}
      </el-tag>
    </template>
  </el-table-column>

  <el-table-column label="Created At" width="160">
    <template slot-scope="scope">
      {{ formatDate(scope.row.created_at) }}
    </template>
  </el-table-column>

  <el-table-column label="Updated At" width="160">
    <template slot-scope="scope">
      {{ formatDate(scope.row.updated_at) }}
    </template>
  </el-table-column>
</el-table>

# Build & Test

You can use zadig-plugin dev for real-time preview during development.

Debugging tips:

  • Open browser developer tools and watch Console & Network tabs
  • If GitLab access fails, first use "Config → Test Connection" to check URL/Token/Permissions

After successful local debugging, you can build the production version:

# Build for production
zadig run build

After the build, plugin.js will be generated in the dist/ directory—this is what you'll upload to Zadig.

# Uploading and Publishing in Zadig

  1. Log in to Zadig → go to "System Settings → Plugin Management"

  2. Click "New Plugin" and fill in:

    1. Plugin Name: GitLab MR Viewer
    2. Plugin Type: Navigation Page (Page)
    3. Route Path: /gitlab-mr-plugin
    4. Plugin Description: View GitLab Merge Requests assigned to me
    5. Plugin Status: Enabled
    6. Upload build artifact: dist/plugin.js
  3. Go back to the main sidebar, click "Add Page", select type "Plugin", and pick the plugin you just created

# Configuration and Feature Demonstration

  1. Open the plugin page and click "Config" in the upper right

  2. Fill in:

    1. GitLab URL:

      • For GitLab.com: enter https://gitlab.com
      • For self-hosted instances: enter https://gitlab.example.com (replace with your actual domain)
    2. Access Token: paste the token you just created (with api and read_user permissions)

  3. Click "Test Connection"—if it passes, click "Save" to enable.

# Future Extension Potential

  • MR details embedding: Directly view MR overview, changed files, and diffs within the plugin (can start simple then gradually enhance).

  • Approval and commenting: Support commenting, emoji reactions, change suggestions, and reading/submitting Approve/Request Changes in the plugin.

  • Label/Milestone management: Quickly assign labels and milestones to MRs for lightweight categorization and tracking.

  • Batch operations: Batch assign, label, or subscribe to MRs in the filtered results.

# 🔜 Conclusion & Upcoming Content

Through this example, you can experience the flexibility and openness of the Zadig IDP plugin system. With the SDK, you can quickly develop, easily integrate internal systems, and encapsulate collaboration capabilities, so developers can work efficiently in a unified place.

📦 The example plugin in this article is open source

Check out the GitHub repo for more: 👉 koderover/zadig-idp-plugins (opens new window)

In the next article, we will cover:

"How to Build a General-Purpose iFrame Integration Plugin in Zadig" — Creating a unified entry across systems and enabling seamless synergy between R&D and business systems.

📌 Stay tuned for the latest updates and in-depth guides in the Zadig IDP Practical Series!

Background Image

作为一名软件工程师,我们一直给各行各业写软件提升效率,但是软件工程本身却是非常低效,为什么市面上没有一个工具可以让研发团队不这么累,还能更好、更快地满足大客户的交付需求?我们是否能够打造一个面向开发者的交付平台呢?我们开源打造 Zadig 正是去满足这个愿望。

—— Zadig 创始人 Landy