зеркало из https://github.com/github/vitess-gh.git
Merge branch 'master' into resharding
This commit is contained in:
Коммит
68426d028c
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
Загрузка…
Ссылка в новой задаче