Repository: kubernetes/dashboard
Pull request #4094 (2019-07-19 09:06:04)

Add plugin support on the frontend
Ref: #4069 
- [x] List registered plugins in a ResourceList based component
- [x] Add a link to all plugins in the Sidebar
- [x] Add a route for plugin UI to render
- [x] Render the AOT compiled plugin at runtime.
- [x] Error Handling
- [x] Fetch plugins based on the namespace

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/dashboard/pull/4094#" title="Author self-approved">ajatprabha</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **ianlewis**
You can assign the PR to them by writing `/assign @ianlewis` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fdashboard).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes/dashboard/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["ianlewis"]} -->
ajatprabha: Current progress:
1. List of installed plugins  

![Screenshot from 2019-07-22 17-04-19](https://user-images.githubusercontent.com/11836668/61630983-c28bc800-aca6-11e9-9e0a-dbb7c3a04b21.png)

2. A sample plugin displaying available pods in the cluster  

![Screenshot from 2019-07-22 17-03-43](https://user-images.githubusercontent.com/11836668/61630979-c1f33180-aca6-11e9-8e98-3018f24aee9e.png)


maciaszczykm: Testing it :)
maciaszczykm: The plugin works, that's great.

Few remarks from my side:
- If there is no `Plugin` CRD registered in the cluster I'd avoid listing `Plugins` in the menu. Thanks to that users can use dashboard like they do now if the have no plugins. Otherwise, when they enter plugin page they will see errors:

<img width="2118" alt="Zrzut ekranu 2019-07-25 o 11 34 45" src="https://user-images.githubusercontent.com/2823399/61864018-f5190900-aed0-11e9-977b-878175ac85f2.png">

Another thing to consider here might be adding CRD from our backend and listing `Plugins` in the menu only if there are any plugins in the cluster.

- I'd like to know more how the plugins can be created. Which parts of the backend I can use, which parts of the frontend I can use. How to build the plugins. It will require some detailed documentation.

- Can you add plugin that uses material design card and styling is similar to the rest of the app? It would be valuable to show it to the people.

- I'd really appreciate possibility to pin some of the plugins to the menu like we advised for CRDs. I mean in this demo `plugin1` could be pinned to the menu and then appear just under `Plugins`. Like `Pods` appear under `Workloads`.

At the moment that's it. I really appreciate all the effort and I think most crucial part is already done. Keep it up!
floreks: I have found couple of issues while testing:
- Not working with secure Dashboard (login enabled)
![Zrzut ekranu z 2019-07-25 12-19-29](https://user-images.githubusercontent.com/2285385/61868419-a15eed80-aed9-11e9-9e39-27fc3e181642.png)
- Delete and edit is available on the plugin list but it is not working
![Zrzut ekranu z 2019-07-25 12-24-36](https://user-images.githubusercontent.com/2285385/61868439-acb21900-aed9-11e9-941c-dd27657951ac.png)
- Sorting not applied on the plugin list when first entering the page, only after manually hitting sort or clicking again on `Plugins` entry.

Questions:
1. How dependencies are calculated?
2. Are we able to inject plugin view into any view?

Besides that, really nice progress. It looks better and better.
k8s-ci-robot: @ajatprabha: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
codecov[bot]: # [Codecov](https://codecov.io/gh/kubernetes/dashboard/pull/4094?src=pr&el=h1) Report
> Merging [#4094](https://codecov.io/gh/kubernetes/dashboard/pull/4094?src=pr&el=desc) into [master](https://codecov.io/gh/kubernetes/dashboard/commit/9fb529e10635811d3545f4f930b5b20274c1b730?src=pr&el=desc) will **increase** coverage by `0.09%`.
> The diff coverage is `27.11%`.

[![Impacted file tree graph](https://codecov.io/gh/kubernetes/dashboard/pull/4094/graphs/tree.svg?width=650&token=Wj7jysJ9A5&height=150&src=pr)](https://codecov.io/gh/kubernetes/dashboard/pull/4094?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #4094      +/-   ##
==========================================
+ Coverage   46.33%   46.42%   +0.09%     
==========================================
  Files         198      200       +2     
  Lines        9159     9225      +66     
  Branches      105      105              
==========================================
+ Hits         4244     4283      +39     
- Misses       4656     4678      +22     
- Partials      259      264       +5
```


| [Impacted Files](https://codecov.io/gh/kubernetes/dashboard/pull/4094?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/app/backend/api/types.go](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9iYWNrZW5kL2FwaS90eXBlcy5nbw==) | `56% <ø> (ø)` | :arrow_up: |
| [src/app/backend/plugin/config.go](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9iYWNrZW5kL3BsdWdpbi9jb25maWcuZ28=) | `0% <0%> (ø)` | |
| [src/app/backend/handler/apihandler.go](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9iYWNrZW5kL2hhbmRsZXIvYXBpaGFuZGxlci5nbw==) | `27.64% <0%> (+0.57%)` | :arrow_up: |
| [...rontend/common/components/resourcelist/groupids.ts](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9mcm9udGVuZC9jb21tb24vY29tcG9uZW50cy9yZXNvdXJjZWxpc3QvZ3JvdXBpZHMudHM=) | `100% <100%> (ø)` | :arrow_up: |
| [src/app/frontend/login/component.ts](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9mcm9udGVuZC9sb2dpbi9jb21wb25lbnQudHM=) | `78.87% <100%> (+0.93%)` | :arrow_up: |
| [.../app/frontend/common/services/resource/endpoint.ts](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9mcm9udGVuZC9jb21tb24vc2VydmljZXMvcmVzb3VyY2UvZW5kcG9pbnQudHM=) | `78.84% <100%> (+0.41%)` | :arrow_up: |
| [src/app/backend/plugin/list.go](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9iYWNrZW5kL3BsdWdpbi9saXN0Lmdv) | `48.88% <47.72%> (-35.73%)` | :arrow_down: |
| [src/app/frontend/common/services/global/plugin.ts](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9mcm9udGVuZC9jb21tb24vc2VydmljZXMvZ2xvYmFsL3BsdWdpbi50cw==) | `50% <50%> (ø)` | |
| [src/app/backend/client/verber.go](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9iYWNrZW5kL2NsaWVudC92ZXJiZXIuZ28=) | `66.26% <50%> (-1.64%)` | :arrow_down: |
| [src/app/backend/plugin/handler.go](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree#diff-c3JjL2FwcC9iYWNrZW5kL3BsdWdpbi9oYW5kbGVyLmdv) | `36.84% <55.55%> (+11.2%)` | :arrow_up: |
| ... and [5 more](https://codecov.io/gh/kubernetes/dashboard/pull/4094/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubernetes/dashboard/pull/4094?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kubernetes/dashboard/pull/4094?src=pr&el=footer). Last update [9fb529e...aef227c](https://codecov.io/gh/kubernetes/dashboard/pull/4094?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

floreks: /lgtm
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/dashboard/pull/4094#" title="Author self-approved">ajatprabha</a>*, *<a href="https://github.com/kubernetes/dashboard/pull/4094#issuecomment-527112547" title="LGTM">floreks</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fdashboard).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes/dashboard/blob/master/OWNERS)~~ [floreks]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/.travis.yml b/.travis.yml
index 80e85e1b8..9a0ec2798 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -35,6 +35,8 @@ before_script:
 jobs:
   include:
     - stage: test
+      before_script:
+        - aio/scripts/install-codegen.sh
       script: npm run check
     - script: npm run test:coverage
       after_success:
diff --git a/aio/scripts/install-codegen.sh b/aio/scripts/install-codegen.sh
new file mode 100755
index 000000000..746d5e187
--- /dev/null
+++ b/aio/scripts/install-codegen.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+# Copyright 2017 The Kubernetes Authors.
+#
+# Licensed 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
+#
+#     http://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.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+function install_codegen() {
+    if [ "$(GO111MODULE=off go get -d k8s.io/code-generator)" != 0 ]; then
+    # this will return zero even if go get throws `build constraints exclude all Go files`
+      return 0
+    fi
+}
+
+install_codegen
diff --git a/aio/scripts/update-codegen.sh b/aio/scripts/update-codegen.sh
index 9d52198a2..e64616f90 100755
--- a/aio/scripts/update-codegen.sh
+++ b/aio/scripts/update-codegen.sh
@@ -29,4 +29,12 @@ CODEGEN_PKG=${GOPATH}/src/k8s.io/code-generator
   github.com/kubernetes/dashboard/src/app/backend/plugin/client github.com/kubernetes/dashboard/src/app/backend/plugin \
   apis:v1alpha1 \
   --go-header-file "${SCRIPT_ROOT}"/aio/scripts/license-header.go.txt \
-  --output-base "$(dirname "${BASH_SOURCE[0]}")/../../../../.."
+  --output-base "$(dirname "${BASH_SOURCE[0]}")/.."
+
+# Remove old generated client
+rm -rf ./src/app/backend/plugin/client
+# Move generated deepcopy funcs and client
+mv "$(dirname "${BASH_SOURCE[0]}")"/../github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1/zz_generated.deepcopy.go ./src/app/backend/plugin/apis/v1alpha1
+mv "$(dirname "${BASH_SOURCE[0]}")"/../github.com/kubernetes/dashboard/src/app/backend/plugin/client ./src/app/backend/plugin
+# Remove empty directory
+rm -rf ./aio/github.com
diff --git a/aio/scripts/verify-codegen.sh b/aio/scripts/verify-codegen.sh
new file mode 100755
index 000000000..60769c8cb
--- /dev/null
+++ b/aio/scripts/verify-codegen.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+# Copyright 2017 The Kubernetes Authors.
+#
+# Licensed 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
+#
+#     http://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.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/../..
+
+DIFFROOT="${SCRIPT_ROOT}/src/app/backend"
+TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/src/app/backend"
+_tmp="${SCRIPT_ROOT}/_tmp"
+
+cleanup() {
+  rm -rf "${_tmp}"
+}
+trap "cleanup" EXIT SIGINT
+
+cleanup
+
+mkdir -p "${TMP_DIFFROOT}"
+cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}"
+
+"${SCRIPT_ROOT}/aio/scripts/update-codegen.sh"
+echo "diffing ${DIFFROOT} against freshly generated codegen"
+ret=0
+diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$?
+cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}"
+if [[ $ret -eq 0 ]]
+then
+  echo "${DIFFROOT} up to date."
+else
+  echo "${DIFFROOT} is out of date. Please run aio/scripts/update-codegen.sh"
+  exit 1
+fi
+
diff --git a/angular.json b/angular.json
index 0104d7c03..e7096577b 100644
--- a/angular.json
+++ b/angular.json
@@ -31,7 +31,12 @@
               "node_modules/c3/c3.min.css",
               "src/app/frontend/index.scss"
             ],
-            "scripts": ["node_modules/sockjs-client/dist/sockjs.min.js"]
+            "scripts": [
+              "node_modules/sockjs-client/dist/sockjs.min.js",
+              "node_modules/systemjs/dist/s.js",
+              "node_modules/systemjs/dist/extras/named-register.js",
+              "node_modules/systemjs/dist/extras/amd.js"
+            ]
           },
           "configurations": {
             "production": {
diff --git a/go.mod b/go.mod
index bc510a8ad..23db6bc9c 100644
--- a/go.mod
+++ b/go.mod
@@ -18,8 +18,6 @@ require (
 	github.com/gorilla/websocket v1.4.0 // indirect
 	github.com/igm/sockjs-go v2.0.1+incompatible // indirect
 	github.com/imdario/mergo v0.3.6 // indirect
-	github.com/json-iterator/go v1.1.6 // indirect
-	github.com/kr/pretty v0.1.0 // indirect
 	github.com/onsi/ginkgo v1.8.0 // indirect
 	github.com/onsi/gomega v1.5.0 // indirect
 	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
@@ -28,15 +26,12 @@ require (
 	github.com/prometheus/procfs v0.0.0-20190102135031-14fa7590c24d // indirect
 	github.com/sirupsen/logrus v1.4.2 // indirect
 	github.com/spf13/pflag v1.0.3
-	golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 // indirect
-	golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
+	golang.org/x/net v0.0.0-20190620200207-3b0461eec859
 	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
-	golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
-	golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f // indirect
 	golang.org/x/text v0.3.2
 	golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
+	golang.org/x/tools v0.0.0-20190814171936-5b18234b3ae0 // indirect
 	gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
-	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gopkg.in/igm/sockjs-go.v2 v2.0.0
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/square/go-jose.v2 v2.2.2
@@ -45,6 +40,7 @@ require (
 	k8s.io/apiextensions-apiserver v0.0.0-20190315093550-53c4693659ed
 	k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
 	k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
+	k8s.io/code-generator v0.0.0-20190717022600-77f3a1fe56bb // indirect
 	k8s.io/heapster v1.5.4
 	k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a // indirect
 )
diff --git a/go.sum b/go.sum
index 7868a0523..3cd18243f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,18 @@
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -20,6 +27,8 @@ github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f h1:8GDPb0tCY8LQ+OJ
 github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f h1:AUj1VoZUfhPhOPHULCQQDnGhRelpFWHMLhQVWDsS0v4=
 github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.6+incompatible h1:tfrHha8zJ01ywiOEC1miGY8st1/igzWB8OmvPgoYX7w=
 github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
@@ -27,22 +36,35 @@ github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6
 github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
+github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
+github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
 github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
 github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
 github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
@@ -59,6 +81,7 @@ github.com/igm/sockjs-go v2.0.1+incompatible/go.mod h1:Yu6pvqjNniWNJe07LPObeCG6R
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
 github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -66,24 +89,32 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
@@ -96,15 +127,19 @@ github.com/prometheus/common v0.0.0-20181218105931-67670fe90761/go.mod h1:daVV7q
 github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190102135031-14fa7590c24d h1:iSxvUGRUGQTUgEo5+8TqMVQoH5AdaphEIjh8AnM7TPo=
 github.com/prometheus/procfs v0.0.0-20190102135031-14fa7590c24d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
 github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
 github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
@@ -114,14 +149,22 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90Pveol
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU=
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -131,6 +174,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -138,6 +182,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4=
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -148,6 +193,16 @@ golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190814171936-5b18234b3ae0 h1:xHOOBMNWwegt6X6+9mJTS6j5NBnSe0Cl+9/IOVGbrSA=
+golang.org/x/tools v0.0.0-20190814171936-5b18234b3ae0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
 google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
@@ -180,16 +235,27 @@ k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719 h1:uV4S5IB5g4Nvi+TBVNf3e9
 k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
 k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g=
 k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k=
+k8s.io/code-generator v0.0.0-20190717022600-77f3a1fe56bb/go.mod h1:cDx5jQmWH25Ff74daM7NVYty9JWw9dvIS9zT9eIubCY=
+k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/heapster v1.5.4 h1:lH2GCZdqRmUKDoyqRgiXbRmIcevaPYTvkguOuYUl8gQ=
 k8s.io/heapster v1.5.4/go.mod h1:h1uhptVXMwC8xtZBYsPXKVi8fpdlYkTs6k949KozGrM=
+k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
 k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30 h1:TRb4wNWoBVrH9plmkp2q86FIDppkbrEXdXlxU3a3BMI=
 k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
+k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058 h1:di3XCwddOR9cWBNpfgXaskhh6cgJuwcK54rvtwUaC10=
+k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4=
 k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4=
 k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
 k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a h1:2jUDc9gJja832Ftp+QbDV0tVhQHMISFn01els+2ZAcw=
 k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
+modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
+modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
+modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
+modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
+modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
+sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
 sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
diff --git a/i18n/messages.fr.xlf b/i18n/messages.fr.xlf
index e1a580496..62efdbd47 100644
--- a/i18n/messages.fr.xlf
+++ b/i18n/messages.fr.xlf
@@ -601,6 +601,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">42</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">42</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/creator/template.html</context>
           <context context-type="linenumber">48</context>
@@ -705,6 +709,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">54</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">63</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crdobject/template.html</context>
           <context context-type="linenumber">52</context>
@@ -793,6 +801,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">70</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">71</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crd/template.html</context>
           <context context-type="linenumber">80</context>
@@ -1357,6 +1369,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">23</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">23</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/condition/template.html</context>
           <context context-type="linenumber">26</context>
@@ -1830,6 +1846,22 @@
           <context context-type="linenumber">21</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="7099af5a120571be073bcfb7babafb38e91a3b50" datatype="html">
+        <source>Plugins</source>
+        <target state="new">Plugins</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">21</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="473fb06a7b7b3992af493059e8f9c8c74f369a97" datatype="html">
+        <source>Dependencies</source>
+        <target state="new">Dependencies</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">54</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="29f86a92691ef11e87a07459d2104cab7bd91f56" datatype="html">
         <source>Secrets</source>
         <target>Secrets</target>
@@ -2778,6 +2810,16 @@
           <context context-type="linenumber">121</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="1d216da7f53e1afcd7bffd53c0a6b0f2502cde4d" datatype="html">
+        <source>Plugins
+      </source>
+        <target state="new">Plugins
+      </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
+          <context context-type="linenumber">126</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="f2f9d18ebacb9e3bcb996d8176609e306864265e" datatype="html">
         <source>Custom Resource Definitions
       </source>
@@ -2785,7 +2827,7 @@
       </target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">131</context>
+          <context context-type="linenumber">136</context>
         </context-group>
       </trans-unit>
       <trans-unit id="03511cfe5e28ed3380ab198c13cf62ebde1e89e6" datatype="html">
@@ -2794,7 +2836,7 @@
         <target>Paramètres</target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">142</context>
+          <context context-type="linenumber">147</context>
         </context-group>
       </trans-unit>
       <trans-unit id="c2fa735eb6b1b2c10171c5acb818f6e0975a9406" datatype="html">
@@ -2803,7 +2845,7 @@
         <target>À propos</target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">147</context>
+          <context context-type="linenumber">152</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7e892ba15f2c6c17e83510e273b3e10fc32ea016" datatype="html">
diff --git a/i18n/messages.ja.xlf b/i18n/messages.ja.xlf
index 37d824bea..2c46d449f 100644
--- a/i18n/messages.ja.xlf
+++ b/i18n/messages.ja.xlf
@@ -545,6 +545,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">23</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">23</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/condition/template.html</context>
           <context context-type="linenumber">26</context>
@@ -657,6 +661,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">42</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">42</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/creator/template.html</context>
           <context context-type="linenumber">48</context>
@@ -761,6 +769,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">54</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">63</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crdobject/template.html</context>
           <context context-type="linenumber">52</context>
@@ -961,6 +973,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">70</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">71</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crd/template.html</context>
           <context context-type="linenumber">80</context>
@@ -1054,6 +1070,22 @@
           <context context-type="linenumber">21</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="7099af5a120571be073bcfb7babafb38e91a3b50" datatype="html">
+        <source>Plugins</source>
+        <target state="new">Plugins</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">21</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="473fb06a7b7b3992af493059e8f9c8c74f369a97" datatype="html">
+        <source>Dependencies</source>
+        <target state="new">Dependencies</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">54</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3c16399a88dd3765114804d3825cc346f995c685" datatype="html">
         <source>Image: </source>
         <target>イメージ: </target>
@@ -2508,6 +2540,16 @@
           <context context-type="linenumber">121</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="1d216da7f53e1afcd7bffd53c0a6b0f2502cde4d" datatype="html">
+        <source>Plugins
+      </source>
+        <target state="new">Plugins
+      </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
+          <context context-type="linenumber">126</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="f2f9d18ebacb9e3bcb996d8176609e306864265e" datatype="html">
         <source>Custom Resource Definitions
       </source>
@@ -2515,17 +2557,16 @@
       </target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">131</context>
+          <context context-type="linenumber">136</context>
         </context-group>
       </trans-unit>
-
       <trans-unit id="03511cfe5e28ed3380ab198c13cf62ebde1e89e6" datatype="html">
         <source>Settings
       </source>
         <target>設定</target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">142</context>
+          <context context-type="linenumber">147</context>
         </context-group>
       </trans-unit>
       <trans-unit id="c2fa735eb6b1b2c10171c5acb818f6e0975a9406" datatype="html">
@@ -2534,7 +2575,7 @@
         <target>Kubernetes Dashboard について</target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">147</context>
+          <context context-type="linenumber">152</context>
         </context-group>
       </trans-unit>
       <trans-unit id="01269ce8051398188d50367ead1831407e77d504" datatype="html">
diff --git a/i18n/messages.ko.xlf b/i18n/messages.ko.xlf
index 6e722c773..3fc5e3546 100644
--- a/i18n/messages.ko.xlf
+++ b/i18n/messages.ko.xlf
@@ -737,6 +737,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">23</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">23</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/condition/template.html</context>
           <context context-type="linenumber">26</context>
@@ -849,6 +853,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">42</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">42</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/creator/template.html</context>
           <context context-type="linenumber">48</context>
@@ -953,6 +961,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">54</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">63</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crdobject/template.html</context>
           <context context-type="linenumber">52</context>
@@ -1153,6 +1165,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">70</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">71</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crd/template.html</context>
           <context context-type="linenumber">80</context>
@@ -1246,6 +1262,22 @@
           <context context-type="linenumber">21</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="7099af5a120571be073bcfb7babafb38e91a3b50" datatype="html">
+        <source>Plugins</source>
+        <target state="new">Plugins</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">21</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="473fb06a7b7b3992af493059e8f9c8c74f369a97" datatype="html">
+        <source>Dependencies</source>
+        <target state="new">Dependencies</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">54</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3c16399a88dd3765114804d3825cc346f995c685" datatype="html">
         <source>Image: </source>
         <target state="new">이미지: </target>
@@ -2562,6 +2594,16 @@
           <context context-type="linenumber">121</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="1d216da7f53e1afcd7bffd53c0a6b0f2502cde4d" datatype="html">
+        <source>Plugins
+      </source>
+        <target state="new">Plugins
+      </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
+          <context context-type="linenumber">126</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="f2f9d18ebacb9e3bcb996d8176609e306864265e" datatype="html">
         <source>Custom Resource Definitions
       </source>
@@ -2569,7 +2611,7 @@
       </target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">131</context>
+          <context context-type="linenumber">136</context>
         </context-group>
       </trans-unit>
       <trans-unit id="03511cfe5e28ed3380ab198c13cf62ebde1e89e6" datatype="html">
@@ -2579,7 +2621,7 @@
       </target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">142</context>
+          <context context-type="linenumber">147</context>
         </context-group>
       </trans-unit>
       <trans-unit id="c2fa735eb6b1b2c10171c5acb818f6e0975a9406" datatype="html">
@@ -2589,7 +2631,7 @@
       </target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">147</context>
+          <context context-type="linenumber">152</context>
         </context-group>
       </trans-unit>
       <trans-unit id="01269ce8051398188d50367ead1831407e77d504" datatype="html">
@@ -3282,6 +3324,7 @@
         
         
         
+        
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/create/from/form/template.html</context>
           <context context-type="linenumber">54</context>
diff --git a/i18n/messages.xlf b/i18n/messages.xlf
index 6f9c62b49..cdf049a5d 100644
--- a/i18n/messages.xlf
+++ b/i18n/messages.xlf
@@ -656,6 +656,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">23</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">23</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/condition/template.html</context>
           <context context-type="linenumber">26</context>
@@ -767,6 +771,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">42</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">42</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/creator/template.html</context>
           <context context-type="linenumber">48</context>
@@ -870,6 +878,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">54</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">63</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crdobject/template.html</context>
           <context context-type="linenumber">52</context>
@@ -1064,6 +1076,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">70</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">71</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crd/template.html</context>
           <context context-type="linenumber">80</context>
@@ -1155,6 +1171,20 @@
           <context context-type="linenumber">21</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="7099af5a120571be073bcfb7babafb38e91a3b50" datatype="html">
+        <source>Plugins</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">21</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="473fb06a7b7b3992af493059e8f9c8c74f369a97" datatype="html">
+        <source>Dependencies</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">54</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3c16399a88dd3765114804d3825cc346f995c685" datatype="html">
         <source>Image: </source>
         <context-group purpose="location">
@@ -2312,12 +2342,20 @@
           <context context-type="linenumber">121</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="1d216da7f53e1afcd7bffd53c0a6b0f2502cde4d" datatype="html">
+        <source>Plugins
+      </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
+          <context context-type="linenumber">126</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="f2f9d18ebacb9e3bcb996d8176609e306864265e" datatype="html">
         <source>Custom Resource Definitions
       </source>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">131</context>
+          <context context-type="linenumber">136</context>
         </context-group>
       </trans-unit>
       <trans-unit id="03511cfe5e28ed3380ab198c13cf62ebde1e89e6" datatype="html">
@@ -2325,7 +2363,7 @@
       </source>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">142</context>
+          <context context-type="linenumber">147</context>
         </context-group>
       </trans-unit>
       <trans-unit id="c2fa735eb6b1b2c10171c5acb818f6e0975a9406" datatype="html">
@@ -2333,7 +2371,7 @@
       </source>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">147</context>
+          <context context-type="linenumber">152</context>
         </context-group>
       </trans-unit>
       <trans-unit id="01269ce8051398188d50367ead1831407e77d504" datatype="html">
diff --git a/i18n/messages.zh.xlf b/i18n/messages.zh.xlf
index be4801d6a..ee208c9a5 100644
--- a/i18n/messages.zh.xlf
+++ b/i18n/messages.zh.xlf
@@ -737,6 +737,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">23</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">23</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/condition/template.html</context>
           <context context-type="linenumber">26</context>
@@ -849,6 +853,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">42</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">42</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/creator/template.html</context>
           <context context-type="linenumber">48</context>
@@ -953,6 +961,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">54</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">63</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crdobject/template.html</context>
           <context context-type="linenumber">52</context>
@@ -1153,6 +1165,10 @@
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/configmap/template.html</context>
           <context context-type="linenumber">70</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">71</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/crd/template.html</context>
           <context context-type="linenumber">80</context>
@@ -1246,6 +1262,22 @@
           <context context-type="linenumber">21</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="7099af5a120571be073bcfb7babafb38e91a3b50" datatype="html">
+        <source>Plugins</source>
+        <target state="new">Plugins</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">21</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="473fb06a7b7b3992af493059e8f9c8c74f369a97" datatype="html">
+        <source>Dependencies</source>
+        <target state="new">Dependencies</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/common/components/resourcelist/plugin/template.html</context>
+          <context context-type="linenumber">54</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3c16399a88dd3765114804d3825cc346f995c685" datatype="html">
         <source>Image: </source>
         <target state="new">镜像: </target>
@@ -2562,6 +2594,16 @@
           <context context-type="linenumber">121</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="1d216da7f53e1afcd7bffd53c0a6b0f2502cde4d" datatype="html">
+        <source>Plugins
+      </source>
+        <target state="new">Plugins
+      </target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
+          <context context-type="linenumber">126</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="f2f9d18ebacb9e3bcb996d8176609e306864265e" datatype="html">
         <source>Custom Resource Definitions
       </source>
@@ -2569,7 +2611,7 @@
       </target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">131</context>
+          <context context-type="linenumber">136</context>
         </context-group>
       </trans-unit>
       <trans-unit id="03511cfe5e28ed3380ab198c13cf62ebde1e89e6" datatype="html">
@@ -2579,7 +2621,7 @@
       </target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">142</context>
+          <context context-type="linenumber">147</context>
         </context-group>
       </trans-unit>
       <trans-unit id="c2fa735eb6b1b2c10171c5acb818f6e0975a9406" datatype="html">
@@ -2589,7 +2631,7 @@
       </target>
         <context-group purpose="location">
           <context context-type="sourcefile">../src/app/frontend/chrome/nav/template.html</context>
-          <context context-type="linenumber">147</context>
+          <context context-type="linenumber">152</context>
         </context-group>
       </trans-unit>
       <trans-unit id="01269ce8051398188d50367ead1831407e77d504" datatype="html">
diff --git a/package-lock.json b/package-lock.json
index 0a4155a58..204d9bf0b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19906,6 +19906,11 @@
       "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
       "dev": true
     },
+    "systemjs": {
+      "version": "3.1.6",
+      "resolved": "https://registry.npmjs.org/systemjs/-/systemjs-3.1.6.tgz",
+      "integrity": "sha512-cXin4Z7FWf/eANmvuR5uE8qfPqMlkjyNZbayO/Mf3+AvSOegYY/3myiWgioKLOWUeB16if3VYqdk7HXXmgOL8Q=="
+    },
     "table": {
       "version": "3.8.3",
       "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz",
diff --git a/package.json b/package.json
index 88e11247b..6a29f8068 100644
--- a/package.json
+++ b/package.json
@@ -35,8 +35,9 @@
     "e2e": "concurrently -kill-others --success first \"npm run start\" \"wait-on http://localhost:8080 && npm run cy:run\"",
     "cluster:start": "./aio/scripts/start-cluster.sh",
     "cluster:stop": "./aio/scripts/stop-cluster.sh",
-    "check": "concurrently \"npm run check:backend\" \"npm run check:frontend\" \"npm run check:license\" \"npm run check:i18n\"",
+    "check": "concurrently \"npm run check:backend\" \"npm run check:codegen\" \"npm run check:frontend\" \"npm run check:license\" \"npm run check:i18n\"",
     "check:backend": "golangci-lint run -c .golangci.yml src/app/backend/...",
+    "check:codegen": "aio/scripts/verify-codegen.sh",
     "check:frontend": "concurrently \"npm run check:frontend:ts\" \"npm run check:frontend:scss\" \"npm run check:frontend:html\"",
     "check:frontend:ts": "gts check",
     "check:frontend:scss": "./aio/scripts/format.sh --styles --check && ./node_modules/sass-lint/bin/sass-lint.js -c .sass-lint.yml 'src/app/frontend/**/*.scss' -v -q",
@@ -122,9 +123,11 @@
     "ngx-filter-pipe": "2.1.2",
     "normalize.css": "8.0.1",
     "roboto-fontface": "0.10.0",
+    "systemjs": "3.1.6",
     "rxjs": "6.5.2",
     "rxjs-compat": "6.5.2",
     "sockjs-client": "1.4.0",
+    "tslib": "^1.9.0",
     "web-animations-js": "2.3.2",
     "xterm": "3.14.5",
     "zone.js": "0.9.1"
diff --git a/src/app/backend/api/types.go b/src/app/backend/api/types.go
index 7617ec4f5..f93aa39ae 100644
--- a/src/app/backend/api/types.go
+++ b/src/app/backend/api/types.go
@@ -137,6 +137,7 @@ const (
 	ResourceKindStatefulSet              = "statefulset"
 	ResourceKindStorageClass             = "storageclass"
 	ResourceKindClusterRole              = "clusterrole"
+	ResourceKindPlugin                   = "plugin"
 	ResourceKindEndpoint                 = "endpoint"
 )
 
@@ -173,6 +174,7 @@ const (
 	ClientTypeStorageClient       = "storageclient"
 	ClientTypeRbacClient          = "rbacclient"
 	ClientTypeAPIExtensionsClient = "apiextensionsclient"
+	ClientTypePluginsClient       = "plugin"
 )
 
 type APIMapping struct {
@@ -212,6 +214,7 @@ var KindToAPIMapping = map[string]APIMapping{
 	ResourceKindStorageClass:             {"storageclasses", ClientTypeStorageClient, false},
 	ResourceKindEndpoint:                 {"endpoints", ClientTypeDefault, true},
 	ResourceKindClusterRole:              {"clusterroles", ClientTypeRbacClient, false},
+	ResourceKindPlugin:                   {"plugins", ClientTypePluginsClient, true},
 }
 
 // IsSelectorMatching returns true when an object with the given selector targets the same
diff --git a/src/app/backend/client/api/types.go b/src/app/backend/client/api/types.go
index c718681a3..f15ca953a 100644
--- a/src/app/backend/client/api/types.go
+++ b/src/app/backend/client/api/types.go
@@ -43,6 +43,7 @@ type ClientManager interface {
 	APIExtensionsClient(req *restful.Request) (apiextensionsclientset.Interface, error)
 	PluginClient(req *restful.Request) (pluginclientset.Interface, error)
 	InsecureAPIExtensionsClient() apiextensionsclientset.Interface
+	InsecurePluginClient() pluginclientset.Interface
 	CanI(req *restful.Request, ssar *v1.SelfSubjectAccessReview) bool
 	Config(req *restful.Request) (*rest.Config, error)
 	ClientCmdConfig(req *restful.Request) (clientcmd.ClientConfig, error)
diff --git a/src/app/backend/client/manager.go b/src/app/backend/client/manager.go
index 4b932f24a..85ac6e39f 100644
--- a/src/app/backend/client/manager.go
+++ b/src/app/backend/client/manager.go
@@ -252,10 +252,18 @@ func (self *clientManager) VerberClient(req *restful.Request, config *rest.Confi
 		return nil, err
 	}
 
+	pluginsclient, err := self.PluginClient(req)
+	if err != nil {
+		return nil, err
+	}
+
 	return NewResourceVerber(k8sClient.CoreV1().RESTClient(),
 		k8sClient.ExtensionsV1beta1().RESTClient(), k8sClient.AppsV1().RESTClient(),
 		k8sClient.BatchV1().RESTClient(), k8sClient.BatchV1beta1().RESTClient(), k8sClient.AutoscalingV1().RESTClient(),
-		k8sClient.StorageV1().RESTClient(), k8sClient.RbacV1().RESTClient(), apiextensionsclient.ApiextensionsV1beta1().RESTClient(), config), nil
+		k8sClient.StorageV1().RESTClient(), k8sClient.RbacV1().RESTClient(),
+		apiextensionsclient.ApiextensionsV1beta1().RESTClient(),
+		pluginsclient.DashboardV1alpha1().RESTClient(),
+		config), nil
 }
 
 // SetTokenManager sets the token manager that will be used for token decryption.
diff --git a/src/app/backend/client/verber.go b/src/app/backend/client/verber.go
index a62e761ba..4a5a4394d 100644
--- a/src/app/backend/client/verber.go
+++ b/src/app/backend/client/verber.go
@@ -39,6 +39,7 @@ type resourceVerber struct {
 	storageClient       RESTClient
 	rbacClient          RESTClient
 	apiExtensionsClient RESTClient
+	pluginsClient       RESTClient
 	config              *restclient.Config
 }
 
@@ -60,6 +61,8 @@ func (verber *resourceVerber) getRESTClientByType(clientType api.ClientType) RES
 		return verber.rbacClient
 	case api.ClientTypeAPIExtensionsClient:
 		return verber.apiExtensionsClient
+	case api.ClientTypePluginsClient:
+		return verber.pluginsClient
 	default:
 		return verber.client
 	}
@@ -113,11 +116,9 @@ type RESTClient interface {
 }
 
 // NewResourceVerber creates a new resource verber that uses the given client for performing operations.
-func NewResourceVerber(client, extensionsClient, appsClient,
-	batchClient, betaBatchClient, autoscalingClient, storageClient,
-	rbacClient, apiExtensionsClient RESTClient, config *restclient.Config) clientapi.ResourceVerber {
+func NewResourceVerber(client, extensionsClient, appsClient, batchClient, betaBatchClient, autoscalingClient, storageClient, rbacClient, apiExtensionsClient, pluginsClient RESTClient, config *restclient.Config) clientapi.ResourceVerber {
 	return &resourceVerber{client, extensionsClient, appsClient,
-		batchClient, betaBatchClient, autoscalingClient, storageClient, rbacClient, apiExtensionsClient, config}
+		batchClient, betaBatchClient, autoscalingClient, storageClient, rbacClient, apiExtensionsClient, pluginsClient, config}
 }
 
 // Delete deletes the resource of the given kind in the given namespace with the given name.
diff --git a/src/app/backend/handler/apihandler.go b/src/app/backend/handler/apihandler.go
index 47073b90c..01aabea3b 100644
--- a/src/app/backend/handler/apihandler.go
+++ b/src/app/backend/handler/apihandler.go
@@ -20,6 +20,10 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/kubernetes/dashboard/src/app/backend/handler/parser"
+
+	"github.com/kubernetes/dashboard/src/app/backend/plugin"
+
 	restful "github.com/emicklei/go-restful"
 	"github.com/kubernetes/dashboard/src/app/backend/api"
 	"github.com/kubernetes/dashboard/src/app/backend/auth"
@@ -27,8 +31,6 @@ import (
 	clientapi "github.com/kubernetes/dashboard/src/app/backend/client/api"
 	"github.com/kubernetes/dashboard/src/app/backend/errors"
 	"github.com/kubernetes/dashboard/src/app/backend/integration"
-	metricapi "github.com/kubernetes/dashboard/src/app/backend/integration/metric/api"
-	"github.com/kubernetes/dashboard/src/app/backend/plugin"
 	"github.com/kubernetes/dashboard/src/app/backend/resource/clusterrole"
 	"github.com/kubernetes/dashboard/src/app/backend/resource/common"
 	"github.com/kubernetes/dashboard/src/app/backend/resource/configmap"
@@ -607,7 +609,7 @@ func (apiHandler *APIHandler) handleGetClusterRoleList(request *restful.Request,
 		return
 	}
 
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := clusterrole.GetClusterRoleList(k8sClient, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -646,7 +648,7 @@ func (apiHandler *APIHandler) handleGetStatefulSetList(request *restful.Request,
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := statefulset.GetStatefulSetList(k8sClient, namespace, dataSelect,
 		apiHandler.iManager.Metric().Client())
@@ -684,7 +686,7 @@ func (apiHandler *APIHandler) handleGetStatefulSetPods(request *restful.Request,
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("statefulset")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := statefulset.GetStatefulSetPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, name, namespace)
 	if err != nil {
@@ -703,7 +705,7 @@ func (apiHandler *APIHandler) handleGetStatefulSetEvents(request *restful.Reques
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("statefulset")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := event.GetResourceEvents(k8sClient, dataSelect, namespace, name)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -720,7 +722,7 @@ func (apiHandler *APIHandler) handleGetServiceList(request *restful.Request, res
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := resourceService.GetServiceList(k8sClient, namespace, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -755,7 +757,7 @@ func (apiHandler *APIHandler) handleGetServiceEvent(request *restful.Request, re
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("service")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := resourceService.GetServiceEvents(k8sClient, dataSelect, namespace, name)
 	if err != nil {
@@ -789,7 +791,7 @@ func (apiHandler *APIHandler) handleGetIngressList(request *restful.Request, res
 		return
 	}
 
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	namespace := parseNamespacePathParameter(request)
 	result, err := ingress.GetIngressList(k8sClient, namespace, dataSelect)
 	if err != nil {
@@ -808,7 +810,7 @@ func (apiHandler *APIHandler) handleGetServicePods(request *restful.Request, res
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("service")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := resourceService.GetServicePods(k8sClient, apiHandler.iManager.Metric().Client(), namespace, name, dataSelect)
 	if err != nil {
@@ -825,7 +827,7 @@ func (apiHandler *APIHandler) handleGetNodeList(request *restful.Request, respon
 		return
 	}
 
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := node.GetNodeList(k8sClient, dataSelect, apiHandler.iManager.Metric().Client())
 	if err != nil {
@@ -843,7 +845,7 @@ func (apiHandler *APIHandler) handleGetNodeDetail(request *restful.Request, resp
 	}
 
 	name := request.PathParameter("name")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := node.GetNodeDetail(k8sClient, apiHandler.iManager.Metric().Client(), name, dataSelect)
 	if err != nil {
@@ -861,7 +863,7 @@ func (apiHandler *APIHandler) handleGetNodeEvents(request *restful.Request, resp
 	}
 
 	name := request.PathParameter("name")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := event.GetNodeEvents(k8sClient, dataSelect, name)
 	if err != nil {
@@ -879,7 +881,7 @@ func (apiHandler *APIHandler) handleGetNodePods(request *restful.Request, respon
 	}
 
 	name := request.PathParameter("name")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := node.GetNodePods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, name)
 	if err != nil {
@@ -1034,7 +1036,7 @@ func (apiHandler *APIHandler) handleGetReplicationControllerList(request *restfu
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := replicationcontroller.GetReplicationControllerList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client())
 	if err != nil {
@@ -1052,7 +1054,7 @@ func (apiHandler *APIHandler) handleGetReplicaSets(request *restful.Request, res
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := replicaset.GetReplicaSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client())
 	if err != nil {
@@ -1090,7 +1092,7 @@ func (apiHandler *APIHandler) handleGetReplicaSetPods(request *restful.Request,
 
 	namespace := request.PathParameter("namespace")
 	replicaSet := request.PathParameter("replicaSet")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := replicaset.GetReplicaSetPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, replicaSet, namespace)
 	if err != nil {
@@ -1110,7 +1112,7 @@ func (apiHandler *APIHandler) handleGetReplicaSetServices(request *restful.Reque
 
 	namespace := request.PathParameter("namespace")
 	replicaSet := request.PathParameter("replicaSet")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := replicaset.GetReplicaSetServices(k8sClient, dataSelect, namespace, replicaSet)
 	if err != nil {
@@ -1130,7 +1132,7 @@ func (apiHandler *APIHandler) handleGetReplicaSetEvents(request *restful.Request
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("replicaSet")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := event.GetResourceEvents(k8sClient, dataSelect, namespace, name)
 	if err != nil {
@@ -1151,7 +1153,7 @@ func (apiHandler *APIHandler) handleGetPodEvents(request *restful.Request, respo
 	log.Println("Getting events related to a pod in namespace")
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("pod")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := pod.GetEventsForPod(k8sClient, dataSelect, namespace, name)
 	if err != nil {
@@ -1198,7 +1200,7 @@ func (apiHandler *APIHandler) handleGetDeployments(request *restful.Request, res
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := deployment.GetDeploymentList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client())
 	if err != nil {
@@ -1235,7 +1237,7 @@ func (apiHandler *APIHandler) handleGetDeploymentEvents(request *restful.Request
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("deployment")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := event.GetResourceEvents(k8sClient, dataSelect, namespace, name)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1253,7 +1255,7 @@ func (apiHandler *APIHandler) handleGetDeploymentOldReplicaSets(request *restful
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("deployment")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := deployment.GetDeploymentOldReplicaSets(k8sClient, dataSelect, namespace, name)
 	if err != nil {
@@ -1272,7 +1274,7 @@ func (apiHandler *APIHandler) handleGetDeploymentNewReplicaSet(request *restful.
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("deployment")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := deployment.GetDeploymentNewReplicaSet(k8sClient, dataSelect, namespace, name)
 	if err != nil {
@@ -1290,7 +1292,7 @@ func (apiHandler *APIHandler) handleGetPods(request *restful.Request, response *
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics // download standard metrics - cpu, and memory - by default
 	result, err := pod.GetPodList(k8sClient, apiHandler.iManager.Metric().Client(), namespace, dataSelect)
 	if err != nil {
@@ -1466,7 +1468,7 @@ func (apiHandler *APIHandler) handleGetReplicationControllerPods(request *restfu
 
 	namespace := request.PathParameter("namespace")
 	rc := request.PathParameter("replicationController")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := replicationcontroller.GetReplicationControllerPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, rc, namespace)
 	if err != nil {
@@ -1502,7 +1504,7 @@ func (apiHandler *APIHandler) handleGetNamespaces(request *restful.Request, resp
 		return
 	}
 
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := ns.GetNamespaceList(k8sClient, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1535,7 +1537,7 @@ func (apiHandler *APIHandler) handleGetNamespaceEvents(request *restful.Request,
 	}
 
 	name := request.PathParameter("name")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := event.GetNamespaceEvents(k8sClient, dataSelect, name)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1588,7 +1590,7 @@ func (apiHandler *APIHandler) handleGetSecretList(request *restful.Request, resp
 		return
 	}
 
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	namespace := parseNamespacePathParameter(request)
 	result, err := secret.GetSecretList(k8sClient, namespace, dataSelect)
 	if err != nil {
@@ -1606,7 +1608,7 @@ func (apiHandler *APIHandler) handleGetConfigMapList(request *restful.Request, r
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := configmap.GetConfigMapList(k8sClient, namespace, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1639,7 +1641,7 @@ func (apiHandler *APIHandler) handleGetPersistentVolumeList(request *restful.Req
 		return
 	}
 
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := persistentvolume.GetPersistentVolumeList(k8sClient, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1672,7 +1674,7 @@ func (apiHandler *APIHandler) handleGetPersistentVolumeClaimList(request *restfu
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := persistentvolumeclaim.GetPersistentVolumeClaimList(k8sClient, namespace, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1724,7 +1726,7 @@ func (apiHandler *APIHandler) handleGetReplicationControllerEvents(request *rest
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("replicationController")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := event.GetResourceEvents(k8sClient, dataSelect, namespace, name)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1743,7 +1745,7 @@ func (apiHandler *APIHandler) handleGetReplicationControllerServices(request *re
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("replicationController")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := replicationcontroller.GetReplicationControllerServices(k8sClient, dataSelect, namespace, name)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1760,7 +1762,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetList(request *restful.Request, r
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := daemonset.GetDaemonSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client())
 	if err != nil {
@@ -1797,7 +1799,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetPods(request *restful.Request, r
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("daemonSet")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := daemonset.GetDaemonSetPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, name, namespace)
 	if err != nil {
@@ -1816,7 +1818,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetServices(request *restful.Reques
 
 	namespace := request.PathParameter("namespace")
 	daemonSet := request.PathParameter("daemonSet")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := daemonset.GetDaemonSetServices(k8sClient, dataSelect, namespace, daemonSet)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1834,7 +1836,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetEvents(request *restful.Request,
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("daemonSet")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := event.GetResourceEvents(k8sClient, dataSelect, namespace, name)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1852,7 +1854,7 @@ func (apiHandler *APIHandler) handleGetHorizontalPodAutoscalerList(request *rest
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := horizontalpodautoscaler.GetHorizontalPodAutoscalerList(k8sClient, namespace, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1886,7 +1888,7 @@ func (apiHandler *APIHandler) handleGetJobList(request *restful.Request, respons
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := job.GetJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client())
 	if err != nil {
@@ -1905,7 +1907,7 @@ func (apiHandler *APIHandler) handleGetJobDetail(request *restful.Request, respo
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("name")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := job.GetJobDetail(k8sClient, namespace, name)
 	if err != nil {
@@ -1924,7 +1926,7 @@ func (apiHandler *APIHandler) handleGetJobPods(request *restful.Request, respons
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("name")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := job.GetJobPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, namespace, name)
 	if err != nil {
@@ -1943,7 +1945,7 @@ func (apiHandler *APIHandler) handleGetJobEvents(request *restful.Request, respo
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("name")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := job.GetJobEvents(k8sClient, dataSelect, namespace, name)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -1960,7 +1962,7 @@ func (apiHandler *APIHandler) handleGetCronJobList(request *restful.Request, res
 	}
 
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := cronjob.GetCronJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client())
 	if err != nil {
@@ -1979,7 +1981,7 @@ func (apiHandler *APIHandler) handleGetCronJobDetail(request *restful.Request, r
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("name")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := cronjob.GetCronJobDetail(k8sClient, namespace, name)
 	if err != nil {
@@ -2003,7 +2005,7 @@ func (apiHandler *APIHandler) handleGetCronJobJobs(request *restful.Request, res
 		active = false
 	}
 
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := cronjob.GetCronJobJobs(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, namespace, name, active)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -2021,7 +2023,7 @@ func (apiHandler *APIHandler) handleGetCronJobEvents(request *restful.Request, r
 
 	namespace := request.PathParameter("namespace")
 	name := request.PathParameter("name")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := cronjob.GetCronJobEvents(k8sClient, dataSelect, namespace, name)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -2054,7 +2056,7 @@ func (apiHandler *APIHandler) handleGetStorageClassList(request *restful.Request
 		return
 	}
 
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := storageclass.GetStorageClassList(k8sClient, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -2088,7 +2090,7 @@ func (apiHandler *APIHandler) handleGetStorageClassPersistentVolumes(request *re
 	}
 
 	name := request.PathParameter("storageclass")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := persistentvolume.GetStorageClassPersistentVolumes(k8sClient,
 		name, dataSelect)
 	if err != nil {
@@ -2108,7 +2110,7 @@ func (apiHandler *APIHandler) handleGetPodPersistentVolumeClaims(request *restfu
 
 	name := request.PathParameter("pod")
 	namespace := request.PathParameter("namespace")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := persistentvolumeclaim.GetPodPersistentVolumeClaims(k8sClient,
 		namespace, name, dataSelect)
 	if err != nil {
@@ -2125,7 +2127,7 @@ func (apiHandler *APIHandler) handleGetCustomResourceDefinitionList(request *res
 		return
 	}
 
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := customresourcedefinition.GetCustomResourceDefinitionList(apiextensionsclient, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -2173,7 +2175,7 @@ func (apiHandler *APIHandler) handleGetCustomResourceObjectList(request *restful
 
 	crdName := request.PathParameter("crd")
 	namespace := parseNamespacePathParameter(request)
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	result, err := customresourcedefinition.GetCustomResourceObjectList(apiextensionsclient, config, namespace, dataSelect, crdName)
 	if err != nil {
 		errors.HandleInternalError(response, err)
@@ -2219,7 +2221,7 @@ func (apiHandler *APIHandler) handleGetCustomResourceObjectEvents(request *restf
 
 	name := request.PathParameter("object")
 	namespace := request.PathParameter("namespace")
-	dataSelect := parseDataSelectPathParameter(request)
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 	dataSelect.MetricQuery = dataselect.StandardMetrics
 	result, err := customresourcedefinition.GetEventsForCustomResourceObject(k8sClient, dataSelect, namespace, name)
 	if err != nil {
@@ -2328,60 +2330,3 @@ func parseNamespacePathParameter(request *restful.Request) *common.NamespaceQuer
 	}
 	return common.NewNamespaceQuery(nonEmptyNamespaces)
 }
-
-func parsePaginationPathParameter(request *restful.Request) *dataselect.PaginationQuery {
-	itemsPerPage, err := strconv.ParseInt(request.QueryParameter("itemsPerPage"), 10, 0)
-	if err != nil {
-		return dataselect.NoPagination
-	}
-
-	page, err := strconv.ParseInt(request.QueryParameter("page"), 10, 0)
-	if err != nil {
-		return dataselect.NoPagination
-	}
-
-	// Frontend pages start from 1 and backend starts from 0
-	return dataselect.NewPaginationQuery(int(itemsPerPage), int(page-1))
-}
-
-func parseFilterPathParameter(request *restful.Request) *dataselect.FilterQuery {
-	return dataselect.NewFilterQuery(strings.Split(request.QueryParameter("filterBy"), ","))
-}
-
-// Parses query parameters of the request and returns a SortQuery object
-func parseSortPathParameter(request *restful.Request) *dataselect.SortQuery {
-	return dataselect.NewSortQuery(strings.Split(request.QueryParameter("sortBy"), ","))
-}
-
-// Parses query parameters of the request and returns a MetricQuery object
-func parseMetricPathParameter(request *restful.Request) *dataselect.MetricQuery {
-	metricNamesParam := request.QueryParameter("metricNames")
-	var metricNames []string
-	if metricNamesParam != "" {
-		metricNames = strings.Split(metricNamesParam, ",")
-	} else {
-		metricNames = nil
-	}
-	aggregationsParam := request.QueryParameter("aggregations")
-	var rawAggregations []string
-	if aggregationsParam != "" {
-		rawAggregations = strings.Split(aggregationsParam, ",")
-	} else {
-		rawAggregations = nil
-	}
-	aggregationModes := metricapi.AggregationModes{}
-	for _, e := range rawAggregations {
-		aggregationModes = append(aggregationModes, metricapi.AggregationMode(e))
-	}
-	return dataselect.NewMetricQuery(metricNames, aggregationModes)
-
-}
-
-// Parses query parameters of the request and returns a DataSelectQuery object
-func parseDataSelectPathParameter(request *restful.Request) *dataselect.DataSelectQuery {
-	paginationQuery := parsePaginationPathParameter(request)
-	sortQuery := parseSortPathParameter(request)
-	filterQuery := parseFilterPathParameter(request)
-	metricQuery := parseMetricPathParameter(request)
-	return dataselect.NewDataSelectQuery(paginationQuery, sortQuery, filterQuery, metricQuery)
-}
diff --git a/src/app/backend/handler/parser/parser.go b/src/app/backend/handler/parser/parser.go
new file mode 100644
index 000000000..3c0eb37b1
--- /dev/null
+++ b/src/app/backend/handler/parser/parser.go
@@ -0,0 +1,81 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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.
+
+package parser
+
+import (
+	"strconv"
+	"strings"
+
+	"github.com/emicklei/go-restful"
+	metricapi "github.com/kubernetes/dashboard/src/app/backend/integration/metric/api"
+	"github.com/kubernetes/dashboard/src/app/backend/resource/dataselect"
+)
+
+func parsePaginationPathParameter(request *restful.Request) *dataselect.PaginationQuery {
+	itemsPerPage, err := strconv.ParseInt(request.QueryParameter("itemsPerPage"), 10, 0)
+	if err != nil {
+		return dataselect.NoPagination
+	}
+
+	page, err := strconv.ParseInt(request.QueryParameter("page"), 10, 0)
+	if err != nil {
+		return dataselect.NoPagination
+	}
+
+	// Frontend pages start from 1 and backend starts from 0
+	return dataselect.NewPaginationQuery(int(itemsPerPage), int(page-1))
+}
+
+func parseFilterPathParameter(request *restful.Request) *dataselect.FilterQuery {
+	return dataselect.NewFilterQuery(strings.Split(request.QueryParameter("filterBy"), ","))
+}
+
+// Parses query parameters of the request and returns a SortQuery object
+func parseSortPathParameter(request *restful.Request) *dataselect.SortQuery {
+	return dataselect.NewSortQuery(strings.Split(request.QueryParameter("sortBy"), ","))
+}
+
+// Parses query parameters of the request and returns a MetricQuery object
+func parseMetricPathParameter(request *restful.Request) *dataselect.MetricQuery {
+	metricNamesParam := request.QueryParameter("metricNames")
+	var metricNames []string
+	if metricNamesParam != "" {
+		metricNames = strings.Split(metricNamesParam, ",")
+	} else {
+		metricNames = nil
+	}
+	aggregationsParam := request.QueryParameter("aggregations")
+	var rawAggregations []string
+	if aggregationsParam != "" {
+		rawAggregations = strings.Split(aggregationsParam, ",")
+	} else {
+		rawAggregations = nil
+	}
+	aggregationModes := metricapi.AggregationModes{}
+	for _, e := range rawAggregations {
+		aggregationModes = append(aggregationModes, metricapi.AggregationMode(e))
+	}
+	return dataselect.NewMetricQuery(metricNames, aggregationModes)
+
+}
+
+// ParseDataSelectPathParameter parses query parameters of the request and returns a DataSelectQuery object
+func ParseDataSelectPathParameter(request *restful.Request) *dataselect.DataSelectQuery {
+	paginationQuery := parsePaginationPathParameter(request)
+	sortQuery := parseSortPathParameter(request)
+	filterQuery := parseFilterPathParameter(request)
+	metricQuery := parseMetricPathParameter(request)
+	return dataselect.NewDataSelectQuery(paginationQuery, sortQuery, filterQuery, metricQuery)
+}
diff --git a/src/app/backend/plugin/apis/v1alpha1/types.go b/src/app/backend/plugin/apis/v1alpha1/types.go
index 5840f0dab..490b04dd0 100644
--- a/src/app/backend/plugin/apis/v1alpha1/types.go
+++ b/src/app/backend/plugin/apis/v1alpha1/types.go
@@ -32,7 +32,8 @@ type Plugin struct {
 
 // PluginSpec holds the specs for the Plugin kind
 type PluginSpec struct {
-	Source Source `json:"source"`
+	Source       Source   `json:"source"`
+	Dependencies []string `json:"dependencies,omitempty"`
 }
 
 // Source holds the information about the plugin's source code origin
diff --git a/src/app/backend/plugin/apis/v1alpha1/zz_generated.deepcopy.go b/src/app/backend/plugin/apis/v1alpha1/zz_generated.deepcopy.go
index dcd50829f..e118d7a93 100644
--- a/src/app/backend/plugin/apis/v1alpha1/zz_generated.deepcopy.go
+++ b/src/app/backend/plugin/apis/v1alpha1/zz_generated.deepcopy.go
@@ -87,6 +87,11 @@ func (in *PluginList) DeepCopyObject() runtime.Object {
 func (in *PluginSpec) DeepCopyInto(out *PluginSpec) {
 	*out = *in
 	in.Source.DeepCopyInto(&out.Source)
+	if in.Dependencies != nil {
+		in, out := &in.Dependencies, &out.Dependencies
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
diff --git a/src/app/backend/plugin/client/clientset/versioned/clientset.go b/src/app/backend/plugin/client/clientset/versioned/clientset.go
index 66d79fe29..4da0630b7 100644
--- a/src/app/backend/plugin/client/clientset/versioned/clientset.go
+++ b/src/app/backend/plugin/client/clientset/versioned/clientset.go
@@ -17,6 +17,8 @@
 package versioned
 
 import (
+	"fmt"
+
 	dashboardv1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1"
 	discovery "k8s.io/client-go/discovery"
 	rest "k8s.io/client-go/rest"
@@ -49,9 +51,14 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface {
 }
 
 // NewForConfig creates a new Clientset for the given config.
+// If config's RateLimiter is not set and QPS and Burst are acceptable,
+// NewForConfig will generate a rate-limiter in configShallowCopy.
 func NewForConfig(c *rest.Config) (*Clientset, error) {
 	configShallowCopy := *c
 	if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
+		if configShallowCopy.Burst <= 0 {
+			return nil, fmt.Errorf("Burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0")
+		}
 		configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
 	}
 	var cs Clientset
diff --git a/src/app/backend/plugin/client/informers/externalversions/apis/interface.go b/src/app/backend/plugin/client/informers/externalversions/apis/interface.go
index 0b3fba862..3cbfe14be 100644
--- a/src/app/backend/plugin/client/informers/externalversions/apis/interface.go
+++ b/src/app/backend/plugin/client/informers/externalversions/apis/interface.go
@@ -14,7 +14,7 @@
 
 // Code generated by informer-gen. DO NOT EDIT.
 
-package dashboard
+package apis
 
 import (
 	v1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/client/informers/externalversions/apis/v1alpha1"
diff --git a/src/app/backend/plugin/config.go b/src/app/backend/plugin/config.go
new file mode 100644
index 000000000..87477560e
--- /dev/null
+++ b/src/app/backend/plugin/config.go
@@ -0,0 +1,89 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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.
+
+package plugin
+
+import (
+	"net/http"
+
+	"github.com/emicklei/go-restful"
+	"github.com/kubernetes/dashboard/src/app/backend/handler/parser"
+	apiErrors "k8s.io/apimachinery/pkg/api/errors"
+)
+
+// Config holds the information required by the frontend application to bootstrap.
+type Config struct {
+	Status         int32      `json:"status"`
+	PluginMetadata []Metadata `json:"plugins"`
+	Errors         []error    `json:"errors,omitempty"`
+}
+
+// Metadata holds least possible plugin information for Config.
+type Metadata struct {
+	Name         string   `json:"name"`
+	Path         string   `json:"path"`
+	Dependencies []string `json:"dependencies"`
+}
+
+func toPluginMetadata(vs []Plugin, f func(plugin Plugin) Metadata) []Metadata {
+	vsm := make([]Metadata, len(vs))
+	for i, v := range vs {
+		vsm[i] = f(v)
+	}
+	return vsm
+}
+
+func (h *Handler) handleConfig(request *restful.Request, response *restful.Response) {
+	pluginClient, err := h.cManager.PluginClient(request)
+	cfg := Config{Status: http.StatusOK, PluginMetadata: []Metadata{}, Errors: []error{}}
+	if err != nil {
+		cfg.Status = statusCodeFromError(err)
+		cfg.Errors = append(cfg.Errors, err)
+		response.WriteHeaderAndEntity(http.StatusOK, cfg)
+		return
+	}
+
+	dataSelect := parser.ParseDataSelectPathParameter(request)
+	result, err := GetPluginList(pluginClient, "", dataSelect)
+	if err != nil {
+		cfg.Status = statusCodeFromError(err)
+		cfg.Errors = append(cfg.Errors, err)
+		response.WriteHeaderAndEntity(http.StatusOK, cfg)
+		return
+	}
+
+	if result != nil && len(result.Errors) > 0 {
+		cfg.Status = statusCodeFromError(result.Errors[0])
+		cfg.Errors = append(cfg.Errors, result.Errors...)
+		response.WriteHeaderAndEntity(http.StatusOK, cfg)
+		return
+	}
+
+	cfg.PluginMetadata = toPluginMetadata(result.Items, func(plugin Plugin) Metadata {
+		return Metadata{
+			Name:         plugin.Name,
+			Path:         plugin.Path,
+			Dependencies: plugin.Dependencies,
+		}
+	})
+	cfg.Errors = result.Errors
+	response.WriteHeaderAndEntity(http.StatusOK, cfg)
+}
+
+func statusCodeFromError(err error) int32 {
+	if statusError, ok := err.(*apiErrors.StatusError); ok {
+		return statusError.Status().Code
+	}
+	return http.StatusUnprocessableEntity
+}
diff --git a/src/app/backend/plugin/handler.go b/src/app/backend/plugin/handler.go
index 2b497795a..356f1db4e 100644
--- a/src/app/backend/plugin/handler.go
+++ b/src/app/backend/plugin/handler.go
@@ -19,6 +19,8 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/kubernetes/dashboard/src/app/backend/handler/parser"
+
 	"github.com/emicklei/go-restful"
 	clientapi "github.com/kubernetes/dashboard/src/app/backend/client/api"
 	"github.com/kubernetes/dashboard/src/app/backend/errors"
@@ -42,7 +44,12 @@ type Handler struct {
 // to list the installed plugins and get the source code for a plugin.
 func (h *Handler) Install(ws *restful.WebService) {
 	ws.Route(
-		ws.GET("/plugins/{namespace}").
+		ws.GET("/plugin/config").
+			To(h.handleConfig),
+	)
+
+	ws.Route(
+		ws.GET("/plugin/{namespace}").
 			To(h.handlePluginList).
 			Writes(PluginList{}))
 
@@ -63,8 +70,9 @@ func (h *Handler) handlePluginList(request *restful.Request, response *restful.R
 		return
 	}
 	namespace := request.PathParameter("namespace")
+	dataSelect := parser.ParseDataSelectPathParameter(request)
 
-	result, err := GetPluginList(pluginClient, namespace)
+	result, err := GetPluginList(pluginClient, namespace, dataSelect)
 	if err != nil {
 		errors.HandleInternalError(response, err)
 		return
@@ -73,16 +81,10 @@ func (h *Handler) handlePluginList(request *restful.Request, response *restful.R
 }
 
 func (h *Handler) servePluginSource(request *restful.Request, response *restful.Response) {
-	pluginClient, err := h.cManager.PluginClient(request)
-	if err != nil {
-		errors.HandleInternalError(response, err)
-		return
-	}
-	k8sClient, err := h.cManager.Client(request)
-	if err != nil {
-		errors.HandleInternalError(response, err)
-		return
-	}
+	// TODO: Change these to secure clients once SystemJS can send proper auth headers.
+	pluginClient := h.cManager.InsecurePluginClient()
+	k8sClient := h.cManager.InsecureClient()
+
 	namespace := request.PathParameter("namespace")
 	// Removes .js extension if it's present
 	pluginName := request.PathParameter("pluginName")
diff --git a/src/app/backend/plugin/list.go b/src/app/backend/plugin/list.go
index 0e7da21ec..1c440c626 100644
--- a/src/app/backend/plugin/list.go
+++ b/src/app/backend/plugin/list.go
@@ -18,7 +18,10 @@ import (
 	"fmt"
 
 	"github.com/kubernetes/dashboard/src/app/backend/api"
+	"github.com/kubernetes/dashboard/src/app/backend/errors"
+	"github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
 	pluginclientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
+	"github.com/kubernetes/dashboard/src/app/backend/resource/dataselect"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -26,33 +29,88 @@ import (
 // map v1alpha1.PluginList to plugin.PluginList
 type PluginList struct {
 	ListMeta api.ListMeta `json:"listMeta"`
-	Plugins  []Plugin     `json:"plugins"`
+	Items    []Plugin     `json:"items"`
 	Errors   []error      `json:"errors"`
 }
 
 // PluginList holds only necessary information and is used to
 // map v1alpha1.Plugin to plugin.Plugin
 type Plugin struct {
-	Name         string   `json:"name"`
-	Path         string   `json:"path"`
-	Dependencies []string `json:"dependencies"`
+	ObjectMeta   api.ObjectMeta `json:"objectMeta"`
+	TypeMeta     api.TypeMeta   `json:"typeMeta"`
+	Name         string         `json:"name"`
+	Path         string         `json:"path"`
+	Dependencies []string       `json:"dependencies"`
 }
 
-// GetPluginList returns all the registered plugins
-func GetPluginList(client pluginclientset.Interface, ns string) (*PluginList, error) {
-	pluginList, err := client.DashboardV1alpha1().Plugins(ns).List(v1.ListOptions{})
-	if err != nil {
-		return nil, err
+type PluginCell v1alpha1.Plugin
+
+func (p PluginCell) GetProperty(name dataselect.PropertyName) dataselect.ComparableValue {
+	switch name {
+	case dataselect.NameProperty:
+		return dataselect.StdComparableString(p.ObjectMeta.Name)
+	case dataselect.CreationTimestampProperty:
+		return dataselect.StdComparableTime(p.ObjectMeta.CreationTimestamp.Time)
+	case dataselect.NamespaceProperty:
+		return dataselect.StdComparableString(p.ObjectMeta.Namespace)
+	default:
+		// if name is not supported then just return a constant dummy value, sort will have no effect.
+		return nil
 	}
+}
 
-	result := &PluginList{Errors: []error{}, Plugins: []Plugin{}}
-	for _, plugin := range pluginList.Items {
-		result.Plugins = append(result.Plugins,
-			Plugin{
-				Name:         plugin.Name,
-				Path:         fmt.Sprintf("/plugin/%s/%s.js", plugin.Namespace, plugin.Name),
-				Dependencies: []string{}})
-		result.ListMeta.TotalItems++
+// GetPluginList returns all the registered plugins
+func GetPluginList(client pluginclientset.Interface, ns string, dsQuery *dataselect.DataSelectQuery) (*PluginList, error) {
+	plugins, err := client.DashboardV1alpha1().Plugins(ns).List(v1.ListOptions{})
+	nonCriticalErrors, criticalError := errors.HandleError(err)
+	if criticalError != nil {
+		return &PluginList{Items: []Plugin{}, Errors: []error{criticalError}}, nil
 	}
+
+	result := toPluginList(plugins.Items, nonCriticalErrors, dsQuery)
 	return result, nil
 }
+
+func toPluginList(plugins []v1alpha1.Plugin, nonCriticalErrors []error, dsQuery *dataselect.DataSelectQuery) *PluginList {
+	result := &PluginList{
+		Items:    make([]Plugin, 0),
+		ListMeta: api.ListMeta{TotalItems: len(plugins)},
+		Errors:   nonCriticalErrors,
+	}
+
+	pluginCells, filteredTotal := dataselect.GenericDataSelectWithFilter(toCells(plugins), dsQuery)
+	plugins = fromCells(pluginCells)
+	result.ListMeta = api.ListMeta{TotalItems: filteredTotal}
+
+	for _, item := range plugins {
+		result.Items = append(result.Items, toPlugin(item))
+	}
+
+	return result
+}
+
+func toPlugin(plugin v1alpha1.Plugin) Plugin {
+	return Plugin{
+		ObjectMeta:   api.NewObjectMeta(plugin.ObjectMeta),
+		TypeMeta:     api.NewTypeMeta(api.ResourceKindPlugin),
+		Name:         plugin.ObjectMeta.Name,
+		Path:         fmt.Sprintf("/api/v1/%s/%s/%s.js", api.ResourceKindPlugin, plugin.Namespace, plugin.Name),
+		Dependencies: append([]string{}, plugin.Spec.Dependencies...),
+	}
+}
+
+func toCells(std []v1alpha1.Plugin) []dataselect.DataCell {
+	cells := make([]dataselect.DataCell, len(std))
+	for i := range std {
+		cells[i] = PluginCell(std[i])
+	}
+	return cells
+}
+
+func fromCells(cells []dataselect.DataCell) []v1alpha1.Plugin {
+	std := make([]v1alpha1.Plugin, len(cells))
+	for i := range std {
+		std[i] = v1alpha1.Plugin(cells[i].(PluginCell))
+	}
+	return std
+}
diff --git a/src/app/frontend/chrome/nav/component.ts b/src/app/frontend/chrome/nav/component.ts
index 408eadedb..c59f3b732 100644
--- a/src/app/frontend/chrome/nav/component.ts
+++ b/src/app/frontend/chrome/nav/component.ts
@@ -16,6 +16,7 @@ import {Component, OnInit, ViewChild} from '@angular/core';
 import {MatDrawer} from '@angular/material';
 
 import {NavService} from '../../common/services/nav/service';
+import {PluginsConfigService} from '../../common/services/global/plugin';
 
 @Component({
   selector: 'kd-nav',
@@ -25,10 +26,17 @@ import {NavService} from '../../common/services/nav/service';
 export class NavComponent implements OnInit {
   @ViewChild(MatDrawer, {static: true}) private readonly nav_: MatDrawer;
 
-  constructor(private readonly navService_: NavService) {}
+  constructor(
+    private readonly navService_: NavService,
+    private readonly pluginsConfigService_: PluginsConfigService,
+  ) {}
 
   ngOnInit(): void {
     this.navService_.setNav(this.nav_);
     this.navService_.setVisibility(true);
   }
+
+  showPlugin(): boolean {
+    return this.pluginsConfigService_.status() === 200;
+  }
 }
diff --git a/src/app/frontend/chrome/nav/template.html b/src/app/frontend/chrome/nav/template.html
index 639d4846b..0d28f19b5 100644
--- a/src/app/frontend/chrome/nav/template.html
+++ b/src/app/frontend/chrome/nav/template.html
@@ -120,6 +120,11 @@ limitations under the License.
                    state="/secret"
                    i18n>Secrets
       </kd-nav-item>
+      <kd-nav-item *ngIf="showPlugin()"
+                   class="kd-nav-group-item"
+                   state="/plugin"
+                   i18n>Plugins
+      </kd-nav-item>
     </div>
 
     <mat-divider></mat-divider>
diff --git a/src/app/frontend/chrome/routing.ts b/src/app/frontend/chrome/routing.ts
index 0c5f780b8..298f3c5e6 100644
--- a/src/app/frontend/chrome/routing.ts
+++ b/src/app/frontend/chrome/routing.ts
@@ -112,6 +112,10 @@ const routes: Routes = [
         path: 'service',
         loadChildren: 'resource/discovery/service/module#ServiceModule',
       },
+      {
+        path: 'plugin',
+        loadChildren: 'plugin/module#PluginModule',
+      },
 
       // Config group
       {
diff --git a/src/app/frontend/common/components/module.ts b/src/app/frontend/common/components/module.ts
index 0ad487390..d8d1d42fd 100644
--- a/src/app/frontend/common/components/module.ts
+++ b/src/app/frontend/common/components/module.ts
@@ -77,6 +77,7 @@ import {NamespaceListComponent} from './resourcelist/namespace/component';
 import {NodeListComponent} from './resourcelist/node/component';
 import {PersistentVolumeListComponent} from './resourcelist/persistentvolume/component';
 import {PersistentVolumeClaimListComponent} from './resourcelist/persistentvolumeclaim/component';
+import {PluginListComponent} from './resourcelist/plugin/component';
 import {PodListComponent} from './resourcelist/pod/component';
 import {ReplicaSetListComponent} from './resourcelist/replicaset/component';
 import {ReplicationControllerListComponent} from './resourcelist/replicationcontroller/component';
@@ -111,6 +112,7 @@ const components = [
   CronJobListComponent,
   ClusterRoleListComponent,
   ConfigMapListComponent,
+  PluginListComponent,
   ColumnComponent,
   ChipDialog,
   ContainerCardComponent,
diff --git a/src/app/frontend/common/components/resourcelist/groupids.ts b/src/app/frontend/common/components/resourcelist/groupids.ts
index ac7662a77..4abef2595 100644
--- a/src/app/frontend/common/components/resourcelist/groupids.ts
+++ b/src/app/frontend/common/components/resourcelist/groupids.ts
@@ -35,6 +35,7 @@ export enum ListIdentifier {
   statefulSet = 'statefulSetList',
   event = 'event',
   resource = 'resource',
+  plugin = 'plugin',
 }
 
 export enum ListGroupIdentifier {
diff --git a/src/app/frontend/common/components/resourcelist/plugin/component.ts b/src/app/frontend/common/components/resourcelist/plugin/component.ts
new file mode 100644
index 000000000..2e289e49d
--- /dev/null
+++ b/src/app/frontend/common/components/resourcelist/plugin/component.ts
@@ -0,0 +1,63 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {HttpParams} from '@angular/common/http';
+import {Component, Input} from '@angular/core';
+import {Plugin, PluginList} from '@api/backendapi';
+import {Observable} from 'rxjs/Observable';
+import {ResourceListBase} from '../../../resources/list';
+import {NotificationsService} from '../../../services/global/notifications';
+import {EndpointManager, Resource} from '../../../services/resource/endpoint';
+import {NamespacedResourceService} from '../../../services/resource/resource';
+import {MenuComponent} from '../../list/column/menu/component';
+import {ListGroupIdentifier, ListIdentifier} from '../groupids';
+
+@Component({
+  selector: 'kd-plugin-list',
+  templateUrl: './template.html',
+})
+export class PluginListComponent extends ResourceListBase<PluginList, Plugin> {
+  @Input() endpoint = EndpointManager.resource(Resource.plugin, true).list();
+
+  constructor(
+    private readonly plugin_: NamespacedResourceService<PluginList>,
+    notifications: NotificationsService,
+  ) {
+    super('plugin', notifications);
+    this.id = ListIdentifier.plugin;
+    this.groupId = ListGroupIdentifier.none;
+
+    // Register action columns.
+    this.registerActionColumn<MenuComponent>('menu', MenuComponent);
+
+    // Register dynamic columns.
+    this.registerDynamicColumn('namespace', 'name', this.shouldShowNamespaceColumn_.bind(this));
+  }
+
+  getResourceObservable(params?: HttpParams): Observable<PluginList> {
+    return this.plugin_.get(this.endpoint, undefined, undefined, params);
+  }
+
+  map(pluginList: PluginList): Plugin[] {
+    return pluginList.items;
+  }
+
+  getDisplayColumns(): string[] {
+    return ['name', 'dependencies', 'age'];
+  }
+
+  private shouldShowNamespaceColumn_(): boolean {
+    return this.namespaceService_.areMultipleNamespacesSelected();
+  }
+}
diff --git a/src/app/frontend/common/components/resourcelist/plugin/template.html b/src/app/frontend/common/components/resourcelist/plugin/template.html
new file mode 100644
index 000000000..2cfe895f3
--- /dev/null
+++ b/src/app/frontend/common/components/resourcelist/plugin/template.html
@@ -0,0 +1,101 @@
+<!--
+Copyright 2017 The Kubernetes Authors.
+
+Licensed 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
+
+    http://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.
+-->
+
+<kd-card role="table"
+         [hidden]="isHidden()">
+  <div title
+       fxLayout="row"
+       i18n>Plugins</div>
+  <div description><span class="kd-muted-light"
+          i18n>Items:&nbsp;</span>{{totalItems}}</div>
+  <div actions>
+    <kd-card-list-filter></kd-card-list-filter>
+  </div>
+
+  <div content
+       [hidden]="showZeroState()">
+    <div kdLoadingSpinner
+         [isLoading]="isLoading"></div>
+
+    <mat-table [dataSource]="getData()"
+               matSort
+               matSortActive="age"
+               matSortDisableClear
+               matSortDirection="asc">
+      <ng-container matColumnDef="name">
+        <mat-header-cell *matHeaderCellDef
+                         mat-sort-header
+                         disableClear="true"
+                         i18n>Name</mat-header-cell>
+        <mat-cell *matCellDef="let plugin">
+          <a [routerLink]="getDetailsHref(plugin.objectMeta.name, plugin.objectMeta.namespace)"
+             queryParamsHandling="preserve">
+            {{plugin.objectMeta.name}}
+          </a>
+        </mat-cell>
+      </ng-container>
+
+      <ng-container matColumnDef="dependencies">
+        <mat-header-cell *matHeaderCellDef
+                         disableClear="true"
+                         i18n>Dependencies</mat-header-cell>
+        <mat-cell *matCellDef="let plugin">
+          <span>{{ plugin.dependencies | commaSeparated }}</span>
+        </mat-cell>
+      </ng-container>
+
+      <ng-container *ngIf="shouldShowColumn('namespace')"
+                    matColumnDef="namespace">
+        <mat-header-cell *matHeaderCellDef
+                         i18n>Namespace</mat-header-cell>
+        <mat-cell *matCellDef="let plugin">{{plugin.objectMeta.namespace}}</mat-cell>
+      </ng-container>
+
+      <ng-container matColumnDef="age">
+        <mat-header-cell *matHeaderCellDef
+                         mat-sort-header
+                         disableClear="true"
+                         i18n>Age</mat-header-cell>
+        <mat-cell *matCellDef="let plugin">
+          <kd-date [date]="plugin.objectMeta.creationTimestamp"
+                   relative></kd-date>
+        </mat-cell>
+      </ng-container>
+
+      <ng-container *ngFor="let col of getActionColumns()"
+                    [matColumnDef]="col.name">
+        <mat-header-cell *matHeaderCellDef></mat-header-cell>
+        <mat-cell *matCellDef="let plugin">
+          <kd-dynamic-cell [component]="col.component"
+                           [resource]="plugin"></kd-dynamic-cell>
+        </mat-cell>
+      </ng-container>
+
+      <mat-header-row *matHeaderRowDef="getColumns()"></mat-header-row>
+      <mat-row *matRowDef="let row; columns: getColumns();"></mat-row>
+    </mat-table>
+
+    <mat-paginator [length]="totalItems"
+                   [pageSize]="itemsPerPage"
+                   [hidePageSize]="true"
+                   [showFirstLastButtons]="true"></mat-paginator>
+  </div>
+
+  <div content
+       [hidden]="!showZeroState()">
+    <kd-list-zero-state></kd-list-zero-state>
+  </div>
+</kd-card>
diff --git a/src/app/frontend/common/services/global/module.ts b/src/app/frontend/common/services/global/module.ts
index c300592b3..e31486e52 100644
--- a/src/app/frontend/common/services/global/module.ts
+++ b/src/app/frontend/common/services/global/module.ts
@@ -33,6 +33,9 @@ import {KdStateService} from './state';
 import {ThemeService} from './theme';
 import {TitleService} from './title';
 import {VerberService} from './verber';
+import {PluginsConfigService} from './plugin';
+import {PluginLoaderService} from '../pluginloader/pluginloader.service';
+import {ClientPluginLoaderService} from '../pluginloader/clientloader.service';
 import {PinnerService} from './pinner';
 
 @NgModule({
@@ -42,6 +45,7 @@ import {PinnerService} from './pinner';
     LocalSettingsService,
     GlobalSettingsService,
     ConfigService,
+    PluginsConfigService,
     TitleService,
     AuthService,
     CsrfTokenService,
@@ -61,9 +65,10 @@ import {PinnerService} from './pinner';
       deps: [
         GlobalSettingsService,
         LocalSettingsService,
-        PinnerService,
         ConfigService,
         HistoryService,
+        PluginsConfigService,
+        PinnerService,
       ],
       multi: true,
     },
@@ -72,6 +77,7 @@ import {PinnerService} from './pinner';
       useClass: AuthInterceptor,
       multi: true,
     },
+    {provide: PluginLoaderService, useClass: ClientPluginLoaderService},
   ],
 })
 export class GlobalServicesModule {
@@ -87,10 +93,12 @@ export function init(
   pinner: PinnerService,
   config: ConfigService,
   history: HistoryService,
+  pluginsConfig: PluginsConfigService,
 ): Function {
   return () => {
     globalSettings.init();
     localSettings.init();
+    pluginsConfig.init();
     pinner.init();
     config.init();
     history.init();
diff --git a/src/app/frontend/common/services/global/plugin.ts b/src/app/frontend/common/services/global/plugin.ts
new file mode 100644
index 000000000..94465c782
--- /dev/null
+++ b/src/app/frontend/common/services/global/plugin.ts
@@ -0,0 +1,52 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {HttpClient} from '@angular/common/http';
+import {Injectable} from '@angular/core';
+import {PluginMetadata, PluginsConfig} from '@api/frontendapi';
+import {Observable} from 'rxjs/Observable';
+
+@Injectable()
+export class PluginsConfigService {
+  private readonly pluginConfigPath_ = 'api/v1/plugin/config';
+  private config_: PluginsConfig = {status: 204, plugins: [], errors: []};
+
+  constructor(private readonly http: HttpClient) {}
+
+  init(): void {
+    this.fetchConfig();
+  }
+
+  refreshConfig(): void {
+    this.fetchConfig();
+  }
+
+  private fetchConfig(): void {
+    this.getConfig()
+      .toPromise()
+      .then(config => (this.config_ = config));
+  }
+
+  private getConfig(): Observable<PluginsConfig> {
+    return this.http.get<PluginsConfig>(this.pluginConfigPath_);
+  }
+
+  pluginsMetadata(): PluginMetadata[] {
+    return this.config_.plugins;
+  }
+
+  status(): number {
+    return this.config_.status;
+  }
+}
diff --git a/src/app/frontend/common/services/pluginloader/clientloader.service.ts b/src/app/frontend/common/services/pluginloader/clientloader.service.ts
new file mode 100644
index 000000000..398467ac2
--- /dev/null
+++ b/src/app/frontend/common/services/pluginloader/clientloader.service.ts
@@ -0,0 +1,60 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {Injectable, NgModuleFactory} from '@angular/core';
+
+import {PLUGIN_EXTERNALS_MAP} from './pluginexternals';
+import {PluginLoaderService} from './pluginloader.service';
+import {PluginsConfigService} from '../global/plugin';
+
+const systemJS = window.System;
+
+@Injectable()
+export class ClientPluginLoaderService extends PluginLoaderService {
+  constructor(private pluginsConfigService_: PluginsConfigService) {
+    super();
+  }
+
+  provideExternals() {
+    Object.keys(PLUGIN_EXTERNALS_MAP).forEach(externalKey =>
+      window.define(externalKey, [], () => {
+        // @ts-ignore
+        return PLUGIN_EXTERNALS_MAP[externalKey];
+      }),
+    );
+  }
+
+  load<T>(pluginName: string): Promise<NgModuleFactory<T>> {
+    const plugins = this.pluginsConfigService_.pluginsMetadata();
+    const plugin = plugins.find(p => p.name === pluginName);
+    if (!plugin) {
+      throw Error(`Can't find plugin "${pluginName}"`);
+    }
+
+    const depsPromises = (plugin.dependencies || []).map(dep => {
+      const dependency = plugins.find(d => d.name === dep);
+      if (!dependency) {
+        throw Error(`Can't find dependency "${dep}" for plugin "${pluginName}"`);
+      }
+
+      return systemJS.import(dependency.path).then(m => {
+        window['define'](dep, [], () => m.default);
+      });
+    });
+
+    return Promise.all(depsPromises).then(() => {
+      return systemJS.import(plugin.path).then(module => module.default.default);
+    });
+  }
+}
diff --git a/src/app/frontend/common/services/pluginloader/pluginexternals.ts b/src/app/frontend/common/services/pluginloader/pluginexternals.ts
new file mode 100644
index 000000000..70661e1bb
--- /dev/null
+++ b/src/app/frontend/common/services/pluginloader/pluginexternals.ts
@@ -0,0 +1,29 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 * as common from '@angular/common';
+import * as core from '@angular/core';
+import * as forms from '@angular/forms';
+import * as router from '@angular/router';
+import * as rxjs from 'rxjs';
+import * as tslib from 'tslib';
+
+export const PLUGIN_EXTERNALS_MAP = {
+  'ng.core': core,
+  'ng.common': common,
+  'ng.forms': forms,
+  'ng.router': router,
+  rxjs,
+  tslib,
+};
diff --git a/src/app/frontend/common/services/pluginloader/pluginloader.service.ts b/src/app/frontend/common/services/pluginloader/pluginloader.service.ts
new file mode 100644
index 000000000..2e27587cc
--- /dev/null
+++ b/src/app/frontend/common/services/pluginloader/pluginloader.service.ts
@@ -0,0 +1,26 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {Injectable, NgModuleFactory} from '@angular/core';
+
+@Injectable()
+export abstract class PluginLoaderService {
+  protected constructor() {
+    this.provideExternals();
+  }
+
+  abstract provideExternals(): void;
+
+  abstract load<T>(pluginName: string): Promise<NgModuleFactory<T>>;
+}
diff --git a/src/app/frontend/common/services/resource/endpoint.ts b/src/app/frontend/common/services/resource/endpoint.ts
index 2ef11aced..0f39e4dc1 100644
--- a/src/app/frontend/common/services/resource/endpoint.ts
+++ b/src/app/frontend/common/services/resource/endpoint.ts
@@ -40,6 +40,7 @@ export enum Resource {
   service = 'service',
   event = 'event',
   container = 'container',
+  plugin = 'plugin',
 }
 
 export enum Utility {
diff --git a/src/app/frontend/login/component.spec.ts b/src/app/frontend/login/component.spec.ts
index 35c12e5c7..0e9f90626 100644
--- a/src/app/frontend/login/component.spec.ts
+++ b/src/app/frontend/login/component.spec.ts
@@ -26,6 +26,8 @@ import {K8SError, KdError} from 'common/errors/errors';
 import {AuthService} from 'common/services/global/authentication';
 import {from, Observable, of} from 'rxjs';
 import {LoginComponent} from './component';
+import {PluginsConfigService} from '../common/services/global/plugin';
+import {PluginMetadata} from '@api/frontendapi';
 
 const queries = {
   submitButton: '.kd-login-button[type="submit"]',
@@ -67,6 +69,17 @@ class MockRouter {
   navigate(): void {}
 }
 
+class MockPluginsConfigService {
+  init(): void {}
+  refreshConfig(): void {}
+  static pluginsMetadata(): PluginMetadata[] {
+    return [];
+  }
+  static status(): number {
+    return 200;
+  }
+}
+
 describe('LoginComponent', () => {
   let component: LoginComponent;
   let fixture: ComponentFixture<LoginComponent>;
@@ -102,6 +115,10 @@ describe('LoginComponent', () => {
           provide: Router,
           useClass: MockRouter,
         },
+        {
+          provide: PluginsConfigService,
+          useClass: MockPluginsConfigService,
+        },
       ],
     }).compileComponents();
   }));
diff --git a/src/app/frontend/login/component.ts b/src/app/frontend/login/component.ts
index 0a13abd61..27db06dda 100644
--- a/src/app/frontend/login/component.ts
+++ b/src/app/frontend/login/component.ts
@@ -26,6 +26,7 @@ import {map} from 'rxjs/operators';
 
 import {AsKdError, K8SError} from '../common/errors/errors';
 import {AuthService} from '../common/services/global/authentication';
+import {PluginsConfigService} from '../common/services/global/plugin';
 
 enum LoginModes {
   Kubeconfig = 'kubeconfig',
@@ -56,6 +57,7 @@ export class LoginComponent implements OnInit {
     private readonly http_: HttpClient,
     private readonly ngZone_: NgZone,
     private readonly route_: ActivatedRoute,
+    private readonly pluginConfigService_: PluginsConfigService,
   ) {}
 
   ngOnInit(): void {
@@ -100,6 +102,7 @@ export class LoginComponent implements OnInit {
           return;
         }
 
+        this.pluginConfigService_.refreshConfig();
         this.ngZone_.run(() => {
           this.state_.navigate(['overview']);
         });
diff --git a/src/app/frontend/plugin/component.ts b/src/app/frontend/plugin/component.ts
new file mode 100644
index 000000000..97c4401a2
--- /dev/null
+++ b/src/app/frontend/plugin/component.ts
@@ -0,0 +1,21 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {Component} from '@angular/core';
+
+@Component({
+  selector: 'kd-plugin',
+  templateUrl: './template.html',
+})
+export class PluginComponent {}
diff --git a/src/app/frontend/plugin/detail/component.ts b/src/app/frontend/plugin/detail/component.ts
new file mode 100644
index 000000000..573ebf3a5
--- /dev/null
+++ b/src/app/frontend/plugin/detail/component.ts
@@ -0,0 +1,30 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {Component} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+
+@Component({
+  selector: 'kd-plugin-detail',
+  template: `
+    <kd-plugin-holder [pluginName]="this.pluginName()"></kd-plugin-holder>
+  `,
+})
+export class PluginDetailComponent {
+  constructor(private readonly activatedRoute_: ActivatedRoute) {}
+
+  pluginName(): string {
+    return this.activatedRoute_.snapshot.params.pluginName;
+  }
+}
diff --git a/src/app/frontend/plugin/holder.ts b/src/app/frontend/plugin/holder.ts
new file mode 100644
index 000000000..9bc81247a
--- /dev/null
+++ b/src/app/frontend/plugin/holder.ts
@@ -0,0 +1,60 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {Component, Injector, Input, OnInit, ViewChild, ViewContainerRef} from '@angular/core';
+
+import {PluginLoaderService} from '../common/services/pluginloader/pluginloader.service';
+
+@Component({
+  selector: 'kd-plugin-holder',
+  template: `
+    <div>
+      <div class="plugin">
+        <mat-card *ngIf="entryError">This plugin has no entry component</mat-card>
+        <ng-template #pluginViewRef #elseBlock></ng-template>
+      </div>
+    </div>
+  `,
+})
+export class PluginHolderComponent implements OnInit {
+  @ViewChild('pluginViewRef', {read: ViewContainerRef, static: true}) vcRef: ViewContainerRef;
+  @Input('pluginName') private pluginName: string;
+  entryError = false;
+
+  constructor(private injector: Injector, private pluginLoader: PluginLoaderService) {}
+
+  ngOnInit() {
+    try {
+      this.loadPlugin(this.pluginName);
+    } catch (e) {
+      console.log(e);
+    }
+  }
+
+  loadPlugin(pluginName: string) {
+    this.pluginLoader.load(pluginName).then(moduleFactory => {
+      const moduleRef = moduleFactory.create(this.injector);
+      // tslint:disable-next-line:no-any
+      const entryComponent = (moduleFactory.moduleType as any).entry;
+      try {
+        const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(
+          entryComponent,
+        );
+        this.vcRef.createComponent(compFactory);
+      } catch (e) {
+        this.entryError = true;
+      }
+    });
+  }
+}
diff --git a/src/app/frontend/plugin/list/component.ts b/src/app/frontend/plugin/list/component.ts
new file mode 100644
index 000000000..12165c5a3
--- /dev/null
+++ b/src/app/frontend/plugin/list/component.ts
@@ -0,0 +1,21 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {Component} from '@angular/core';
+
+@Component({
+  selector: 'kd-plugins',
+  template: '<kd-plugin-list></kd-plugin-list>',
+})
+export class PluginListComponent {}
diff --git a/src/app/frontend/plugin/module.ts b/src/app/frontend/plugin/module.ts
new file mode 100644
index 000000000..a7d687386
--- /dev/null
+++ b/src/app/frontend/plugin/module.ts
@@ -0,0 +1,33 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {NgModule} from '@angular/core';
+import {ComponentsModule} from '../common/components/module';
+import {SharedModule} from '../shared.module';
+import {PluginHolderComponent} from './holder';
+import {PluginListComponent} from './list/component';
+import {PluginsRoutingModule} from './routing';
+import {PluginComponent} from './component';
+import {PluginDetailComponent} from './detail/component';
+
+@NgModule({
+  imports: [SharedModule, ComponentsModule, PluginsRoutingModule],
+  declarations: [
+    PluginListComponent,
+    PluginDetailComponent,
+    PluginHolderComponent,
+    PluginComponent,
+  ],
+})
+export class PluginModule {}
diff --git a/src/app/frontend/plugin/routing.ts b/src/app/frontend/plugin/routing.ts
new file mode 100644
index 000000000..457f2421d
--- /dev/null
+++ b/src/app/frontend/plugin/routing.ts
@@ -0,0 +1,41 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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 {NgModule} from '@angular/core';
+import {Route, RouterModule} from '@angular/router';
+import {PluginListComponent} from './list/component';
+import {PluginDetailComponent} from './detail/component';
+
+export const PLUGIN_LIST_ROUTE: Route = {
+  path: '',
+  component: PluginListComponent,
+  data: {
+    breadcrumb: 'Plugins',
+  },
+};
+
+export const PLUGIN_DETAIL_ROUTE: Route = {
+  path: ':pluginNamespace/:pluginName',
+  component: PluginDetailComponent,
+  data: {
+    breadcrumb: '{{ pluginName }}',
+    parent: PLUGIN_LIST_ROUTE,
+  },
+};
+
+@NgModule({
+  imports: [RouterModule.forChild([PLUGIN_LIST_ROUTE, PLUGIN_DETAIL_ROUTE])],
+  exports: [RouterModule],
+})
+export class PluginsRoutingModule {}
diff --git a/src/app/frontend/plugin/template.html b/src/app/frontend/plugin/template.html
new file mode 100644
index 000000000..15fbb38ed
--- /dev/null
+++ b/src/app/frontend/plugin/template.html
@@ -0,0 +1,19 @@
+<!--
+Copyright 2017 The Kubernetes Authors.
+
+Licensed 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
+
+    http://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.
+-->
+
+<div class="kd-plugin">
+  <router-outlet name="pluginView"></router-outlet>
+</div>
diff --git a/src/app/frontend/systemjs.d.ts b/src/app/frontend/systemjs.d.ts
new file mode 100644
index 000000000..6787ca62d
--- /dev/null
+++ b/src/app/frontend/systemjs.d.ts
@@ -0,0 +1,24 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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.
+
+interface Window {
+  // tslint:disable-next-line:no-any
+  define: (name: string, deps: string[], definitionFn: () => any) => void;
+
+  System: {
+    // @ts-ignore
+    // tslint:disable-next-line:no-any
+    import: (path) => Promise<any>;
+  };
+}
diff --git a/src/app/frontend/typings/backendapi.d.ts b/src/app/frontend/typings/backendapi.d.ts
index 020863864..0fa291af0 100644
--- a/src/app/frontend/typings/backendapi.d.ts
+++ b/src/app/frontend/typings/backendapi.d.ts
@@ -1128,3 +1128,13 @@ export interface SJSCloseEvent extends SockJSSimpleEvent {
 export interface SJSMessageEvent extends SockJSSimpleEvent {
   data: string;
 }
+
+export interface Plugin extends Resource {
+  name: string;
+  path: string;
+  dependencies: string[];
+}
+
+export interface PluginList extends ResourceList {
+  items?: Plugin[];
+}
diff --git a/src/app/frontend/typings/frontendapi.ts b/src/app/frontend/typings/frontendapi.ts
index 39c17f033..7d5b37ecd 100644
--- a/src/app/frontend/typings/frontendapi.ts
+++ b/src/app/frontend/typings/frontendapi.ts
@@ -121,3 +121,15 @@ export interface ResourcesRatio {
 export interface StateError {
   error: KdError;
 }
+
+export interface PluginMetadata {
+  name: string;
+  path: string;
+  dependencies: string[];
+}
+
+export interface PluginsConfig {
+  status: number;
+  plugins: PluginMetadata[];
+  errors?: object[];
+}


Test patch:
diff --git a/aio/test-resources/plugin-crd.yml b/aio/test-resources/plugin-crd.yml
index 86204c7c3..cba1ce43f 100644
--- a/aio/test-resources/plugin-crd.yml
+++ b/aio/test-resources/plugin-crd.yml
@@ -35,6 +35,10 @@ spec:
               required:
                 - configMapRef
                 - filename
+            dependencies:
+              type: array
+              items:
+                type: string
           required:
             - source
       required:
diff --git a/aio/test-resources/plugin-test.yml b/aio/test-resources/plugin-test.yml
index b7f6975b9..58106b562 100644
--- a/aio/test-resources/plugin-test.yml
+++ b/aio/test-resources/plugin-test.yml
@@ -12,14 +12,28 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# A manifest that creates a plugin resource.
+# A manifest that creates test plugin resources.
 
 apiVersion: dashboard.k8s.io/v1alpha1
 kind: Plugin
 metadata:
-  name: plugin-test
+  name: k8s-plugin
 spec:
   source:
     configMapRef:
-      name: plugin-test-src
+      name: k8s-plugin-src
+    filename: k8s-plugin.js
+
+---
+
+apiVersion: dashboard.k8s.io/v1alpha1
+kind: Plugin
+metadata:
+  name: plugin1
+spec:
+  source:
+    configMapRef:
+      name: plugin1-src
     filename: plugin1.js
+  dependencies:
+    - "k8s-plugin"
diff --git a/src/app/backend/auth/manager_test.go b/src/app/backend/auth/manager_test.go
index f5d9a30ab..729e5bc58 100644
--- a/src/app/backend/auth/manager_test.go
+++ b/src/app/backend/auth/manager_test.go
@@ -64,6 +64,10 @@ func (self *fakeClientManager) InsecureAPIExtensionsClient() apiextensionsclient
 	return nil
 }
 
+func (self *fakeClientManager) InsecurePluginClient() pluginclientset.Interface {
+	return nil
+}
+
 func (self *fakeClientManager) SetTokenManager(manager authApi.TokenManager) {}
 
 func (self *fakeClientManager) Config(req *restful.Request) (*rest.Config, error) {
@@ -83,8 +87,7 @@ func (self *fakeClientManager) HasAccess(authInfo api.AuthInfo) error {
 }
 
 func (self *fakeClientManager) VerberClient(req *restful.Request, config *rest.Config) (clientapi.ResourceVerber, error) {
-	return client.NewResourceVerber(nil, nil, nil, nil, nil,
-		nil, nil, nil, nil, nil), nil
+	return client.NewResourceVerber(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil), nil
 }
 
 func (self *fakeClientManager) CanI(req *restful.Request, ssar *v1.SelfSubjectAccessReview) bool {
diff --git a/src/app/backend/plugin/config_test.go b/src/app/backend/plugin/config_test.go
new file mode 100644
index 000000000..138085974
--- /dev/null
+++ b/src/app/backend/plugin/config_test.go
@@ -0,0 +1,131 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed 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
+//
+//     http://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.
+
+package plugin
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+
+	"github.com/emicklei/go-restful"
+	authApi "github.com/kubernetes/dashboard/src/app/backend/auth/api"
+	clientapi "github.com/kubernetes/dashboard/src/app/backend/client/api"
+	"github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
+	fakePluginClientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/fake"
+	v1 "k8s.io/api/authorization/v1"
+	coreV1 "k8s.io/api/core/v1"
+	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
+	metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	fakeK8sClient "k8s.io/client-go/kubernetes/fake"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/clientcmd/api"
+)
+
+func Test_handleConfig(t *testing.T) {
+	ns := "default"
+	pluginName := "test-plugin"
+	filename := "plugin-test.js"
+	cfgMapName := "plugin-test-cfgMap"
+	h := Handler{&fakeClientManager{}}
+
+	pcs, _ := h.cManager.PluginClient(nil)
+	_, _ = pcs.DashboardV1alpha1().Plugins(ns).Create(&v1alpha1.Plugin{
+		ObjectMeta: metaV1.ObjectMeta{Name: pluginName, Namespace: ns},
+		Spec: v1alpha1.PluginSpec{
+			Source: v1alpha1.Source{
+				ConfigMapRef: &coreV1.ConfigMapEnvSource{
+					LocalObjectReference: coreV1.LocalObjectReference{Name: cfgMapName},
+				},
+				Filename: filename}},
+	})
+
+	httpReq, _ := http.NewRequest(http.MethodGet, "/api/v1/plugin/config", nil)
+	req := restful.NewRequest(httpReq)
+
+	httpWriter := httptest.NewRecorder()
+	resp := restful.NewResponse(httpWriter)
+
+	h.handleConfig(req, resp)
+}
+
+type fakeClientManager struct {
+	k8sClient    kubernetes.Interface
+	pluginClient versioned.Interface
+}
+
+func (cm *fakeClientManager) Client(req *restful.Request) (kubernetes.Interface, error) {
+	panic("implement me")
+}
+
+func (cm *fakeClientManager) InsecureClient() kubernetes.Interface {
+	if cm.k8sClient == nil {
+		cm.k8sClient = fakeK8sClient.NewSimpleClientset()
+	}
+	return cm.k8sClient
+}
+
+func (cm *fakeClientManager) APIExtensionsClient(req *restful.Request) (clientset.Interface, error) {
+	panic("implement me")
+}
+
+func (cm *fakeClientManager) PluginClient(req *restful.Request) (versioned.Interface, error) {
+	if cm.pluginClient == nil {
+		cm.pluginClient = fakePluginClientset.NewSimpleClientset()
+	}
+	return cm.pluginClient, nil
+}
+
+func (cm *fakeClientManager) InsecureAPIExtensionsClient() clientset.Interface {
+	panic("implement me")
+}
+
+func (cm *fakeClientManager) InsecurePluginClient() versioned.Interface {
+	if cm.pluginClient == nil {
+		cm.pluginClient = fakePluginClientset.NewSimpleClientset()
+	}
+	return cm.pluginClient
+}
+
+func (cm *fakeClientManager) CanI(req *restful.Request, ssar *v1.SelfSubjectAccessReview) bool {
+	panic("implement me")
+}
+
+func (cm *fakeClientManager) Config(req *restful.Request) (*rest.Config, error) {
+	panic("implement me")
+}
+
+func (cm *fakeClientManager) ClientCmdConfig(req *restful.Request) (clientcmd.ClientConfig, error) {
+	panic("implement me")
+}
+
+func (cm *fakeClientManager) CSRFKey() string {
+	panic("implement me")
+}
+
+func (cm *fakeClientManager) HasAccess(authInfo api.AuthInfo) error {
+	panic("implement me")
+}
+
+func (cm *fakeClientManager) VerberClient(req *restful.Request, config *rest.Config) (clientapi.ResourceVerber, error) {
+	panic("implement me")
+}
+
+func (cm *fakeClientManager) SetTokenManager(manager authApi.TokenManager) {
+	panic("implement me")
+}
diff --git a/src/app/backend/plugin/detail_test.go b/src/app/backend/plugin/detail_test.go
index eec149a27..6a479ecb0 100644
--- a/src/app/backend/plugin/detail_test.go
+++ b/src/app/backend/plugin/detail_test.go
@@ -16,8 +16,12 @@ package plugin
 
 import (
 	"bytes"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 
+	"github.com/emicklei/go-restful"
+
 	"github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
 	fakePluginClientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/fake"
 	coreV1 "k8s.io/api/core/v1"
@@ -71,3 +75,30 @@ func TestGetPluginSource(t *testing.T) {
 		t.Error("bytes in configMap and bytes from GetPluginSource are different")
 	}
 }
+
+func Test_servePluginSource(t *testing.T) {
+	ns := "default"
+	pluginName := "test-plugin"
+	filename := "plugin-test.js"
+	cfgMapName := "plugin-test-cfgMap"
+	h := Handler{&fakeClientManager{}}
+
+	pcs, _ := h.cManager.PluginClient(nil)
+	_, _ = pcs.DashboardV1alpha1().Plugins(ns).Create(&v1alpha1.Plugin{
+		ObjectMeta: v1.ObjectMeta{Name: pluginName, Namespace: ns},
+		Spec: v1alpha1.PluginSpec{
+			Source: v1alpha1.Source{
+				ConfigMapRef: &coreV1.ConfigMapEnvSource{
+					LocalObjectReference: coreV1.LocalObjectReference{Name: cfgMapName},
+				},
+				Filename: filename}},
+	})
+
+	httpReq, _ := http.NewRequest(http.MethodGet, "/api/v1/plugin/default/test-plugin", nil)
+	req := restful.NewRequest(httpReq)
+
+	httpWriter := httptest.NewRecorder()
+	resp := restful.NewResponse(httpWriter)
+
+	h.servePluginSource(req, resp)
+}
diff --git a/src/app/backend/plugin/list_test.go b/src/app/backend/plugin/list_test.go
index 5b858e3e7..a73f4b602 100644
--- a/src/app/backend/plugin/list_test.go
+++ b/src/app/backend/plugin/list_test.go
@@ -15,8 +15,14 @@
 package plugin
 
 import (
+	"net/http"
+	"net/http/httptest"
 	"testing"
 
+	"github.com/emicklei/go-restful"
+
+	"github.com/kubernetes/dashboard/src/app/backend/resource/dataselect"
+
 	"github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
 	fakePluginClientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/fake"
 	coreV1 "k8s.io/api/core/v1"
@@ -41,7 +47,16 @@ func TestGetPluginList(t *testing.T) {
 				Filename: filename}},
 	})
 
-	data, err := GetPluginList(pcs, ns)
+	dsQuery := dataselect.DataSelectQuery{
+		PaginationQuery: &dataselect.PaginationQuery{
+			ItemsPerPage: 10,
+			Page:         1,
+		},
+		SortQuery:   dataselect.NoSort,
+		FilterQuery: dataselect.NoFilter,
+		MetricQuery: dataselect.NoMetrics,
+	}
+	data, err := GetPluginList(pcs, ns, &dsQuery)
 	if err != nil {
 		t.Errorf("error while fetching plugins: %s", err)
 	}
@@ -50,3 +65,30 @@ func TestGetPluginList(t *testing.T) {
 		t.Errorf("there should be one plugin registered, got %d", data.ListMeta.TotalItems)
 	}
 }
+
+func Test_handlePluginList(t *testing.T) {
+	ns := "default"
+	pluginName := "test-plugin"
+	filename := "plugin-test.js"
+	cfgMapName := "plugin-test-cfgMap"
+	h := Handler{&fakeClientManager{}}
+
+	pcs, _ := h.cManager.PluginClient(nil)
+	_, _ = pcs.DashboardV1alpha1().Plugins(ns).Create(&v1alpha1.Plugin{
+		ObjectMeta: v1.ObjectMeta{Name: pluginName, Namespace: ns},
+		Spec: v1alpha1.PluginSpec{
+			Source: v1alpha1.Source{
+				ConfigMapRef: &coreV1.ConfigMapEnvSource{
+					LocalObjectReference: coreV1.LocalObjectReference{Name: cfgMapName},
+				},
+				Filename: filename}},
+	})
+
+	httpReq, _ := http.NewRequest(http.MethodGet, "/api/v1/plugin/default?itemsPerPage=10&page=1&sortBy=d,creationTimestamp", nil)
+	req := restful.NewRequest(httpReq)
+
+	httpWriter := httptest.NewRecorder()
+	resp := restful.NewResponse(httpWriter)
+
+	h.handlePluginList(req, resp)
+}