mirror of
https://github.com/bingohuang/docker-labs.git
synced 2025-07-13 17:42:53 +08:00
Initial commit
This commit is contained in:
parent
f2ae4344fd
commit
dde49d8700
35
api.go
Normal file
35
api.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/urfave/negroni"
|
||||
"github.com/xetorthio/play-with-docker/handlers"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := bone.New()
|
||||
|
||||
mux.Get("/", http.HandlerFunc(handlers.NewSession))
|
||||
mux.Get("/sessions/:sessionId", http.HandlerFunc(handlers.GetSession))
|
||||
mux.Post("/sessions/:sessionId/instances", http.HandlerFunc(handlers.NewInstance))
|
||||
mux.Delete("/sessions/:sessionId/instances/:instanceId", http.HandlerFunc(handlers.DeleteInstance))
|
||||
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./www/index.html")
|
||||
})
|
||||
mux.Get("/p/:sessionId", h)
|
||||
mux.Get("/assets/*", http.FileServer(http.Dir("./www")))
|
||||
|
||||
mux.Get("/exec/:id", websocket.Handler(handlers.Exec))
|
||||
|
||||
n := negroni.Classic()
|
||||
n.UseHandler(mux)
|
||||
|
||||
log.Fatal(http.ListenAndServe(":3000", n))
|
||||
|
||||
}
|
21
handlers/delete_instance.go
Normal file
21
handlers/delete_instance.go
Normal file
@ -0,0 +1,21 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/xetorthio/play-with-docker/services"
|
||||
)
|
||||
|
||||
func DeleteInstance(rw http.ResponseWriter, req *http.Request) {
|
||||
sessionId := bone.GetValue(req, "sessionId")
|
||||
instanceId := bone.GetValue(req, "instanceId")
|
||||
|
||||
s := services.GetSession(sessionId)
|
||||
i := services.GetInstance(s, instanceId)
|
||||
err := services.DeleteInstance(s, i)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
36
handlers/exec.go
Normal file
36
handlers/exec.go
Normal file
@ -0,0 +1,36 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/websocket"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/xetorthio/play-with-docker/services"
|
||||
)
|
||||
|
||||
// Echo the data received on the WebSocket.
|
||||
func Exec(ws *websocket.Conn) {
|
||||
id := bone.GetValue(ws.Request(), "id")
|
||||
ctx := context.Background()
|
||||
conn, err := services.GetExecConnection(id, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
go func() {
|
||||
io.Copy(ws, conn.Reader)
|
||||
}()
|
||||
go func() {
|
||||
io.Copy(conn.Conn, ws)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
}
|
||||
//io.Copy(ws, os.Stdout)
|
||||
//go func() {
|
||||
//io.Copy(*conn, ws)
|
||||
//}()
|
||||
}
|
22
handlers/get_session.go
Normal file
22
handlers/get_session.go
Normal file
@ -0,0 +1,22 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/xetorthio/play-with-docker/services"
|
||||
)
|
||||
|
||||
func GetSession(rw http.ResponseWriter, req *http.Request) {
|
||||
sessionId := bone.GetValue(req, "sessionId")
|
||||
|
||||
session := services.GetSession(sessionId)
|
||||
|
||||
if session == nil {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(rw).Encode(session)
|
||||
}
|
23
handlers/new_instance.go
Normal file
23
handlers/new_instance.go
Normal file
@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/xetorthio/play-with-docker/services"
|
||||
)
|
||||
|
||||
func NewInstance(rw http.ResponseWriter, req *http.Request) {
|
||||
sessionId := bone.GetValue(req, "sessionId")
|
||||
|
||||
s := services.GetSession(sessionId)
|
||||
i, err := services.NewInstance(s)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
//TODO: Set a status error
|
||||
} else {
|
||||
json.NewEncoder(rw).Encode(i)
|
||||
}
|
||||
}
|
19
handlers/new_session.go
Normal file
19
handlers/new_session.go
Normal file
@ -0,0 +1,19 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/xetorthio/play-with-docker/services"
|
||||
)
|
||||
|
||||
func NewSession(rw http.ResponseWriter, req *http.Request) {
|
||||
s, err := services.NewSession()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
//TODO: Return some error code
|
||||
} else {
|
||||
http.Redirect(rw, req, fmt.Sprintf("/p/%s", s.Id), http.StatusFound)
|
||||
}
|
||||
}
|
89
services/docker.go
Normal file
89
services/docker.go
Normal file
@ -0,0 +1,89 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
ptypes "github.com/xetorthio/play-with-docker/types"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var c *client.Client
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
c, err = client.NewEnvClient()
|
||||
if err != nil {
|
||||
// this wont happen if daemon is offline, only for some critical errors
|
||||
log.Fatal("Cannot initialize docker client")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetContainerInfo(id string) (types.ContainerJSON, error) {
|
||||
return c.ContainerInspect(context.Background(), id)
|
||||
}
|
||||
|
||||
func CreateNetwork(name string) error {
|
||||
opts := types.NetworkCreate{Attachable: true}
|
||||
_, err := c.NetworkCreate(context.Background(), name, opts)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetExecConnection(id string, ctx context.Context) (*types.HijackedResponse, error) {
|
||||
conf := types.ExecConfig{Tty: true, AttachStdin: true, AttachStderr: true, AttachStdout: true, Cmd: []string{"sh"}}
|
||||
resp, err := c.ContainerExecCreate(ctx, id, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//err = c.ContainerExecStart(context.Background(), resp.ID, types.ExecStartCheck{Tty: true})
|
||||
//if err != nil {
|
||||
//return nil, err
|
||||
//}
|
||||
|
||||
conn, err := c.ContainerExecAttach(ctx, resp.ID, conf)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &conn, nil
|
||||
|
||||
}
|
||||
|
||||
func CreateInstance(net string) (*ptypes.Instance, error) {
|
||||
|
||||
h := &container.HostConfig{NetworkMode: container.NetworkMode(net), Privileged: true}
|
||||
conf := &container.Config{Image: "docker:dind"}
|
||||
container, err := c.ContainerCreate(context.Background(), conf, h, nil, "")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.ContainerStart(context.Background(), container.ID, types.ContainerStartOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cinfo, err := GetContainerInfo(container.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ptypes.Instance{Name: strings.Replace(cinfo.Name, "/", "", 1), IP: cinfo.NetworkSettings.Networks[net].IPAddress}, nil
|
||||
}
|
||||
|
||||
func DeleteContainer(id string) error {
|
||||
return c.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true})
|
||||
}
|
39
services/instance.go
Normal file
39
services/instance.go
Normal file
@ -0,0 +1,39 @@
|
||||
package services
|
||||
|
||||
import "github.com/xetorthio/play-with-docker/types"
|
||||
|
||||
var instances map[string]map[string]*types.Instance
|
||||
|
||||
func init() {
|
||||
instances = make(map[string]map[string]*types.Instance)
|
||||
}
|
||||
|
||||
func NewInstance(session *types.Session) (*types.Instance, error) {
|
||||
//TODO: Validate that a session can only have 10 instances
|
||||
|
||||
//TODO: Create in redis
|
||||
|
||||
instance, err := CreateInstance(session.Id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if instances[session.Id] == nil {
|
||||
instances[session.Id] = make(map[string]*types.Instance)
|
||||
}
|
||||
instances[session.Id][instance.Name] = instance
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func GetInstance(session *types.Session, instanceId string) *types.Instance {
|
||||
//TODO: Use redis
|
||||
i := instances[session.Id][instanceId]
|
||||
return i
|
||||
}
|
||||
func DeleteInstance(session *types.Session, instance *types.Instance) error {
|
||||
//TODO: Use redis
|
||||
delete(instances[session.Id], instance.Name)
|
||||
return DeleteContainer(instance.Name)
|
||||
}
|
39
services/session.go
Normal file
39
services/session.go
Normal file
@ -0,0 +1,39 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/twinj/uuid"
|
||||
"github.com/xetorthio/play-with-docker/types"
|
||||
)
|
||||
|
||||
var sessions map[string]*types.Session
|
||||
|
||||
func init() {
|
||||
sessions = make(map[string]*types.Session)
|
||||
}
|
||||
|
||||
func NewSession() (*types.Session, error) {
|
||||
s := &types.Session{}
|
||||
s.Id = uuid.NewV4().String()
|
||||
s.Instances = map[string]*types.Instance{}
|
||||
|
||||
//TODO: Store in something like redis
|
||||
sessions[s.Id] = s
|
||||
|
||||
if err := CreateNetwork(s.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//TODO: Schedule deletion after an hour
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func GetSession(sessionId string) *types.Session {
|
||||
//TODO: Use redis
|
||||
s := sessions[sessionId]
|
||||
if instances[sessionId] != nil {
|
||||
s.Instances = instances[sessionId]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
11
types/session.go
Normal file
11
types/session.go
Normal file
@ -0,0 +1,11 @@
|
||||
package types
|
||||
|
||||
type Session struct {
|
||||
Id string `json:"id"`
|
||||
Instances map[string]*Instance `json:"instances"`
|
||||
}
|
||||
|
||||
type Instance struct {
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
}
|
67
www/assets/app.js
Normal file
67
www/assets/app.js
Normal file
@ -0,0 +1,67 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var app = angular.module('DockerPlay', ['ngMaterial']);
|
||||
|
||||
app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', function($scope, $log, $http, $location, $timeout) {
|
||||
$scope.sessionId = window.location.pathname.replace('/p/', '');
|
||||
$scope.instances = [];
|
||||
$scope.selectedInstance = null;
|
||||
|
||||
$scope.newInstance = function() {
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: '/sessions/' + $scope.sessionId + '/instances',
|
||||
}).then(function(response) {
|
||||
var i = response.data;
|
||||
$scope.instances.push(i);
|
||||
$scope.showInstance(i);
|
||||
}, function(response) {
|
||||
console.log('error', response);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.getSession = function(sessionId) {
|
||||
$http({
|
||||
method: 'GET',
|
||||
url: '/sessions/' + $scope.sessionId,
|
||||
}).then(function(response) {
|
||||
var i = response.data;
|
||||
for (var k in i.instances) {
|
||||
var instance = i.instances[k];
|
||||
|
||||
$scope.instances.push(instance);
|
||||
}
|
||||
if ($scope.instances.length) {
|
||||
$scope.showInstance($scope.instances[0]);
|
||||
}
|
||||
}, function(response) {
|
||||
console.log('error', response);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.showInstance = function(instance) {
|
||||
$scope.selectedInstance = instance;
|
||||
if (!instance.isAttached) {
|
||||
$timeout(function() {createTerminal(instance.name)});
|
||||
instance.isAttached = true;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.deleteInstance = function(instance) {
|
||||
$http({
|
||||
method: 'DELETE',
|
||||
url: '/sessions/' + $scope.sessionId + '/instances/' + instance.name,
|
||||
}).then(function(response) {
|
||||
$scope.instances = $scope.instances.filter(function(i) { return i.name != instance.name });
|
||||
if ($scope.instances.length) {
|
||||
$scope.showInstance($scope.instances[0]);
|
||||
}
|
||||
}, function(response) {
|
||||
console.log('error', response);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.getSession($scope.sessionId);
|
||||
}]);
|
||||
})();
|
134
www/assets/attach.js
Normal file
134
www/assets/attach.js
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Implements the attach method, that
|
||||
* attaches the terminal to a WebSocket stream.
|
||||
*
|
||||
* The bidirectional argument indicates, whether the terminal should
|
||||
* send data to the socket as well and is true, by default.
|
||||
*/
|
||||
|
||||
(function (attach) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = attach(require('../../src/xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../src/xterm'], attach);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
attach(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This module provides methods for attaching a terminal to a WebSocket
|
||||
* stream.
|
||||
*
|
||||
* @module xterm/addons/attach/attach
|
||||
*/
|
||||
var exports = {};
|
||||
|
||||
/**
|
||||
* Attaches the given terminal to the given socket.
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be attached to the given socket.
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
exports.attach = function (term, socket, bidirectional, buffered) {
|
||||
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||
term.socket = socket;
|
||||
|
||||
term._flushBuffer = function () {
|
||||
term.write(term._attachSocketBuffer);
|
||||
term._attachSocketBuffer = null;
|
||||
clearTimeout(term._attachSocketBufferTimer);
|
||||
term._attachSocketBufferTimer = null;
|
||||
};
|
||||
|
||||
term._pushToBuffer = function (data) {
|
||||
if (term._attachSocketBuffer) {
|
||||
term._attachSocketBuffer += data;
|
||||
} else {
|
||||
term._attachSocketBuffer = data;
|
||||
setTimeout(term._flushBuffer, 10);
|
||||
}
|
||||
};
|
||||
|
||||
term._getMessage = function (ev) {
|
||||
if (buffered) {
|
||||
term._pushToBuffer(ev.data);
|
||||
} else {
|
||||
term.write(ev.data);
|
||||
}
|
||||
};
|
||||
|
||||
term._sendData = function (data) {
|
||||
socket.send(data);
|
||||
};
|
||||
|
||||
socket.addEventListener('message', term._getMessage);
|
||||
|
||||
if (bidirectional) {
|
||||
term.on('data', term._sendData);
|
||||
}
|
||||
|
||||
socket.addEventListener('close', term.detach.bind(term, socket));
|
||||
socket.addEventListener('error', term.detach.bind(term, socket));
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the given terminal from the given socket
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be detached from the given socket.
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
exports.detach = function (term, socket) {
|
||||
term.off('data', term._sendData);
|
||||
|
||||
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
||||
|
||||
if (socket) {
|
||||
socket.removeEventListener('message', term._getMessage);
|
||||
}
|
||||
|
||||
delete term.socket;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches the current terminal to the given socket
|
||||
*
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
Xterm.prototype.attach = function (socket, bidirectional, buffered) {
|
||||
return exports.attach(this, socket, bidirectional, buffered);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the current terminal from the given socket.
|
||||
*
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
Xterm.prototype.detach = function (socket) {
|
||||
return exports.detach(this, socket);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
57
www/assets/main.js
Normal file
57
www/assets/main.js
Normal file
@ -0,0 +1,57 @@
|
||||
//var term,
|
||||
//protocol,
|
||||
//socketURL,
|
||||
//socket,
|
||||
//pid,
|
||||
//charWidth,
|
||||
//charHeight;
|
||||
|
||||
//var terminalContainer = document.getElementById('terminal-container'),
|
||||
//optionElements = {
|
||||
//cursorBlink: document.querySelector('#option-cursor-blink')
|
||||
//},
|
||||
//colsElement = document.getElementById('cols'),
|
||||
//rowsElement = document.getElementById('rows');
|
||||
|
||||
//function setTerminalSize () {
|
||||
//var cols = parseInt(colsElement.value),
|
||||
//rows = parseInt(rowsElement.value),
|
||||
//width = (cols * charWidth).toString() + 'px',
|
||||
//height = (rows * charHeight).toString() + 'px';
|
||||
|
||||
//terminalContainer.style.width = width;
|
||||
//terminalContainer.style.height = height;
|
||||
//term.resize(cols, rows);
|
||||
//}
|
||||
|
||||
//colsElement.addEventListener('change', setTerminalSize);
|
||||
//rowsElement.addEventListener('change', setTerminalSize);
|
||||
|
||||
//optionElements.cursorBlink.addEventListener('change', createTerminal);
|
||||
|
||||
//createTerminal();
|
||||
|
||||
function createTerminal(name) {
|
||||
var terminalContainer = document.getElementById('terminal-' + name);
|
||||
// Clean terminal
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
term = new Terminal({
|
||||
cursorBlink: false
|
||||
});
|
||||
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/exec/' + name;
|
||||
|
||||
term.open(terminalContainer);
|
||||
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
|
||||
}
|
||||
|
||||
|
||||
function runRealTerminal() {
|
||||
term.attach(socket);
|
||||
term._initialized = true;
|
||||
}
|
3
www/assets/style.css
Normal file
3
www/assets/style.css
Normal file
@ -0,0 +1,3 @@
|
||||
.selected button {
|
||||
background-color: rgba(158,158,158,0.2);
|
||||
}
|
2196
www/assets/xterm.css
Normal file
2196
www/assets/xterm.css
Normal file
File diff suppressed because it is too large
Load Diff
2137
www/assets/xterm.js
Normal file
2137
www/assets/xterm.js
Normal file
File diff suppressed because it is too large
Load Diff
23
www/assets/xterm.js.map
Normal file
23
www/assets/xterm.js.map
Normal file
File diff suppressed because one or more lines are too long
79
www/index.html
Normal file
79
www/index.html
Normal file
@ -0,0 +1,79 @@
|
||||
<!doctype html>
|
||||
<html ng-app="DockerPlay" ng-controller="PlayController">
|
||||
<head>
|
||||
<title>Docker Playground</title>
|
||||
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css">
|
||||
<link rel="stylesheet" href="/assets/xterm.css" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div layout="column" style="height:100%;" ng-cloak>
|
||||
|
||||
<section layout="row" flex>
|
||||
|
||||
<md-sidenav
|
||||
class="md-sidenav-left"
|
||||
md-component-id="left"
|
||||
md-is-locked-open="$mdMedia('gt-md')"
|
||||
md-whiteframe="4">
|
||||
|
||||
<md-toolbar class="md-theme-indigo">
|
||||
<h1 class="md-toolbar-tools">Instances</h1>
|
||||
</md-toolbar>
|
||||
<md-content layout-padding>
|
||||
<md-button ng-click="newInstance()" class="md-primary">
|
||||
+ Add new instance
|
||||
</md-button>
|
||||
|
||||
<md-list>
|
||||
<md-list-item class="md-3-line" ng-repeat="instance in instances" ng-click="showInstance(instance)" ng-class="instance.name == selectedInstance.name ? 'selected' : false">
|
||||
<div class="md-list-item-text">{{instance.name}}</div>
|
||||
<md-divider ng-if="!$last"></md-divider>
|
||||
</md-list-item>
|
||||
</md-list>
|
||||
</md-content>
|
||||
|
||||
</md-sidenav>
|
||||
|
||||
<md-content flex layout-padding ng-if="!instances.length">
|
||||
<div layout="column" layout-align="top center">
|
||||
<p>
|
||||
Add instances to your playground.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div flex></div>
|
||||
</md-content>
|
||||
<md-content flex layout-padding ng-repeat="instance in instances" ng-show="instance.name == selectedInstance.name">
|
||||
<md-card md-theme="default" md-theme-watch>
|
||||
<md-card-title>
|
||||
<md-card-title-text>
|
||||
<span class="md-headline">{{instance.name}}</span>
|
||||
<span class="md-subhead">{{instance.ip}}</span>
|
||||
<md-card-title-text>
|
||||
</md-card-title>
|
||||
<md-card-actions layout="row">
|
||||
<md-button class="md-warn" ng-click="deleteInstance(instance)">Delete</md-button>
|
||||
</md-card-actions>
|
||||
<md-card-content>
|
||||
<div id="terminal-{{instance.name}}"></div>
|
||||
</md-card-content>
|
||||
</md-card>
|
||||
</md-content>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script>
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script>
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script>
|
||||
<script src="/assets/app.js"></script>
|
||||
<script src="/assets/main.js"></script>
|
||||
<script src="/assets/xterm.js"></script>
|
||||
<script src="/assets/attach.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user