Merge pull request #993 from influxdata/feature/admin
Add Admin for Users, Roles, Permissions, and Queries for InfluxDB and InfluxEnterprisepull/10616/head
commit
4abf2408e3
|
@ -6,9 +6,13 @@
|
|||
3. [#968](https://github.com/influxdata/chronograf/issue/968): Fix wrong database used in dashboards
|
||||
|
||||
### Features
|
||||
1. [#993](https://github.com/influxdata/chronograf/pull/993): Add Admin page with ability to manage Users, Roles, and Permissions for InfluxDB and Enterprise
|
||||
2. [#993](https://github.com/influxdata/chronograf/pull/993): Add ability to manage Queries for InfluxDB and Enterprise
|
||||
|
||||
### UI Improvements
|
||||
1. [#989](https://github.com/influxdata/chronograf/pull/989) Add a canned dashboard for mesos
|
||||
2. [#993](https://github.com/influxdata/chronograf/pull/993): Improve multi-select dropdown
|
||||
3. [#993](https://github.com/influxdata/chronograf/pull/993): Provide better error information to users
|
||||
|
||||
## v1.2.0-beta4 [2017-02-24]
|
||||
|
||||
|
|
|
@ -66,16 +66,20 @@ func (c *RolesStore) Get(ctx context.Context, name string) (*chronograf.Role, er
|
|||
|
||||
// Update the Role's permissions and roles
|
||||
func (c *RolesStore) Update(ctx context.Context, u *chronograf.Role) error {
|
||||
perms := ToEnterprise(u.Permissions)
|
||||
if err := c.Ctrl.SetRolePerms(ctx, u.Name, perms); err != nil {
|
||||
return err
|
||||
if u.Permissions != nil {
|
||||
perms := ToEnterprise(u.Permissions)
|
||||
if err := c.Ctrl.SetRolePerms(ctx, u.Name, perms); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
users := make([]string, len(u.Users))
|
||||
for i, u := range u.Users {
|
||||
users[i] = u.Name
|
||||
if u.Users != nil {
|
||||
users := make([]string, len(u.Users))
|
||||
for i, u := range u.Users {
|
||||
users[i] = u.Name
|
||||
}
|
||||
return c.Ctrl.SetRoleUsers(ctx, u.Name, users)
|
||||
}
|
||||
return c.Ctrl.SetRoleUsers(ctx, u.Name, users)
|
||||
return nil
|
||||
}
|
||||
|
||||
// All is all Roles in influx
|
||||
|
|
|
@ -18,6 +18,7 @@ func (c *UserStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.Us
|
|||
return nil, err
|
||||
}
|
||||
perms := ToEnterprise(u.Permissions)
|
||||
|
||||
if err := c.Ctrl.SetUserPerms(ctx, u.Name, perms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
'arrow-parens': 0,
|
||||
'comma-dangle': [2, 'always-multiline'],
|
||||
'no-cond-assign': 2,
|
||||
'no-console': 2,
|
||||
'no-console': ['error', {allow: ['error']}],
|
||||
'no-constant-condition': 2,
|
||||
'no-control-regex': 2,
|
||||
'no-debugger': 2,
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
const express = require('express');
|
||||
const request = require('request');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use('/', (req, res) => {
|
||||
console.log(`${req.method} ${req.url}`);
|
||||
|
||||
const headers = {};
|
||||
headers['Access-Control-Allow-Origin'] = '*';
|
||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS';
|
||||
headers['Access-Control-Allow-Credentials'] = false;
|
||||
headers['Access-Control-Max-Age'] = '86400'; // 24 hours
|
||||
headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept';
|
||||
res.writeHead(200, headers);
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.end();
|
||||
}
|
||||
else {
|
||||
const url = 'http://localhost:8888' + req.url;
|
||||
req.pipe(request(url)).pipe(res);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3888, () => {
|
||||
console.log('corsless proxy server now running')
|
||||
});
|
|
@ -17,9 +17,7 @@
|
|||
"test:lint": "npm run lint; npm run test",
|
||||
"test:dev": "nodemon --exec npm run test:lint",
|
||||
"clean": "rm -rf build",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook",
|
||||
"proxy": "node ./corsless"
|
||||
"storybook": "node ./storybook"
|
||||
},
|
||||
"author": "",
|
||||
"eslintConfig": {
|
||||
|
|
|
@ -0,0 +1,292 @@
|
|||
import reducer from 'src/admin/reducers/admin'
|
||||
|
||||
import {
|
||||
addUser,
|
||||
addRole,
|
||||
syncUser,
|
||||
syncRole,
|
||||
editUser,
|
||||
editRole,
|
||||
loadRoles,
|
||||
loadPermissions,
|
||||
deleteRole,
|
||||
deleteUser,
|
||||
filterRoles,
|
||||
filterUsers,
|
||||
} from 'src/admin/actions'
|
||||
|
||||
let state = undefined
|
||||
|
||||
// Users
|
||||
const u1 = {
|
||||
name: 'acidburn',
|
||||
roles: [
|
||||
{
|
||||
name: 'hax0r',
|
||||
permissions: {
|
||||
allowed: [
|
||||
'ViewAdmin',
|
||||
'ViewChronograf',
|
||||
'CreateDatabase',
|
||||
'CreateUserAndRole',
|
||||
'AddRemoveNode',
|
||||
'DropDatabase',
|
||||
'DropData',
|
||||
'ReadData',
|
||||
'WriteData',
|
||||
'Rebalance',
|
||||
'ManageShard',
|
||||
'ManageContinuousQuery',
|
||||
'ManageQuery',
|
||||
'ManageSubscription',
|
||||
'Monitor',
|
||||
'CopyShard',
|
||||
'KapacitorAPI',
|
||||
'KapacitorConfigAPI'
|
||||
],
|
||||
scope: 'all',
|
||||
},
|
||||
}
|
||||
],
|
||||
permissions: [],
|
||||
links: {self: '/chronograf/v1/sources/1/users/acidburn'},
|
||||
}
|
||||
const u2 = {
|
||||
name: 'zerocool',
|
||||
roles: [],
|
||||
permissions: [],
|
||||
links: {self: '/chronograf/v1/sources/1/users/zerocool'},
|
||||
}
|
||||
const users = [u1, u2]
|
||||
const newDefaultUser = {
|
||||
name: '',
|
||||
password: '',
|
||||
roles: [],
|
||||
permissions: [],
|
||||
links: {self: ''},
|
||||
isNew: true,
|
||||
}
|
||||
|
||||
// Roles
|
||||
const r1 = {
|
||||
name: 'hax0r',
|
||||
users: [],
|
||||
permissions: [
|
||||
{
|
||||
allowed: [
|
||||
'ViewAdmin',
|
||||
'ViewChronograf',
|
||||
'CreateDatabase',
|
||||
'CreateUserAndRole',
|
||||
'AddRemoveNode',
|
||||
'DropDatabase',
|
||||
'DropData',
|
||||
'ReadData',
|
||||
'WriteData',
|
||||
'Rebalance',
|
||||
'ManageShard',
|
||||
'ManageContinuousQuery',
|
||||
'ManageQuery',
|
||||
'ManageSubscription',
|
||||
'Monitor',
|
||||
'CopyShard',
|
||||
'KapacitorAPI',
|
||||
'KapacitorConfigAPI'
|
||||
],
|
||||
scope: 'all',
|
||||
},
|
||||
],
|
||||
links: {self: '/chronograf/v1/sources/1/roles/hax0r'}
|
||||
}
|
||||
const r2 = {
|
||||
name: 'l33tus3r',
|
||||
links: {self: '/chronograf/v1/sources/1/roles/l33tus3r'}
|
||||
}
|
||||
const roles = [r1, r2]
|
||||
const newDefaultRole = {
|
||||
name: '',
|
||||
users: [],
|
||||
permissions: [],
|
||||
links: {self: ''},
|
||||
isNew: true,
|
||||
}
|
||||
|
||||
// Permissions
|
||||
const global = {scope: 'all', allowed: ['p1', 'p2']}
|
||||
const scoped = {scope: 'db1', allowed: ['p1', 'p3']}
|
||||
const permissions = [global, scoped]
|
||||
|
||||
describe('Admin.Reducers', () => {
|
||||
it('it can add a user', () => {
|
||||
state = {
|
||||
users: [
|
||||
u1,
|
||||
]
|
||||
}
|
||||
|
||||
const actual = reducer(state, addUser())
|
||||
const expected = {
|
||||
users: [
|
||||
{...newDefaultUser, isEditing: true},
|
||||
u1,
|
||||
],
|
||||
}
|
||||
|
||||
expect(actual.users).to.deep.equal(expected.users)
|
||||
})
|
||||
|
||||
it('it can sync a stale user', () => {
|
||||
const staleUser = {...u1, roles: []}
|
||||
state = {users: [u2, staleUser]}
|
||||
|
||||
const actual = reducer(state, syncUser(staleUser, u1))
|
||||
const expected = {
|
||||
users: [u2, u1],
|
||||
}
|
||||
|
||||
expect(actual.users).to.deep.equal(expected.users)
|
||||
})
|
||||
|
||||
it('it can edit a user', () => {
|
||||
const updates = {name: 'onecool'}
|
||||
state = {
|
||||
users: [u2, u1],
|
||||
}
|
||||
|
||||
const actual = reducer(state, editUser(u2, updates))
|
||||
const expected = {
|
||||
users: [{...u2, ...updates}, u1]
|
||||
}
|
||||
|
||||
expect(actual.users).to.deep.equal(expected.users)
|
||||
})
|
||||
|
||||
it('it can add a role', () => {
|
||||
state = {
|
||||
roles: [
|
||||
r1,
|
||||
]
|
||||
}
|
||||
|
||||
const actual = reducer(state, addRole())
|
||||
const expected = {
|
||||
roles: [
|
||||
{...newDefaultRole, isEditing: true},
|
||||
r1,
|
||||
],
|
||||
}
|
||||
|
||||
expect(actual.roles).to.deep.equal(expected.roles)
|
||||
})
|
||||
|
||||
it('it can sync a stale role', () => {
|
||||
const staleRole = {...r1, permissions: []}
|
||||
state = {roles: [r2, staleRole]}
|
||||
|
||||
const actual = reducer(state, syncRole(staleRole, r1))
|
||||
const expected = {
|
||||
roles: [r2, r1],
|
||||
}
|
||||
|
||||
expect(actual.roles).to.deep.equal(expected.roles)
|
||||
})
|
||||
|
||||
it('it can edit a role', () => {
|
||||
const updates = {name: 'onecool'}
|
||||
state = {
|
||||
roles: [r2, r1],
|
||||
}
|
||||
|
||||
const actual = reducer(state, editRole(r2, updates))
|
||||
const expected = {
|
||||
roles: [{...r2, ...updates}, r1]
|
||||
}
|
||||
|
||||
expect(actual.roles).to.deep.equal(expected.roles)
|
||||
})
|
||||
|
||||
it('it can load the roles', () => {
|
||||
const actual = reducer(state, loadRoles({roles}))
|
||||
const expected = {
|
||||
roles,
|
||||
}
|
||||
|
||||
expect(actual.roles).to.deep.equal(expected.roles)
|
||||
})
|
||||
|
||||
it('it can delete a role', () => {
|
||||
state = {
|
||||
roles: [
|
||||
r1,
|
||||
]
|
||||
}
|
||||
|
||||
const actual = reducer(state, deleteRole(r1))
|
||||
const expected = {
|
||||
roles: [],
|
||||
}
|
||||
|
||||
expect(actual.roles).to.deep.equal(expected.roles)
|
||||
})
|
||||
|
||||
it('it can delete a user', () => {
|
||||
state = {
|
||||
users: [
|
||||
u1,
|
||||
]
|
||||
}
|
||||
|
||||
const actual = reducer(state, deleteUser(u1))
|
||||
const expected = {
|
||||
users: [],
|
||||
}
|
||||
|
||||
expect(actual.users).to.deep.equal(expected.users)
|
||||
})
|
||||
|
||||
it('can filter roles w/ "x" text', () => {
|
||||
state = {
|
||||
roles,
|
||||
}
|
||||
|
||||
const text = 'x'
|
||||
|
||||
const actual = reducer(state, filterRoles(text))
|
||||
const expected = {
|
||||
roles: [
|
||||
{...r1, hidden: false},
|
||||
{...r2, hidden: true},
|
||||
],
|
||||
}
|
||||
|
||||
expect(actual.roles).to.deep.equal(expected.roles)
|
||||
})
|
||||
|
||||
it('can filter users w/ "zero" text', () => {
|
||||
state = {
|
||||
users,
|
||||
}
|
||||
|
||||
const text = 'zero'
|
||||
|
||||
const actual = reducer(state, filterUsers(text))
|
||||
const expected = {
|
||||
users: [
|
||||
{...u1, hidden: true},
|
||||
{...u2, hidden: false},
|
||||
],
|
||||
}
|
||||
|
||||
expect(actual.users).to.deep.equal(expected.users)
|
||||
})
|
||||
|
||||
// Permissions
|
||||
it('it can load the permissions', () => {
|
||||
const actual = reducer(state, loadPermissions({permissions}))
|
||||
const expected = {
|
||||
permissions,
|
||||
}
|
||||
|
||||
expect(actual.permissions).to.deep.equal(expected.permissions)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,230 @@
|
|||
import {
|
||||
getUsers as getUsersAJAX,
|
||||
getRoles as getRolesAJAX,
|
||||
getPermissions as getPermissionsAJAX,
|
||||
createUser as createUserAJAX,
|
||||
createRole as createRoleAJAX,
|
||||
deleteUser as deleteUserAJAX,
|
||||
deleteRole as deleteRoleAJAX,
|
||||
updateRole as updateRoleAJAX,
|
||||
updateUser as updateUserAJAX,
|
||||
} from 'src/admin/apis'
|
||||
|
||||
import {killQuery as killQueryProxy} from 'shared/apis/metaQuery'
|
||||
import {publishNotification} from 'src/shared/actions/notifications';
|
||||
import {ADMIN_NOTIFICATION_DELAY} from 'src/admin/constants'
|
||||
|
||||
export const loadUsers = ({users}) => ({
|
||||
type: 'LOAD_USERS',
|
||||
payload: {
|
||||
users,
|
||||
},
|
||||
})
|
||||
|
||||
export const loadRoles = ({roles}) => ({
|
||||
type: 'LOAD_ROLES',
|
||||
payload: {
|
||||
roles,
|
||||
},
|
||||
})
|
||||
|
||||
export const loadPermissions = ({permissions}) => ({
|
||||
type: 'LOAD_PERMISSIONS',
|
||||
payload: {
|
||||
permissions,
|
||||
},
|
||||
})
|
||||
|
||||
export const addUser = () => ({
|
||||
type: 'ADD_USER',
|
||||
})
|
||||
|
||||
export const addRole = () => ({
|
||||
type: 'ADD_ROLE',
|
||||
})
|
||||
|
||||
export const syncUser = (staleUser, syncedUser) => ({
|
||||
type: 'SYNC_USER',
|
||||
payload: {
|
||||
staleUser,
|
||||
syncedUser,
|
||||
},
|
||||
})
|
||||
|
||||
export const syncRole = (staleRole, syncedRole) => ({
|
||||
type: 'SYNC_ROLE',
|
||||
payload: {
|
||||
staleRole,
|
||||
syncedRole,
|
||||
},
|
||||
})
|
||||
|
||||
export const editUser = (user, updates) => ({
|
||||
type: 'EDIT_USER',
|
||||
payload: {
|
||||
user,
|
||||
updates,
|
||||
},
|
||||
})
|
||||
|
||||
export const editRole = (role, updates) => ({
|
||||
type: 'EDIT_ROLE',
|
||||
payload: {
|
||||
role,
|
||||
updates,
|
||||
},
|
||||
})
|
||||
|
||||
export const killQuery = (queryID) => ({
|
||||
type: 'KILL_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const setQueryToKill = (queryIDToKill) => ({
|
||||
type: 'SET_QUERY_TO_KILL',
|
||||
payload: {
|
||||
queryIDToKill,
|
||||
},
|
||||
})
|
||||
|
||||
export const loadQueries = (queries) => ({
|
||||
type: 'LOAD_QUERIES',
|
||||
payload: {
|
||||
queries,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteUser = (user) => ({
|
||||
type: 'DELETE_USER',
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteRole = (role) => ({
|
||||
type: 'DELETE_ROLE',
|
||||
payload: {
|
||||
role,
|
||||
},
|
||||
})
|
||||
|
||||
export const filterUsers = (text) => ({
|
||||
type: 'FILTER_USERS',
|
||||
payload: {
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
export const filterRoles = (text) => ({
|
||||
type: 'FILTER_ROLES',
|
||||
payload: {
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
// async actions
|
||||
export const loadUsersAsync = (url) => async (dispatch) => {
|
||||
const {data} = await getUsersAJAX(url)
|
||||
dispatch(loadUsers(data))
|
||||
}
|
||||
|
||||
export const loadRolesAsync = (url) => async (dispatch) => {
|
||||
const {data} = await getRolesAJAX(url)
|
||||
dispatch(loadRoles(data))
|
||||
}
|
||||
|
||||
export const loadPermissionsAsync = (url) => async (dispatch) => {
|
||||
const {data} = await getPermissionsAJAX(url)
|
||||
dispatch(loadPermissions(data))
|
||||
}
|
||||
|
||||
export const createUserAsync = (url, user) => async (dispatch) => {
|
||||
try {
|
||||
const {data} = await createUserAJAX(url, user)
|
||||
dispatch(publishNotification('success', 'User created successfully'))
|
||||
dispatch(syncUser(user, data))
|
||||
} catch (error) {
|
||||
// undo optimistic update
|
||||
dispatch(publishNotification('error', `Failed to create user: ${error.data.message}`))
|
||||
setTimeout(() => dispatch(deleteUser(user)), ADMIN_NOTIFICATION_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
export const createRoleAsync = (url, role) => async (dispatch) => {
|
||||
try {
|
||||
const {data} = await createRoleAJAX(url, role)
|
||||
dispatch(publishNotification('success', 'Role created successfully'))
|
||||
dispatch(syncRole(role, data))
|
||||
} catch (error) {
|
||||
// undo optimistic update
|
||||
dispatch(publishNotification('error', `Failed to create role: ${error.data.message}`))
|
||||
setTimeout(() => dispatch(deleteRole(role)), ADMIN_NOTIFICATION_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
export const killQueryAsync = (source, queryID) => (dispatch) => {
|
||||
// optimistic update
|
||||
dispatch(killQuery(queryID))
|
||||
dispatch(setQueryToKill(null))
|
||||
|
||||
// kill query on server
|
||||
killQueryProxy(source, queryID)
|
||||
}
|
||||
|
||||
export const deleteRoleAsync = (role, addFlashMessage) => (dispatch) => {
|
||||
// optimistic update
|
||||
dispatch(deleteRole(role))
|
||||
|
||||
// delete role on server
|
||||
deleteRoleAJAX(role.links.self, addFlashMessage, role.name)
|
||||
}
|
||||
|
||||
export const deleteUserAsync = (user, addFlashMessage) => (dispatch) => {
|
||||
// optimistic update
|
||||
dispatch(deleteUser(user))
|
||||
|
||||
// delete user on server
|
||||
deleteUserAJAX(user.links.self, addFlashMessage, user.name)
|
||||
}
|
||||
|
||||
export const updateRoleUsersAsync = (role, users) => async (dispatch) => {
|
||||
try {
|
||||
const {data} = await updateRoleAJAX(role.links.self, users, role.permissions)
|
||||
dispatch(publishNotification('success', 'Role users updated'))
|
||||
dispatch(syncRole(role, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to update role: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
export const updateRolePermissionsAsync = (role, permissions) => async (dispatch) => {
|
||||
try {
|
||||
const {data} = await updateRoleAJAX(role.links.self, role.users, permissions)
|
||||
dispatch(publishNotification('success', 'Role permissions updated'))
|
||||
dispatch(syncRole(role, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to updated role: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
export const updateUserPermissionsAsync = (user, permissions) => async (dispatch) => {
|
||||
try {
|
||||
const {data} = await updateUserAJAX(user.links.self, user.roles, permissions)
|
||||
dispatch(publishNotification('success', 'User permissions updated'))
|
||||
dispatch(syncUser(user, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to updated user: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
export const updateUserRolesAsync = (user, roles) => async (dispatch) => {
|
||||
try {
|
||||
const {data} = await updateUserAJAX(user.links.self, roles, user.permissions)
|
||||
dispatch(publishNotification('success', 'User roles updated'))
|
||||
dispatch(syncUser(user, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to updated user: ${error.data.message}`))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
export const getUsers = async (url) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'GET',
|
||||
url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getRoles = async (url) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'GET',
|
||||
url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getPermissions = async (url) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'GET',
|
||||
url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const createUser = async (url, user) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'POST',
|
||||
url,
|
||||
data: user,
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const createRole = async (url, role) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'POST',
|
||||
url,
|
||||
data: role,
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteRole = async (url, addFlashMessage, rolename) => {
|
||||
try {
|
||||
const response = await AJAX({
|
||||
method: 'DELETE',
|
||||
url,
|
||||
})
|
||||
addFlashMessage({
|
||||
type: 'success',
|
||||
text: `${rolename} successfully deleted.`,
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
addFlashMessage({
|
||||
type: 'error',
|
||||
text: `Error deleting: ${rolename}.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteUser = async (url, addFlashMessage, username) => {
|
||||
try {
|
||||
const response = await AJAX({
|
||||
method: 'DELETE',
|
||||
url,
|
||||
})
|
||||
addFlashMessage({
|
||||
type: 'success',
|
||||
text: `${username} successfully deleted.`,
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
addFlashMessage({
|
||||
type: 'error',
|
||||
text: `Error deleting: ${username}.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const updateRole = async (url, users, permissions) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'PATCH',
|
||||
url,
|
||||
data: {
|
||||
users,
|
||||
permissions,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const updateUser = async (url, roles, permissions) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'PATCH',
|
||||
url,
|
||||
data: {
|
||||
roles,
|
||||
permissions,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'src/shared/components/Tabs';
|
||||
import UsersTable from 'src/admin/components/UsersTable'
|
||||
import RolesTable from 'src/admin/components/RolesTable'
|
||||
import QueriesPage from 'src/admin/containers/QueriesPage'
|
||||
|
||||
const AdminTabs = ({
|
||||
users,
|
||||
roles,
|
||||
permissions,
|
||||
source,
|
||||
hasRoles,
|
||||
isEditingUsers,
|
||||
isEditingRoles,
|
||||
onClickCreate,
|
||||
onEditUser,
|
||||
onSaveUser,
|
||||
onCancelEditUser,
|
||||
onEditRole,
|
||||
onSaveRole,
|
||||
onCancelEditRole,
|
||||
onDeleteRole,
|
||||
onDeleteUser,
|
||||
onFilterRoles,
|
||||
onFilterUsers,
|
||||
onUpdateRoleUsers,
|
||||
onUpdateRolePermissions,
|
||||
onUpdateUserRoles,
|
||||
onUpdateUserPermissions,
|
||||
}) => {
|
||||
let tabs = [
|
||||
{
|
||||
type: 'Users',
|
||||
component: (
|
||||
<UsersTable
|
||||
users={users}
|
||||
allRoles={roles}
|
||||
hasRoles={hasRoles}
|
||||
permissions={permissions}
|
||||
isEditing={isEditingUsers}
|
||||
onSave={onSaveUser}
|
||||
onCancel={onCancelEditUser}
|
||||
onClickCreate={onClickCreate}
|
||||
onEdit={onEditUser}
|
||||
onDelete={onDeleteUser}
|
||||
onFilter={onFilterUsers}
|
||||
onUpdatePermissions={onUpdateUserPermissions}
|
||||
onUpdateRoles={onUpdateUserRoles}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'Roles',
|
||||
component: (
|
||||
<RolesTable
|
||||
roles={roles}
|
||||
allUsers={users}
|
||||
permissions={permissions}
|
||||
isEditing={isEditingRoles}
|
||||
onClickCreate={onClickCreate}
|
||||
onEdit={onEditRole}
|
||||
onSave={onSaveRole}
|
||||
onCancel={onCancelEditRole}
|
||||
onDelete={onDeleteRole}
|
||||
onFilter={onFilterRoles}
|
||||
onUpdateRoleUsers={onUpdateRoleUsers}
|
||||
onUpdateRolePermissions={onUpdateRolePermissions}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'Queries',
|
||||
component: (<QueriesPage source={source} />),
|
||||
},
|
||||
]
|
||||
|
||||
if (!hasRoles) {
|
||||
tabs = tabs.filter(t => t.type !== 'Roles')
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs className="row">
|
||||
<TabList customClass="col-md-3 admin-tabs">
|
||||
{
|
||||
tabs.map((t, i) => (<Tab key={tabs[i].type}>{tabs[i].type}</Tab>))
|
||||
}
|
||||
</TabList>
|
||||
<TabPanels customClass="col-md-9">
|
||||
{
|
||||
tabs.map((t, i) => (<TabPanel key={tabs[i].type}>{t.component}</TabPanel>))
|
||||
}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
AdminTabs.propTypes = {
|
||||
users: arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
roles: arrayOf(shape({
|
||||
name: string,
|
||||
})),
|
||||
})),
|
||||
roles: arrayOf(shape()),
|
||||
source: shape(),
|
||||
permissions: arrayOf(string),
|
||||
isEditingUsers: bool,
|
||||
isEditingRoles: bool,
|
||||
onClickCreate: func.isRequired,
|
||||
onEditUser: func.isRequired,
|
||||
onSaveUser: func.isRequired,
|
||||
onCancelEditUser: func.isRequired,
|
||||
onEditRole: func.isRequired,
|
||||
onSaveRole: func.isRequired,
|
||||
onCancelEditRole: func.isRequired,
|
||||
onDeleteRole: func.isRequired,
|
||||
onDeleteUser: func.isRequired,
|
||||
onFilterRoles: func.isRequired,
|
||||
onFilterUsers: func.isRequired,
|
||||
onUpdateRoleUsers: func.isRequired,
|
||||
onUpdateRolePermissions: func.isRequired,
|
||||
hasRoles: bool.isRequired,
|
||||
onUpdateUserPermissions: func,
|
||||
onUpdateUserRoles: func,
|
||||
}
|
||||
|
||||
export default AdminTabs
|
|
@ -0,0 +1,31 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const ConfirmButtons = ({onConfirm, item, onCancel}) => (
|
||||
<div>
|
||||
<button
|
||||
className="btn btn-xs btn-info"
|
||||
onClick={() => onCancel(item)}
|
||||
>
|
||||
<span className="icon remove"></span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-success"
|
||||
onClick={() => onConfirm(item)}
|
||||
>
|
||||
<span className="icon checkmark"></span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
} = PropTypes
|
||||
|
||||
ConfirmButtons.propTypes = {
|
||||
onConfirm: func.isRequired,
|
||||
item: shape({}).isRequired,
|
||||
onCancel: func.isRequired,
|
||||
}
|
||||
|
||||
export default ConfirmButtons
|
|
@ -0,0 +1,71 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import ConfirmButtons from 'src/admin/components/ConfirmButtons'
|
||||
|
||||
const DeleteButton = ({onConfirm}) => (
|
||||
<button
|
||||
className="btn btn-xs btn-danger admin-table--delete"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)
|
||||
|
||||
class DeleteRow extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isConfirmed: false,
|
||||
}
|
||||
this.handleConfirm = ::this.handleConfirm
|
||||
this.handleCancel = ::this.handleCancel
|
||||
}
|
||||
|
||||
handleConfirm() {
|
||||
this.setState({isConfirmed: true})
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
this.setState({isConfirmed: false})
|
||||
}
|
||||
|
||||
handleClickOutside() {
|
||||
this.setState({isConfirmed: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {onDelete, item} = this.props
|
||||
const {isConfirmed} = this.state
|
||||
|
||||
if (isConfirmed) {
|
||||
return (
|
||||
<ConfirmButtons
|
||||
onConfirm={onDelete}
|
||||
item={item}
|
||||
onCancel={this.handleCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DeleteButton onConfirm={this.handleConfirm} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
} = PropTypes
|
||||
|
||||
DeleteButton.propTypes = {
|
||||
onConfirm: func.isRequired,
|
||||
}
|
||||
|
||||
DeleteRow.propTypes = {
|
||||
item: shape({}),
|
||||
onDelete: func.isRequired,
|
||||
}
|
||||
|
||||
export default OnClickOutside(DeleteRow)
|
|
@ -0,0 +1,19 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const EmptyRow = ({tableName}) => (
|
||||
<tr className="table-empty-state">
|
||||
<th colSpan="5">
|
||||
<p>You don't have any {tableName},<br/>why not create one?</p>
|
||||
</th>
|
||||
</tr>
|
||||
)
|
||||
|
||||
const {
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
EmptyRow.propTypes = {
|
||||
tableName: string.isRequired,
|
||||
}
|
||||
|
||||
export default EmptyRow
|
|
@ -0,0 +1,59 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
class FilterBar extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
filterText: '',
|
||||
}
|
||||
|
||||
this.handleText = ::this.handleText
|
||||
}
|
||||
|
||||
handleText(e) {
|
||||
this.setState(
|
||||
{filterText: e.target.value},
|
||||
this.props.onFilter(e.target.value)
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.onFilter('')
|
||||
}
|
||||
|
||||
render() {
|
||||
const {type, isEditing, onClickCreate} = this.props
|
||||
return (
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<div className="users__search-widget input-group admin__search-widget">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={`Filter ${type}...`}
|
||||
value={this.state.filterText}
|
||||
onChange={this.handleText}
|
||||
/>
|
||||
<div className="input-group-addon">
|
||||
<span className="icon search" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary" disabled={isEditing} onClick={() => onClickCreate(type)}>Create {type.substring(0, type.length - 1)}</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
FilterBar.propTypes = {
|
||||
onFilter: func.isRequired,
|
||||
type: string,
|
||||
isEditing: bool,
|
||||
onClickCreate: func,
|
||||
}
|
||||
|
||||
export default FilterBar
|
|
@ -0,0 +1,67 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const QueriesTable = ({queries, onKillQuery, onConfirm}) => (
|
||||
<div>
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-body">
|
||||
<table className="table v-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Database</th>
|
||||
<th>Query</th>
|
||||
<th>Running</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{queries.map((q) => {
|
||||
return (
|
||||
<tr key={q.id}>
|
||||
<td>{q.database}</td>
|
||||
<td><code>{q.query}</code></td>
|
||||
<td>{q.duration}</td>
|
||||
<td className="text-right">
|
||||
<button className="btn btn-xs btn-link-danger" onClick={onKillQuery} data-toggle="modal" data-query-id={q.id} data-target="#killModal">
|
||||
Kill
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal fade" id="killModal" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 className="modal-title" id="myModalLabel">Are you sure you want to kill this query?</h4>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-default" data-dismiss="modal">No</button>
|
||||
<button type="button" className="btn btn-danger" data-dismiss="modal" onClick={onConfirm}>Yes, kill it!</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
shape,
|
||||
} = PropTypes
|
||||
|
||||
QueriesTable.propTypes = {
|
||||
queries: arrayOf(shape()),
|
||||
onConfirm: func,
|
||||
onKillQuery: func,
|
||||
}
|
||||
|
||||
export default QueriesTable
|
|
@ -0,0 +1,59 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
class RoleEditingRow extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.handleKeyPress = ::this.handleKeyPress
|
||||
this.handleEdit = ::this.handleEdit
|
||||
}
|
||||
|
||||
handleKeyPress(role) {
|
||||
return (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.props.onSave(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleEdit(role) {
|
||||
return (e) => {
|
||||
this.props.onEdit(role, {[e.target.name]: e.target.value})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {role} = this.props
|
||||
return (
|
||||
<td>
|
||||
<div className="admin-table--edit-cell">
|
||||
<input
|
||||
className="form-control"
|
||||
name="name"
|
||||
type="text"
|
||||
value={role.name || ''}
|
||||
placeholder="role name"
|
||||
onChange={this.handleEdit(role)}
|
||||
onKeyPress={this.handleKeyPress(role)}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
} = PropTypes
|
||||
|
||||
RoleEditingRow.propTypes = {
|
||||
role: shape().isRequired,
|
||||
isNew: bool,
|
||||
onEdit: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
}
|
||||
|
||||
export default RoleEditingRow
|
|
@ -0,0 +1,109 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
import RoleEditingRow from 'src/admin/components/RoleEditingRow'
|
||||
import MultiSelectDropdown from 'shared/components/MultiSelectDropdown'
|
||||
import ConfirmButtons from 'src/admin/components/ConfirmButtons'
|
||||
import DeleteRow from 'src/admin/components/DeleteRow'
|
||||
|
||||
const RoleRow = ({
|
||||
role: {name, permissions, users},
|
||||
role,
|
||||
allUsers,
|
||||
allPermissions,
|
||||
isNew,
|
||||
isEditing,
|
||||
onEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onUpdateRoleUsers,
|
||||
onUpdateRolePermissions,
|
||||
}) => {
|
||||
const handleUpdateUsers = (u) => {
|
||||
onUpdateRoleUsers(role, u.map((n) => ({name: n})))
|
||||
}
|
||||
|
||||
const handleUpdatePermissions = (allowed) => {
|
||||
onUpdateRolePermissions(role, [{scope: 'all', allowed}])
|
||||
}
|
||||
|
||||
const perms = _.get(permissions, ['0', 'allowed'], [])
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<tr className="admin-table--edit-row">
|
||||
<RoleEditingRow role={role} onEdit={onEdit} onSave={onSave} isNew={isNew} />
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td className="text-right" style={{width: "85px"}}>
|
||||
<ConfirmButtons item={role} onConfirm={onSave} onCancel={onCancel} />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td>
|
||||
{
|
||||
allPermissions && allPermissions.length ?
|
||||
<MultiSelectDropdown
|
||||
items={allPermissions}
|
||||
selectedItems={perms}
|
||||
label={perms.length ? '' : 'Select Permissions'}
|
||||
onApply={handleUpdatePermissions}
|
||||
/> : null
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{
|
||||
allUsers && allUsers.length ?
|
||||
<MultiSelectDropdown
|
||||
items={allUsers.map((u) => u.name)}
|
||||
selectedItems={users === undefined ? [] : users.map((u) => u.name)}
|
||||
label={users && users.length ? '' : 'Select Users'}
|
||||
onApply={handleUpdateUsers}
|
||||
/> : null
|
||||
}
|
||||
</td>
|
||||
<td className="text-right" style={{width: "85px"}}>
|
||||
<DeleteRow onDelete={onDelete} item={role} />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
RoleRow.propTypes = {
|
||||
role: shape({
|
||||
name: string,
|
||||
permissions: arrayOf(shape({
|
||||
name: string,
|
||||
})),
|
||||
users: arrayOf(shape({
|
||||
name: string,
|
||||
})),
|
||||
}).isRequired,
|
||||
isNew: bool,
|
||||
isEditing: bool,
|
||||
onCancel: func,
|
||||
onEdit: func,
|
||||
onSave: func,
|
||||
onDelete: func.isRequired,
|
||||
allUsers: arrayOf(shape()),
|
||||
allPermissions: arrayOf(string),
|
||||
onUpdateRoleUsers: func.isRequired,
|
||||
onUpdateRolePermissions: func.isRequired,
|
||||
}
|
||||
|
||||
export default RoleRow
|
|
@ -0,0 +1,90 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import RoleRow from 'src/admin/components/RoleRow'
|
||||
import EmptyRow from 'src/admin/components/EmptyRow'
|
||||
import FilterBar from 'src/admin/components/FilterBar'
|
||||
|
||||
const RolesTable = ({
|
||||
roles,
|
||||
allUsers,
|
||||
permissions,
|
||||
isEditing,
|
||||
onClickCreate,
|
||||
onEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onFilter,
|
||||
onUpdateRoleUsers,
|
||||
onUpdateRolePermissions,
|
||||
}) => (
|
||||
<div className="panel panel-info">
|
||||
<FilterBar type="roles" onFilter={onFilter} isEditing={isEditing} onClickCreate={onClickCreate} />
|
||||
<div className="panel-body">
|
||||
<table className="table v-center admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Permissions</th>
|
||||
<th>Users</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
roles.length ?
|
||||
roles.filter(r => !r.hidden).map((role) =>
|
||||
<RoleRow
|
||||
key={role.links.self}
|
||||
allUsers={allUsers}
|
||||
allPermissions={permissions}
|
||||
role={role}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
onDelete={onDelete}
|
||||
onUpdateRoleUsers={onUpdateRoleUsers}
|
||||
onUpdateRolePermissions={onUpdateRolePermissions}
|
||||
isEditing={role.isEditing}
|
||||
isNew={role.isNew}
|
||||
/>
|
||||
) : <EmptyRow tableName={'Roles'} />
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
RolesTable.propTypes = {
|
||||
roles: arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
permissions: arrayOf(shape({
|
||||
name: string,
|
||||
scope: string.isRequired,
|
||||
})),
|
||||
users: arrayOf(shape({
|
||||
name: string,
|
||||
})),
|
||||
})),
|
||||
isEditing: bool,
|
||||
onClickCreate: func.isRequired,
|
||||
onEdit: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
onCancel: func.isRequired,
|
||||
onDelete: func.isRequired,
|
||||
onFilter: func,
|
||||
allUsers: arrayOf(shape()),
|
||||
permissions: arrayOf(string),
|
||||
onUpdateRoleUsers: func.isRequired,
|
||||
onUpdateRolePermissions: func.isRequired,
|
||||
}
|
||||
|
||||
export default RolesTable
|
|
@ -0,0 +1,72 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
class UserEditingRow extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.handleKeyPress = ::this.handleKeyPress
|
||||
this.handleEdit = ::this.handleEdit
|
||||
}
|
||||
|
||||
handleKeyPress(user) {
|
||||
return (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.props.onSave(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleEdit(user) {
|
||||
return (e) => {
|
||||
this.props.onEdit(user, {[e.target.name]: e.target.value})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {user, isNew} = this.props
|
||||
return (
|
||||
<td>
|
||||
<div className="admin-table--edit-cell">
|
||||
<input
|
||||
className="form-control"
|
||||
name="name"
|
||||
type="text"
|
||||
value={user.name || ''}
|
||||
placeholder="Username"
|
||||
onChange={this.handleEdit(user)}
|
||||
onKeyPress={this.handleKeyPress(user)}
|
||||
autoFocus={true}
|
||||
/>
|
||||
{
|
||||
isNew ?
|
||||
<input
|
||||
className="form-control"
|
||||
name="password"
|
||||
type="text"
|
||||
value={user.password || ''}
|
||||
placeholder="Password"
|
||||
onChange={this.handleEdit(user)}
|
||||
onKeyPress={this.handleKeyPress(user)}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
} = PropTypes
|
||||
|
||||
UserEditingRow.propTypes = {
|
||||
user: shape().isRequired,
|
||||
isNew: bool,
|
||||
onEdit: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
}
|
||||
|
||||
export default UserEditingRow
|
|
@ -0,0 +1,110 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
import UserEditingRow from 'src/admin/components/UserEditingRow'
|
||||
import MultiSelectDropdown from 'shared/components/MultiSelectDropdown'
|
||||
import ConfirmButtons from 'src/admin/components/ConfirmButtons'
|
||||
import DeleteRow from 'src/admin/components/DeleteRow'
|
||||
|
||||
const UserRow = ({
|
||||
user: {name, roles, permissions},
|
||||
user,
|
||||
allRoles,
|
||||
allPermissions,
|
||||
hasRoles,
|
||||
isNew,
|
||||
isEditing,
|
||||
onEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onUpdatePermissions,
|
||||
onUpdateRoles,
|
||||
}) => {
|
||||
const handleUpdatePermissions = (allowed) => {
|
||||
onUpdatePermissions(user, [{scope: 'all', allowed}])
|
||||
}
|
||||
|
||||
const handleUpdateRoles = (roleNames) => {
|
||||
onUpdateRoles(user, allRoles.filter(r => roleNames.find(rn => rn === r.name)))
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<tr className="admin-table--edit-row">
|
||||
<UserEditingRow user={user} onEdit={onEdit} onSave={onSave} isNew={isNew} />
|
||||
{hasRoles ? <td></td> : null}
|
||||
<td></td>
|
||||
<td className="text-right" style={{width: "85px"}}>
|
||||
<ConfirmButtons item={user} onConfirm={onSave} onCancel={onCancel} />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
{
|
||||
hasRoles ?
|
||||
<td>
|
||||
<MultiSelectDropdown
|
||||
items={allRoles.map((r) => r.name)}
|
||||
selectedItems={roles ? roles.map((r) => r.name) : []/* TODO remove check when server returns empty list */}
|
||||
label={roles && roles.length ? '' : 'Select Roles'}
|
||||
onApply={handleUpdateRoles}
|
||||
/>
|
||||
</td> :
|
||||
null
|
||||
}
|
||||
<td>
|
||||
{
|
||||
allPermissions && allPermissions.length ?
|
||||
<MultiSelectDropdown
|
||||
items={allPermissions}
|
||||
selectedItems={_.get(permissions, ['0', 'allowed'], [])}
|
||||
label={permissions && permissions.length ? '' : 'Select Permissions'}
|
||||
onApply={handleUpdatePermissions}
|
||||
/> : null
|
||||
}
|
||||
</td>
|
||||
<td className="text-right" style={{width: "85px"}}>
|
||||
<DeleteRow onDelete={onDelete} item={user} />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
UserRow.propTypes = {
|
||||
user: shape({
|
||||
name: string,
|
||||
roles: arrayOf(shape({
|
||||
name: string,
|
||||
})),
|
||||
permissions: arrayOf(shape({
|
||||
name: string,
|
||||
})),
|
||||
}).isRequired,
|
||||
allRoles: arrayOf(shape()),
|
||||
allPermissions: arrayOf(string),
|
||||
hasRoles: bool,
|
||||
isNew: bool,
|
||||
isEditing: bool,
|
||||
onCancel: func,
|
||||
onEdit: func,
|
||||
onSave: func,
|
||||
onDelete: func.isRequired,
|
||||
onUpdatePermissions: func,
|
||||
onUpdateRoles: func,
|
||||
}
|
||||
|
||||
export default UserRow
|
|
@ -0,0 +1,94 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import UserRow from 'src/admin/components/UserRow'
|
||||
import EmptyRow from 'src/admin/components/EmptyRow'
|
||||
import FilterBar from 'src/admin/components/FilterBar'
|
||||
|
||||
const UsersTable = ({
|
||||
users,
|
||||
allRoles,
|
||||
hasRoles,
|
||||
permissions,
|
||||
isEditing,
|
||||
onClickCreate,
|
||||
onEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onFilter,
|
||||
onUpdatePermissions,
|
||||
onUpdateRoles,
|
||||
}) => (
|
||||
<div className="panel panel-info">
|
||||
<FilterBar type="users" onFilter={onFilter} isEditing={isEditing} onClickCreate={onClickCreate} />
|
||||
<div className="panel-body">
|
||||
<table className="table v-center admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
{hasRoles && <th>Roles</th>}
|
||||
<th>Permissions</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
users.length ?
|
||||
users.filter(u => !u.hidden).map(user =>
|
||||
<UserRow
|
||||
key={user.links.self}
|
||||
user={user}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
onDelete={onDelete}
|
||||
isEditing={user.isEditing}
|
||||
isNew={user.isNew}
|
||||
allRoles={allRoles}
|
||||
hasRoles={hasRoles}
|
||||
allPermissions={permissions}
|
||||
onUpdatePermissions={onUpdatePermissions}
|
||||
onUpdateRoles={onUpdateRoles}
|
||||
/>) :
|
||||
<EmptyRow tableName={'Users'} />
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
UsersTable.propTypes = {
|
||||
users: arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
roles: arrayOf(shape({
|
||||
name: string,
|
||||
})),
|
||||
permissions: arrayOf(shape({
|
||||
name: string,
|
||||
scope: string.isRequired,
|
||||
})),
|
||||
})),
|
||||
isEditing: bool,
|
||||
onClickCreate: func.isRequired,
|
||||
onEdit: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
onCancel: func.isRequired,
|
||||
onDelete: func.isRequired,
|
||||
onFilter: func,
|
||||
allRoles: arrayOf(shape()),
|
||||
permissions: arrayOf(string),
|
||||
hasRoles: bool.isRequired,
|
||||
onUpdatePermissions: func,
|
||||
onUpdateRoles: func,
|
||||
}
|
||||
|
||||
export default UsersTable
|
|
@ -0,0 +1,11 @@
|
|||
export const TIMES = [
|
||||
{test: /ns/, magnitude: 0},
|
||||
{test: /µs/, magnitude: 1},
|
||||
{test: /u/, magnitude: 1},
|
||||
{test: /^\d*ms/, magnitude: 2},
|
||||
{test: /^\d*s/, magnitude: 3},
|
||||
{test: /^\d*m\d*s/, magnitude: 4},
|
||||
{test: /^\d*h\d*m\d*s/, magnitude: 5},
|
||||
];
|
||||
|
||||
export const ADMIN_NOTIFICATION_DELAY = 1500 // milliseconds
|
|
@ -0,0 +1,263 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {
|
||||
loadUsersAsync,
|
||||
loadRolesAsync,
|
||||
loadPermissionsAsync,
|
||||
addUser,
|
||||
addRole,
|
||||
deleteUser, // TODO rename to removeUser throughout + tests
|
||||
deleteRole, // TODO rename to removeUser throughout + tests
|
||||
editUser,
|
||||
editRole,
|
||||
createUserAsync,
|
||||
createRoleAsync,
|
||||
deleteUserAsync,
|
||||
deleteRoleAsync,
|
||||
updateRoleUsersAsync,
|
||||
updateRolePermissionsAsync,
|
||||
updateUserPermissionsAsync,
|
||||
updateUserRolesAsync,
|
||||
filterUsers as filterUsersAction,
|
||||
filterRoles as filterRolesAction,
|
||||
} from 'src/admin/actions'
|
||||
|
||||
import AdminTabs from 'src/admin/components/AdminTabs'
|
||||
|
||||
const isValidUser = (user) => {
|
||||
const minLen = 3
|
||||
return (user.name.length >= minLen && user.password.length >= minLen)
|
||||
}
|
||||
|
||||
const isValidRole = (role) => {
|
||||
const minLen = 3
|
||||
return (role.name.length >= minLen)
|
||||
}
|
||||
|
||||
class AdminPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.handleClickCreate = ::this.handleClickCreate
|
||||
this.handleEditUser = ::this.handleEditUser
|
||||
this.handleEditRole = ::this.handleEditRole
|
||||
this.handleSaveUser = ::this.handleSaveUser
|
||||
this.handleSaveRole = ::this.handleSaveRole
|
||||
this.handleCancelEditUser = ::this.handleCancelEditUser
|
||||
this.handleCancelEditRole = ::this.handleCancelEditRole
|
||||
this.handleDeleteRole = ::this.handleDeleteRole
|
||||
this.handleDeleteUser = ::this.handleDeleteUser
|
||||
this.handleUpdateRoleUsers = ::this.handleUpdateRoleUsers
|
||||
this.handleUpdateRolePermissions = ::this.handleUpdateRolePermissions
|
||||
this.handleUpdateUserPermissions = ::this.handleUpdateUserPermissions
|
||||
this.handleUpdateUserRoles = ::this.handleUpdateUserRoles
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {source, loadUsers, loadRoles, loadPermissions} = this.props
|
||||
|
||||
loadUsers(source.links.users)
|
||||
loadPermissions(source.links.permissions)
|
||||
if (source.links.roles) {
|
||||
loadRoles(source.links.roles)
|
||||
}
|
||||
}
|
||||
|
||||
handleClickCreate(type) {
|
||||
if (type === 'users') {
|
||||
this.props.addUser()
|
||||
} else if (type === 'roles') {
|
||||
this.props.addRole()
|
||||
}
|
||||
}
|
||||
|
||||
handleEditUser(user, updates) {
|
||||
this.props.editUser(user, updates)
|
||||
}
|
||||
|
||||
handleEditRole(role, updates) {
|
||||
this.props.editRole(role, updates)
|
||||
}
|
||||
|
||||
async handleSaveUser(user) {
|
||||
if (!isValidUser(user)) {
|
||||
this.props.addFlashMessage({type: 'error', text: 'Username and/or password too short'})
|
||||
return
|
||||
}
|
||||
if (user.isNew) {
|
||||
this.props.createUser(this.props.source.links.users, user)
|
||||
} else {
|
||||
// TODO update user
|
||||
}
|
||||
}
|
||||
|
||||
async handleSaveRole(role) {
|
||||
if (!isValidRole(role)) {
|
||||
this.props.addFlashMessage({type: 'error', text: 'Role name too short'})
|
||||
return
|
||||
}
|
||||
if (role.isNew) {
|
||||
this.props.createRole(this.props.source.links.roles, role)
|
||||
} else {
|
||||
// TODO update role
|
||||
// console.log('update')
|
||||
}
|
||||
}
|
||||
|
||||
handleCancelEditUser(user) {
|
||||
this.props.removeUser(user)
|
||||
}
|
||||
|
||||
handleCancelEditRole(role) {
|
||||
this.props.removeRole(role)
|
||||
}
|
||||
|
||||
handleDeleteRole(role) {
|
||||
this.props.deleteRole(role, this.props.addFlashMessage)
|
||||
}
|
||||
|
||||
handleDeleteUser(user) {
|
||||
this.props.deleteUser(user, this.props.addFlashMessage)
|
||||
}
|
||||
|
||||
handleUpdateRoleUsers(role, users) {
|
||||
this.props.updateRoleUsers(role, users)
|
||||
}
|
||||
|
||||
handleUpdateRolePermissions(role, permissions) {
|
||||
this.props.updateRolePermissions(role, permissions)
|
||||
}
|
||||
|
||||
handleUpdateUserPermissions(user, permissions) {
|
||||
this.props.updateUserPermissions(user, permissions)
|
||||
}
|
||||
|
||||
handleUpdateUserRoles(user, roles) {
|
||||
this.props.updateUserRoles(user, roles)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {users, roles, source, permissions, filterUsers, filterRoles} = this.props
|
||||
const hasRoles = !!source.links.roles
|
||||
const globalPermissions = permissions.find((p) => p.scope === 'all')
|
||||
const allowed = globalPermissions ? globalPermissions.allowed : []
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1>
|
||||
Admin
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
{
|
||||
users ?
|
||||
<AdminTabs
|
||||
users={users}
|
||||
roles={roles}
|
||||
source={source}
|
||||
permissions={allowed}
|
||||
hasRoles={hasRoles}
|
||||
isEditingUsers={users.some(u => u.isEditing)}
|
||||
isEditingRoles={roles.some(r => r.isEditing)}
|
||||
onClickCreate={this.handleClickCreate}
|
||||
onEditUser={this.handleEditUser}
|
||||
onEditRole={this.handleEditRole}
|
||||
onSaveUser={this.handleSaveUser}
|
||||
onSaveRole={this.handleSaveRole}
|
||||
onCancelEditUser={this.handleCancelEditUser}
|
||||
onCancelEditRole={this.handleCancelEditRole}
|
||||
onDeleteUser={this.handleDeleteUser}
|
||||
onDeleteRole={this.handleDeleteRole}
|
||||
onFilterUsers={filterUsers}
|
||||
onFilterRoles={filterRoles}
|
||||
onUpdateRoleUsers={this.handleUpdateRoleUsers}
|
||||
onUpdateRolePermissions={this.handleUpdateRolePermissions}
|
||||
onUpdateUserPermissions={this.handleUpdateUserPermissions}
|
||||
onUpdateUserRoles={this.handleUpdateUserRoles}
|
||||
/> :
|
||||
<span>Loading...</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
AdminPage.propTypes = {
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
links: shape({
|
||||
users: string.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
users: arrayOf(shape()),
|
||||
roles: arrayOf(shape()),
|
||||
permissions: arrayOf(shape()),
|
||||
loadUsers: func,
|
||||
loadRoles: func,
|
||||
loadPermissions: func,
|
||||
addUser: func,
|
||||
addRole: func,
|
||||
removeUser: func,
|
||||
removeRole: func,
|
||||
editUser: func,
|
||||
editRole: func,
|
||||
createUser: func,
|
||||
createRole: func,
|
||||
deleteRole: func,
|
||||
deleteUser: func,
|
||||
addFlashMessage: func,
|
||||
filterRoles: func,
|
||||
filterUsers: func,
|
||||
updateRoleUsers: func,
|
||||
updateRolePermissions: func,
|
||||
updateUserPermissions: func,
|
||||
updateUserRoles: func,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({admin: {users, roles, permissions}}) => ({
|
||||
users,
|
||||
roles,
|
||||
permissions,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
loadUsers: bindActionCreators(loadUsersAsync, dispatch),
|
||||
loadRoles: bindActionCreators(loadRolesAsync, dispatch),
|
||||
loadPermissions: bindActionCreators(loadPermissionsAsync, dispatch),
|
||||
addUser: bindActionCreators(addUser, dispatch),
|
||||
addRole: bindActionCreators(addRole, dispatch),
|
||||
removeUser: bindActionCreators(deleteUser, dispatch),
|
||||
removeRole: bindActionCreators(deleteRole, dispatch),
|
||||
editUser: bindActionCreators(editUser, dispatch),
|
||||
editRole: bindActionCreators(editRole, dispatch),
|
||||
createUser: bindActionCreators(createUserAsync, dispatch),
|
||||
createRole: bindActionCreators(createRoleAsync, dispatch),
|
||||
deleteUser: bindActionCreators(deleteUserAsync, dispatch),
|
||||
deleteRole: bindActionCreators(deleteRoleAsync, dispatch),
|
||||
filterUsers: bindActionCreators(filterUsersAction, dispatch),
|
||||
filterRoles: bindActionCreators(filterRolesAction, dispatch),
|
||||
updateRoleUsers: bindActionCreators(updateRoleUsersAsync, dispatch),
|
||||
updateRolePermissions: bindActionCreators(updateRolePermissionsAsync, dispatch),
|
||||
updateUserPermissions: bindActionCreators(updateUserPermissionsAsync, dispatch),
|
||||
updateUserRoles: bindActionCreators(updateUserRolesAsync, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AdminPage)
|
|
@ -0,0 +1,134 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import flatten from 'lodash/flatten'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
|
||||
import {
|
||||
showDatabases,
|
||||
showQueries,
|
||||
} from 'shared/apis/metaQuery'
|
||||
|
||||
import QueriesTable from 'src/admin/components/QueriesTable'
|
||||
import showDatabasesParser from 'shared/parsing/showDatabases'
|
||||
import showQueriesParser from 'shared/parsing/showQueries'
|
||||
import {TIMES} from 'src/admin/constants'
|
||||
import {
|
||||
loadQueries as loadQueriesAction,
|
||||
setQueryToKill as setQueryToKillAction,
|
||||
killQueryAsync,
|
||||
} from 'src/admin/actions'
|
||||
|
||||
class QueriesPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.updateQueries = ::this.updateQueries
|
||||
this.handleConfirmKillQuery = ::this.handleConfirmKillQuery
|
||||
this.handleKillQuery = ::this.handleKillQuery
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateQueries()
|
||||
const updateInterval = 5000
|
||||
this.intervalID = setInterval(this.updateQueries, updateInterval)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.intervalID)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {queries} = this.props;
|
||||
|
||||
return (
|
||||
<QueriesTable queries={queries} onConfirm={this.handleConfirmKillQuery} onKillQuery={this.handleKillQuery} />
|
||||
);
|
||||
}
|
||||
|
||||
updateQueries() {
|
||||
const {source, addFlashMessage, loadQueries} = this.props
|
||||
showDatabases(source.links.proxy).then((resp) => {
|
||||
const {databases, errors} = showDatabasesParser(resp.data)
|
||||
if (errors.length) {
|
||||
errors.forEach((message) => addFlashMessage({type: 'error', text: message}))
|
||||
return;
|
||||
}
|
||||
|
||||
const fetches = databases.map((db) => showQueries(source.links.proxy, db))
|
||||
|
||||
Promise.all(fetches).then((queryResponses) => {
|
||||
const allQueries = [];
|
||||
queryResponses.forEach((queryResponse) => {
|
||||
const result = showQueriesParser(queryResponse.data);
|
||||
if (result.errors.length) {
|
||||
result.erorrs.forEach((message) => this.props.addFlashMessage({type: 'error', text: message}));
|
||||
}
|
||||
|
||||
allQueries.push(...result.queries);
|
||||
});
|
||||
|
||||
const queries = uniqBy(flatten(allQueries), (q) => q.id);
|
||||
|
||||
// sorting queries by magnitude, so generally longer queries will appear atop the list
|
||||
const sortedQueries = queries.sort((a, b) => {
|
||||
const aTime = TIMES.find((t) => a.duration.match(t.test));
|
||||
const bTime = TIMES.find((t) => b.duration.match(t.test));
|
||||
return +aTime.magnitude <= +bTime.magnitude;
|
||||
});
|
||||
|
||||
loadQueries(sortedQueries)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleKillQuery(e) {
|
||||
e.stopPropagation();
|
||||
const id = e.target.dataset.queryId;
|
||||
|
||||
this.props.setQueryToKill(id)
|
||||
}
|
||||
|
||||
handleConfirmKillQuery() {
|
||||
const {queryIDToKill, source, killQuery} = this.props;
|
||||
if (queryIDToKill === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
killQuery(source.links.proxy, queryIDToKill)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
string,
|
||||
shape,
|
||||
} = PropTypes
|
||||
|
||||
QueriesPage.propTypes = {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string,
|
||||
}),
|
||||
}),
|
||||
queries: arrayOf(shape()),
|
||||
addFlashMessage: func,
|
||||
loadQueries: func,
|
||||
queryIDToKill: string,
|
||||
setQueryToKill: func,
|
||||
killQuery: func,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({admin: {queries, queryIDToKill}}) => ({
|
||||
queries,
|
||||
queryIDToKill,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
loadQueries: bindActionCreators(loadQueriesAction, dispatch),
|
||||
setQueryToKill: bindActionCreators(setQueryToKillAction, dispatch),
|
||||
killQuery: bindActionCreators(killQueryAsync, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueriesPage)
|
|
@ -0,0 +1,2 @@
|
|||
import AdminPage from './containers/AdminPage';
|
||||
export {AdminPage};
|
|
@ -0,0 +1,154 @@
|
|||
import reject from 'lodash/reject'
|
||||
|
||||
const newDefaultUser = {
|
||||
name: '',
|
||||
password: '',
|
||||
roles: [],
|
||||
permissions: [],
|
||||
links: {self: ''},
|
||||
isNew: true,
|
||||
}
|
||||
const newDefaultRole = {
|
||||
name: '',
|
||||
permissions: [],
|
||||
users: [],
|
||||
links: {self: ''},
|
||||
isNew: true,
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
users: null,
|
||||
roles: [],
|
||||
permissions: [],
|
||||
queries: [],
|
||||
queryIDToKill: null,
|
||||
}
|
||||
|
||||
export default function admin(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'LOAD_USERS': {
|
||||
return {...state, ...action.payload}
|
||||
}
|
||||
|
||||
case 'LOAD_ROLES': {
|
||||
return {...state, ...action.payload}
|
||||
}
|
||||
|
||||
case 'LOAD_PERMISSIONS': {
|
||||
return {...state, ...action.payload}
|
||||
}
|
||||
|
||||
case 'ADD_USER': {
|
||||
const newUser = {...newDefaultUser, isEditing: true}
|
||||
return {
|
||||
...state,
|
||||
users: [
|
||||
newUser,
|
||||
...state.users,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
case 'ADD_ROLE': {
|
||||
const newRole = {...newDefaultRole, isEditing: true}
|
||||
return {
|
||||
...state,
|
||||
roles: [
|
||||
newRole,
|
||||
...state.roles,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
case 'SYNC_USER': {
|
||||
const {staleUser, syncedUser} = action.payload
|
||||
const newState = {
|
||||
users: state.users.map(u => u.links.self === staleUser.links.self ? {...syncedUser} : u),
|
||||
}
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'SYNC_ROLE': {
|
||||
const {staleRole, syncedRole} = action.payload
|
||||
const newState = {
|
||||
roles: state.roles.map(r => r.links.self === staleRole.links.self ? {...syncedRole} : r),
|
||||
}
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'EDIT_USER': {
|
||||
const {user, updates} = action.payload
|
||||
const newState = {
|
||||
users: state.users.map(u => u.links.self === user.links.self ? {...u, ...updates} : u),
|
||||
}
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'EDIT_ROLE': {
|
||||
const {role, updates} = action.payload
|
||||
const newState = {
|
||||
roles: state.roles.map(r => r.links.self === role.links.self ? {...r, ...updates} : r),
|
||||
}
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'DELETE_USER': {
|
||||
const {user} = action.payload
|
||||
const newState = {
|
||||
users: state.users.filter(u => u.links.self !== user.links.self),
|
||||
}
|
||||
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'DELETE_ROLE': {
|
||||
const {role} = action.payload
|
||||
const newState = {
|
||||
roles: state.roles.filter(r => r.links.self !== role.links.self),
|
||||
}
|
||||
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'LOAD_QUERIES': {
|
||||
return {...state, ...action.payload}
|
||||
}
|
||||
|
||||
case 'FILTER_USERS': {
|
||||
const {text} = action.payload
|
||||
const newState = {
|
||||
users: state.users.map(u => {
|
||||
u.hidden = !u.name.toLowerCase().includes(text)
|
||||
return u
|
||||
}),
|
||||
}
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'FILTER_ROLES': {
|
||||
const {text} = action.payload
|
||||
const newState = {
|
||||
roles: state.roles.map(r => {
|
||||
r.hidden = !r.name.toLowerCase().includes(text)
|
||||
return r
|
||||
}),
|
||||
}
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'KILL_QUERY': {
|
||||
const {queryID} = action.payload
|
||||
const nextState = {
|
||||
queries: reject(state.queries, (q) => +q.id === +queryID),
|
||||
}
|
||||
|
||||
return {...state, ...nextState}
|
||||
}
|
||||
|
||||
case 'SET_QUERY_TO_KILL': {
|
||||
return {...state, ...action.payload}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
|
@ -6,10 +6,6 @@ import AJAX from 'utils/ajax';
|
|||
import _ from 'lodash';
|
||||
import NoKapacitorError from '../../shared/components/NoKapacitorError';
|
||||
|
||||
// Kevin: because we were getting strange errors saying
|
||||
// "Failed prop type: Required prop `source` was not specified in `AlertsApp`."
|
||||
// Tim and I decided to make the source and addFlashMessage props not required.
|
||||
// FIXME: figure out why that wasn't working
|
||||
const AlertsApp = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, {PropTypes} from 'react';
|
|||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
import MultiSelectDropdown from './MultiSelectDropdown';
|
||||
import MultiSelectDropdown from 'src/shared/components/MultiSelectDropdown';
|
||||
import Dropdown from 'src/shared/components/Dropdown';
|
||||
|
||||
import {INFLUXQL_FUNCTIONS} from '../constants';
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import OnClickOutside from 'shared/components/OnClickOutside';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
const {func, arrayOf, string} = PropTypes;
|
||||
const MultiSelectDropdown = React.createClass({
|
||||
propTypes: {
|
||||
onApply: func.isRequired,
|
||||
items: arrayOf(PropTypes.string.isRequired).isRequired,
|
||||
selectedItems: arrayOf(string.isRequired).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
isOpen: false,
|
||||
localSelectedItems: this.props.selectedItems,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!_.isEqual(this.state.localSelectedItems, nextProps.selectedItems)) {
|
||||
this.setState({
|
||||
localSelectedItems: nextProps.selectedItems,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleClickOutside() {
|
||||
this.setState({isOpen: false});
|
||||
},
|
||||
|
||||
toggleMenu(e) {
|
||||
e.stopPropagation();
|
||||
this.setState({isOpen: !this.state.isOpen});
|
||||
},
|
||||
|
||||
onSelect(item, e) {
|
||||
e.stopPropagation();
|
||||
|
||||
const {localSelectedItems} = this.state;
|
||||
|
||||
let nextItems;
|
||||
if (this.isSelected(item)) {
|
||||
nextItems = localSelectedItems.filter((i) => i !== item);
|
||||
} else {
|
||||
nextItems = localSelectedItems.concat(item);
|
||||
}
|
||||
|
||||
this.setState({localSelectedItems: nextItems});
|
||||
},
|
||||
|
||||
isSelected(item) {
|
||||
return this.state.localSelectedItems.indexOf(item) > -1;
|
||||
},
|
||||
|
||||
onApplyFunctions(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
this.setState({isOpen: false});
|
||||
this.props.onApply(this.state.localSelectedItems);
|
||||
},
|
||||
|
||||
render() {
|
||||
const {localSelectedItems} = this.state;
|
||||
const {isOpen} = this.state;
|
||||
const labelText = isOpen ? "0 Selected" : "Apply Function";
|
||||
|
||||
return (
|
||||
<div className={classNames('dropdown multi-select-dropdown', {open: isOpen})}>
|
||||
<div onClick={this.toggleMenu} className="btn btn-xs btn-info dropdown-toggle" type="button">
|
||||
<span className="multi-select-dropdown__label">
|
||||
{
|
||||
localSelectedItems.length ? localSelectedItems.map((s) => s).join(', ') : labelText
|
||||
}
|
||||
</span>
|
||||
<span className="caret"></span>
|
||||
</div>
|
||||
{this.renderMenu()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderMenu() {
|
||||
const {items} = this.props;
|
||||
|
||||
return (
|
||||
<div className="dropdown-options">
|
||||
<li className="multi-select-dropdown__apply" onClick={this.onApplyFunctions}>
|
||||
<div className="btn btn-xs btn-info btn-block">Apply</div>
|
||||
</li>
|
||||
<ul className="dropdown-menu multi-select-dropdown__menu" aria-labelledby="dropdownMenu1">
|
||||
{items.map((listItem, i) => {
|
||||
return (
|
||||
<li className={classNames('multi-select-dropdown__item', {active: this.isSelected(listItem)})} key={i} onClick={_.wrap(listItem, this.onSelect)}>
|
||||
<a href="#">{listItem}</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default OnClickOutside(MultiSelectDropdown);
|
|
@ -14,6 +14,7 @@ import {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, KapacitorTasksPage
|
|||
import DataExplorer from 'src/data_explorer';
|
||||
import {DashboardsPage, DashboardPage} from 'src/dashboards';
|
||||
import {CreateSource, SourcePage, ManageSources} from 'src/sources';
|
||||
import {AdminPage} from 'src/admin';
|
||||
import NotFound from 'src/shared/components/NotFound';
|
||||
import configureStore from 'src/store/configureStore';
|
||||
import {getMe, getSources} from 'shared/apis';
|
||||
|
@ -127,6 +128,7 @@ const Root = React.createClass({
|
|||
<Route path="alert-rules" component={KapacitorRulesPage} />
|
||||
<Route path="alert-rules/:ruleID" component={KapacitorRulePage} />
|
||||
<Route path="alert-rules/new" component={KapacitorRulePage} />
|
||||
<Route path="admin" component={AdminPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" component={NotFound} />
|
||||
|
|
|
@ -7,18 +7,16 @@ export function showDatabases(source) {
|
|||
return proxy({source, query});
|
||||
}
|
||||
|
||||
export function showQueries(host, db, clusterID) {
|
||||
const statement = 'SHOW QUERIES';
|
||||
const url = buildInfluxUrl({host, statement, database: db});
|
||||
export function showQueries(source, db) {
|
||||
const query = 'SHOW QUERIES';
|
||||
|
||||
return proxy(url, clusterID);
|
||||
return proxy({source, query, db});
|
||||
}
|
||||
|
||||
export function killQuery(host, queryId, clusterID) {
|
||||
const statement = `KILL QUERY ${queryId}`;
|
||||
const url = buildInfluxUrl({host, statement});
|
||||
export function killQuery(source, queryId) {
|
||||
const query = `KILL QUERY ${queryId}`;
|
||||
|
||||
return proxy(url, clusterID);
|
||||
return proxy({source, query});
|
||||
}
|
||||
|
||||
export function showMeasurements(source, db) {
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import classNames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
const labelText = ({localSelectedItems, isOpen, label}) => {
|
||||
if (label) {
|
||||
return label
|
||||
} else if (localSelectedItems.length) {
|
||||
return localSelectedItems.map((s) => s).join(', ')
|
||||
}
|
||||
|
||||
// TODO: be smarter about the text displayed here
|
||||
if (isOpen) {
|
||||
return '0 Selected'
|
||||
}
|
||||
return 'None'
|
||||
}
|
||||
|
||||
class MultiSelectDropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
localSelectedItems: this.props.selectedItems,
|
||||
}
|
||||
|
||||
this.onSelect = ::this.onSelect
|
||||
this.onApplyFunctions = ::this.onApplyFunctions
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!_.isEqual(this.state.localSelectedItems, nextProps.selectedItems)) {
|
||||
this.setState({
|
||||
localSelectedItems: nextProps.selectedItems,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleClickOutside() {
|
||||
this.setState({isOpen: false})
|
||||
}
|
||||
|
||||
toggleMenu(e) {
|
||||
e.stopPropagation()
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
onSelect(item, e) {
|
||||
e.stopPropagation()
|
||||
|
||||
const {localSelectedItems} = this.state
|
||||
|
||||
let nextItems
|
||||
if (this.isSelected(item)) {
|
||||
nextItems = localSelectedItems.filter((i) => i !== item)
|
||||
} else {
|
||||
nextItems = localSelectedItems.concat(item)
|
||||
}
|
||||
|
||||
this.setState({localSelectedItems: nextItems})
|
||||
}
|
||||
|
||||
isSelected(item) {
|
||||
return this.state.localSelectedItems.indexOf(item) > -1
|
||||
}
|
||||
|
||||
onApplyFunctions(e) {
|
||||
e.stopPropagation()
|
||||
|
||||
this.setState({isOpen: false})
|
||||
this.props.onApply(this.state.localSelectedItems)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {localSelectedItems, isOpen} = this.state
|
||||
const {label} = this.props
|
||||
|
||||
return (
|
||||
<div className={classNames('dropdown multi-select-dropdown', {open: isOpen})}>
|
||||
<div onClick={::this.toggleMenu} className="btn btn-xs btn-info dropdown-toggle" type="button">
|
||||
<div className="multi-select-dropdown__label">
|
||||
{
|
||||
labelText({localSelectedItems, isOpen, label})
|
||||
}
|
||||
</div>
|
||||
<span className="caret"></span>
|
||||
</div>
|
||||
{this.renderMenu()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderMenu() {
|
||||
const {items} = this.props
|
||||
|
||||
return (
|
||||
<div className="dropdown-options">
|
||||
<li className="multi-select-dropdown__apply" onClick={this.onApplyFunctions} style={{listStyle: 'none'}}>
|
||||
<div className="btn btn-xs btn-info btn-block">Apply</div>
|
||||
</li>
|
||||
<ul className="dropdown-menu multi-select-dropdown__menu" aria-labelledby="dropdownMenu1">
|
||||
{items.map((listItem, i) => {
|
||||
return (
|
||||
<li
|
||||
key={i}
|
||||
className={classNames('multi-select-dropdown__item', {active: this.isSelected(listItem)})}
|
||||
onClick={_.wrap(listItem, this.onSelect)}
|
||||
>
|
||||
<a href="#">{listItem}</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
MultiSelectDropdown.propTypes = {
|
||||
onApply: func.isRequired,
|
||||
items: arrayOf(string.isRequired).isRequired,
|
||||
selectedItems: arrayOf(string.isRequired).isRequired,
|
||||
label: string,
|
||||
}
|
||||
|
||||
export default OnClickOutside(MultiSelectDropdown)
|
|
@ -28,6 +28,7 @@ export const TabList = React.createClass({
|
|||
activeIndex: number,
|
||||
onActivate: func,
|
||||
isKapacitorTabs: string,
|
||||
customClass: string,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
|
@ -53,6 +54,14 @@ export const TabList = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (this.props.customClass) {
|
||||
return (
|
||||
<div className={this.props.customClass}>
|
||||
<div className="btn-group btn-group-lg tab-group">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="btn-group btn-group-lg tab-group">{children}</div>
|
||||
);
|
||||
|
@ -63,11 +72,13 @@ export const TabPanels = React.createClass({
|
|||
propTypes: {
|
||||
children: node.isRequired,
|
||||
activeIndex: number,
|
||||
customClass: string,
|
||||
},
|
||||
|
||||
// if only 1 child, children array index lookup will fail
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className={this.props.customClass ? this.props.customClass : null}>
|
||||
{this.props.children[this.props.activeIndex]}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
|
||||
const Tooltip = ({tip, children}) => (
|
||||
<div>
|
||||
<div data-tip={tip}>{children}</div>
|
||||
<ReactTooltip effect="solid" html={true} offset={{top: 2}} place="bottom" class="influx-tooltip place-bottom" />
|
||||
</div>
|
||||
)
|
||||
|
||||
const {
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
Tooltip.propTypes = {
|
||||
tip: string,
|
||||
children: shape({}),
|
||||
}
|
||||
|
||||
export default Tooltip
|
|
@ -471,4 +471,6 @@ export const STROKE_WIDTH = {
|
|||
export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds.
|
||||
export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds.
|
||||
|
||||
export const RES_UNAUTHORIZED = 401
|
||||
|
||||
export const AUTOREFRESH_DEFAULT = 15000 // in milliseconds
|
||||
|
|
|
@ -47,6 +47,9 @@ const SideNav = React.createClass({
|
|||
<NavListItem link={`${sourcePrefix}/manage-sources`}>InfluxDB</NavListItem>
|
||||
<NavListItem link={`${sourcePrefix}/kapacitor-config`}>Kapacitor</NavListItem>
|
||||
</NavBlock>
|
||||
<NavBlock icon="crown" link={`${sourcePrefix}/admin`}>
|
||||
<NavHeader link={`${sourcePrefix}/admin`} title="Admin" />
|
||||
</NavBlock>
|
||||
{loggedIn ? (
|
||||
<NavBlock icon="user-outline" className="sidebar__square-last">
|
||||
<a className="sidebar__menu-item" href="/oauth/logout">Logout</a>
|
||||
|
|
|
@ -3,6 +3,7 @@ import {combineReducers} from 'redux';
|
|||
import thunkMiddleware from 'redux-thunk';
|
||||
import makeQueryExecuter from 'src/shared/middleware/queryExecuter';
|
||||
import resizeLayout from 'src/shared/middleware/resizeLayout';
|
||||
import adminReducer from 'src/admin/reducers/admin';
|
||||
import sharedReducers from 'src/shared/reducers';
|
||||
import dataExplorerReducers from 'src/data_explorer/reducers';
|
||||
import rulesReducer from 'src/kapacitor/reducers/rules';
|
||||
|
@ -12,6 +13,7 @@ import persistStateEnhancer from './persistStateEnhancer';
|
|||
const rootReducer = combineReducers({
|
||||
...sharedReducers,
|
||||
...dataExplorerReducers,
|
||||
admin: adminReducer,
|
||||
rules: rulesReducer,
|
||||
dashboardUI,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,80 @@
|
|||
$ms-normal-left-padding: 9px;
|
||||
$ms-item-height: 26px;
|
||||
$ms-checkbox-size: 14px;
|
||||
$ms-checkbox-dot-size: 6px;
|
||||
$ms-checkbox-bg: $c-sapphire;
|
||||
$ms-checkbox-bg-hover: $c-ocean;
|
||||
$ms-checkbox-dot: $g20-white;
|
||||
|
||||
.multi-select-dropdown {
|
||||
.multi-select-dropdown__item > a {
|
||||
color: $c-neutrino !important;
|
||||
height: $ms-item-height;
|
||||
line-height: $ms-item-height;
|
||||
position: relative;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-right: $ms-normal-left-padding;
|
||||
padding-left: ($ms-normal-left-padding + $ms-checkbox-size + ($ms-normal-left-padding - 2px));
|
||||
|
||||
&,
|
||||
&:focus,
|
||||
&:active,
|
||||
&:active:focus {
|
||||
background: none !important;
|
||||
|
||||
&:hover {
|
||||
background: $c-pool;
|
||||
background: -moz-linear-gradient(left, $c-pool 0%, $c-pool 100%) !important;
|
||||
background: -webkit-linear-gradient(left, $c-pool 0%,$c-pool 100%) !important;
|
||||
background: linear-gradient(to right, $c-pool 0%,$c-pool 100%) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$c-pool', endColorstr='$c-pool',GradientType=1 ) !important;
|
||||
}
|
||||
}
|
||||
/* Shared Checkbox Styles */
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 50%;
|
||||
}
|
||||
/* Before = Checkbox */
|
||||
&:before {
|
||||
width: $ms-checkbox-size;
|
||||
height: $ms-checkbox-size;
|
||||
border-radius: $radius-small;
|
||||
background-color: $ms-checkbox-bg;
|
||||
left: $ms-normal-left-padding;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
/* After = Dot */
|
||||
&:after {
|
||||
width: $ms-checkbox-dot-size;
|
||||
height: $ms-checkbox-dot-size;
|
||||
background-color: $ms-checkbox-dot;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%,-50%) scale(2,2);
|
||||
opacity: 0;
|
||||
left: ($ms-normal-left-padding + ($ms-checkbox-size / 2));
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
transform 0.25s ease;
|
||||
}
|
||||
/* Hover State */
|
||||
&:hover {
|
||||
color: $g20-white !important;
|
||||
}
|
||||
}
|
||||
.dropdown-toggle {
|
||||
width: 110px;
|
||||
|
||||
&.btn-xs {
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
&__apply {
|
||||
margin: 0;
|
||||
|
@ -45,6 +119,22 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Checked State */
|
||||
.multi-select-dropdown li.multi-select-dropdown__item.active > a {
|
||||
&,
|
||||
&:focus,
|
||||
&:active,
|
||||
&:active:focus {
|
||||
background: none !important;
|
||||
}
|
||||
color: $g20-white !important;
|
||||
|
||||
&:after {
|
||||
transform: translate(-50%,-50%) scale(1,1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
/* Open State */
|
||||
.multi-select-dropdown.open {
|
||||
.dropdown-options {
|
||||
|
@ -56,3 +146,14 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.multi-select-dropdown__label {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
width: calc(100% - #{($ms-normal-left-padding * 2)});
|
||||
left: $ms-normal-left-padding;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
Custom Search Widget
|
||||
----------------------------------------------
|
||||
*/
|
||||
$search-widget-height: 36px;
|
||||
|
||||
.users__search-widget {
|
||||
position: relative;
|
||||
|
||||
input.form-control {
|
||||
height: $search-widget-height;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
@ -19,7 +22,7 @@
|
|||
.input-group-addon {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
line-height: 38px;
|
||||
line-height: calc(#{$search-widget-height} - 2px);
|
||||
position: absolute;
|
||||
color: $g10-wolf;
|
||||
top: 0;
|
||||
|
@ -32,4 +35,7 @@
|
|||
transition:
|
||||
color 0.25s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
.admin__search-widget {
|
||||
width: 300px;
|
||||
}
|
||||
|
|
|
@ -108,6 +108,64 @@ table .monotype {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Table Styles for Admin Pages
|
||||
----------------------------------------------
|
||||
*/
|
||||
.admin-table {
|
||||
.admin-table--delete {
|
||||
visibility: hidden;
|
||||
}
|
||||
tbody tr {
|
||||
transition: background-color 0.25s ease;
|
||||
&:hover {
|
||||
background-color: $g4-onyx;
|
||||
}
|
||||
}
|
||||
tbody tr:hover .admin-table--delete {
|
||||
visibility: visible;
|
||||
}
|
||||
.multi-select-dropdown {
|
||||
width: 100%;
|
||||
min-width: 150px;
|
||||
}
|
||||
.dropdown-toggle {
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
|
||||
.caret {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
tbody tr:hover .dropdown-toggle {
|
||||
color: $g20-white !important;
|
||||
background-color: $c-pool;
|
||||
|
||||
.caret {opacity: 1;}
|
||||
}
|
||||
}
|
||||
.admin-table--edit-row {
|
||||
background-color: $g4-onyx;
|
||||
}
|
||||
.admin-table--edit-cell {
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
|
||||
> input {
|
||||
height: 30px;
|
||||
padding: 0 9px;
|
||||
flex-grow: 1;
|
||||
margin: 0 2px;
|
||||
min-width: 110px;
|
||||
|
||||
&:first-child {margin-left: 0;}
|
||||
&:last-child {margin-right: 0;}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Responsive Tables
|
||||
----------------------------------------------
|
||||
|
@ -119,4 +177,4 @@ table .monotype {
|
|||
border-color: $g5-pepper;
|
||||
@include custom-scrollbar($g5-pepper, $c-pool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
.panel-title {
|
||||
color: $g10-wolf !important;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 30px;
|
||||
background-color: $g3-castle;
|
||||
|
@ -42,23 +41,41 @@
|
|||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
th,td {
|
||||
border-color: $g5-pepper;
|
||||
}
|
||||
th {
|
||||
color: $g17-whisper;
|
||||
}
|
||||
td {
|
||||
color: $g14-chromium;
|
||||
}
|
||||
tbody tr:last-child td {
|
||||
border-bottom: 2px solid $g5-pepper;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel.panel-info {
|
||||
background-color: $g3-castle;
|
||||
border: 0;
|
||||
|
||||
.panel-body,
|
||||
.panel-heading {
|
||||
background-color: transparent;
|
||||
}
|
||||
.panel-body {
|
||||
padding: 30px;
|
||||
}
|
||||
.panel-heading {
|
||||
border-color: $g4-onyx;
|
||||
.panel-title { color: $g14-chromium;}
|
||||
}
|
||||
}
|
||||
.panel .panel-body table {
|
||||
margin: 0;
|
||||
|
||||
th,td {
|
||||
border-color: $g5-pepper;
|
||||
}
|
||||
th {
|
||||
color: $g17-whisper;
|
||||
}
|
||||
td {
|
||||
color: $g14-chromium;
|
||||
}
|
||||
tbody tr:last-child td {
|
||||
border-bottom: 2px solid $g5-pepper;
|
||||
}
|
||||
}
|
||||
|
||||
table thead th {
|
||||
@include no-user-select();
|
||||
}
|
||||
|
@ -258,7 +275,6 @@ input {
|
|||
max-width: 100%;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
min-height: 70px;
|
||||
max-height: 290px;
|
||||
overflow: auto;
|
||||
@include custom-scrollbar($c-pool, $c-laser);
|
||||
|
@ -752,8 +768,45 @@ $form-static-checkbox-size: 16px;
|
|||
|
||||
|
||||
|
||||
/*
|
||||
Admin Tabs
|
||||
----------------------------------------------
|
||||
*/
|
||||
.admin-tabs .btn-group {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.tab {
|
||||
font-weight: 500 !important;
|
||||
border-radius: $radius !important;
|
||||
transition:
|
||||
background-color 0.25s ease,
|
||||
color 0.25s ease !important;
|
||||
border: 0 !important;
|
||||
text-align: left;
|
||||
height: 60px !important;
|
||||
line-height: 60px !important;
|
||||
padding: 0 0 0 16px !important;
|
||||
font-size: 17px;
|
||||
background-color: transparent !important;
|
||||
color: $g11-sidewalk !important;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:active:hover {
|
||||
background-color: $g3-castle !important;
|
||||
color: $g15-platinum !important;
|
||||
}
|
||||
&.active {
|
||||
background-color: $g4-onyx !important;
|
||||
color: $g18-cloud !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
@include no-user-select();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import axios from 'axios';
|
|||
|
||||
let links
|
||||
|
||||
const UNAUTHORIZED = 401
|
||||
import {RES_UNAUTHORIZED} from 'shared/constants'
|
||||
|
||||
export default async function AJAX({
|
||||
url,
|
||||
|
@ -13,10 +13,9 @@ export default async function AJAX({
|
|||
params = {},
|
||||
headers = {},
|
||||
}) {
|
||||
let response
|
||||
|
||||
try {
|
||||
const basepath = window.basepath || ''
|
||||
let response
|
||||
|
||||
url = `${basepath}${url}`
|
||||
|
||||
|
@ -47,9 +46,11 @@ export default async function AJAX({
|
|||
...response,
|
||||
}
|
||||
} catch (error) {
|
||||
if (!response.status === UNAUTHORIZED) {
|
||||
const {response} = error
|
||||
if (!response.status === RES_UNAUTHORIZED) {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
// console.error(error) // eslint-disable-line no-console
|
||||
const {auth} = links
|
||||
throw {auth, ...response} // eslint-disable-line no-throw-literal
|
||||
}
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import AJAX from 'utils/ajax';
|
||||
|
||||
// TODO: delete this once all references
|
||||
// to it have been removed
|
||||
export function buildInfluxUrl() {
|
||||
return "You dont need me anymore";
|
||||
}
|
||||
|
||||
export function proxy({source, query, db, rp}) {
|
||||
return AJAX({
|
||||
method: 'POST',
|
||||
url: source,
|
||||
data: {
|
||||
query,
|
||||
db,
|
||||
rp,
|
||||
},
|
||||
});
|
||||
export const proxy = async ({source, query, db, rp}) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'POST',
|
||||
url: source,
|
||||
data: {
|
||||
query,
|
||||
db,
|
||||
rp,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react'
|
||||
import {storiesOf, action, linkTo} from '@kadira/storybook'
|
||||
import Center from './components/Center'
|
||||
|
||||
import MultiSelectDropdown from 'shared/components/MultiSelectDropdown'
|
||||
import Tooltip from 'shared/components/Tooltip'
|
||||
|
||||
storiesOf('MultiSelectDropdown', module)
|
||||
.add('Select Roles w/label', () => (
|
||||
<Center>
|
||||
<MultiSelectDropdown
|
||||
items={[
|
||||
'Admin',
|
||||
'User',
|
||||
'Chrono Girafferoo',
|
||||
'Prophet',
|
||||
'Susford',
|
||||
]}
|
||||
selectedItems={[
|
||||
'User',
|
||||
'Chrono Girafferoo',
|
||||
]}
|
||||
label={'Select Roles'}
|
||||
onApply={action('onApply')}
|
||||
/>
|
||||
</Center>
|
||||
))
|
||||
.add('Selected Item list', () => (
|
||||
<Center>
|
||||
<MultiSelectDropdown
|
||||
items={[
|
||||
'Admin',
|
||||
'User',
|
||||
'Chrono Giraffe',
|
||||
'Prophet',
|
||||
'Susford',
|
||||
]}
|
||||
selectedItems={[
|
||||
'User',
|
||||
'Chrono Giraffe',
|
||||
]}
|
||||
onApply={action('onApply')}
|
||||
/>
|
||||
</Center>
|
||||
))
|
||||
.add('0 selected items', () => (
|
||||
<Center>
|
||||
<MultiSelectDropdown
|
||||
items={[
|
||||
'Admin',
|
||||
'User',
|
||||
'Chrono Giraffe',
|
||||
'Prophet',
|
||||
'Susford',
|
||||
]}
|
||||
selectedItems={[]}
|
||||
onApply={action('onApply')}
|
||||
/>
|
||||
</Center>
|
||||
))
|
||||
|
||||
storiesOf('Tooltip', module)
|
||||
.add('Delete', () => (
|
||||
<Center>
|
||||
<Tooltip tip={`Are you sure? TrashIcon`}>
|
||||
<div className="btn btn-info btn-sm">
|
||||
Delete
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Center>
|
||||
))
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react'
|
||||
|
||||
const Center = ({children}) => (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%)',
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Center
|
|
@ -3,3 +3,4 @@ import 'src/style/chronograf.scss';
|
|||
|
||||
// Kapacitor Stories
|
||||
import './kapacitor'
|
||||
import './admin'
|
||||
|
|
|
@ -12,7 +12,7 @@ import queryConfigs from './stubs/queryConfigs';
|
|||
|
||||
// Actions for Spies
|
||||
import * as kapacitorActions from 'src/kapacitor/actions/view'
|
||||
import * as queryActions from 'src/chronograf/actions/view';
|
||||
import * as queryActions from 'src/data_explorer/actions/view';
|
||||
|
||||
// Components
|
||||
import KapacitorRule from 'src/kapacitor/components/KapacitorRule';
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
const express = require('express')
|
||||
const request = require('request')
|
||||
const {default: storybook} = require('@kadira/storybook/dist/server/middleware')
|
||||
|
||||
const app = express()
|
||||
|
||||
const handler = (req, res) => {
|
||||
console.log(`${req.method} ${req.url}`)
|
||||
const url = 'http://localhost:8888' + req.url
|
||||
req.pipe(request(url)).pipe(res)
|
||||
}
|
||||
|
||||
app.use(storybook('./.storybook'))
|
||||
app.get('/chronograf/v1/*', handler)
|
||||
app.post('/chronograf/v1/*', handler)
|
||||
|
||||
app.listen(6006, () => {
|
||||
console.log('storybook proxy server now running')
|
||||
})
|
Loading…
Reference in New Issue