diff --git a/create-neo4j4.sh b/create-neo4j4.sh index 4bd0c46f..60e0b616 100755 --- a/create-neo4j4.sh +++ b/create-neo4j4.sh @@ -4,7 +4,8 @@ docker stop neo4j-empty PASSWORD=admin CWD=`pwd` -NEO4J=neo4j:4.0.0-enterprise +#NEO4J=neo4j:4.0.2-enterprise +NEO4J=neo4j:4.0.2 docker run -d --name neo4j-empty --rm \ -p 127.0.0.1:7474:7474 \ diff --git a/package.json b/package.json index 2a5272e3..55e7e813 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "halin", "description": "Halin helps you monitor and improve your Neo4j graph", - "version": "0.12.2", + "version": "0.13.0", "neo4jDesktop": { "apiVersion": "^1.2.0" }, @@ -39,7 +39,7 @@ "react-sortable-tree": "^2.2.0", "react-table": "^6.8.6", "react-timeseries-charts": "^0.16.1", - "react-toastify": "^5.4.1", + "react-toastify": "^5.5.0", "ringjs": "^0.0.1", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^0.88.2", diff --git a/release-notes.md b/release-notes.md index d422d4b6..d30bbc45 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,5 +1,14 @@ # Halin Release Notes +## 0.13.0 Limited Alerting + +- Added alerts for certain memory conditions, such as heap re-allocation and imminent out-of-memory errors (OOMs) +- Fixes #132: Halin will warn you when you are approaching the need for high_limit storage. +- Members with non-standard ports will now have their port displayed in +their label. This helps separate members running on the same host. +- Main nav menu is labeled for help with navigation +- Thanks to Hugo for several points of feedback on this release. + ## 0.12.2 Patch Release - Fixes #127: Neo4j 3.5 standalone members appear properly, not as LEADER diff --git a/src/api/HalinContext.js b/src/api/HalinContext.js index 512a3555..d8c8a863 100644 --- a/src/api/HalinContext.js +++ b/src/api/HalinContext.js @@ -392,7 +392,6 @@ export default class HalinContext { this.getClusterManager().addEvent({ type: 'halin', message: 'Halin monitoring started', - address: 'all members', }); report('Initialization complete'); return this; diff --git a/src/api/cluster/ClusterManager.js b/src/api/cluster/ClusterManager.js index 6fe992bc..08990a34 100644 --- a/src/api/cluster/ClusterManager.js +++ b/src/api/cluster/ClusterManager.js @@ -85,6 +85,7 @@ export default class ClusterManager { _.set(data, 'date', moment.utc().toISOString()); _.set(data, 'payload', event.payload || null); _.set(data, 'id', uuid.v4()); + _.set(data, 'address', data.event || 'all members'); this.eventLog.push(data); // Notify listeners. @@ -166,6 +167,7 @@ export default class ClusterManager { .then(result => { this.addEvent({ type: 'adduser', + alert: true, message: `Added user "${username}"`, payload: username, }); @@ -188,6 +190,7 @@ export default class ClusterManager { type: 'deleteuser', message: `Deleted user "${username}"`, payload: username, + alert: true, }); return result; }) @@ -205,6 +208,7 @@ export default class ClusterManager { type: 'addrole', message: `Created role "${role}"`, payload: role, + alert: true, }); return result; }); @@ -222,6 +226,7 @@ export default class ClusterManager { type: 'deleterole', message: `Deleted role "${role}"`, payload: role, + alert: true, }); return result; }); @@ -229,13 +234,13 @@ export default class ClusterManager { /** Specific to a particular node */ addNodeRole(node, username, role) { - sentry.info('ADD ROLE', { username, role }); + sentry.info('ADD ROLE', { username, role, addr: node.getBoltAddress() }); return node.run('call dbms.security.addRoleToUser($role, $username)', { username, role }, neo4j.SYSTEM_DB); } /** Specific to a particular node */ removeNodeRole(node, username, role) { - sentry.info('REMOVE ROLE', { username, role }); + sentry.info('REMOVE ROLE', { username, role, addr: node.getBoltAddress() }); return node.run('call dbms.security.removeRoleFromUser($role, $username)', { username, role }, neo4j.SYSTEM_DB); } @@ -335,7 +340,11 @@ export default class ClusterManager { }); }; - const allPromises = this.ctx.members().map(node => { + const membersToRunAgainst = this.ctx.supportsSystemGraph() ? + [this.ctx.getSystemDBWriter()] : + this.ctx.members(); + + const allPromises = membersToRunAgainst.map(node => { return gatherRoles(node) .then(rolesHere => determineDifferences(rolesHere, node)) .then(roleChanges => applyChanges(roleChanges, node)) @@ -343,6 +352,7 @@ export default class ClusterManager { this.addEvent({ type: 'roleassoc', message: `Associated "${username}" to roles ${roles.map(r => `"${r}"`).join(', ')}`, + alert: true, payload: { username, roles }, }); }) @@ -442,6 +452,7 @@ export default class ClusterManager { .then(() => this.ctx.getDatabaseSet().refresh(this.ctx)) .then(() => this.addEvent({ type: 'database', + alert: true, message: `Stopped database ${db.name}`, payload: db, })); @@ -458,6 +469,7 @@ export default class ClusterManager { .then(() => this.ctx.getDatabaseSet().refresh(this.ctx)) .then(() => this.addEvent({ type: 'database', + alert: true, message: `Started database ${db.name}`, payload: db, })); @@ -474,6 +486,7 @@ export default class ClusterManager { .then(() => this.ctx.getDatabaseSet().refresh(this.ctx)) .then(() => this.addEvent({ type: 'database', + alert: true, message: `Dropped database ${db.name}`, })); } @@ -495,6 +508,7 @@ export default class ClusterManager { .then(() => this.ctx.getDatabaseSet().refresh(this.ctx)) .then(() => this.addEvent({ type: 'database', + alert: true, message: `Created database ${name}`, })); } diff --git a/src/api/cluster/ClusterMember.js b/src/api/cluster/ClusterMember.js index 8f9f6e72..73970969 100644 --- a/src/api/cluster/ClusterMember.js +++ b/src/api/cluster/ClusterMember.js @@ -216,20 +216,33 @@ export default class ClusterMember { return this.boltAddress; } + getPort() { + const parsed = Parser.parse(this.getBoltAddress()); + return parsed.port || 7687; + } + getAddress() { const parsed = Parser.parse(this.getBoltAddress()); return parsed.host; } getLabel() { - const addr = this.getAddress() || 'NO_ADDRESS'; + const parts = Parser.parse(this.getBoltAddress()); + + const addr = parts.host || 'NO_ADDRESS'; + parts.port = parts.port || '7687'; + + // Add the port on only if it's non-standard. This also helps + // disambiguate when localhost is used 3x. + const port = parts.port === '7687' ? '' : ':' + parts.port; + if (addr.match(/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/)) { // IP address - return addr; + return addr + port; } // Return the first portion of the hostname. - return addr.split('.')[0]; + return addr.split('.')[0] + port; } protocols() { diff --git a/src/api/cluster/ClusterMember.test.js b/src/api/cluster/ClusterMember.test.js index ab5985ac..f8f97cce 100644 --- a/src/api/cluster/ClusterMember.test.js +++ b/src/api/cluster/ClusterMember.test.js @@ -8,6 +8,7 @@ import _ from 'lodash'; describe('ClusterMember', function () { const host = 'foo-host'; + const label = 'foo-host:7777'; // Non-standard port gets label const boltAddress = `bolt://${host}:7777`; const httpAddress = `http://${host}:8888`; const entry = { @@ -78,7 +79,7 @@ describe('ClusterMember', function () { }); it('knows its bolt address', () => expect(c.getBoltAddress()).toEqual(boltAddress)); - it('knows how to label by host', () => expect(c.getLabel()).toEqual(host)); + it('knows how to label by host', () => expect(c.getLabel()).toEqual(label)); it('knows how to extract protocols', () => { const prots = c.protocols(); expect(prots).toContain('http'); diff --git a/src/api/cluster/ClusterMemberSet.js b/src/api/cluster/ClusterMemberSet.js index 048aa2dc..ec8d3715 100644 --- a/src/api/cluster/ClusterMemberSet.js +++ b/src/api/cluster/ClusterMemberSet.js @@ -4,6 +4,7 @@ import ClusterMember from './ClusterMember'; import errors from '../driver/errors'; import sentry from '../sentry'; import uuid from 'uuid'; +import util from '../data/util'; const REFRESH_INTERVAL = 5000; @@ -18,10 +19,18 @@ export default class ClusterMemberSet { this.clusterMembers = []; this.clustered = false; this.timeout = null; + this.memFeeds = {}; + + // Stores utilization statistics about each member + this.stats = {}; } members() { return this.clusterMembers; } + getStats() { + return this.stats; + } + shutdown() { if (this.timeout) { clearTimeout(this.timeout); } @@ -82,6 +91,83 @@ export default class ClusterMemberSet { return this.timeout; } + updateStats(halin, addr, data) { + const stats = _.merge({ lastUpdated: new Date() }, _.clone(data)); + + const prevHeap = _.get(this.stats[addr], 'heapCommitted'); + const nowHeap = _.get(data, 'heapCommitted'); + + stats.pctUsed = (data.heapUsed || 0) / (data.heapCommitted || -1); + stats.pctFree = 1 - stats.pctUsed; + + if (prevHeap && nowHeap !== prevHeap) { + const a = util.humanDataSize(prevHeap); + const b = util.humanDataSize(nowHeap); + + halin.getClusterManager().addEvent({ + type: 'memory', + alert: true, + address: addr, + message: `Heap allocation changed on ${addr} from ${a} to ${b}`, + payload: { + old: _.clone(this.stats[addr]), + new: _.clone(stats), + }, + }); + } + + if (stats.pctFree <= 0.1) { + halin.getClusterManager().addEvent({ + type: 'memory', + alert: true, + error: true, + address: addr, + message: `Heap is >= 90% utilization on ${addr}`, + payload: _.clone(stats), + }); + } else if (stats.pctFree <= 0.02) { + halin.getClusterManager().addEvent({ + type: 'memory', + alert: true, + error: true, + address: addr, + message: `Heap is >= 98% utilization on ${addr}`, + payload: _.clone(stats), + }); + } + + this.stats[addr] = stats; + return stats; + } + + getMemoryFeed(halin, clusterMember) { + const addr = clusterMember.getBoltAddress(); + + if (this.memFeeds[addr]) { + return Promise.resolve(this.memFeeds[addr]); + } + + const memFeed = halin.getDataFeed(_.merge({ + node: clusterMember, + }, queryLibrary.JMX_MEMORY_STATS)); + + return new Promise((resolve, reject) => { + const onMemData = (newData /* , dataFeed */) => { + const data = _.get(newData, 'data[0]'); + return this.updateStats(halin, addr, data); + }; + + const onError = (err, dataFeed) => { + sentry.fine('ClusterMemberSet: failed to get mem data', addr, err); + reject(err, dataFeed); + }; + + memFeed.addListener(onMemData); + memFeed.onError = onError; + return resolve(memFeed); + }); + } + /** * Ping a cluster node with a trivial query, just to keep connections * alive and verify it's still listening. This forces driver creation @@ -182,7 +268,11 @@ export default class ClusterMemberSet { // SETUP ACTIONS FOR ANY NEW MEMBER const driver = halin.driverFor(member.getBoltAddress()); member.setDriver(driver); - const setup = this.ping(halin, member) + + const setup = Promise.all([ + this.ping(halin, member), + this.getMemoryFeed(halin, member), + ]) .then(() => member.checkComponents()); promises.push(setup); diff --git a/src/api/data/queries/dbms/listIndexes.js b/src/api/data/queries/dbms/listIndexes.js new file mode 100644 index 00000000..30543c76 --- /dev/null +++ b/src/api/data/queries/dbms/listIndexes.js @@ -0,0 +1,39 @@ +import HalinQuery from '../HalinQuery'; + +export default new HalinQuery({ + description: 'Lists database indexes', + query: ` + CALL db.indexes() + YIELD id, name, state, populationPercent, uniqueness, type, + entityType, labelsOrTypes, properties, provider + RETURN id, name, state, populationPercent, uniqueness, type, + entityType, labelsOrTypes, properties, provider + ORDER BY name asc; + `, + columns: [ + { Header: 'ID', accessor: 'id' }, + { Header: 'Name', accessor: 'name', }, + { Header: 'State', accessor: 'state' }, + { Header: 'Population', accessor: 'populationPercent' }, + { Header: 'Uniqueness', accessor: 'uniqueness' }, + { Header: 'Type', accessor: 'type' }, + { Header: 'Entity', accessor: 'entityType' }, + { Header: 'Labels/Types', accessor: 'labelsOrTypes' }, + { Header: 'Properties', accessor: 'properties' }, + { Header: 'Provider', accessor: 'provider' }, + ], + exampleResult: [ + { + id: 1, + name: 'index_70bffdb', + state: 'ONLINE', + populationPercent: 100.0, + uniqueness: 'NONUNIQUE', + type: 'BTREE', + entityType: 'NODE', + labelsOrTypes: ['Label'], + properties: ['name'], + provider: 'native-btree-1.0', + }, + ], +}); \ No newline at end of file diff --git a/src/api/data/queries/query-library.js b/src/api/data/queries/query-library.js index d6e5b942..17cad9f5 100644 --- a/src/api/data/queries/query-library.js +++ b/src/api/data/queries/query-library.js @@ -36,6 +36,7 @@ import DBMS_GET_MAX_HEAP from './dbms/getMaxHeap'; import DBMS_FUNCTIONS from './dbms/functions'; import DBMS_PROCEDURES from './dbms/procedures'; import DBMS_LIST_CONFIG from './dbms/listConfig'; +import DBMS_LIST_INDEXES from './dbms/listIndexes'; import DBMS_SECURITY_USER_ROLES from './dbms/security/userRoles'; import DBMS_LIST_TRANSACTIONS from './dbms/listTransactions'; import DBMS_LIST_CONNECTIONS from './dbms/listConnections'; @@ -104,6 +105,7 @@ const allQueries = { DBMS_FUNCTIONS, DBMS_PROCEDURES, DBMS_LIST_CONFIG, + DBMS_LIST_INDEXES, DBMS_SECURITY_USER_ROLES, DBMS_LIST_TRANSACTIONS, DBMS_LIST_CONNECTIONS, diff --git a/src/api/diagnostic/advisor/Advice.js b/src/api/diagnostic/advisor/Advice.js index 93957127..55b01662 100644 --- a/src/api/diagnostic/advisor/Advice.js +++ b/src/api/diagnostic/advisor/Advice.js @@ -11,12 +11,13 @@ export default class Advice { * @param evidence (optional) data showing the issue * @param advice (optional) corrective action if applicable. */ - constructor({ level, addr=Advice.CLUSTER, finding, evidence=null, advice='none' }) { + constructor({ level, addr=Advice.CLUSTER, database='all', finding, evidence=null, advice='none' }) { this.addr = addr; this.level = level; this.finding = finding; this.evidence = evidence; this.advice = advice; + this.database = database; if (!level || !addr || !finding) { throw new Error('Advice requires at a minimum a level, addr, and finding'); diff --git a/src/api/diagnostic/advisor/index.js b/src/api/diagnostic/advisor/index.js index b2ef1d9c..73df8712 100644 --- a/src/api/diagnostic/advisor/index.js +++ b/src/api/diagnostic/advisor/index.js @@ -25,6 +25,7 @@ import security from './rules/security/'; import plugins from './rules/plugins'; import transactions from './rules/transactions'; import retention from './rules/retention'; +import storeConfig from './rules/storeConfig'; const dummy = diag => { return [ @@ -68,6 +69,7 @@ const rules = [ ...categorize(plugins, 'Plugins'), ...categorize(transactions, 'Transactions'), ...categorize(retention, 'Disk'), + ...categorize(storeConfig, 'Storage'), ]; /** diff --git a/src/api/diagnostic/advisor/rules/storeConfig.js b/src/api/diagnostic/advisor/rules/storeConfig.js new file mode 100644 index 00000000..155e90f0 --- /dev/null +++ b/src/api/diagnostic/advisor/rules/storeConfig.js @@ -0,0 +1,52 @@ +import Advice from '../Advice'; +import util from '../../../data/util'; + +const nodeStorage = pkg => { + const findings = []; + + // For what this is about, see: https://github.com/moxious/halin/issues/132 + const THRESHOLD = (34 * 1000000000) * 0.9; // 90% of 34 billion + + pkg.nodes.forEach(node => { + const addr = node.basics.address; + const val = node.configuration['dbms.record_format'] || 'standard'; + + // The warning check doesn't apply if they've already enabled high limits. + if (val === 'high_limit') { + findings.push(Advice.info({ + addr, + finding: 'You are using dbms.record_format=high limit', + })); + return null; + } + + pkg.databases.filter(db => db.nodeCount !== -1).forEach(database => { + const count = database.nodeCount; + const name = database.name; + + if (count >= THRESHOLD) { + findings.push(Advice.warn({ + addr, + database: name, + finding: `On database ${name} you have almost exhausted the number of available IDs`, + advice: 'Change dbms.record_format to high_limit; see https://neo4j.com/docs/operations-manual/current/reference/configuration-settings/#config_dbms.record_format for more details', + })); + } else { + const pct = util.roundPct(count / THRESHOLD); + + findings.push(Advice.pass({ + addr, + database: name, + finding: `Database ${name} has ${count} nodes, or ${pct}% of the threshold limit`, + advice: 'No action needed; see https://neo4j.com/docs/operations-manual/current/reference/configuration-settings/#config_dbms.record_format for what this is about', + })); + } + }); + }); + + return findings; +}; + +export default [ + nodeStorage, +]; \ No newline at end of file diff --git a/src/api/diagnostic/advisor/test-package.json b/src/api/diagnostic/advisor/test-package.json index ccad07f4..3fca684c 100644 --- a/src/api/diagnostic/advisor/test-package.json +++ b/src/api/diagnostic/advisor/test-package.json @@ -971,6 +971,7 @@ "@babel/preset-react": "^7.0.0" } }, + "databases": [], "nodes": [ { "basics": { diff --git a/src/api/diagnostic/collection/index.js b/src/api/diagnostic/collection/index.js index 6b680efd..8dd9e230 100644 --- a/src/api/diagnostic/collection/index.js +++ b/src/api/diagnostic/collection/index.js @@ -203,10 +203,14 @@ const gatherConfig = (halin, node) => () => { * @return Promise{Object} of diagnostic information about that node. */ const memberDiagnostics = (halin, clusterMember) => { + // Keyed by address => utilization object. + const utilizationStats = halin.getMemberSet().getStats(); + const basics = { basics: clusterMember.asJSON(), + utilization: utilizationStats[clusterMember.getBoltAddress()], }; - + /* GATHER STEPS. * Each of these is a promise that is guaranteed not to fail because it's wrapped. * Each is wrapped in a closure (they all return functions) to help us control concurrency. @@ -268,11 +272,28 @@ const halinDiagnostics = halinContext => { return Promise.resolve(halin); }; -const clusterManagerDiagnostics = halinContext => { - const getDatabasesPromise = Promise.resolve(halinContext.databases().map(db => db.asJSON())) - .then(arrayOfDBJSON => ({ databases: arrayOfDBJSON })); - - return getDatabasesPromise; +const databaseDiagnostics = halinContext => { + // For each database, run a count on that database alone, then merge it into + // the databases's regular descriptive stats. + const promises = halinContext.databases().map(db => { + const leader = db.getLeader(halinContext, false); + + return db.getLabel() === neo4j.SYSTEM_DB ? + Promise.resolve({ nodeCount: -1 }) : // Don't run count on systemdb + leader.run('MATCH (n) RETURN count(n) as n', {}, db.getLabel()) + .then(results => neo4j.unpackResults(results, { + required: ['n'], + })) + .then(results => ({ nodeCount: results[0].n })) + .catch(err => { + // Don't let this error stop us. + sentry.warn(`Failed to run node count on ${leader.getBoltAddress()} for ${db.getLabel()}`, err); + return { nodeCount: -1 }; + }) + .then(nodeCount => _.merge(nodeCount, db.asJSON())); + }); + + return Promise.all(promises).then(results => ({ databases: results })); }; /** @@ -316,12 +337,12 @@ const runDiagnostics = (halinContext, tag=uuid.v4()) => { }); const halinDiags = halinDiagnostics(halinContext); - const clusterManagerDiags = clusterManagerDiagnostics(halinContext); + const dbDiags = databaseDiagnostics(halinContext); const neo4jDesktopDiags = neo4jDesktopDiagnostics(halinContext); // Each object resolves to a diagnostic object with 1 key, and sub properties. // All diagnostics are just a merge of those objects. - return Promise.all([root, halinDiags, clusterManagerDiags, allNodeDiags, neo4jDesktopDiags]) + return Promise.all([root, halinDiags, dbDiags, allNodeDiags, neo4jDesktopDiags]) .then(arrayOfObjects => _.merge(...arrayOfObjects)) }; diff --git a/src/api/status/index.js b/src/api/status/index.js index d3a449e3..280e4fca 100644 --- a/src/api/status/index.js +++ b/src/api/status/index.js @@ -1,7 +1,7 @@ import React from 'react'; import { Message } from 'semantic-ui-react'; import { toast } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; +// import 'react-toastify/dist/ReactToastify.css'; import sentry from '../sentry/index'; import _ from 'lodash'; @@ -9,7 +9,7 @@ const message = (header, body) => ({ header, body }); const toastContent = messageObject =>
-

