Merge branch 'master' into resharding

This commit is contained in:
Alain Jobart 2015-07-23 14:16:58 -07:00
Родитель 3ddcb87784 d32df71caf
Коммит 68426d028c
23 изменённых файлов: 567 добавлений и 424 удалений

Просмотреть файл

@ -215,3 +215,4 @@ ln -sf $VTTOP/misc/git/pre-commit $VTTOP/.git/hooks/pre-commit
echo
echo "bootstrap finished - run 'source dev.env' in your shell before building."

Просмотреть файл

@ -4,17 +4,24 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"github.com/youtube/vitess/go/vt/schemamanager"
"github.com/youtube/vitess/go/vt/tabletmanager/tmclient"
"github.com/youtube/vitess/go/vt/topo"
"golang.org/x/net/context"
)
// This file implements a REST-style API for the vtctld web interface.
const apiPrefix = "/api/"
const (
apiPrefix = "/api/"
jsonContentType = "application/json; charset=utf-8"
)
func handleCollection(collection string, getFunc func(*http.Request) (interface{}, error)) {
http.HandleFunc(apiPrefix+collection+"/", func(w http.ResponseWriter, r *http.Request) {
@ -31,6 +38,7 @@ func handleCollection(collection string, getFunc func(*http.Request) (interface{
// JSON marshals a nil slice as "null", but we prefer "[]".
if val := reflect.ValueOf(obj); val.Kind() == reflect.Slice && val.IsNil() {
w.Header().Set("Content-Type", jsonContentType)
w.Write([]byte("[]"))
return
}
@ -41,6 +49,7 @@ func handleCollection(collection string, getFunc func(*http.Request) (interface{
httpErrorf(w, r, "json error: %v", err)
return
}
w.Header().Set("Content-Type", jsonContentType)
w.Write(data)
})
}
@ -60,6 +69,14 @@ func getItemPath(url string) string {
return parts[1]
}
func unmarshalRequest(r *http.Request, v interface{}) error {
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
return json.Unmarshal(data, v)
}
func initAPI(ctx context.Context, ts topo.Server, actions *ActionRepository) {
tabletHealthCache := newTabletHealthCache(ts)
@ -206,4 +223,20 @@ func initAPI(ctx context.Context, ts topo.Server, actions *ActionRepository) {
ep, _, err := ts.GetEndPoints(ctx, parts[0], parts[1], parts[2], topo.TabletType(parts[3]))
return ep, err
})
// Schema Change
http.HandleFunc(apiPrefix+"schema/apply", func(w http.ResponseWriter, r *http.Request) {
req := struct{ Keyspace, SQL string }{}
if err := unmarshalRequest(r, &req); err != nil {
httpErrorf(w, r, "can't unmarshal request: %v", err)
return
}
executor := schemamanager.NewTabletExecutor(
tmclient.NewTabletManagerClient(),
ts)
schemamanager.Run(ctx,
schemamanager.NewUIController(req.SQL, req.Keyspace, w), executor)
})
}

Просмотреть файл

@ -71,6 +71,12 @@ func HandleExplorer(name, url, templateName string, exp Explorer) {
panic(fmt.Sprintf("Only one Explorer can be registered in vtctld. Trying to register %q, but %q was already registered.", name, explorerName))
}
// Topo explorer API for client-side vtctld app.
handleCollection("topodata", func(r *http.Request) (interface{}, error) {
return exp.HandlePath(actionRepo, path.Clean(url+getItemPath(r.URL.Path)), r), nil
})
// Old server-side explorer.
explorer = exp
explorerName = name
indexContent.ToplevelLinks[name+" Explorer"] = url

Просмотреть файл

@ -105,9 +105,9 @@ func (qre *QueryExecutor) Execute() (reply *mproto.QueryResult, err error) {
case planbuilder.PLAN_SET:
reply, err = qre.execSet()
case planbuilder.PLAN_OTHER:
conn, err := qre.getConn(qre.qe.connPool)
if err != nil {
return nil, err
conn, connErr := qre.getConn(qre.qe.connPool)
if connErr != nil {
return nil, connErr
}
defer conn.Recycle()
reply, err = qre.execSQL(conn, qre.query, true)
@ -183,7 +183,7 @@ func (qre *QueryExecutor) execDmlAutoCommit() (reply *mproto.QueryResult, err er
default:
return nil, NewTabletError(ErrFatal, "unsupported query: %s", qre.query)
}
return reply, nil
return reply, err
}
func (qre *QueryExecutor) checkPermissions() error {
@ -385,7 +385,7 @@ func (qre *QueryExecutor) execDirect(conn poolConn) (*mproto.QueryResult, error)
return nil, err
}
result.Fields = qre.plan.Fields
return result, err
return result, nil
}
return qre.fullFetch(conn, qre.plan.FullQuery, qre.bindVars, nil)
}

Просмотреть файл

@ -6,7 +6,7 @@
<h2 ng-bind="action.title"></h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="hide()">
<md-icon md-svg-src="close", aria-label="Close dialog"></md-icon>
<md-icon md-font-set="material-icons">close</md-icon>
</md-button>
</div>
</md-toolbar>
@ -19,8 +19,9 @@
</div>
<md-content ng-if="result.$resolved" class="md-padding">
<h2 ng-bind="result.Error === undefined || result.Error ? 'Action Failed' : 'Action Succeeded'"></h2>
<p ng-bind="result.Output"></p>
<h2 ng-if="result.Error">Action Failed</h2>
<h2 ng-if="!result.Error && !result.Output">Action Succeeded</h2>
<pre ng-bind="result.Output"></pre>
</md-content>
</md-dialog-content>

80
web/vtctld/actions.js Normal file
Просмотреть файл

@ -0,0 +1,80 @@
app.factory('actions', function($mdDialog, keyspaces, shards, tablets) {
var svc = {};
function actionDialogController($scope, $mdDialog, action, result) {
$scope.hide = function() { $mdDialog.hide(); };
$scope.action = action;
$scope.result = result;
}
function showResult(ev, action, result) {
$mdDialog.show({
controller: actionDialogController,
templateUrl: 'action-dialog.html',
parent: angular.element(document.body),
targetEvent: ev,
locals: {
action: action,
result: result
}
});
}
function confirm(ev, action, doIt) {
if (action.confirm) {
var dialog = $mdDialog.confirm()
.parent(angular.element(document.body))
.title('Confirm ' + action.title)
.content(action.confirm)
.ariaLabel('Confirm action')
.ok('OK')
.cancel('Cancel')
.targetEvent(ev);
$mdDialog.show(dialog).then(doIt);
} else {
doIt();
}
}
svc.applyKeyspace = function(ev, action, keyspace) {
confirm(ev, action, function() {
var result = keyspaces.action({
keyspace: keyspace, action: action.name
}, '');
showResult(ev, action, result);
});
};
svc.applyShard = function(ev, action, keyspace, shard) {
confirm(ev, action, function() {
var result = shards.action({
keyspace: keyspace,
shard: shard,
action: action.name
}, '');
showResult(ev, action, result);
});
};
svc.applyTablet = function(ev, action, tabletAlias) {
confirm(ev, action, function() {
var result = tablets.action({
tablet: tabletAlias.Cell+'-'+tabletAlias.Uid,
action: action.name
}, '');
showResult(ev, action, result);
});
};
svc.applyFunc = function(ev, action, func) {
confirm(ev, action, function() {
showResult(ev, action, func());
});
};
svc.label = function(action) {
return action.confirm ? action.title + '...' : action.title;
};
return svc;
});

33
web/vtctld/api.js Normal file
Просмотреть файл

@ -0,0 +1,33 @@
app.factory('cells', function($resource) {
return $resource('/api/cells/');
});
app.factory('keyspaces', function($resource) {
return $resource('/api/keyspaces/:keyspace', {}, {
'action': {method: 'POST'}
});
});
app.factory('shards', function($resource) {
return $resource('/api/shards/:keyspace/:shard', {}, {
'action': {method: 'POST'}
});
});
app.factory('tablets', function($resource) {
return $resource('/api/tablets/:tablet', {}, {
'action': {method: 'POST'}
});
});
app.factory('tabletinfo', function($resource) {
return $resource('/api/tablets/:tablet/:info');
});
app.factory('endpoints', function($resource) {
return $resource('/api/endpoints/:cell/:keyspace/:shard/:tabletType');
});
app.factory('topodata', function($resource) {
return $resource('/api/topodata/:path');
});

Просмотреть файл

@ -24,7 +24,7 @@ md-sidenav md-list .md-button {
color: inherit;
font-weight: 500;
text-align: left;
width: 100%;
width: 95%;
}
md-sidenav md-list .md-button.selected {
@ -49,15 +49,34 @@ md-toolbar h1 {
/* Vitess */
.shard-tile {
background-color: #eeeeee;
background-color: #eeeeee;
}
.shard-tile a {
color: inherit;
text-decoration: none;
text-align: center;
color: inherit;
text-decoration: none;
text-align: center;
}
.card-table-row {
padding: 8px 0px 8px 0px;
}
.breadcrumb {
color: #E8EAF6;
background-color: #3F51B5;
padding: 0px 12px 0px 12px;
}
.breadcrumb md-icon {
color: #E8EAF6;
}
.breadcrumb-divider {
font-size: 1.5em;
font-weight: 900;
}
textarea.code {
font-family: monospace;
}

Просмотреть файл

@ -17,7 +17,8 @@ app.constant('routes', [
urlPattern: '/keyspaces/',
templateUrl: 'keyspaces.html',
controller: 'KeyspacesCtrl',
showInNav: true
showInNav: true,
icon: 'dashboard'
},
{
name: 'shard',
@ -34,7 +35,8 @@ app.constant('routes', [
urlPattern: '/schema/',
templateUrl: 'schema.html',
controller: 'SchemaCtrl',
showInNav: true
showInNav: true,
icon: 'storage'
},
{
name: 'topo',
@ -43,18 +45,13 @@ app.constant('routes', [
urlPattern: '/topo/:path*?',
templateUrl: 'topo.html',
controller: 'TopoCtrl',
showInNav: true
showInNav: true,
icon: 'folder'
},
]);
app.config(function($mdThemingProvider, $mdIconProvider, $routeProvider,
app.config(function($mdThemingProvider, $routeProvider,
$resourceProvider, routes) {
$mdIconProvider
.icon("menu", "img/menu.svg", 24)
.icon("close", "img/close.svg", 24)
.icon("refresh", "img/refresh.svg", 24)
.icon("more_vert", "img/more_vert.svg", 24);
$mdThemingProvider.theme('default')
.primaryPalette('indigo')
.accentPalette('red');
@ -71,7 +68,10 @@ app.controller('AppCtrl', function($scope, $mdSidenav, $route, $location,
$scope.routes = routes;
$scope.refreshRoute = function() {
$route.current.locals.$scope.refreshData();
if ($route.current && $route.current.locals.$scope.refreshData)
$route.current.locals.$scope.refreshData();
else
$route.reload();
};
$scope.toggleNav = function() { $mdSidenav('left').toggle(); }
@ -88,276 +88,3 @@ app.controller('AppCtrl', function($scope, $mdSidenav, $route, $location,
$location.path(path);
};
});
app.controller('KeyspacesCtrl', function($scope, keyspaces, shards, actions) {
$scope.keyspaceActions = [
{name: 'ValidateKeyspace', title: 'Validate Keyspace'},
{name: 'ValidateSchemaKeyspace', title: 'Validate Schema'},
{name: 'ValidateVersionKeyspace', title: 'Validate Version'},
{name: 'ValidatePermissionsKeyspace', title: 'Validate Permissions'},
];
$scope.actions = actions;
$scope.refreshData = function() {
// Get list of keyspace names.
keyspaces.query(function(ksnames) {
$scope.keyspaces = [];
ksnames.forEach(function(name) {
// Get a list of shards for each keyspace.
$scope.keyspaces.push({
name: name,
shards: shards.query({keyspace: name})
});
});
});
};
$scope.refreshData();
});
app.controller('ShardCtrl', function($scope, $routeParams, $timeout,
shards, tablets, tabletinfo, actions) {
var keyspace = $routeParams.keyspace;
var shard = $routeParams.shard;
$scope.keyspace = {name: keyspace};
$scope.shard = {name: shard};
$scope.actions = actions;
$scope.shardActions = [
{name: 'ValidateShard', title: 'Validate Shard'},
{name: 'ValidateSchemaShard', title: 'Validate Schema'},
{name: 'ValidateVersionShard', title: 'Validate Version'},
{name: 'ValidatePermissionsShard', title: 'Validate Permissions'},
];
$scope.tabletActions = [
{name: 'Ping', title: 'Ping'},
{name: 'RefreshState', title: 'Refresh State', confirm: 'This will tell the tablet to re-read its topology record and adjust its state accordingly.'},
{name: 'ReloadSchema', title: 'Reload Schema', confirm: 'This will tell the tablet to refresh its schema cache by querying mysqld.'},
{name: 'ScrapTablet', title: 'Scrap', confirm: 'This will tell the tablet to remove itself from serving.'},
{name: 'ScrapTabletForce', title: 'Scrap (force)', confirm: 'This will externally remove the tablet from serving, without telling the tablet.'},
{name: 'DeleteTablet', title: 'Delete', confirm: 'This will delete the tablet record from topology.'},
];
$scope.tabletType = function(tablet) {
// Use streaming health result if present.
if (tablet.streamHealth && tablet.streamHealth.$resolved) {
if (!tablet.streamHealth.target)
return 'spare';
if (tablet.streamHealth.target.tablet_type)
return vtTabletTypes[tablet.streamHealth.target.tablet_type];
}
return tablet.Type;
};
$scope.tabletHealthError = function(tablet) {
if (tablet.streamHealth && tablet.streamHealth.realtime_stats
&& tablet.streamHealth.realtime_stats.health_error) {
return tablet.streamHealth.realtime_stats.health_error;
}
return '';
};
$scope.tabletAccent = function(tablet) {
if ($scope.tabletHealthError(tablet))
return 'md-warn md-hue-2';
switch ($scope.tabletType(tablet)) {
case 'master': return 'md-hue-2';
case 'replica': return 'md-hue-3';
default: return 'md-hue-1';
}
};
$scope.refreshData = function() {
// Get the shard data.
shards.get({keyspace: keyspace, shard: shard}, function(shardData) {
shardData.name = shard;
$scope.shard = shardData;
});
// Get a list of tablet aliases in the shard, in all cells.
tablets.query({shard: keyspace+'/'+shard}, function(tabletAliases) {
// Group them by cell.
var cellMap = {};
tabletAliases.forEach(function(tabletAlias) {
if (cellMap[tabletAlias.Cell] === undefined)
cellMap[tabletAlias.Cell] = [];
cellMap[tabletAlias.Cell].push(tabletAlias);
});
// Turn the cell map into a list, sorted by cell name.
var cellList = [];
Object.keys(cellMap).sort().forEach(function(cellName) {
// Sort the tablets within each cell.
var tabletAliases = cellMap[cellName];
tabletAliases.sort(function(a, b) { return a.Uid - b.Uid; });
// Fetch tablet data.
var tabletData = [];
tabletAliases.forEach(function(tabletAlias) {
var alias = tabletAlias.Cell+'-'+tabletAlias.Uid;
var tablet = tablets.get({tablet: alias}, function(tablet) {
// Annotate result with some extra stuff.
tablet.links = vtconfig.tabletLinks(tablet);
});
tablet.Alias = tabletAlias;
tabletData.push(tablet);
});
// Add tablet data to the cell list.
cellList.push({
name: cellName,
tablets: tabletData
});
});
$scope.cells = cellList;
});
};
var selectedCell;
$scope.setSelectedCell = function(cell) {
selectedCell = cell;
refreshStreamHealth();
};
function refreshStreamHealth() {
if (selectedCell) {
selectedCell.tablets.forEach(function (tablet) {
if (tablet.Alias) {
// Get latest streaming health result.
tabletinfo.get({tablet: tablet.Alias.Cell+'-'+tablet.Alias.Uid, info: 'health'}, function(health) {
tablet.streamHealth = health;
});
}
});
}
};
$scope.refreshData();
function periodicRefresh() {
refreshStreamHealth();
$timeout(periodicRefresh, 3000);
}
periodicRefresh();
});
app.controller('SchemaCtrl', function() {
});
app.controller('TopoCtrl', function($scope, $routeParams) {
$scope.path = $routeParams.path;
});
app.factory('actions', function($mdDialog, keyspaces, shards, tablets) {
var svc = {};
function actionDialogController($scope, $mdDialog, action, result) {
$scope.hide = function() { $mdDialog.hide(); };
$scope.action = action;
$scope.result = result;
}
function showResult(ev, action, result) {
$mdDialog.show({
controller: actionDialogController,
templateUrl: 'action-dialog.html',
parent: angular.element(document.body),
targetEvent: ev,
locals: {
action: action,
result: result
}
});
}
function confirm(ev, action, doIt) {
if (action.confirm) {
var dialog = $mdDialog.confirm()
.parent(angular.element(document.body))
.title('Confirm ' + action.title)
.content(action.confirm)
.ariaLabel('Confirm action')
.ok('OK')
.cancel('Cancel')
.targetEvent(ev);
$mdDialog.show(dialog).then(doIt);
} else {
doIt();
}
}
svc.applyKeyspace = function(ev, action, keyspace) {
confirm(ev, action, function() {
var result = keyspaces.action({
keyspace: keyspace, action: action.name
}, '');
showResult(ev, action, result);
});
};
svc.applyShard = function(ev, action, keyspace, shard) {
confirm(ev, action, function() {
var result = shards.action({
keyspace: keyspace,
shard: shard,
action: action.name
}, '');
showResult(ev, action, result);
});
};
svc.applyTablet = function(ev, action, tabletAlias) {
confirm(ev, action, function() {
var result = tablets.action({
tablet: tabletAlias.Cell+'-'+tabletAlias.Uid,
action: action.name
}, '');
showResult(ev, action, result);
});
};
svc.label = function(action) {
return action.confirm ? action.title + '...' : action.title;
};
return svc;
});
app.factory('cells', function($resource) {
return $resource('/api/cells/');
});
app.factory('keyspaces', function($resource) {
return $resource('/api/keyspaces/:keyspace', {}, {
'action': {method: 'POST'}
});
});
app.factory('shards', function($resource) {
return $resource('/api/shards/:keyspace/:shard', {}, {
'action': {method: 'POST'}
});
});
app.factory('tablets', function($resource) {
return $resource('/api/tablets/:tablet', {}, {
'action': {method: 'POST'}
});
});
app.factory('tabletinfo', function($resource) {
return $resource('/api/tablets/:tablet/:info');
});
app.factory('endpoints', function($resource) {
return $resource('/api/endpoints/:cell/:keyspace/:shard/:tabletType');
});

Просмотреть файл

@ -1,12 +1,12 @@
// This file contains config that may need to be changed
// on a site-local basis.
vtconfig = {
tabletLinks: function(tablet) {
return [
{
title: 'Status',
href: 'http://'+tablet.Hostname+':'+tablet.Portmap.vt+'/debug/status'
}
];
}
tabletLinks: function(tablet) {
return [
{
title: 'Status',
href: 'http://'+tablet.Hostname+':'+tablet.Portmap.vt+'/debug/status'
}
];
}
};

Просмотреть файл

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>

До

Ширина:  |  Высота:  |  Размер: 202 B

Просмотреть файл

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24">
<path d="M0 0h24v24h-24z" fill="none"/>
<path d="M3 18h18v-2h-18v2zm0-5h18v-2h-18v2zm0-7v2h18v-2h-18z"/>
</svg>

До

Ширина:  |  Высота:  |  Размер: 208 B

Просмотреть файл

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<path d="M0 0h18v18h-18z" fill="none"/>
<path d="M9 5.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5-1.5.67-1.5 1.5.67 1.5 1.5 1.5zm0 2c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0 5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"/>
</svg>

До

Ширина:  |  Высота:  |  Размер: 378 B

Просмотреть файл

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g id="Header">
<g>
<rect x="-618" y="-2616" fill="none" width="1400" height="3600"/>
</g>
</g>
<g id="Label">
</g>
<g id="Icon">
<g>
<path d="M17.6,6.4C16.2,4.9,14.2,4,12,4c-4.4,0-8,3.6-8,8s3.6,8,8,8c3.7,0,6.8-2.6,7.7-6h-2.1c-0.8,2.3-3,4-5.6,4
c-3.3,0-6-2.7-6-6s2.7-6,6-6c1.7,0,3.1,0.7,4.2,1.8L13,11h7V4L17.6,6.4z"/>
<rect fill="none" width="24" height="24"/>
</g>
</g>
<g id="Grid" display="none">
<g display="inline">
</g>
</g>
</svg>

До

Ширина:  |  Высота:  |  Размер: 952 B

Просмотреть файл

@ -9,7 +9,8 @@
<meta name="description" content="">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />
<link rel='stylesheet' href='http://fonts.googleapis.com/css?family=Roboto:400,500,700,400italic'>
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Roboto:400,500,700,400italic">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/angular_material/0.10.0/angular-material.min.css">
<link rel="stylesheet" href="./app.css"/>
@ -18,38 +19,39 @@
<body ng-app="vtctld" layout="row" ng-controller="AppCtrl as app">
<md-sidenav class="site-sidenav md-sidenav-left md-whiteframe-z2"
md-component-id="left"
md-is-locked-open="$mdMedia('gt-md')">
md-component-id="left"
md-is-locked-open="$mdMedia('gt-md')">
<md-toolbar class="md-whiteframe-z1">
<h1>Vitess</h1>
</md-toolbar>
<md-toolbar class="md-whiteframe-z1">
<h1>Vitess</h1>
</md-toolbar>
<md-list>
<md-list-item ng-repeat="route in routes | filter:{showInNav:true}">
<md-button ng-class="{ 'selected': navIsSelected(route) }" ng-click="navigate(route.urlBase)">
{{route.title}}
</md-button>
</md-list-item>
</md-list>
<md-list>
<md-list-item ng-repeat="route in routes | filter:{showInNav:true}">
<md-button ng-class="{ 'selected': navIsSelected(route) }" ng-href="#{{route.urlBase}}">
<md-icon md-font-set="material-icons" ng-bind="route.icon"></md-icon>
{{route.title}}
</md-button>
</md-list-item>
</md-list>
</md-sidenav>
<div flex layout="column" tabIndex="-1" role="main" class="md-whiteframe-z2">
<md-toolbar layout="row" class="md-whiteframe-z1">
<md-button hide-gt-md ng-click="toggleNav()" aria-label="Toggle Nav Bar">
<md-icon md-svg-icon="menu"></md-icon>
</md-button>
<md-toolbar layout="row" class="md-whiteframe-z1" layout-align="space-between center">
<md-button class="md-icon-button" hide-gt-md ng-click="toggleNav()" aria-label="Toggle Nav Bar">
<md-icon md-font-set="material-icons">menu</md-icon>
</md-button>
<h1 ng-bind="navTitle()"></h1>
<h1 ng-bind="navTitle()"></h1>
<md-button ng-click="refreshRoute()" aria-label="Refresh">
<md-icon md-svg-icon="refresh"></md-icon>
</md-button>
</md-toolbar>
<md-button class="md-icon-button" ng-click="refreshRoute()" aria-label="Refresh">
<md-icon md-font-set="material-icons">refresh</md-icon>
</md-button>
</md-toolbar>
<div flex layout="column" id="content" ng-view></div>
<div flex layout="column" id="content" ng-view></div>
</div>
@ -62,6 +64,12 @@
<script src="./config.js"></script>
<script src="./app.js"></script>
<script src="./api.js"></script>
<script src="./keyspaces.js"></script>
<script src="./shard.js"></script>
<script src="./topo.js"></script>
<script src="./actions.js"></script>
<script src="./schema.js"></script>
</body>
</html>

Просмотреть файл

@ -1,46 +1,46 @@
<div ng-if="keyspaces === undefined || keyspaces === null" flex layout="column" layout-align="center center">
<md-progress-circular md-mode="indeterminate"></md-progress-circular>
<h3>Loading...</h3>
<md-progress-circular md-mode="indeterminate"></md-progress-circular>
<h3>Loading...</h3>
</div>
<div ng-if="keyspaces.length == 0" flex layout="column" layout-align="center center">
<h3>No keyspaces found.</h3>
<h3>No keyspaces found.</h3>
</div>
<md-content class="md-padding">
<md-card ng-repeat="keyspace in keyspaces">
<md-toolbar>
<div class="md-toolbar-tools">
<h2>{{keyspace.name}}</h2>
<span flex></span>
<md-menu>
<md-button class="md-icon-button" aria-label="Keyspace actions" ng-click="$mdOpenMenu()" md-menu-origin>
<md-icon md-svg-icon="more_vert"></md-icon>
</md-button>
<md-menu-content>
<md-menu-item ng-repeat="action in keyspaceActions">
<md-toolbar>
<div class="md-toolbar-tools">
<h2>{{keyspace.name}}</h2>
<span flex></span>
<md-menu>
<md-button class="md-icon-button" aria-label="Keyspace actions" ng-click="$mdOpenMenu()" md-menu-origin>
<md-icon md-font-set="material-icons">more_vert</md-icon>
</md-button>
<md-menu-content>
<md-menu-item ng-repeat="action in keyspaceActions">
<md-button ng-click="actions.applyKeyspace($event,action,keyspace.name)">{{actions.label(action)}}</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
</div>
</md-toolbar>
</md-menu-content>
</md-menu>
</div>
</md-toolbar>
<md-card-content>
<md-card-content>
<h3 ng-bind="keyspace.shards.length == 0 ? 'No shards found.' : 'Shards'"></h3>
<h3 ng-bind="keyspace.shards.length == 0 ? 'No shards found.' : 'Shards'"></h3>
<md-grid-list md-cols-sm="2" md-cols-md="4" md-cols-gt-md="8"
md-row-height="2:1" md-gutter="1em">
<md-grid-tile ng-repeat="shard in keyspace.shards" class="shard-tile md-whiteframe-z1">
<a ng-href="#/shard/{{keyspace.name}}/{{shard}}" layout-fill layout="column" layout-align="center">
<h3>{{shard}}</h3>
</a>
<a ng-href="#/shard/{{keyspace.name}}/{{shard}}" layout-fill layout="column" layout-align="center">
<h3>{{shard}}</h3>
</a>
</md-grid-tile>
</md-grid-list>
</md-card-content>
</md-card-content>
</md-card>
</md-content>

25
web/vtctld/keyspaces.js Normal file
Просмотреть файл

@ -0,0 +1,25 @@
app.controller('KeyspacesCtrl', function($scope, keyspaces, shards, actions) {
$scope.keyspaceActions = [
{name: 'ValidateKeyspace', title: 'Validate Keyspace'},
{name: 'ValidateSchemaKeyspace', title: 'Validate Schema'},
{name: 'ValidateVersionKeyspace', title: 'Validate Version'},
{name: 'ValidatePermissionsKeyspace', title: 'Validate Permissions'},
];
$scope.actions = actions;
$scope.refreshData = function() {
// Get list of keyspace names.
keyspaces.query(function(ksnames) {
$scope.keyspaces = [];
ksnames.forEach(function(name) {
// Get a list of shards for each keyspace.
$scope.keyspaces.push({
name: name,
shards: shards.query({keyspace: name})
});
});
});
};
$scope.refreshData();
});

Просмотреть файл

@ -1 +1,20 @@
<md-content class="md-padding">
<md-tabs md-border-bottom md-center-tabs="false">
<md-tab label="Apply Schema">
<md-select placeholder="Select Keyspace" ng-model="schemaChange.Keyspace">
<md-option ng-repeat="keyspace in keyspaces" value="{{keyspace}}">{{keyspace}}</md-option>
</md-select>
<md-input-container flex>
<label>Schema Change SQL</label>
<textarea ng-model="schemaChange.SQL" class="code"></textarea>
</md-input-container>
<md-button class="md-primary" ng-click="submitSchema($event)">Submit</md-button>
</md-tab>
</md-tabs>
</md-content>

33
web/vtctld/schema.js Normal file
Просмотреть файл

@ -0,0 +1,33 @@
app.controller('SchemaCtrl', function($scope, $http, $mdDialog,
actions, keyspaces) {
$scope.schemaChange = {Keyspace: '', SQL: ''};
$scope.refreshData = function() {
$scope.keyspaces = keyspaces.query();
};
$scope.refreshData();
$scope.submitSchema = function(ev) {
var action = {
title: 'Apply Schema',
confirm: 'This will execute the provided SQL on all shards in the keyspace.'
};
actions.applyFunc(ev, action, function() {
var result = {$resolved: false};
$http.post('/api/schema/apply', $scope.schemaChange)
.success(function(data) {
result.$resolved = true;
result.Output = data;
result.Error = false;
})
.error(function(data) {
result.$resolved = true;
result.Output = data;
result.Error = true;
});
return result;
});
};
});

Просмотреть файл

@ -7,7 +7,7 @@
<span flex></span>
<md-menu>
<md-button class="md-icon-button" aria-label="Shard actions" ng-click="$mdOpenMenu()" md-menu-origin>
<md-icon md-svg-icon="more_vert"></md-icon>
<md-icon md-font-set="material-icons">more_vert</md-icon>
</md-button>
<md-menu-content>
<md-menu-item ng-repeat="action in shardActions">
@ -20,22 +20,22 @@
<md-card-content>
<div layout="column">
<div layout="row" layout-align="start center">
<h3 flex="50">Keyspace/Shard</h3>
<div flex ng-bind="keyspace.name+'/'+shard.name"></div>
</div>
<div layout="row" layout-align="start center">
<h3 flex="50">Keyspace/Shard</h3>
<div flex ng-bind="keyspace.name+'/'+shard.name"></div>
</div>
<md-divider></md-divider>
<md-divider></md-divider>
<div layout="row" layout-align="start center">
<h3 flex="50">Master Tablet</h3>
<div flex>
<span ng-if="!shard.MasterAlias.Uid">None</span>
<a ng-if="shard.MasterAlias.Uid"
ng-href="#/tablets/{{shard.MasterAlias.Cell}}-{{shard.MasterAlias.Uid}}"
ng-bind="shard.MasterAlias.Cell+'-'+shard.MasterAlias.Uid"></a>
</div>
</div>
<div layout="row" layout-align="start center">
<h3 flex="50">Master Tablet</h3>
<div flex>
<span ng-if="!shard.MasterAlias.Uid">None</span>
<a ng-if="shard.MasterAlias.Uid"
ng-href="#/tablets/{{shard.MasterAlias.Cell}}-{{shard.MasterAlias.Uid}}"
ng-bind="shard.MasterAlias.Cell+'-'+shard.MasterAlias.Uid"></a>
</div>
</div>
</div>
</md-card-content>
@ -54,29 +54,29 @@
<md-card ng-repeat="tablet in cell.tablets" flex-sm="95" flex-md="95" flex-gt-md="45">
<md-toolbar ng-class="tabletAccent(tablet)">
<div class="md-toolbar-tools">
<h3>{{tablet.Alias.Cell}}-{{tablet.Alias.Uid}} [{{tabletType(tablet)}}]</h3>
<span flex></span>
<md-menu>
<md-button class="md-icon-button" aria-label="Tablet actions" ng-click="$mdOpenMenu()" md-menu-origin>
<md-icon md-svg-icon="more_vert"></md-icon>
</md-button>
<md-menu-content>
<md-menu-item ng-repeat="action in tabletActions">
<md-toolbar ng-class="tabletAccent(tablet)">
<div class="md-toolbar-tools">
<h3>{{tablet.Alias.Cell}}-{{tablet.Alias.Uid}} [{{tabletType(tablet)}}]</h3>
<span flex></span>
<md-menu>
<md-button class="md-icon-button" aria-label="Tablet actions" ng-click="$mdOpenMenu()" md-menu-origin>
<md-icon md-font-set="material-icons">more_vert</md-icon>
</md-button>
<md-menu-content>
<md-menu-item ng-repeat="action in tabletActions">
<md-button ng-click="actions.applyTablet($event, action, tablet.Alias)">{{actions.label(action)}}</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
</div>
</md-toolbar>
</md-menu-content>
</md-menu>
</div>
</md-toolbar>
<md-card-content layout="column">
<md-card-content layout="column">
<div class="card-table-row" layout="row" layout-align="space-between" layout-wrap>
<div class="card-table-row" layout="row" layout-align="space-between" layout-wrap>
<strong>Host</strong>
<a href="http://{{tablet.Hostname}}:{{tablet.Portmap.vt}}">{{tablet.Hostname}}:{{tablet.Portmap.vt}}</a>
</div>
<a href="http://{{tablet.Hostname}}:{{tablet.Portmap.vt}}">{{tablet.Hostname}}:{{tablet.Portmap.vt}}</a>
</div>
<md-divider></md-divider>
@ -87,7 +87,7 @@
<md-divider></md-divider>
<div class="card-table-row" layout="row" layout-align="space-between" layout-wrap>
<div class="card-table-row" layout="row" layout-align="space-between" layout-wrap>
<strong>Seconds Behind Master</strong>
<span ng-bind="tablet.streamHealth.realtime_stats.second_behind_master || 0"></span>
</div>
@ -99,11 +99,11 @@
<span ng-bind="tablet.streamHealth.realtime_stats.health_error || 'None'"></span>
</div>
</md-card-content>
</md-card-content>
<div class="md-actions" layout="row" layout-align="end center">
<md-button ng-repeat="link in tablet.links" ng-href="{{link.href}}">{{link.title}}</md-button>
</div>
<div class="md-actions" layout="row" layout-align="end center">
<md-button ng-repeat="link in tablet.links" ng-href="{{link.href}}">{{link.title}}</md-button>
</div>
</md-card>

135
web/vtctld/shard.js Normal file
Просмотреть файл

@ -0,0 +1,135 @@
app.controller('ShardCtrl', function($scope, $routeParams, $timeout, $route,
shards, tablets, tabletinfo, actions) {
var keyspace = $routeParams.keyspace;
var shard = $routeParams.shard;
$scope.keyspace = {name: keyspace};
$scope.shard = {name: shard};
$scope.actions = actions;
$scope.shardActions = [
{name: 'ValidateShard', title: 'Validate Shard'},
{name: 'ValidateSchemaShard', title: 'Validate Schema'},
{name: 'ValidateVersionShard', title: 'Validate Version'},
{name: 'ValidatePermissionsShard', title: 'Validate Permissions'},
];
$scope.tabletActions = [
{name: 'Ping', title: 'Ping'},
{name: 'RefreshState', title: 'Refresh State', confirm: 'This will tell the tablet to re-read its topology record and adjust its state accordingly.'},
{name: 'ReloadSchema', title: 'Reload Schema', confirm: 'This will tell the tablet to refresh its schema cache by querying mysqld.'},
{name: 'ScrapTablet', title: 'Scrap', confirm: 'This will tell the tablet to remove itself from serving.'},
{name: 'ScrapTabletForce', title: 'Scrap (force)', confirm: 'This will externally remove the tablet from serving, without telling the tablet.'},
{name: 'DeleteTablet', title: 'Delete', confirm: 'This will delete the tablet record from topology.'},
];
$scope.tabletType = function(tablet) {
// Use streaming health result if present.
if (tablet.streamHealth && tablet.streamHealth.$resolved) {
if (!tablet.streamHealth.target)
return 'spare';
if (tablet.streamHealth.target.tablet_type)
return vtTabletTypes[tablet.streamHealth.target.tablet_type];
}
return tablet.Type;
};
$scope.tabletHealthError = function(tablet) {
if (tablet.streamHealth && tablet.streamHealth.realtime_stats
&& tablet.streamHealth.realtime_stats.health_error) {
return tablet.streamHealth.realtime_stats.health_error;
}
return '';
};
$scope.tabletAccent = function(tablet) {
if ($scope.tabletHealthError(tablet))
return 'md-warn md-hue-2';
switch ($scope.tabletType(tablet)) {
case 'master': return 'md-hue-2';
case 'replica': return 'md-hue-3';
default: return 'md-hue-1';
}
};
$scope.refreshData = function() {
// Get the shard data.
shards.get({keyspace: keyspace, shard: shard}, function(shardData) {
shardData.name = shard;
$scope.shard = shardData;
});
// Get a list of tablet aliases in the shard, in all cells.
tablets.query({shard: keyspace+'/'+shard}, function(tabletAliases) {
// Group them by cell.
var cellMap = {};
tabletAliases.forEach(function(tabletAlias) {
if (cellMap[tabletAlias.Cell] === undefined)
cellMap[tabletAlias.Cell] = [];
cellMap[tabletAlias.Cell].push(tabletAlias);
});
// Turn the cell map into a list, sorted by cell name.
var cellList = [];
Object.keys(cellMap).sort().forEach(function(cellName) {
// Sort the tablets within each cell.
var tabletAliases = cellMap[cellName];
tabletAliases.sort(function(a, b) { return a.Uid - b.Uid; });
// Fetch tablet data.
var tabletData = [];
tabletAliases.forEach(function(tabletAlias) {
var alias = tabletAlias.Cell+'-'+tabletAlias.Uid;
var tablet = tablets.get({tablet: alias}, function(tablet) {
// Annotate result with some extra stuff.
tablet.links = vtconfig.tabletLinks(tablet);
});
tablet.Alias = tabletAlias;
tabletData.push(tablet);
});
// Add tablet data to the cell list.
cellList.push({
name: cellName,
tablets: tabletData
});
});
$scope.cells = cellList;
});
};
var selectedCell;
$scope.setSelectedCell = function(cell) {
selectedCell = cell;
refreshStreamHealth();
};
function refreshStreamHealth() {
if (selectedCell) {
selectedCell.tablets.forEach(function (tablet) {
if (tablet.Alias) {
// Get latest streaming health result.
tabletinfo.get({tablet: tablet.Alias.Cell+'-'+tablet.Alias.Uid, info: 'health'}, function(health) {
tablet.streamHealth = health;
});
}
});
}
};
$scope.refreshData();
function periodicRefresh() {
if ($route.current.name != 'shard') return;
refreshStreamHealth();
$timeout(periodicRefresh, 3000);
}
periodicRefresh();
});

Просмотреть файл

@ -1 +1,37 @@
<h1 ng-bind="path"></h1>
<div ng-if="!node.$resolved" flex layout="column" layout-align="center center">
<md-progress-circular md-mode="indeterminate"></md-progress-circular>
<h3>Loading...</h3>
</div>
<md-content ng-if="node.$resolved" class="md-padding">
<div class="breadcrumb md-whiteframe-z1">
<md-button href="#/topo/">
<md-icon md-font-set="material-icons">folder</md-icon>
</md-button>
<span class="breadcrumb-divider">/</span>
<span ng-repeat="elem in breadcrumbs">
<md-button ng-href="#/topo/{{elem.path}}">{{elem.name}}</md-button>
<span ng-if="!$last" class="breadcrumb-divider">/</span>
</span>
</div>
<h2 ng-bind="node.Path"></h2>
<md-list ng-if="node.Children">
<md-list-item class="md-whiteframe-z1" ng-repeat="child in node.Children" ng-click="navigate('/topo/' + (path ? path+'/' : '') + child)">
<md-icon md-font-set="material-icons">folder</md-icon>
<h3 flex>{{child}}</h3>
</md-list-item>
</md-list>
<md-content ng-if="node.Data" class="md-padding md-whiteframe-z2">
<pre>{{node.Data}}</pre>
</md-content>
<div ng-if="node.Error">
<h2>Error</h2>
<p>{{node.Error}}</p>
</div>
</md-content>

20
web/vtctld/topo.js Normal file
Просмотреть файл

@ -0,0 +1,20 @@
app.controller('TopoCtrl', function($scope, $routeParams, topodata) {
var path = $routeParams.path;
$scope.path = path;
var crumbs = [];
if (path) {
var elems = path.split('/');
while (elems.length > 0) {
var elemPath = elems.join('/');
crumbs.unshift({name: elems.pop(), path: elemPath});
}
}
$scope.breadcrumbs = crumbs;
$scope.refreshData = function() {
$scope.node = topodata.get({path: path});
};
$scope.refreshData();
});