mirror of
https://github.com/portainer/portainer.git
synced 2025-07-22 14:59:41 +02:00
feat(global): add templates support ('apps') (#154)
This commit is contained in:
parent
faccf2a651
commit
1c8aa35479
15 changed files with 366 additions and 13 deletions
|
@ -1,8 +1,9 @@
|
||||||
FROM scratch
|
FROM centurylink/ca-certs
|
||||||
|
|
||||||
COPY dist /
|
COPY dist /
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
ENTRYPOINT ["/ui-for-docker"]
|
ENTRYPOINT ["/ui-for-docker"]
|
||||||
|
|
|
@ -142,6 +142,14 @@ server {
|
||||||
|
|
||||||
Replace `ADDRESS:PORT` with the CloudInovasi UI container details.
|
Replace `ADDRESS:PORT` with the CloudInovasi UI container details.
|
||||||
|
|
||||||
|
### Host your own apps
|
||||||
|
|
||||||
|
You can specify an URL to your own templates (**Apps**) definitions using the `--templates` or `-t` flags.
|
||||||
|
|
||||||
|
By default, CloudInovasi templates will be used (https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json).
|
||||||
|
|
||||||
|
For more information about hosting your own template definition and the format, see: https://github.com/cloud-inovasi/ui-templates
|
||||||
|
|
||||||
### Available options
|
### Available options
|
||||||
|
|
||||||
The following options are available for the `ui-for-docker` binary:
|
The following options are available for the `ui-for-docker` binary:
|
||||||
|
@ -157,3 +165,4 @@ The following options are available for the `ui-for-docker` binary:
|
||||||
* `--tlskey`: Path to the TLS key (default `/certs/key.pem`)
|
* `--tlskey`: Path to the TLS key (default `/certs/key.pem`)
|
||||||
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
|
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
|
||||||
* `--logo`: URL to a picture to be displayed as a logo in the UI
|
* `--logo`: URL to a picture to be displayed as a logo in the UI
|
||||||
|
* `--templates`, `-t`: URL to templates (apps) definitions
|
||||||
|
|
23
api/api.go
23
api/api.go
|
@ -9,11 +9,12 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
api struct {
|
api struct {
|
||||||
endpoint *url.URL
|
endpoint *url.URL
|
||||||
bindAddress string
|
bindAddress string
|
||||||
assetPath string
|
assetPath string
|
||||||
dataPath string
|
dataPath string
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
templatesURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
apiConfig struct {
|
apiConfig struct {
|
||||||
|
@ -26,6 +27,7 @@ type (
|
||||||
TLSCACertPath string
|
TLSCACertPath string
|
||||||
TLSCertPath string
|
TLSCertPath string
|
||||||
TLSKeyPath string
|
TLSKeyPath string
|
||||||
|
TemplatesURL string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,10 +50,11 @@ func newAPI(apiConfig apiConfig) *api {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &api{
|
return &api{
|
||||||
endpoint: endpointURL,
|
endpoint: endpointURL,
|
||||||
bindAddress: apiConfig.BindAddress,
|
bindAddress: apiConfig.BindAddress,
|
||||||
assetPath: apiConfig.AssetPath,
|
assetPath: apiConfig.AssetPath,
|
||||||
dataPath: apiConfig.DataPath,
|
dataPath: apiConfig.DataPath,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
|
templatesURL: apiConfig.TemplatesURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,9 @@ func (a *api) newHandler(settings *Settings) http.Handler {
|
||||||
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
|
||||||
settingsHandler(w, r, settings)
|
settingsHandler(w, r, settings)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
templatesHandler(w, r, a.templatesURL)
|
||||||
|
})
|
||||||
return CSRFHandler(newCSRFWrapper(mux))
|
return CSRFHandler(newCSRFWrapper(mux))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ func main() {
|
||||||
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
|
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
|
||||||
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
||||||
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
|
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
|
||||||
|
templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json").Short('t').String()
|
||||||
)
|
)
|
||||||
kingpin.Parse()
|
kingpin.Parse()
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ func main() {
|
||||||
TLSCACertPath: *tlscacert,
|
TLSCACertPath: *tlscacert,
|
||||||
TLSCertPath: *tlscert,
|
TLSCertPath: *tlscert,
|
||||||
TLSKeyPath: *tlskey,
|
TLSKeyPath: *tlskey,
|
||||||
|
TemplatesURL: *templates,
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := &Settings{
|
settings := &Settings{
|
||||||
|
|
|
@ -12,7 +12,7 @@ type Settings struct {
|
||||||
Logo string `json:"logo"`
|
Logo string `json:"logo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// configurationHandler defines a handler function used to encode the configuration in JSON
|
// settingsHandler defines a handler function used to encode the configuration in JSON
|
||||||
func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
|
func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
|
||||||
json.NewEncoder(w).Encode(*s)
|
json.NewEncoder(w).Encode(*s)
|
||||||
}
|
}
|
||||||
|
|
27
api/templates.go
Normal file
27
api/templates.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// templatesHandler defines a handler function used to retrieve the templates from a URL and put them in the response
|
||||||
|
func templatesHandler(w http.ResponseWriter, r *http.Request, templatesURL string) {
|
||||||
|
resp, err := http.Get(templatesURL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Error making request to %s: %s", templatesURL, err.Error()), http.StatusInternalServerError)
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(body)
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ angular.module('uifordocker', [
|
||||||
'network',
|
'network',
|
||||||
'networks',
|
'networks',
|
||||||
'createNetwork',
|
'createNetwork',
|
||||||
|
'templates',
|
||||||
'volumes',
|
'volumes',
|
||||||
'createVolume'])
|
'createVolume'])
|
||||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
|
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
|
||||||
|
@ -118,6 +119,11 @@ angular.module('uifordocker', [
|
||||||
templateUrl: 'app/components/network/network.html',
|
templateUrl: 'app/components/network/network.html',
|
||||||
controller: 'NetworkController'
|
controller: 'NetworkController'
|
||||||
})
|
})
|
||||||
|
.state('templates', {
|
||||||
|
url: '/templates/',
|
||||||
|
templateUrl: 'app/components/templates/templates.html',
|
||||||
|
controller: 'TemplatesController'
|
||||||
|
})
|
||||||
.state('volumes', {
|
.state('volumes', {
|
||||||
url: '/volumes/',
|
url: '/volumes/',
|
||||||
templateUrl: 'app/components/volumes/volumes.html',
|
templateUrl: 'app/components/volumes/volumes.html',
|
||||||
|
@ -156,4 +162,5 @@ angular.module('uifordocker', [
|
||||||
.constant('DOCKER_ENDPOINT', 'dockerapi')
|
.constant('DOCKER_ENDPOINT', 'dockerapi')
|
||||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
|
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
|
||||||
.constant('CONFIG_ENDPOINT', 'settings')
|
.constant('CONFIG_ENDPOINT', 'settings')
|
||||||
|
.constant('TEMPLATES_ENDPOINT', 'templates')
|
||||||
.constant('UI_VERSION', 'v1.7.0');
|
.constant('UI_VERSION', 'v1.7.0');
|
||||||
|
|
76
app/components/templates/templates.html
Normal file
76
app/components/templates/templates.html
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Apps list">
|
||||||
|
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
|
||||||
|
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</rd-header-title>
|
||||||
|
<rd-header-content>Apps</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-rocket" title="Available apps">
|
||||||
|
<div class="pull-right">
|
||||||
|
<i id="loadTemplatesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||||
|
</div>
|
||||||
|
</rd-widget-header>
|
||||||
|
<rd-widget-body classes="padding">
|
||||||
|
<div class="template-list">
|
||||||
|
<div ng-repeat="tpl in templates" class="container-template hvr-grow" id="template_{{ $index }}" ng-click="selectTemplate($index)">
|
||||||
|
<img class="logo" ng-src="{{ tpl.logo }}" />
|
||||||
|
<div class="title">{{ tpl.title }}</div>
|
||||||
|
<div class="comment">{{ tpl.comment }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" ng-if="selectedTemplate">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-cogs" title="Configuration"></rd-widget-header>
|
||||||
|
<rd-widget-body classes="padding">
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="form-group" ng-if="globalNetworkCount === 0">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- name-and-network-inputs -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image_registry" class="col-sm-2 control-label text-left">Name</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
|
||||||
|
</div>
|
||||||
|
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<select class="selectpicker form-control" ng-model="formValues.network">
|
||||||
|
<option selected disabled hidden value="">Select a network</option>
|
||||||
|
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !name-and-network-inputs -->
|
||||||
|
<div ng-repeat="var in selectedTemplate.env" class="form-group">
|
||||||
|
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" ng-if="selectedTemplate">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||||
|
<div>
|
||||||
|
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-default btn-lg" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
170
app/components/templates/templatesController.js
Normal file
170
app/components/templates/templatesController.js
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
angular.module('templates', [])
|
||||||
|
.controller('TemplatesController', ['$scope', '$q', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Templates', 'Messages', 'errorMsgFilter',
|
||||||
|
function ($scope, $q, $state, Config, Container, Image, Volume, Network, Templates, Messages, errorMsgFilter) {
|
||||||
|
$scope.templates = [];
|
||||||
|
$scope.selectedTemplate = null;
|
||||||
|
$scope.formValues = {
|
||||||
|
network: "",
|
||||||
|
name: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
var selectedItem = -1;
|
||||||
|
|
||||||
|
// TODO: centralize, already present in createContainerController
|
||||||
|
function createContainer(config) {
|
||||||
|
Container.create(config, function (d) {
|
||||||
|
if (d.Id) {
|
||||||
|
var reqBody = config.HostConfig || {};
|
||||||
|
reqBody.id = d.Id;
|
||||||
|
Container.start(reqBody, function (cd) {
|
||||||
|
$('#createContainerSpinner').hide();
|
||||||
|
Messages.send('Container Started', d.Id);
|
||||||
|
$state.go('containers', {}, {reload: true});
|
||||||
|
}, function (e) {
|
||||||
|
$('#createContainerSpinner').hide();
|
||||||
|
Messages.error('Error', errorMsgFilter(e));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$('#createContainerSpinner').hide();
|
||||||
|
Messages.error('Error', errorMsgFilter(d));
|
||||||
|
}
|
||||||
|
}, function (e) {
|
||||||
|
$('#createContainerSpinner').hide();
|
||||||
|
Messages.error('Error', errorMsgFilter(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: centralize, already present in createContainerController
|
||||||
|
function pullImageAndCreateContainer(imageConfig, containerConfig) {
|
||||||
|
Image.create(imageConfig, function (data) {
|
||||||
|
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||||
|
if (err) {
|
||||||
|
var detail = data[data.length - 1];
|
||||||
|
$('#createContainerSpinner').hide();
|
||||||
|
Messages.error('Error', detail.error);
|
||||||
|
} else {
|
||||||
|
createContainer(containerConfig);
|
||||||
|
}
|
||||||
|
}, function (e) {
|
||||||
|
$('#createContainerSpinner').hide();
|
||||||
|
Messages.error('Error', 'Unable to pull image ' + image);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConfigFromTemplate(template) {
|
||||||
|
var containerConfig = {
|
||||||
|
Env: [],
|
||||||
|
OpenStdin: false,
|
||||||
|
Tty: false,
|
||||||
|
ExposedPorts: {},
|
||||||
|
HostConfig: {
|
||||||
|
RestartPolicy: {
|
||||||
|
Name: 'no'
|
||||||
|
},
|
||||||
|
PortBindings: {},
|
||||||
|
Binds: [],
|
||||||
|
NetworkMode: $scope.formValues.network,
|
||||||
|
Privileged: false
|
||||||
|
},
|
||||||
|
Image: template.image,
|
||||||
|
Volumes: {},
|
||||||
|
name: $scope.formValues.name
|
||||||
|
};
|
||||||
|
if (template.env) {
|
||||||
|
template.env.forEach(function (v) {
|
||||||
|
if (v.value) {
|
||||||
|
containerConfig.Env.push(v.name + "=" + v.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (template.ports) {
|
||||||
|
template.ports.forEach(function (p) {
|
||||||
|
containerConfig.ExposedPorts[p] = {};
|
||||||
|
containerConfig.HostConfig.PortBindings[p] = [{ HostPort: ""}];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return containerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareVolumeQueries(template, containerConfig) {
|
||||||
|
var volumeQueries = [];
|
||||||
|
if (template.volumes) {
|
||||||
|
template.volumes.forEach(function (vol) {
|
||||||
|
volumeQueries.push(
|
||||||
|
Volume.create({}, function (d) {
|
||||||
|
if (d.Name) {
|
||||||
|
Messages.send("Volume created", d.Name);
|
||||||
|
containerConfig.Volumes[vol] = {};
|
||||||
|
containerConfig.HostConfig.Binds.push(d.Name + ':' + vol);
|
||||||
|
} else {
|
||||||
|
Messages.error('Unable to create volume', errorMsgFilter(d));
|
||||||
|
}
|
||||||
|
}, function (e) {
|
||||||
|
Messages.error('Unable to create volume', e.data);
|
||||||
|
}).$promise
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return volumeQueries;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.createTemplate = function() {
|
||||||
|
$('#createContainerSpinner').show();
|
||||||
|
var template = $scope.selectedTemplate;
|
||||||
|
var containerConfig = createConfigFromTemplate(template);
|
||||||
|
var imageConfig = {
|
||||||
|
fromImage: template.image.split(':')[0],
|
||||||
|
tag: template.image.split(':')[1] ? template.image.split(':')[1] : 'latest'
|
||||||
|
};
|
||||||
|
var createVolumeQueries = prepareVolumeQueries(template, containerConfig);
|
||||||
|
$q.all(createVolumeQueries).then(function (d) {
|
||||||
|
pullImageAndCreateContainer(imageConfig, containerConfig);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.selectTemplate = function(id) {
|
||||||
|
$('#template_' + id).toggleClass("container-template--selected");
|
||||||
|
if (selectedItem === id) {
|
||||||
|
selectedItem = -1;
|
||||||
|
$scope.selectedTemplate = null;
|
||||||
|
} else {
|
||||||
|
$('#template_' + selectedItem).toggleClass("container-template--selected");
|
||||||
|
selectedItem = id;
|
||||||
|
$scope.selectedTemplate = $scope.templates[id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function initTemplates() {
|
||||||
|
Templates.get(function (data) {
|
||||||
|
$scope.templates = data;
|
||||||
|
$('#loadTemplatesSpinner').hide();
|
||||||
|
}, function (e) {
|
||||||
|
$('#loadTemplatesSpinner').hide();
|
||||||
|
Messages.error("Unable to retrieve apps list", e.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.$promise.then(function (c) {
|
||||||
|
var swarm = c.swarm;
|
||||||
|
Network.query({}, function (d) {
|
||||||
|
var networks = d;
|
||||||
|
if (swarm) {
|
||||||
|
networks = d.filter(function (network) {
|
||||||
|
if (network.Scope === 'global') {
|
||||||
|
return network;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$scope.globalNetworkCount = networks.length;
|
||||||
|
networks.push({Name: "bridge"});
|
||||||
|
networks.push({Name: "host"});
|
||||||
|
networks.push({Name: "none"});
|
||||||
|
} else {
|
||||||
|
$scope.formValues.network = "bridge";
|
||||||
|
}
|
||||||
|
$scope.availableNetworks = networks;
|
||||||
|
}, function (e) {
|
||||||
|
Messages.error("Unable to retrieve available networks", e.data);
|
||||||
|
});
|
||||||
|
initTemplates();
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -153,6 +153,11 @@ angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
|
||||||
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
|
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
|
||||||
return $resource(CONFIG_ENDPOINT).get();
|
return $resource(CONFIG_ENDPOINT).get();
|
||||||
}])
|
}])
|
||||||
|
.factory('Templates', ['$resource', 'TEMPLATES_ENDPOINT', function TemplatesFactory($resource, TEMPLATES_ENDPOINT) {
|
||||||
|
return $resource(TEMPLATES_ENDPOINT, {}, {
|
||||||
|
get: {method: 'GET', isArray: true}
|
||||||
|
});
|
||||||
|
}])
|
||||||
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) {
|
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var url = DOCKER_ENDPOINT;
|
var url = DOCKER_ENDPOINT;
|
||||||
|
|
|
@ -210,3 +210,48 @@ input[type="radio"] {
|
||||||
.btn-ico {
|
.btn-ico {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.template-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-template {
|
||||||
|
font-size: 1em;
|
||||||
|
width: 256px;
|
||||||
|
height: 128px;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #f6f6f6;
|
||||||
|
color: #30426a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-template--selected {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
color: #2d3e63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-template:hover {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
color: #2d3e63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-template .logo {
|
||||||
|
max-width: 48px;
|
||||||
|
max-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-template .title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-template .comment {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"angular-ui-select": "~0.17.1",
|
"angular-ui-select": "~0.17.1",
|
||||||
"bootstrap": "~3.3.6",
|
"bootstrap": "~3.3.6",
|
||||||
"font-awesome": "~4.6.3",
|
"font-awesome": "~4.6.3",
|
||||||
|
"Hover": "2.0.2",
|
||||||
"jquery": "1.11.1",
|
"jquery": "1.11.1",
|
||||||
"jquery.gritter": "1.7.4",
|
"jquery.gritter": "1.7.4",
|
||||||
"lodash": "4.12.0",
|
"lodash": "4.12.0",
|
||||||
|
|
|
@ -87,7 +87,8 @@ module.exports = function (grunt) {
|
||||||
'bower_components/font-awesome/css/font-awesome.min.css',
|
'bower_components/font-awesome/css/font-awesome.min.css',
|
||||||
'bower_components/rdash-ui/dist/css/rdash.min.css',
|
'bower_components/rdash-ui/dist/css/rdash.min.css',
|
||||||
'bower_components/angular-ui-select/dist/select.min.css',
|
'bower_components/angular-ui-select/dist/select.min.css',
|
||||||
'bower_components/xterm.js/src/xterm.css'
|
'bower_components/xterm.js/src/xterm.css',
|
||||||
|
'bower_components/Hover/css/hover-min.css'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
clean: {
|
clean: {
|
||||||
|
|
|
@ -41,6 +41,9 @@
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
<a ui-sref="index">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
|
<a ui-sref="index">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="sidebar-list">
|
||||||
|
<a ui-sref="templates">Apps <span class="menu-icon fa fa-rocket"></span></a>
|
||||||
|
</li>
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
|
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue