diff --git a/bootstrap.sh b/bootstrap.sh
index a641a7235e..dbee0f4c80 100755
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -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."
+
diff --git a/go/cmd/vtctld/api.go b/go/cmd/vtctld/api.go
index f60f5da415..c4b5332046 100644
--- a/go/cmd/vtctld/api.go
+++ b/go/cmd/vtctld/api.go
@@ -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)
+ })
}
diff --git a/go/cmd/vtctld/explorer.go b/go/cmd/vtctld/explorer.go
index c1ca548d0c..cb6e8c117b 100644
--- a/go/cmd/vtctld/explorer.go
+++ b/go/cmd/vtctld/explorer.go
@@ -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
diff --git a/go/vt/tabletserver/query_executor.go b/go/vt/tabletserver/query_executor.go
index a1eda00ea5..2b92473712 100644
--- a/go/vt/tabletserver/query_executor.go
+++ b/go/vt/tabletserver/query_executor.go
@@ -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)
}
diff --git a/web/vtctld/action-dialog.html b/web/vtctld/action-dialog.html
index b3e58d9c35..7c582d8ee2 100644
--- a/web/vtctld/action-dialog.html
+++ b/web/vtctld/action-dialog.html
@@ -6,7 +6,7 @@
-
+ close
@@ -19,8 +19,9 @@
-
-
+Action Failed
+Action Succeeded
+
diff --git a/web/vtctld/actions.js b/web/vtctld/actions.js
new file mode 100644
index 0000000000..439592a95b
--- /dev/null
+++ b/web/vtctld/actions.js
@@ -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;
+});
diff --git a/web/vtctld/api.js b/web/vtctld/api.js
new file mode 100644
index 0000000000..013e85cf11
--- /dev/null
+++ b/web/vtctld/api.js
@@ -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');
+});
diff --git a/web/vtctld/app.css b/web/vtctld/app.css
index b49cf69b3a..9313f0e7fd 100644
--- a/web/vtctld/app.css
+++ b/web/vtctld/app.css
@@ -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;
+}
diff --git a/web/vtctld/app.js b/web/vtctld/app.js
index df0b0c5eb8..4ab24edc6c 100644
--- a/web/vtctld/app.js
+++ b/web/vtctld/app.js
@@ -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');
-});
diff --git a/web/vtctld/config.js b/web/vtctld/config.js
index 1c76e0c84e..4e42d53e9a 100644
--- a/web/vtctld/config.js
+++ b/web/vtctld/config.js
@@ -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'
+ }
+ ];
+ }
};
diff --git a/web/vtctld/img/close.svg b/web/vtctld/img/close.svg
deleted file mode 100644
index 865788b755..0000000000
--- a/web/vtctld/img/close.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/vtctld/img/menu.svg b/web/vtctld/img/menu.svg
deleted file mode 100644
index 3dda0b1b1d..0000000000
--- a/web/vtctld/img/menu.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/web/vtctld/img/more_vert.svg b/web/vtctld/img/more_vert.svg
deleted file mode 100644
index 6e1d96db64..0000000000
--- a/web/vtctld/img/more_vert.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/web/vtctld/img/refresh.svg b/web/vtctld/img/refresh.svg
deleted file mode 100644
index bfd3c59ffa..0000000000
--- a/web/vtctld/img/refresh.svg
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
diff --git a/web/vtctld/index.html b/web/vtctld/index.html
index cfcc0376bc..0e38050336 100644
--- a/web/vtctld/index.html
+++ b/web/vtctld/index.html
@@ -9,7 +9,8 @@
-
+
+
@@ -18,38 +19,39 @@
+ md-component-id="left"
+ md-is-locked-open="$mdMedia('gt-md')">
-
- Vitess
-
+
+ Vitess
+
-
-
-
- {{route.title}}
-
-
-
+
+
+
+
+ {{route.title}}
+
+
+
-
-
-
-
+
+
+ menu
+
-
+
-
-
-
-
+
+ refresh
+
+
-
+
@@ -62,6 +64,12 @@
+
+
+
+
+
+