{messageObject.header}

+ { messageObject.header ?

{messageObject.header}

: '' }
{messageObject.body}
; @@ -41,6 +41,13 @@ export default { toastContent, + /** + * Allows you to toastify a component. THIS REQUIRES that the component have state containing + * fields "error" and/or "message". If an error is present you'll get an error message, otherwise + * and informative message. + * @param component a React component with state including either message or error + * @param overrideOptions any options to toast which will override defaults + */ toastify: (component, overrideOptions) => { if (!_.get(component, 'state.error') && !_.get(component, 'state.message')) { sentry.warn('Toastify called on a component with nothing to say', component.state); @@ -56,11 +63,11 @@ export default { }); const errorOptions = { - autoClose: false, + autoClose: 60000, }; const successOptions = { - autoClose: true, + autoClose: 5000, }; const toastBody = (component.state.error ? diff --git a/src/components/configuration/roles/AssignRoleModal/AssignRoleModal.js b/src/components/configuration/roles/AssignRoleModal/AssignRoleModal.js index 73c38a9b..a0c3897a 100644 --- a/src/components/configuration/roles/AssignRoleModal/AssignRoleModal.js +++ b/src/components/configuration/roles/AssignRoleModal/AssignRoleModal.js @@ -111,8 +111,12 @@ class AssignRoleModal extends Component { }; componentDidMount() { - this.loadUsers(); - this.loadRoles(); + this.setState({ + loadPromise: Promise.all([ + this.loadUsers(), + this.loadRoles(), + ]), + }); } UNSAFE_componentWillReceiveProps(props) { @@ -120,11 +124,17 @@ class AssignRoleModal extends Component { if (props.open) { // Load fresh data each time the dialog opens so that users // don't get stale stuff - this.loadUsers(); - this.loadRoles(); + this.setState({ + loadPromise: Promise.all([ + this.loadUsers(), + this.loadRoles(), + ]) + }); } - this.setState({ open: props.open }); + this.setState({ + open: props.open, + }); } } diff --git a/src/components/configuration/roles/Neo4jRoles/Neo4jRoles.js b/src/components/configuration/roles/Neo4jRoles/Neo4jRoles.js index 84ec679c..6d2f148b 100644 --- a/src/components/configuration/roles/Neo4jRoles/Neo4jRoles.js +++ b/src/components/configuration/roles/Neo4jRoles/Neo4jRoles.js @@ -104,24 +104,21 @@ class Neo4jRoles extends Component { if (clusterOpRes.success) { this.setState({ pending: false, - message: status.fromClusterOp(action, clusterOpRes), error: null, }); } else { this.setState({ pending: false, - message: null, error: status.fromClusterOp(action, clusterOpRes), }); } }) .catch(err => this.setState({ pending: false, - message: null, error: status.message('Error', `Could not delete role ${row.role}: ${err}`), })) - .finally(() => status.toastify(this)); + .finally(() => this.state.error ? status.toastify(this) : null); } open = (row) => { diff --git a/src/components/configuration/roles/NewRoleForm/NewRoleForm.js b/src/components/configuration/roles/NewRoleForm/NewRoleForm.js index 2cfae6ab..31b20426 100644 --- a/src/components/configuration/roles/NewRoleForm/NewRoleForm.js +++ b/src/components/configuration/roles/NewRoleForm/NewRoleForm.js @@ -9,7 +9,6 @@ class NewRoleForm extends Component { state = { role: null, pending: false, - message: null, error: null, }; @@ -32,24 +31,21 @@ class NewRoleForm extends Component { if (clusterOpRes.success) { this.setState({ pending: false, - message: status.fromClusterOp(action, clusterOpRes), error: null, }); } else { this.setState({ pending: false, - message: null, error: status.fromClusterOp(action, clusterOpRes), }); } }) .catch(err => this.setState({ pending: false, - message: null, error: status.message('Error', `Could not create role ${this.state.role}: ${err}`), })) - .finally(() => status.toastify(this)); + .finally(() => this.state.error ? status.toastify(this) : null); } formValid() { diff --git a/src/components/configuration/users/Neo4jUsers/Neo4jUsers.js b/src/components/configuration/users/Neo4jUsers/Neo4jUsers.js index 0acb973b..7ea53f5d 100644 --- a/src/components/configuration/users/Neo4jUsers/Neo4jUsers.js +++ b/src/components/configuration/users/Neo4jUsers/Neo4jUsers.js @@ -58,7 +58,6 @@ class Neo4jUsers extends Component { state = { childRefresh: 1, refresh: 1, - message: null, error: null, }; @@ -107,7 +106,6 @@ class Neo4jUsers extends Component { this.setState({ refresh: val, childRefresh: val, - message: null, error: null, }); } @@ -134,24 +132,21 @@ class Neo4jUsers extends Component { if (clusterOpRes.success) { this.setState({ pending: false, - message: status.fromClusterOp(action, clusterOpRes), error: null, }); } else { this.setState({ pending: false, - message: null, error: status.fromClusterOp(action, clusterOpRes), }); } }) .catch(err => this.setState({ pending: false, - message: null, error: status.message('Error', `Could not delete user ${row.username}: ${err}`), })) - .finally(() => status.toastify(this)); + .finally(() => this.state.error ? status.toastify(this) : null); } openAssign = (row) => { @@ -171,26 +166,23 @@ class Neo4jUsers extends Component { if (clusterOpResult instanceof Error) { newState = { assignOpen: false, - message: null, error: status.message(`Error on ${action}`, `${clusterOpResult}`), }; } else if (clusterOpResult.success) { newState = { assignOpen: false, - message: status.fromClusterOp(action, clusterOpResult), error: false, }; } else { newState = { assignOpen: false, - message: null, error: status.fromClusterOp(action, clusterOpResult), }; } // Fire the toast message after update is complete. - this.setState(newState, () => status.toastify(this)); + this.setState(newState, () => this.state.error ? status.toastify(this) : null); } closeAssign = () => { diff --git a/src/components/configuration/users/NewUserForm/NewUserForm.js b/src/components/configuration/users/NewUserForm/NewUserForm.js index b346e9f6..3cf0bac6 100644 --- a/src/components/configuration/users/NewUserForm/NewUserForm.js +++ b/src/components/configuration/users/NewUserForm/NewUserForm.js @@ -10,7 +10,6 @@ class NewUserForm extends Component { password: '', requireChange: false, pending: false, - message: null, error: null, }; @@ -20,7 +19,7 @@ class NewUserForm extends Component { } createUser() { - this.setState({ pending: true }); + this.setState({ pending: true, error: false }); const mgr = window.halinContext.getClusterManager(); @@ -37,24 +36,21 @@ class NewUserForm extends Component { if (clusterOpRes.success) { this.setState({ pending: false, - message: status.fromClusterOp(action, clusterOpRes), error: null, }); } else { this.setState({ pending: false, - message: null, error: status.fromClusterOp(action, clusterOpRes), }); } }) .catch(err => this.setState({ pending: false, - message: null, error: status.message('Error', `Could not create ${user.username}: ${err}`), })) - .finally(() => status.toastify(this)); + .finally(() => this.state.error ? status.toastify(this) : null); } valid() { diff --git a/src/components/database/AdministerDatabase/AdministerDatabase.js b/src/components/database/AdministerDatabase/AdministerDatabase.js index 3d7ef9a0..58dae72c 100644 --- a/src/components/database/AdministerDatabase/AdministerDatabase.js +++ b/src/components/database/AdministerDatabase/AdministerDatabase.js @@ -8,6 +8,7 @@ import neo4j from '../../../api/driver'; class AdministerDatabase extends Component { state = { pending: false, + error: null, dropConfirmOpen: false, }; @@ -49,24 +50,19 @@ class AdministerDatabase extends Component { } doOperation(operationPromise, successMessage, failMessage) { - this.setState({ pending: true }); + this.setState({ pending: true, error: null }); return operationPromise - .then(() => { - this.setState({ - message: status.message('Success', successMessage), - error: null, - }); - }) .catch(err => { this.setState({ - message: null, error: status.message('Error', failMessage + `: ${err}`), }); }) .finally(() => { this.setState({ pending: false }); - status.toastify(this); + if (this.state.error) { + status.toastify(this); + } }); } diff --git a/src/components/database/CreateDatabase/CreateDatabase.js b/src/components/database/CreateDatabase/CreateDatabase.js index 5f5955c5..351ee3aa 100644 --- a/src/components/database/CreateDatabase/CreateDatabase.js +++ b/src/components/database/CreateDatabase/CreateDatabase.js @@ -9,7 +9,6 @@ import Spinner from '../../ui/scaffold/Spinner/Spinner'; const defaultState = { name: '', pending: false, - message: null, error: null, }; @@ -33,24 +32,20 @@ class CreateDatabase extends Component { console.log('Creating database', this.state.name); this.setState({ pending: true }); - let message, error; + let error = null; return window.halinContext.getClusterManager().createDatabase(this.state.name) - .then(() => { - sentry.info(`Success creating ${this.state.name}`); - message = status.message('Success', `Created database ${this.state.name}`); - error = null; - }) .catch(err => { sentry.error('Failed to create database', err); error = status.message('Error', `Failed to create database ${this.state.name}: ${err}`); - message = null; }) .finally(() => { - sentry.fine(`Resetting create form`); - this.setState({ pending: false, error, message, name: '' }); - status.toastify(this); + this.setState({ pending: false, error, name: '' }); + + if (error) { + status.toastify(this); + } }); } diff --git a/src/components/db/LogsPane/LogsPane.js b/src/components/db/LogsPane/LogsPane.js index 24b11adf..d368e0e3 100644 --- a/src/components/db/LogsPane/LogsPane.js +++ b/src/components/db/LogsPane/LogsPane.js @@ -358,7 +358,7 @@ class LogsPane extends Component { viewerFor(file) { return ( ); } diff --git a/src/components/diagnostic/advisor/Advisor.js b/src/components/diagnostic/advisor/Advisor.js index e80db380..c7ebd400 100644 --- a/src/components/diagnostic/advisor/Advisor.js +++ b/src/components/diagnostic/advisor/Advisor.js @@ -6,6 +6,14 @@ import moment from 'moment'; import './Advisor.css'; import _ from 'lodash'; +const filterMethod = (filter, row) => { + if (filter.value === "all") { + return true; + } + + return row[filter.id] === filter.value; +}; + export default class Advisor extends Component { state = { findings: null, @@ -14,13 +22,7 @@ export default class Advisor extends Component { Header: 'Level', accessor: 'level', width: 100, - filterMethod: (filter, row) => { - if (filter.value === "all") { - return true; - } - - return row[filter.id] === filter.value; - }, + filterMethod, Filter: ({ filter, onChange }) => onChange(event.target.value)} @@ -68,17 +64,29 @@ export default class Advisor extends Component { } , }, + { + Header: 'Database', + accessor: 'database', + filterMethod, + width: 100, + Filter: ({ filter, onChange }) => + , + }, { Header: 'Category', accessor: 'category', width: 100, - filterMethod: (filter, row) => { - if (filter.value === "all") { - return true; - } - - return row[filter.id] === filter.value; - }, + filterMethod, Filter: ({ filter, onChange }) =>