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 }) =>