OPS-134: New repairing (#108)

This commit is contained in:
Rinat Arsaev 2022-07-18 14:48:13 +03:00 committed by GitHub
parent d7aee88a0d
commit afb9894e92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 505 additions and 17 deletions

View File

@ -12,7 +12,7 @@ jobs:
- name: Build
run: npm run build
- name: Deploy image
uses: valitydev/action-deploy-docker@v2.0.3
uses: valitydev/action-deploy-docker@v2
with:
registry-username: ${{ github.actor }}
registry-access-token: ${{ secrets.GITHUB_TOKEN }}

14
package-lock.json generated
View File

@ -37,7 +37,7 @@
"@vality/magista-proto": "1.0.1-9f37374.0",
"@vality/messages-proto": "1.0.1-8c5435c.0",
"@vality/payout-manager-proto": "1.0.1-dbed280.0",
"@vality/repairer-proto": "1.0.1-8ccd6f7.0",
"@vality/repairer-proto": "1.0.1-675b6f4.0",
"@vality/thrift-ts": "2.2.2-e8bc2ab.0",
"@vality/woody": "0.1.1",
"angular-file": "3.6.0",
@ -5528,9 +5528,9 @@
"integrity": "sha512-uprNZIFMyLZbFg1TNVelr961pJoNDWUindLl6uhKwzvfSGOkGyko0XPQlD6WTziM7v4ctvsRkLf6xdPnkznjaA=="
},
"node_modules/@vality/repairer-proto": {
"version": "1.0.1-8ccd6f7.0",
"resolved": "https://registry.npmjs.org/@vality/repairer-proto/-/repairer-proto-1.0.1-8ccd6f7.0.tgz",
"integrity": "sha512-wJY7fpXONM6fWXN5cAnw1by24aSq7AcsotNBWNbAF7lDtq/xDGZoIpEHut7VCbZOUfdK0pYkzVL0FNRYTVyFrA=="
"version": "1.0.1-675b6f4.0",
"resolved": "https://registry.npmjs.org/@vality/repairer-proto/-/repairer-proto-1.0.1-675b6f4.0.tgz",
"integrity": "sha512-l5a9e7HCKD803xRgDlpIhhQUhb+JWXTPRb2ek0c1Ky4g2kkmBLB4PomBUKYiDjdHGo4tHO9IPBPTCDZVCl1cOA=="
},
"node_modules/@vality/thrift-ts": {
"version": "2.2.2-e8bc2ab.0",
@ -25935,9 +25935,9 @@
"integrity": "sha512-uprNZIFMyLZbFg1TNVelr961pJoNDWUindLl6uhKwzvfSGOkGyko0XPQlD6WTziM7v4ctvsRkLf6xdPnkznjaA=="
},
"@vality/repairer-proto": {
"version": "1.0.1-8ccd6f7.0",
"resolved": "https://registry.npmjs.org/@vality/repairer-proto/-/repairer-proto-1.0.1-8ccd6f7.0.tgz",
"integrity": "sha512-wJY7fpXONM6fWXN5cAnw1by24aSq7AcsotNBWNbAF7lDtq/xDGZoIpEHut7VCbZOUfdK0pYkzVL0FNRYTVyFrA=="
"version": "1.0.1-675b6f4.0",
"resolved": "https://registry.npmjs.org/@vality/repairer-proto/-/repairer-proto-1.0.1-675b6f4.0.tgz",
"integrity": "sha512-l5a9e7HCKD803xRgDlpIhhQUhb+JWXTPRb2ek0c1Ky4g2kkmBLB4PomBUKYiDjdHGo4tHO9IPBPTCDZVCl1cOA=="
},
"@vality/thrift-ts": {
"version": "2.2.2-e8bc2ab.0",

View File

@ -45,7 +45,7 @@
"@vality/magista-proto": "1.0.1-9f37374.0",
"@vality/messages-proto": "1.0.1-8c5435c.0",
"@vality/payout-manager-proto": "1.0.1-dbed280.0",
"@vality/repairer-proto": "1.0.1-8ccd6f7.0",
"@vality/repairer-proto": "1.0.1-675b6f4.0",
"@vality/thrift-ts": "2.2.2-e8bc2ab.0",
"@vality/woody": "0.1.1",
"angular-file": "3.6.0",

View File

@ -13,7 +13,7 @@ export class RepairManagementService extends createThriftApi<CodegenClient>() {
constructor(injector: Injector) {
super(injector, {
service,
path: '/v1/repairer', // TODO
path: '/v1/repair',
metadata: () =>
import('@vality/repairer-proto/lib/metadata.json').then((m) => m.default),
context,

View File

@ -48,7 +48,16 @@ export class AppComponent implements OnInit {
activateRoles: [PaymentAdjustmentRole.Create],
},
{ name: 'Merchants', route: '/parties', activateRoles: [PartyRole.Get] },
{ name: 'Repairing', route: '/repairing', activateRoles: [DomainConfigRole.Checkout] },
{
name: 'Repairing',
route: '/old-repairing',
activateRoles: [DomainConfigRole.Checkout],
},
{
name: 'New repairing',
route: '/repairing',
activateRoles: [DomainConfigRole.Checkout],
},
{
name: 'Operations',
route: '/operations',

View File

@ -9,7 +9,7 @@ import { RepairingComponent } from './repairing.component';
imports: [
RouterModule.forChild([
{
path: 'repairing',
path: 'old-repairing',
component: RepairingComponent,
canActivate: [AppAuthGuardService],
data: {

View File

@ -7,7 +7,6 @@ import { RepairingService } from './repairing.service';
@Component({
templateUrl: 'repairing.component.html',
styleUrls: ['repairing.component.scss'],
providers: [],
})
export class RepairingComponent {
progress$: Observable<number>;

View File

@ -0,0 +1,30 @@
<cc-base-dialog title="Repair by scenario" gdColumn="1fr">
<div gdGap="16px">
<mat-radio-group gdColumns="1fr 1fr" gdGap="8px" [formControl]="typeControl">
<mat-radio-button [value]="typesEnum.Invoices">Invoices</mat-radio-button>
<mat-radio-button [value]="typesEnum.Withdrawals">Withdrawals</mat-radio-button>
</mat-radio-group>
<cc-metadata-form
*ngIf="typeControl.value !== null"
[formControl]="form"
[metadata]="metadata$ | async"
namespace="repairer"
[type]="
typeControl.value === typesEnum.Invoices
? 'RepairInvoicesRequest'
: 'RepairWithdrawalsRequest'
"
></cc-metadata-form>
<mat-progress-bar *ngIf="progress$ | async"></mat-progress-bar>
</div>
<cc-base-dialog-actions>
<button
mat-button
color="primary"
[disabled]="form.invalid || typeControl.invalid"
(click)="repair()"
>
REPAIR
</button>
</cc-base-dialog-actions>
</cc-base-dialog>

View File

@ -0,0 +1,64 @@
import { Component, Injector } from '@angular/core';
import { Validators } from '@angular/forms';
import { FormControl } from '@ngneat/reactive-forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { RepairInvoicesRequest, RepairWithdrawalsRequest } from '@vality/repairer-proto';
import { BehaviorSubject, from } from 'rxjs';
import {
BaseDialogResponseStatus,
BaseDialogSuperclass,
} from '../../../../../components/base-dialog';
import { progressTo } from '../../../../../utils';
import { RepairManagementService } from '../../../../api/repairer';
import { ErrorService } from '../../../../shared/services/error';
import { NotificationService } from '../../../../shared/services/notification';
enum Types {
Invoices,
Withdrawals,
}
@UntilDestroy()
@Component({
templateUrl: './repair-by-scenario-dialog.component.html',
})
export class RepairByScenarioDialogComponent extends BaseDialogSuperclass<RepairByScenarioDialogComponent> {
typeControl = new FormControl<number>(null, Validators.required);
form = new FormControl<RepairInvoicesRequest | RepairWithdrawalsRequest>(
null,
Validators.required
);
metadata$ = from(import('@vality/repairer-proto/lib/metadata.json').then((m) => m.default));
progress$ = new BehaviorSubject(0);
typesEnum = Types;
constructor(
injector: Injector,
private repairManagementService: RepairManagementService,
private errorService: ErrorService,
private notificationService: NotificationService
) {
super(injector);
}
repair() {
(this.typeControl.value === Types.Invoices
? this.repairManagementService.RepairInvoices(this.form.value as RepairInvoicesRequest)
: this.repairManagementService.RepairWithdrawals(
this.form.value as RepairWithdrawalsRequest
)
)
.pipe(progressTo(this.progress$), untilDestroyed(this))
.subscribe({
next: () => {
this.notificationService.success();
this.dialogRef.close({ status: BaseDialogResponseStatus.Success });
},
error: (err) => {
this.errorService.error(err);
this.notificationService.error();
},
});
}
}

View File

@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '../../shared/services';
import { RepairingComponent } from './repairing.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: RepairingComponent,
canActivate: [AppAuthGuardService],
data: {
roles: [],
},
},
]),
],
exports: [RouterModule],
})
export class RepairingRoutingModule {}

View File

@ -0,0 +1,127 @@
<div fxLayout="column" fxLayoutGap="32px">
<h1 class="cc-display-1">Repairing</h1>
<div fxLayout="column" fxLayoutGap="24px">
<mat-card>
<mat-card-content [formGroup]="filters" gdColumns="1fr 1fr 1fr" gdGap="16px">
<mat-form-field>
<mat-label>IDs</mat-label>
<input formControlName="ids" matInput />
<mat-hint>id0,id1</mat-hint>
</mat-form-field>
<mat-form-field>
<mat-label>Namespace</mat-label>
<input formControlName="ns" matInput />
</mat-form-field>
<cc-date-range formControlName="timespan"></cc-date-range>
<mat-form-field>
<mat-label>Provider ID</mat-label>
<input formControlName="provider_id" matInput />
</mat-form-field>
<mat-form-field>
<mat-label>Status</mat-label>
<mat-select formControlName="status">
<mat-option [value]="null">any</mat-option>
<mat-option
*ngFor="let statusName of statuses"
[value]="status[statusName]"
>
{{ statusName }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Error message</mat-label>
<input formControlName="error_message" matInput />
</mat-form-field>
</mat-card-content>
<mat-card-footer *ngIf="inProgress$ | async">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-card-footer>
</mat-card>
<ng-container *ngIf="!(inProgress$ | async) || (machines$ | async)">
<cc-actions>
<button mat-button (click)="update()">UPDATE</button>
<button mat-button color="primary" (click)="repairByScenario()">
REPAIR BY SCENARIO
</button>
<button
mat-button
color="primary"
[disabled]="!selection?.selected?.length"
(click)="repair()"
>
SIMPLE REPAIR {{ selection?.selected?.length || '' }}
</button>
</cc-actions>
<cc-empty-search-result *ngIf="!(machines$ | async).length"></cc-empty-search-result>
<mat-card *ngIf="(machines$ | async).length" fxLayout="column" fxLayoutGap="18px">
<table mat-table [dataSource]="machines$ | async">
<cc-select-column
[dataSource]="machines$ | async"
(changed)="selection = $event"
></cc-select-column>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let i">{{ i.id }}</td>
</ng-container>
<ng-container matColumnDef="namespace">
<th mat-header-cell *matHeaderCellDef>Namespace</th>
<td mat-cell *matCellDef="let i">{{ i.ns }}</td>
</ng-container>
<ng-container matColumnDef="createdAt">
<th mat-header-cell *matHeaderCellDef>Created at</th>
<td mat-cell *matCellDef="let i">
{{ i.created_at | date: 'dd.MM.yyyy HH:mm:ss' }}
</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef matTooltip="Hover to view details">
<span matBadge="" matBadgeSize="small" matBadgeOverlap="false">
Status
</span>
</th>
<td
mat-cell
*matCellDef="let i"
[matTooltip]="i.error_message"
matTooltipPosition="left"
>
{{ statusNameByValue[i.status] }}
</td>
</ng-container>
<ng-container matColumnDef="provider">
<th mat-header-cell *matHeaderCellDef>Provider</th>
<td mat-cell *matCellDef="let i">
{{ i.provider_id }}
</td>
</ng-container>
<ng-container matColumnDef="history">
<th mat-header-cell *matHeaderCellDef matTooltip="Hover to view details">
<span matBadge="" matBadgeSize="small" matBadgeOverlap="false"
>History</span
>
</th>
<td
mat-cell
*matCellDef="let i"
[matTooltip]="i.history?.length ? (i.history | json) : ''"
matTooltipPosition="left"
>
{{ i.history?.length || '' }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<cc-show-more-button
*ngIf="hasMore$ | async"
[inProgress]="inProgress$ | async"
(fetchMore)="fetchMore()"
></cc-show-more-button>
</mat-card>
</ng-container>
</div>
</div>

View File

@ -0,0 +1,152 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { DateRange } from '@angular/material/datepicker';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Machine, Namespace, ProviderID, RepairStatus } from '@vality/repairer-proto';
import isEmpty from 'lodash-es/isEmpty';
import isNil from 'lodash-es/isNil';
import omitBy from 'lodash-es/omitBy';
import { Moment } from 'moment';
import { filter, map, switchMap } from 'rxjs/operators';
import { BaseDialogResponseStatus } from '../../../components/base-dialog';
import { BaseDialogService } from '../../../components/base-dialog/services/base-dialog.service';
import { ConfirmActionDialogComponent } from '../../../components/confirm-action-dialog';
import { getEnumKeys } from '../../../utils';
import { RepairManagementService } from '../../api/repairer';
import { QueryParamsService } from '../../shared/services';
import { ErrorService } from '../../shared/services/error';
import { NotificationService } from '../../shared/services/notification';
import { RepairByScenarioDialogComponent } from './components/repair-by-scenario-dialog/repair-by-scenario-dialog.component';
import { MachinesService } from './services/machines.service';
interface Filters {
ids: string;
ns: Namespace;
timespan: DateRange<Moment>;
provider_id: ProviderID;
status: RepairStatus;
error_message: string;
}
@UntilDestroy()
@Component({
selector: 'cc-repairing',
templateUrl: './repairing.component.html',
styles: [
`
:host {
display: block;
padding: 24px 16px;
}
`,
],
providers: [MachinesService],
})
export class RepairingComponent implements OnInit {
machines$ = this.machinesService.searchResult$;
inProgress$ = this.machinesService.doAction$;
hasMore$ = this.machinesService.hasMore$;
filters = this.fb.group<Filters>({
ids: null,
ns: null,
timespan: null,
provider_id: null,
status: null,
error_message: null,
});
selection: SelectionModel<Machine>;
displayedColumns = ['_select', 'id', 'namespace', 'createdAt', 'provider', 'status', 'history'];
statusNameByValue = Object.fromEntries(Object.entries(RepairStatus).map(([k, v]) => [v, k]));
status = RepairStatus;
statuses = getEnumKeys(RepairStatus);
constructor(
private machinesService: MachinesService,
private fb: FormBuilder,
private qp: QueryParamsService<Filters>,
private baseDialogService: BaseDialogService,
private repairManagementService: RepairManagementService,
private notificationService: NotificationService,
private errorService: ErrorService
) {}
ngOnInit() {
this.filters.valueChanges
.pipe(
map(() => {
return omitBy(this.filters.value, isEmpty);
}),
untilDestroyed(this)
)
.subscribe((v: Filters) => this.qp.set(v));
this.qp.params$
.pipe(
map(({ ids, ns, timespan: d, provider_id, status, error_message }) => {
const timespan = omitBy(
{
from_time: d?.start?.toISOString(),
to_time: d?.end?.toISOString(),
},
isNil
);
return omitBy(
{
ids: ids?.split(/[,.;\s]/)?.filter(Boolean),
ns,
provider_id,
status,
error_message,
timespan: Object.keys(timespan).length ? timespan : null,
},
isEmpty
);
}),
untilDestroyed(this)
)
.subscribe((params) => this.machinesService.search(params));
}
update() {
this.machinesService.refresh();
}
fetchMore() {
this.machinesService.fetchMore();
}
repair() {
this.baseDialogService
.open(ConfirmActionDialogComponent, {
title: `Simple repair ${this.selection.selected.length} machines`,
})
.afterClosed()
.pipe(
filter(({ status }) => status === BaseDialogResponseStatus.Success),
switchMap(() =>
this.repairManagementService.SimpleRepairAll(
this.selection.selected.map(({ id, ns }) => ({ id, ns }))
)
),
untilDestroyed(this)
)
.subscribe({
next: () => {
this.notificationService.success();
},
error: (err) => {
this.notificationService.error();
this.errorService.error(err);
},
});
}
repairByScenario() {
this.baseDialogService
.open(RepairByScenarioDialogComponent)
.afterClosed()
.pipe(untilDestroyed(this))
.subscribe();
}
}

View File

@ -0,0 +1,51 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActionsModule } from '../../../components/actions';
import { BaseDialogModule } from '../../../components/base-dialog';
import { EmptySearchResultModule } from '../../../components/empty-search-result';
import { TableModule } from '../../../components/table';
import { MetadataFormModule } from '../../shared';
import { DateRangeModule } from '../../shared/components/date-range/date-range.module';
import { RepairByScenarioDialogComponent } from './components/repair-by-scenario-dialog/repair-by-scenario-dialog.component';
import { RepairingRoutingModule } from './repairing-routing.module';
import { RepairingComponent } from './repairing.component';
@NgModule({
imports: [
CommonModule,
RepairingRoutingModule,
TableModule,
MatCardModule,
ReactiveFormsModule,
FlexLayoutModule,
MatProgressBarModule,
MatButtonModule,
EmptySearchResultModule,
MatTableModule,
MatTooltipModule,
MatBadgeModule,
MatFormFieldModule,
MatInputModule,
DateRangeModule,
MatSelectModule,
ActionsModule,
BaseDialogModule,
MetadataFormModule,
MatRadioModule,
],
declarations: [RepairingComponent, RepairByScenarioDialogComponent],
})
export class RepairingModule {}

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { Machine, SearchRequest } from '@vality/repairer-proto';
import { map } from 'rxjs/operators';
import { RepairManagementService } from '../../../api/repairer';
import { PartialFetcher } from '../../../shared/services';
@Injectable()
export class MachinesService extends PartialFetcher<Machine, SearchRequest> {
constructor(private repairManagementService: RepairManagementService) {
super();
}
protected fetch(params: SearchRequest, continuationToken: string) {
return this.repairManagementService
.Search({ limit: 100, continuation_token: continuationToken, ...params })
.pipe(
map(({ machines, continuation_token }) => ({
result: machines,
continuationToken: continuation_token,
}))
);
}
}

View File

@ -11,6 +11,10 @@ const ROUTES: Routes = [
loadChildren: () =>
import('./withdrawals/withdrawals.module').then((m) => m.WithdrawalsModule),
},
{
path: 'repairing',
loadChildren: () => import('./repairing/repairing.module').then((m) => m.RepairingModule),
},
];
@NgModule({

View File

@ -1,8 +1,14 @@
.cc-actions {
::ng-deep & > *:last-child {
margin-left: auto;
::ng-deep .cc-actions {
& > *:first-child:not(:last-child) {
margin-right: auto !important;
@media screen and (max-width: 959px) {
margin-left: initial;
margin-right: initial !important;
}
}
& > *:first-child:last-child {
margin-left: auto !important;
@media screen and (max-width: 959px) {
margin-left: initial !important;
}
}
}