Compare commits

...

5 Commits

Author SHA1 Message Date
deviantony
37704d400b poc(search): doco/comments 2021-10-25 19:57:12 -04:00
deviantony
f4de5f279a poc(search): doco/comments 2021-10-25 19:49:03 -04:00
deviantony
d03c66ff12 Merge branch 'develop' into poc-searchbar 2021-10-25 19:36:31 -04:00
deviantony
6bd4370ab1 poc(search): add missing files 2021-10-25 19:35:16 -04:00
deviantony
83102b9178 poc(search): POC for a global resource searchbar 2021-10-25 19:33:03 -04:00
18 changed files with 812 additions and 9 deletions

View File

@@ -23,6 +23,7 @@ import (
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
"github.com/portainer/portainer/api/http/handler/roles"
"github.com/portainer/portainer/api/http/handler/search"
"github.com/portainer/portainer/api/http/handler/settings"
"github.com/portainer/portainer/api/http/handler/ssl"
"github.com/portainer/portainer/api/http/handler/stacks"
@@ -59,6 +60,7 @@ type Handler struct {
RegistryHandler *registries.Handler
ResourceControlHandler *resourcecontrols.Handler
RoleHandler *roles.Handler
SearchHandler *search.Handler
SettingsHandler *settings.Handler
SSLHandler *ssl.Handler
StackHandler *stacks.Handler
@@ -201,6 +203,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.ResourceControlHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/roles"):
http.StripPrefix("/api", h.RoleHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/search"):
http.StripPrefix("/api", h.SearchHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/settings"):
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/stacks"):

View File

@@ -0,0 +1,26 @@
package search
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle tag operations.
type Handler struct {
*mux.Router
DataStore portainer.DataStore
}
// NewHandler creates a handler to manage search operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/search",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.search))).Methods(http.MethodGet)
return h
}

View File

@@ -0,0 +1,315 @@
package search
import (
"net/http"
"strings"
"fmt"
"github.com/portainer/portainer/api"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
)
type searchResponse struct {
Results []searchResult `json:"Results"`
ResultCount int `json:"ResultCount"`
}
type searchResult struct {
Label string `json:"Label"`
ResultType string `json:"Type"`
Environment string `json:"Environment"`
EnvironmentType string `json:"EnvironmentType"`
ResourceID string `json:"ResourceID"`
EnvironmentID int `json:"EnvironmentID"`
}
// @id Search
// @summary Search for resources
// @description Search for any resource inside this Portainer instance (through snapshots).
// @description **Access policy**: authenticated
// @tags search
// @security jwt
// @produce json
// @param query query string true "Query used to search and filter resources"
// @success 200 {object} searchResponse "Success"
// @failure 400 "Bad request"
// @failure 500 "Server error"
// @router /search [get]
func (handler *Handler) search(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
query, err := request.RetrieveQueryParameter(r, "query", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: query", err}
}
query = strings.ToLower(query)
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
}
results := make([]searchResult, 0)
for _, endpoint := range endpoints {
if match(endpoint.Name, query) {
result := searchResult{
Label: endpoint.Name,
ResultType: "ENVIRONMENT",
EnvironmentType: envTypeFromEndpoint(&endpoint),
EnvironmentID: int(endpoint.ID),
}
results = append(results, result)
}
if len(endpoint.Snapshots) > 0 {
containers, _ := containerMatch(&endpoint, query)
results = append(results, containers...)
images, _ := imageMatch(&endpoint, query)
results = append(results, images...)
networks, _ := networkMatch(&endpoint, query)
results = append(results, networks...)
volumes, _ := volumeMatch(&endpoint, query)
results = append(results, volumes...)
}
}
searchResponse := searchResponse{
Results: results,
ResultCount: len(results),
}
return response.JSON(w, searchResponse)
}
func volumeMatch(endpoint *portainer.Endpoint, query string) ([]searchResult, error) {
results := make([]searchResult, 0)
volumeRoot, ok := endpoint.Snapshots[0].SnapshotRaw.Volumes.(map[string]interface{})
if !ok {
fmt.Println("Unable to retrieve volume data from snapshot")
return results, nil
}
if volumeRoot["Volumes"] == nil {
fmt.Println("Unable to retrieve volume data from snapshot")
return results, nil
}
volumes, ok := volumeRoot["Volumes"].([]interface{})
if !ok {
fmt.Println("Unable to retrieve volume data from snapshot")
return results, nil
}
for _, volume := range volumes {
volumeObject, ok := volume.(map[string]interface{})
if !ok {
fmt.Println("Unable to retrieve volume data from volumes snapshot")
continue
}
name := volumeObject["Name"]
if name == nil {
continue
}
volumeName := name.(string)
if match(volumeName, query) {
result := searchResult{
Label: volumeName,
ResultType: "VOLUME",
Environment: endpoint.Name,
EnvironmentType: envTypeFromEndpoint(endpoint),
EnvironmentID: int(endpoint.ID),
ResourceID: volumeName,
}
results = append(results, result)
}
}
return results, nil
}
func networkMatch(endpoint *portainer.Endpoint, query string) ([]searchResult, error) {
results := make([]searchResult, 0)
networks, ok := endpoint.Snapshots[0].SnapshotRaw.Networks.([]interface{})
if !ok {
fmt.Println("Unable to retrieve networks data from snapshot")
return results, nil
}
for _, network := range networks {
networkObject, ok := network.(map[string]interface{})
if !ok {
fmt.Println("Unable to retrieve network data from networks snapshot")
continue
}
name := networkObject["Name"]
if name == nil {
continue
}
id := networkObject["Id"]
if id == nil {
continue
}
networkName := name.(string)
networkId := id.(string)
if match(networkName, query) {
result := searchResult{
Label: networkName,
ResultType: "NETWORK",
Environment: endpoint.Name,
EnvironmentType: envTypeFromEndpoint(endpoint),
EnvironmentID: int(endpoint.ID),
ResourceID: networkId,
}
results = append(results, result)
}
}
return results, nil
}
func imageMatch(endpoint *portainer.Endpoint, query string) ([]searchResult, error) {
results := make([]searchResult, 0)
images, ok := endpoint.Snapshots[0].SnapshotRaw.Images.([]interface{})
if !ok {
fmt.Println("Unable to retrieve images data from snapshot")
return results, nil
}
for _, image := range images {
imgObject, ok := image.(map[string]interface{})
if !ok {
fmt.Println("Unable to retrieve image data from images snapshot")
continue
}
repoTags := imgObject["RepoTags"]
if repoTags == nil {
continue
}
id := imgObject["Id"]
if id == nil {
continue
}
imageId := id.(string)
repoTagsArray := repoTags.([]interface{})
if len(repoTagsArray) > 0 {
for _, tag := range repoTagsArray {
tagName := tag.(string)
if match(tagName, query) {
result := searchResult{
Label: tagName,
ResultType: "IMAGE",
Environment: endpoint.Name,
EnvironmentType: envTypeFromEndpoint(endpoint),
EnvironmentID: int(endpoint.ID),
ResourceID: imageId,
}
results = append(results, result)
}
}
}
}
return results, nil
}
func containerMatch(endpoint *portainer.Endpoint, query string) ([]searchResult, error) {
results := make([]searchResult, 0)
containers, ok := endpoint.Snapshots[0].SnapshotRaw.Containers.([]interface{})
if !ok {
fmt.Println("Unable to retrieve containers data from snapshot")
return results, nil
}
for _, container := range containers {
cntrObject, ok := container.(map[string]interface{})
if !ok {
fmt.Println("Unable to retrieve container data from containers snapshot")
continue
}
cntrNameEntry := cntrObject["Names"]
if cntrNameEntry == nil {
continue
}
id := cntrObject["Id"]
if id == nil {
continue
}
containerId := id.(string)
cntrNameArray := cntrNameEntry.([]interface{})
if len(cntrNameArray) > 0 {
containerName := cntrNameArray[0].(string)
cName := strings.TrimPrefix(containerName, "/")
if match(cName, query) {
result := searchResult{
Label: cName,
ResultType: "CONTAINER",
Environment: endpoint.Name,
EnvironmentType: envTypeFromEndpoint(endpoint),
EnvironmentID: int(endpoint.ID),
ResourceID: containerId,
}
results = append(results, result)
}
}
}
return results, nil
}
func envTypeFromEndpoint(endpoint *portainer.Endpoint) string {
switch endpoint.Type {
case 1:
return "docker"
case 2:
return "docker"
case 3:
return "azure"
case 4:
return "edge"
case 5:
return "kubernetes"
case 6:
return "kubernetes"
case 7:
return "edge"
}
return "unsupported"
}
func match(data, filter string) bool {
return strings.Contains(data, filter);
}

View File

@@ -34,6 +34,7 @@ import (
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
"github.com/portainer/portainer/api/http/handler/roles"
"github.com/portainer/portainer/api/http/handler/search"
"github.com/portainer/portainer/api/http/handler/settings"
sslhandler "github.com/portainer/portainer/api/http/handler/ssl"
"github.com/portainer/portainer/api/http/handler/stacks"
@@ -192,6 +193,9 @@ func (server *Server) Start() error {
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
resourceControlHandler.DataStore = server.DataStore
var searchHandler = search.NewHandler(requestBouncer);
searchHandler.DataStore = server.DataStore
var settingsHandler = settings.NewHandler(requestBouncer)
settingsHandler.DataStore = server.DataStore
settingsHandler.FileService = server.FileService
@@ -267,6 +271,7 @@ func (server *Server) Start() error {
MOTDHandler: motdHandler,
RegistryHandler: registryHandler,
ResourceControlHandler: resourceControlHandler,
SearchHandler: searchHandler,
SettingsHandler: settingsHandler,
SSLHandler: sslHandler,
StatusHandler: statusHandler,

View File

@@ -52,7 +52,7 @@
* Header
*/
.row.header {
height: 60px;
height: 80px;
background: var(--bg-row-header-color);
margin-bottom: 15px;
}

View File

@@ -12,6 +12,7 @@ angular
.constant('API_ENDPOINT_MOTD', 'api/motd')
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
.constant('API_ENDPOINT_SEARCH', 'api/search')
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
.constant('API_ENDPOINT_STACKS', 'api/stacks')
.constant('API_ENDPOINT_STATUS', 'api/status')

View File

@@ -23,7 +23,6 @@
ng-change="$ctrl.onTextFilterChange()"
ng-model-options="{ debounce: 300 }"
placeholder="Search by name, group, tag, status, URL..."
auto-focus
data-cy="home-endpointsSearchInput"
/>
</div>

View File

@@ -8,7 +8,7 @@ angular.module('portainer.app').directive('rdHeaderContent', [
scope.username = Authentication.getUserDetails().username;
},
template:
'<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="portainer.account" style="margin-right: 5px;"><u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u></a><a ui-sref="portainer.logout({performApiLogout: true})" class="text-danger" style="margin-right: 25px;" data-cy="template-logoutButton"><u><i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u></a></div></div>',
'<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="portainer.account" style="margin-right: 5px;"><u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u></a><a ui-sref="portainer.logout({performApiLogout: true})" class="text-danger" style="margin-right: 25px;" data-cy="template-logoutButton"><u><i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u></a></div></div><search></search>',
restrict: 'E',
};
return directive;

View File

@@ -0,0 +1,207 @@
import autocomplete from 'autocompleter';
import './search.css';
import searchUIModel from '../../search/models';
// This is a POC showcasing the ability to search for resources across environments
// It leverages snapshots to search across environment resources and exposes a new API over /api/search
// It has the following current limitations:
// * Redirecting to a resource located inside a Swarm cluster has not been validated (nodeName injection required)
// * Redirecting to a resource located inside an Edge environment has not been validated
// * Redirecting to an Aure environment has not been validated
// * Since Kubernetes environments do not use the snapshot feature, it is not possible to search across k8s resources
// * Since Azure ACI environments do not use the snapshot feature, it is not possible to search across ACI resources
// * Snapshots are also not supporting Swarm resources, it is not possible to search across Swarm services/configs/secrets
// * It is not possible to search across stacks
// * The error handling layer and debugging messages in the backend are pretty lightweight
// * It uses some CSS hacks with :before to properly display icons in HTML inputs
// * It wasn't tested/validated at scale (e.g. a lot of environments/resources)
// * Do not apply dark mode / high contrast theme
// TLDR
// * No support for ACI environments
// * No support for Kubernetes environments
// * No support to search across stacks
// * Edge/Swarm+Agent not tested likely to not work
// * Not tested at scale
// * Do not support dark mode / high contrast themes
export default class SearchController {
/* @ngInject */
constructor($state, SearchService, Notifications, EndpointProvider) {
this.$state = $state;
this.SearchService = SearchService;
this.Notifications = Notifications;
this.EndpointProvider = EndpointProvider;
this.redirectUserBaseOnSelectedResource = this.redirectUserBaseOnSelectedResource.bind(this);
this.changeAppState = this.changeAppState.bind(this);
this.search = this.search.bind(this);
}
changeAppState(envId, envType) {
if (envType === "azure") {
this.$state.go('azure.dashboard', { endpointId: envId });
return;
}
// TODO: need to properly handle Edge environments redirects
// if (envType === "edge") {}
if (envType === "kubernetes") {
this.$state.go('kubernetes.dashboard', { endpointId: envId });
return;
}
this.$state.go('docker.dashboard', { endpointId: envId });
};
redirectUserBaseOnSelectedResource(resource) {
if (resource.group === "ENVIRONMENT") {
this.changeAppState(resource.envId, resource.envType);
return;
}
this.EndpointProvider.setEndpointID(resource.envId);
switch (resource.group) {
case "CONTAINER":
this.$state.go('docker.containers.container', { endpointId: resource.envId, id: resource.resourceId });
break;
case "IMAGE":
this.$state.go('docker.images.image', { endpointId: resource.envId, id: resource.resourceId });
break;
case "VOLUME":
this.$state.go('docker.volumes.volume', { endpointId: resource.envId, id: resource.resourceId });
break;
case "NETWORK":
this.$state.go('docker.networks.network', { endpointId: resource.envId, id: resource.resourceId });
break;
}
}
search(query, updateCallback) {
this.SearchService.search(query).then(function success(data) {
if (data.ResultCount) {
const transformedData = transformAPItoUIModel(data.Results);
updateCallback(transformedData);
}
})
.catch(function error(err) {
this.Notifications.error('Failure', err, 'Unable to execute search request');
});
}
renderResource(item) {
let div = document.createElement("div");
div.className = "boxitem";
let iconSpan = document.createElement("span");
iconSpan.className = "boxitem-icon";
let iconSpanHTML = "";
switch (item.group) {
case "ENVIRONMENT":
switch (item.envType) {
case "docker":
iconSpanHTML = '<i class="boxitem-icon-ico-docker" aria-hidden="true"></i>';
break;
case "kubernetes":
iconSpanHTML = '<i class="boxitem-icon-ico-kubernetes" aria-hidden="true"></i>';
break;
case "edge":
iconSpanHTML = '<i class="boxitem-icon-ico-edge" aria-hidden="true"></i>';
break;
default:
iconSpanHTML = '<i class="boxitem-icon-ico-generic" aria-hidden="true"></i>';
}
break;
case "CONTAINER":
iconSpanHTML = '<i class="boxitem-icon-ico-container" aria-hidden="true"></i>';
break;
case "IMAGE":
iconSpanHTML = '<i class="boxitem-icon-ico-image" aria-hidden="true"></i>';
break;
case "NETWORK":
iconSpanHTML = '<i class="boxitem-icon-ico-network" aria-hidden="true"></i>';
break;
case "VOLUME":
iconSpanHTML = '<i class="boxitem-icon-ico-volume" aria-hidden="true"></i>';
break;
default:
iconSpanHTML = '<i class="boxitem-icon-ico-generic" aria-hidden="true"></i>';
}
iconSpan.innerHTML = iconSpanHTML;
div.appendChild(iconSpan);
let nameSpan = document.createElement("span");
nameSpan.className = "boxitem-name";
nameSpan.textContent = item.label;
div.appendChild(nameSpan);
let endpointSpan = document.createElement("span");
endpointSpan.className = "boxitem-env";
let endpointSpanIcon = document.createElement("span");
let endpointIconSpanHTML = "";
if (item.group != "ENVIRONMENT") {
switch (item.envType) {
case "docker":
endpointIconSpanHTML = '<i class="boxitem-icon-ico-docker" aria-hidden="true"></i>';
break;
case "kubernetes":
endpointIconSpanHTML = '<i class="boxitem-icon-ico-kubernetes" aria-hidden="true"></i>';
break;
case "edge":
endpointIconSpanHTML = '<i class="boxitem-icon-ico-edge" aria-hidden="true"></i>';
break;
default:
endpointIconSpanHTML = '<i class="boxitem-icon-ico-generic" aria-hidden="true"></i>';
}
}
endpointSpanIcon.innerHTML = endpointIconSpanHTML;
endpointSpan.appendChild(endpointSpanIcon);
let endpointSpanName = document.createElement("span");
endpointSpanName.className = "boxitem-env-name";
endpointSpanName.textContent = item.envName;
endpointSpan.appendChild(endpointSpanName);
div.appendChild(endpointSpan);
return div;
}
renderGroup(groupName) {
let div = document.createElement("div");
div.className = "boxgroup";
div.textContent = groupName;
return div;
}
initSearchInput() {
const input = document.getElementById("resource-search");
autocomplete({
onSelect: this.redirectUserBaseOnSelectedResource,
fetch: this.search,
input: input,
minLength: 2,
emptyMsg: 'No resource found',
render: this.renderResource,
renderGroup: this.renderGroup,
debounceWaitMs: 200,
});
}
$onInit() {
this.initSearchInput();
}
}
function transformAPItoUIModel(data) {
var uiModel = data.map(function (item) {
return new searchUIModel(item);
});
return uiModel;
}

View File

@@ -0,0 +1,107 @@
.search-bar {
width: 98%;
margin-top: 5px;
}
.autocomplete .group {
background: white;
}
.searchbox {
width: 100%;
background: none;
border: none;
padding: 2px;
}
.boxitem {
margin: 2px;
}
.boxgroup {
margin-left: 2px;
margin-bottom: 5px;
color: #767676;
font-weight: bold;
}
.boxitem-icon {
float: left;
}
.boxitem-name {
margin-left: 25px;
}
.boxitem-icon-ico-container:before {
position: absolute;
font-family: 'Font Awesome 5 Free';
font-weight: 900;
content: '\f1b3';
color: #286090;
}
.boxitem-icon-ico-image:before {
position: absolute;
font-family: 'Font Awesome 5 Free';
font-weight: 900;
content: '\f24d';
color: #286090;
}
.boxitem-icon-ico-volume:before {
position: absolute;
font-family: 'Font Awesome 5 Free';
font-weight: 900;
content: '\f0a0';
color: #286090;
}
.boxitem-icon-ico-network:before {
position: absolute;
font-family: 'Font Awesome 5 Free';
font-weight: 900;
content: '\f0e8';
color: #286090;
}
.boxitem-icon-ico-docker:before {
position: absolute;
font-family: 'Font Awesome 5 Brands';
font-weight: 900;
content: '\f395';
color: #286090;
}
.boxitem-icon-ico-kubernetes:before {
position: absolute;
font-family: 'Font Awesome 5 Brands';
font-weight: 900;
content: '\f655';
color: #286090;
}
.boxitem-icon-ico-edge:before {
position: absolute;
font-family: 'Font Awesome 5 Free';
font-weight: 900;
content: '\f0c2';
color: #286090;
}
.boxitem-icon-ico-generic:before {
position: absolute;
font-family: 'Font Awesome 5 Free';
font-weight: 900;
content: '\f128';
color: #286090;
}
.boxitem-env {
float: right;
}
.boxitem-env-name {
margin-left: 25px;
}

View File

@@ -0,0 +1,3 @@
<div class="search-bar">
<input id="resource-search" type="search" class="searchbox" spellcheck=false autocorrect="off" autocomplete="off" autocapitalize="off" placeholder="Search by resource name">
</div>

View File

@@ -0,0 +1,7 @@
import angular from 'angular';
import controller from './search.controller';
angular.module('portainer.app').component('search', {
templateUrl: './search.html',
controller,
});

View File

@@ -0,0 +1,11 @@
export default class searchUIModel {
constructor(data) {
this.label = data.Label;
this.group = data.Type;
this.value = "";
this.envName = data.Environment;
this.envType = data.EnvironmentType;
this.envId = data.EnvironmentID;
this.resourceId = data.ResourceID;
}
}

View File

@@ -0,0 +1,20 @@
import angular from 'angular';
angular.module('portainer.kubernetes').factory('SearchFactory', SearchFactory);
/* @ngInject */
function SearchFactory($resource, API_ENDPOINT_SEARCH) {
const searchUrl = API_ENDPOINT_SEARCH;
return $resource(
searchUrl,
{},
{
search: {
method: 'GET',
url: `${searchUrl}`,
params: { query: '@query' },
},
}
);
}

View File

@@ -0,0 +1,96 @@
import angular from 'angular';
import PortainerError from 'Portainer/error';
angular.module('portainer.app').factory('SearchService', SearchService);
/* @ngInject */
export function SearchService(SearchFactory) {
return {
search,
};
/**
* @description: Searches for resources inside a Portainer instance
* @param {string} query - query keyword used to search/filter resources
* @returns {Promise} - Resolves with a list of resources matching the query keyword
* @throws {PortainerError} - Rejects with error if searching for resources across environments fails
*/
async function search(query) {
try {
return await SearchFactory.search({ query }).$promise;
} catch (err) {
throw new PortainerError('Unable to execute search request', err);
}
}
// /**
// * @description: Show values helm of a helm chart, this basically runs `helm show values`
// * @param {string} repo - repo url to search charts values for
// * @param {string} chart - chart within the repo to retrieve default values
// * @returns {Promise} - Resolves with `values.yaml` of helm chart values for a repo
// * @throws {PortainerError} - Rejects with error if helm show fails
// */
// async function values(repo, chart) {
// try {
// return await HelmFactory.show({ repo, chart, type: 'values' }).$promise;
// } catch (err) {
// throw new PortainerError('Unable to retrieve values from chart', err);
// }
// }
// /**
// * @description: Show values helm of a helm chart, this basically runs `helm show values`
// * @returns {Promise} - Resolves with an object containing list of user helm repos and default/global settings helm repo
// * @throws {PortainerError} - Rejects with error if helm show fails
// */
// async function getHelmRepositories(endpointId) {
// return await HelmFactory.getHelmRepositories({ endpointId }).$promise;
// }
// /**
// * @description: Adds a helm repo for the calling user
// * @param {Object} payload - helm repo url to add for the user
// * @returns {Promise} - Resolves with `values.yaml` of helm chart values for a repo
// * @throws {PortainerError} - Rejects with error if helm show fails
// */
// async function addHelmRepository(endpointId, payload) {
// return await HelmFactory.addHelmRepository({ endpointId }, payload).$promise;
// }
// /**
// * @description: Installs a helm chart, this basically runs `helm install`
// * @returns {Promise} - Resolves with `values.yaml` of helm chart values for a repo
// * @throws {PortainerError} - Rejects with error if helm show fails
// */
// async function install(endpointId, payload) {
// return await HelmFactory.install({ endpointId }, payload).$promise;
// }
// /**
// * @description: Uninstall a helm chart, this basically runs `helm uninstall`
// * @param {Object} options - Options object, release `Name` is the only required option
// * @throws {PortainerError} - Rejects with error if helm show fails
// */
// async function uninstall(endpointId, { Name, ResourcePool }) {
// try {
// await HelmFactory.uninstall({ endpointId, release: Name, namespace: ResourcePool }).$promise;
// } catch (err) {
// throw new PortainerError('Unable to delete release', err);
// }
// }
// /**
// * @description: List all helm releases based on passed in options, this basically runs `helm list`
// * @param {Object} options - Supported CLI flags to pass to Helm (binary) - flags to `helm list`
// * @returns {Promise} - Resolves with list of helm releases
// * @throws {PortainerError} - Rejects with error if helm list fails
// */
// async function listReleases(endpointId, { namespace, selector, filter, output }) {
// try {
// const releases = await HelmFactory.list({ endpointId, selector, namespace, filter, output }).$promise;
// return releases;
// } catch (err) {
// throw new PortainerError('Unable to retrieve release list', err);
// }
// }
}

View File

@@ -13,6 +13,7 @@ import 'angular-loading-bar/build/loading-bar.css';
import 'angular-moment-picker/dist/angular-moment-picker.min.css';
import 'angular-multiselect/isteven-multi-select.css';
import 'spinkit/spinkit.min.css';
import 'autocompleter/autocomplete.min.css'
import angular from 'angular';
import 'moment';
@@ -37,5 +38,6 @@ import 'angular-ui-bootstrap';
import 'angular-moment-picker';
import 'angular-multiselect/isteven-multi-select.js';
import 'angulartics/dist/angulartics.min.js';
import 'autocompleter/autocomplete.min.js'
window.angular = angular;

View File

@@ -78,6 +78,7 @@
"angularjs-scroll-glue": "^2.2.0",
"angularjs-slider": "^6.4.0",
"angulartics": "^1.6.0",
"autocompleter": "^6.1.2",
"babel-plugin-angularjs-annotate": "^0.10.0",
"bootbox": "^5.4.0",
"bootstrap": "^3.4.0",

View File

@@ -1397,7 +1397,6 @@ angular-moment-picker@^0.10.2:
dependencies:
angular-mocks "1.6.1"
angular-sanitize "1.6.1"
lodash-es "^4.17.15"
angular-resource@1.8.0:
version "1.8.0"
@@ -1756,6 +1755,11 @@ auto-ngtemplate-loader@^2.0.1:
ngtemplate-loader "~2.0.1"
var-validator "0.0.3"
autocompleter@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/autocompleter/-/autocompleter-6.1.2.tgz#35a2c8b496c1ebdd798183e26c529e4b7450fd34"
integrity sha512-DfEcgxBJOTJJwxkIRZLv/ggD3g5w/fqzZkdJsOcgk7hjxw36lH/nAfIEXzV7qDE55swnYEe43E/WhZPXmSFfsA==
autoprefixer@^7.1.1:
version "7.2.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.2.6.tgz#256672f86f7c735da849c4f07d008abb056067dc"
@@ -6982,11 +6986,6 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"