app/react/Settings/components/relV2MigrationDashboard.tsx
/* eslint-disable max-lines */
import _ from 'lodash';
import React from 'react';
import { connect } from 'react-redux';
import { ClientTemplateSchema, IStore, RelationshipTypesType } from 'app/istore';
import {
getCurrentPlan,
getHubrecordPage,
sendMigrationRequest as _sendMigrationRequest,
testOneHub as _testOneHub,
createRelationshipMigrationField,
updateRelationshipMigrationField,
deleteRelationshipMigrationField,
} from 'app/Entities/actions/V2NewRelationshipsActions';
import { Icon } from 'app/UI';
import { objectIndex } from 'shared/data_utils/objectIndex';
import {
GetRelationshipMigrationFieldsResponse,
ResponseElement,
} from 'shared/types/api.v2/relationshipMigrationField.get';
import { PlanElement } from 'shared/types/api.v2/relationships.migrate';
import { CreateRelationshipMigRationFieldResponse } from 'shared/types/api.v2/relationshipMigrationField.create';
import { GetMigrationHubRecordsResponse } from 'shared/types/api.v2/migrationHubRecords.get';
const UNUSED_RECORDS_PAGE_SIZE = 10;
type MigrationSummaryType = {
total: number;
used: number;
errors: number;
totalTextReferences: number;
usedTextReferences: number;
time: number;
dryRun: boolean;
};
type OriginalEntityInfo = {
entity: string;
entityTemplate: string;
entityTitle: string;
hub: string;
id: string;
template: string;
templateName: string;
};
type TransformedInfo = {
from: { entity: string };
to: { entity: string };
type: string;
};
type hubTestResult = {
total: number;
used: number;
errors: number;
transformed: TransformedInfo[];
original: OriginalEntityInfo[];
};
const mapPlanElementFromApiResponse = (
response: ResponseElement,
templateIndex: Record<string, ClientTemplateSchema>,
relationTypeIndex: Record<string, RelationshipTypesType>
): PlanElement => {
const template = templateIndex[response.sourceTemplate];
const relationType = relationTypeIndex[response.relationType];
const targetTemplate = response.targetTemplate
? templateIndex[response.targetTemplate]
: undefined;
return {
sourceTemplate: template.name,
sourceTemplateId: response.sourceTemplate,
relationType: relationType.name,
relationTypeId: response.relationType,
targetTemplate: targetTemplate?.name || 'ALL',
targetTemplateId: response.targetTemplate,
inferred: response.infered,
ignored: response.ignored,
};
};
const mapGetPlanResponse = (
response: GetRelationshipMigrationFieldsResponse,
templateIndex: Record<string, ClientTemplateSchema>,
relationTypeIndex: Record<string, RelationshipTypesType>
) => response.map(r => mapPlanElementFromApiResponse(r, templateIndex, relationTypeIndex));
const formatTime = (time: number) => {
const floored = Math.floor(time);
if (floored < 1000) {
return `${floored}ms`;
}
return `${(floored / 1000).toFixed(2)}s`;
};
class _NewRelMigrationDashboard extends React.Component<ComponentPropTypes> {
private migrationSummary?: MigrationSummaryType;
private testedHub?: string;
private hubTestResult?: hubTestResult;
private showMigrationConfirm = false;
private currentPlan: PlanElement[] = [];
private templateIndex: Record<string, ClientTemplateSchema> = {};
private relationTypeIndex: Record<string, RelationshipTypesType> = {};
private templatesNameSorted: ClientTemplateSchema[] = [];
private relationTypesNameSorted: RelationshipTypesType[] = [];
private newPlanElement: PlanElement = {
sourceTemplate: '',
sourceTemplateId: '',
relationType: '',
relationTypeId: '',
targetTemplate: '',
targetTemplateId: '',
inferred: false,
ignored: false,
};
private unusedConnectionsPage = 1;
private unusedConnectionsInfo?: GetMigrationHubRecordsResponse;
async componentDidMount() {
this.templateIndex = objectIndex(
this.props.templates,
t => t._id,
t => t
);
this.templatesNameSorted = _.orderBy(this.props.templates, t => t.name);
this.relationTypeIndex = objectIndex(
this.props.relationTypes,
t => t._id,
t => t
);
this.relationTypesNameSorted = _.orderBy(this.props.relationTypes, t => t.name);
const response = (await getCurrentPlan()) as GetRelationshipMigrationFieldsResponse;
const mapped = mapGetPlanResponse(response, this.templateIndex, this.relationTypeIndex);
const ordered = _.orderBy(mapped, ['sourceTemplate', 'relationType', 'targetTemplate']);
this.currentPlan = ordered;
this.newPlanElement = {
sourceTemplate: this.templatesNameSorted[0].name,
sourceTemplateId: this.templatesNameSorted[0]._id,
relationType: this.relationTypesNameSorted[0].name,
relationTypeId: this.relationTypesNameSorted[0]._id,
targetTemplate: this.templatesNameSorted[1].name,
targetTemplateId: this.templatesNameSorted[1]._id,
};
await this.getUnusedConnections();
this.forceUpdate();
}
async getUnusedConnections(update: boolean = false) {
this.unusedConnectionsInfo = await getHubrecordPage(
this.unusedConnectionsPage,
UNUSED_RECORDS_PAGE_SIZE
);
if (update) {
this.forceUpdate();
}
}
async sendMigrationRequest(dryRun: boolean) {
const summary = await _sendMigrationRequest(dryRun, this.currentPlan);
this.migrationSummary = summary;
this.unusedConnectionsPage = 1;
await this.getUnusedConnections();
}
async performDryRun() {
await this.sendMigrationRequest(true);
this.forceUpdate();
}
async performMigration() {
await this.sendMigrationRequest(false);
this.showMigrationConfirm = false;
this.forceUpdate();
}
async storeTestedHub(event: React.ChangeEvent<HTMLInputElement>) {
this.testedHub = event.target.value;
}
async testOneHub() {
const testresult = await _testOneHub(this.testedHub, this.currentPlan);
this.hubTestResult = testresult;
this.forceUpdate();
}
readyMigration() {
this.showMigrationConfirm = true;
this.forceUpdate();
}
cancelMigration() {
this.showMigrationConfirm = false;
this.forceUpdate();
}
async toggleIgnore(pe: PlanElement) {
pe.ignored = !pe.ignored;
updateRelationshipMigrationField(pe)
.then(() => {
this.forceUpdate();
})
.catch(() => {
pe.ignored = !pe.ignored;
this.forceUpdate();
});
}
storeNewPlanElementSourceTemplate(event: React.ChangeEvent<HTMLSelectElement>) {
this.newPlanElement.sourceTemplateId = event.target.value;
this.newPlanElement.sourceTemplate = this.templateIndex[event.target.value].name;
this.forceUpdate();
}
storeNewPlanElementRelationType(event: React.ChangeEvent<HTMLSelectElement>) {
this.newPlanElement.relationTypeId = event.target.value;
this.newPlanElement.relationType = this.relationTypeIndex[event.target.value].name;
this.forceUpdate();
}
storeNewPlanElementTargetTemplate(event: React.ChangeEvent<HTMLSelectElement>) {
this.newPlanElement.targetTemplateId = event.target.value;
this.newPlanElement.targetTemplate = this.templateIndex[event.target.value].name;
this.forceUpdate();
}
async addNewPlanElement() {
createRelationshipMigrationField(this.newPlanElement).then(
(created: CreateRelationshipMigRationFieldResponse) => {
this.currentPlan.push(
mapPlanElementFromApiResponse(created, this.templateIndex, this.relationTypeIndex)
);
this.forceUpdate();
}
);
}
async removePlanElement(index: number) {
const pe = this.currentPlan[index];
deleteRelationshipMigrationField(pe).then(() => {
this.currentPlan.splice(index, 1);
this.forceUpdate();
});
}
renderUnusedConnections() {
if (!this.unusedConnectionsInfo) {
return <div />;
}
const maxPage = Math.ceil(this.unusedConnectionsInfo.fullCount / UNUSED_RECORDS_PAGE_SIZE);
return (
this.unusedConnectionsInfo && (
<div>
<div>
<button
type="button"
disabled={this.unusedConnectionsPage === 1}
onClick={async () => {
this.unusedConnectionsPage -= 1;
await this.getUnusedConnections(true);
}}
>
←
</button>
<span>
{this.unusedConnectionsPage}/{maxPage}
</span>
<button
type="button"
disabled={this.unusedConnectionsPage === maxPage}
onClick={async () => {
this.unusedConnectionsPage += 1;
await this.getUnusedConnections(true);
}}
>
→
</button>
</div>
{this.unusedConnectionsInfo.hubRecords.map((records, index) => (
<div key={`UnusedConnectionList_${records.hubId}`}>
<div>
{index + 1}---------------------------:{records.hubId}
</div>
{records.connections.map(connection => (
<div key={`unusedConnection_${records.hubId}_${connection.id}`}>
 
{`(${connection.templateName})`}
<Icon icon="link" />
{`${connection.entityTitle}(${
this.templateIndex[connection.entityTemplate].name
})`}
</div>
))}
</div>
))}
</div>
)
);
}
render() {
const oneHubTestEntityTitlesBySharedId = objectIndex(
this.hubTestResult?.original || [],
t => t.entity,
t => t.entityTitle
);
const oneHubTestEntityTemplatesBySharedId = objectIndex(
this.hubTestResult?.original || [],
t => t.entity,
t => t.entityTemplate
);
const oneHubRelTypeNamesById = objectIndex(
this.hubTestResult?.original || [],
t => t.template,
t => t.templateName
);
const displayEntityTitleAndNameFromOriginal = (orig: OriginalEntityInfo) =>
`${oneHubTestEntityTitlesBySharedId[orig.entity]}(${
this.templateIndex[orig.entityTemplate].name
})`;
const displayEntityTitleAndNameFromTransformed = (entity: string) =>
`${oneHubTestEntityTitlesBySharedId[entity]}(${
this.templateIndex[oneHubTestEntityTemplatesBySharedId[entity]].name
})`;
return (
<div className="settings-content" no-translate>
<div className="panel panel-default">
<div className="panel-heading">
<span>Migration Dashboard</span>
</div>
<div className="panel-body">
<button type="button" className="btn" onClick={this.performDryRun.bind(this)}>
Dry Run
</button>
 
<button type="button" className="btn" onClick={this.readyMigration.bind(this)}>
Migrate
</button>
{this.showMigrationConfirm && (
<>
<div>This will clean all your existing v2 relationships. Are you sure?</div>
<button type="button" className="btn" onClick={this.performMigration.bind(this)}>
Perform
</button>
<button type="button" className="btn" onClick={this.cancelMigration.bind(this)}>
Cancel
</button>
</>
)}
<br />
<br />
{this.migrationSummary && (
<div>
<br />
<div>Migration Summary{this.migrationSummary.dryRun ? ' (Dry Run)' : ''}:</div>
<div>Time: {formatTime(this.migrationSummary.time)}</div>
<div>
Total: {this.migrationSummary.total}
{`(text references: ${this.migrationSummary.totalTextReferences})`}
</div>
<div>
Used: {this.migrationSummary.used}
{`(${this.migrationSummary.usedTextReferences})`}
</div>
<div>
Unused: {this.migrationSummary.total - this.migrationSummary.used}
{`(${
this.migrationSummary.totalTextReferences -
this.migrationSummary.usedTextReferences
})`}
</div>
<div>Errors: {this.migrationSummary.errors}</div>
<br />
</div>
)}
<br />
<div>Current migration plan:</div>
{this.currentPlan.map((p, index) => (
<div key={`${p.sourceTemplate}_${p.relationType}_${p.targetTemplate}`}>
{p.sourceTemplate} 
<Icon icon="arrow-right" />
 
{`(${p.relationType})`} 
<Icon icon="arrow-right" />
 
{p.targetTemplate}
  --  
{[p.inferred ? 'inferred' : 'user defined', p.ignored ? 'ignored' : undefined]
.filter(x => x)
.join(', ')}
 
<button
type="button"
onClick={this.toggleIgnore.bind(this, p)}
className={`btn btn-xs${p.ignored ? ' btn-danger' : ' btn-success'} }`}
>
{`${p.ignored ? 'Unignore' : 'Ignore'}`}
</button>
{!p.inferred && (
<button
type="button"
onClick={this.removePlanElement.bind(this, index)}
className="btn btn-xs"
>
Remove
</button>
)}
</div>
))}
<div>
<select
value={this.newPlanElement.sourceTemplateId}
onChange={this.storeNewPlanElementSourceTemplate.bind(this)}
>
{this.templatesNameSorted.map(t => (
<option key={`sourceDropdown_${t._id}`} value={t._id}>
{t.name}
</option>
))}
</select>
<select
value={this.newPlanElement.relationTypeId}
onChange={this.storeNewPlanElementRelationType.bind(this)}
>
{this.relationTypesNameSorted.map(t => (
<option key={`relationDropdown_${t._id}`} value={t._id}>
{t.name}
</option>
))}
</select>
<select
value={this.newPlanElement.targetTemplateId}
onChange={this.storeNewPlanElementTargetTemplate.bind(this)}
>
{this.templatesNameSorted.map(t => (
<option key={`targetDropdown_${t._id}`} value={t._id}>
{t.name}
</option>
))}
</select>
<button type="button" onClick={this.addNewPlanElement.bind(this)}>
Add
</button>
</div>
<br />
<br />
<button type="button" onClick={this.testOneHub.bind(this)}>
Test One Hub
</button>
<input type="text" onChange={this.storeTestedHub.bind(this)} />
{this.hubTestResult && (
<div>
<div>Test Result:</div>
<div>Total: {this.hubTestResult.total}</div>
<div>Used: {this.hubTestResult.used}</div>
<div>Unused: {this.hubTestResult.total - this.hubTestResult.used}</div>
<table>
<thead>
<tr>
<th>Original Hub</th>
<th>Transformed</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{this.hubTestResult.original.map(t => (
<div key={`${t.template}_${t.entity}`}>
{oneHubRelTypeNamesById[t.template]} 
<Icon icon="link" />
 
{displayEntityTitleAndNameFromOriginal(t)}
</div>
))}
</td>
<td>
{this.hubTestResult.transformed.map(t => (
<div key={`${t.from.entity}_${t.to.entity}_${t.type}`}>
{displayEntityTitleAndNameFromTransformed(t.from.entity)} 
<Icon icon="arrow-right" />
 
{oneHubRelTypeNamesById[t.type]} 
<Icon icon="arrow-right" />
 
{displayEntityTitleAndNameFromTransformed(t.to.entity)}
</div>
))}
</td>
</tr>
</tbody>
</table>
</div>
)}
<br />
<br />
{this.renderUnusedConnections()}
</div>
</div>
</div>
);
}
}
type ComponentPropTypes = {
templates: ClientTemplateSchema[];
relationTypes: RelationshipTypesType[];
};
const mapStateToProps = ({ templates, relationTypes }: IStore) => ({
templates: templates.toJS(),
relationTypes: relationTypes.toJS(),
});
const NewRelMigrationDashboard = connect(mapStateToProps)(_NewRelMigrationDashboard);
export { NewRelMigrationDashboard };
export type { ComponentPropTypes };