import { Component, OnInit, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { StateService } from '@uirouter/core';
import { BehaviorSubject, Observable, Subject, delay, map, of } from 'rxjs';
import {
MATCH_DISABLED,
MATCH_HIDDEN,
MATCH_VALIDATOR_ADD,
TI_GRID_KIT_ROW_ACTION_TYPE,
TiDynamicFormComponent,
TiDynamicFormGroupModel,
TiDynamicGridModel,
TiDynamicInputModel,
TiDynamicSelectModel,
TiGridKitEditEvents,
} from '@cloud/ti-dynamic-form';
import {
TiActionmenuItem,
TiMessageService,
TiModalRef,
TiModalService,
TiTableColumns,
TiTableRowData,
TiTableSrcData,
TiValidators,
} from '@cloud/tiny3';
import { TiRenderType, gridConstants } from '@cloud/tinycloud';
import { InstanceStatus } from 'src/ng2app/common/common.constant';
import { CloudBIService } from 'src/ng2app/common/service/cloudBI.service';
import { DMSCommonService } from 'src/ng2app/common/service/common.service';
import { DMSGridKitService } from 'src/ng2app/common/service/dms-grid-kit.service';
import { I18nService } from 'src/ng2app/common/service/i18n.service';
import { MessageService } from 'src/ng2app/common/service/message.service';
import { IInstanceDetail } from 'src/ng2app/instance-detail/instance-detail.interface';
import { TaskStatus } from 'src/ng2app/task-query/task-query.constant';
import { SupportFeature } from '../../instance-detail/instance-detail.constant';
import { BatchEditModalComponent } from '../batch-edit-modal/batch-edit-modal.component';
import { ConfigType, ConfigValueType, FormValidation, SupportFeatures } from '../instance-config.constant';
import { IEditConfig, IInstanceConfig } from '../instance-config.model';
import { KafkaConfigManagerService } from './kafka-config-manager.service';
@Component({
selector: 'kafka-config-manager',
templateUrl: 'kafka-config-manager.component.html',
})
export class KafkaConfigManagerComponent implements OnInit {
@ViewChild('tiDynamicFormComponent')
tiDynamicFormComponent: TiDynamicFormComponent;
model = [
new TiDynamicGridModel({
...((isSingle = false) => {
const id = isSingle ? 'grid-with-batch-edit-single' : 'grid-with-batch-edit-global';
return {
id,
// 动态表单开启编辑能力
supportEdit: true,
gridKitOption: {
enableColsResponsive: true,
rowSelection: 'multiple',
colDefs: ((singleEdit = true) => [
{
headerName: '名称',
field: 'name',
// 可通过该字段关闭该列修改能力。
supportEdit: false,
},
{
headerName: '策略',
field: 'protocol',
cellRenderer: TiRenderType.TextWithTipRenderer,
},
{
headerName: '类型',
field: 'type',
},
{
headerName: '后端协议',
field: 'protocolPort',
valueGetter: params =>
`${params.data.protocolPort.protocol}: ${params.data.protocolPort.sessionTime || '80'}`,
},
{
headerName: '操作',
field: gridConstants.OPERATION_FIELD,
cellRenderer: TiRenderType.OperationRenderer,
cellRendererParams: params => {
const gridDataLimit = new BehaviorSubject({
min: 1,
}).getValue();
const deleteBtn = {
label: '删除',
key: 'op_delete',
actionCallback: obj => {
new Subject().next({
action: TI_GRID_KIT_ROW_ACTION_TYPE.REMOVE,
// 推荐使用data传递删除数据,兼容之前的rowIndex逻辑
// rowIndex: obj.rowIndex
data: obj.data,
});
},
};
// 如果是单行修改,其他行的修改按钮就需要禁用
const hasEditRows = !!new BehaviorSubject([]).getValue().length;
const editDisabledTip = hasEditRows ? '请先保存或者取消处于修改状态的行' : '';
if (singleEdit) {
({
label: '修改策略',
key: 'op-edit',
actionCallback: obj => {
new Subject().next({
// 当前修改的行数据
rowDatas: [obj.data],
// 启动修改态
actionType: TiGridKitEditEvents.EDITING,
callback: ({ editRows }) => {
new BehaviorSubject([]).next(editRows);
(editRows => {})(editRows);
},
});
},
}).disabled = hasEditRows;
({
label: '修改策略',
key: 'op-edit',
actionCallback: obj => {
new Subject().next({
// 当前修改的行数据
rowDatas: [obj.data],
// 启动修改态
actionType: TiGridKitEditEvents.EDITING,
callback: ({ editRows }) => {
new BehaviorSubject([]).next(editRows);
(editRows => {})(editRows);
},
});
},
}).tipStr = editDisabledTip;
}
deleteBtn.disabled = hasEditRows;
deleteBtn.tipStr = editDisabledTip;
// 如果当前只有一行,删除按钮设置成禁用;
if (params.context.parentArray.length <= (gridDataLimit?.min || 0)) {
deleteBtn.disabled = true;
deleteBtn.tipStr = '只有一条记录,不能删除。';
}
return {
...params,
operationList: params.data.isCurrentEdit
? [
{
label: '保存',
key: 'op-edit-cancel',
actionCallback: obj => {
new Subject().next({
// 当前修改的行数据
rowDatas: [obj.data],
// 确定修改
actionType: TiGridKitEditEvents.CONFIRM_EDIT,
// 如果在保存时需要在校验通过后先向后台请求保存,可以使用该接口。根据其返回的布尔值决定后续是否进行前台保存且
// 变成非编辑态(true: 进行后续;false或报错:不进行后续,停留在编辑态)。
// 13.0.120 版本新增
serverSideSaveFn: ({ editedRowDatas }) =>
(editedRowDatas => {
console.log('编辑后的行数据集合:', editedRowDatas);
// return throwError('This is an error!'); // 测试报错的场景
return of(editedRowDatas).pipe(
delay(800),
map(() => true)
);
})(editedRowDatas),
callback: ({ editRows }) => {
new BehaviorSubject([]).next(editRows);
(editRows => {})(editRows);
},
});
},
},
{
label: '取消',
key: 'ti-grid-kit-op-edit-cancel',
actionCallback: obj => {
new Subject().next({
// 当前修改的行数据
rowDatas: [obj.data],
// 取消修改
actionType: TiGridKitEditEvents.CANCEL_EDIT,
callback: ({ editRows }) => {
new BehaviorSubject([]).next(editRows);
(editRows => {})(editRows);
},
});
},
},
]
: [
{
label: '修改策略',
key: 'op-edit',
actionCallback: obj => {
new Subject().next({
// 当前修改的行数据
rowDatas: [obj.data],
// 启动修改态
actionType: TiGridKitEditEvents.EDITING,
callback: ({ editRows }) => {
new BehaviorSubject([]).next(editRows);
(editRows => {})(editRows);
},
});
},
},
deleteBtn,
],
};
},
flex: 1,
},
])(false),
srcData: ((length = 2) => {
const srcData = [];
for (let i = 0; i < length; i++) {
srcData.push({
name: `名称-${i}`,
protocol: `容许-${i}`,
type: `${i % 2 ? 'IPV4' : 'IPV6'}`,
protocolPort: {
sessionTime: '80',
protocol: 'HTTP',
},
});
}
return srcData;
})(1),
// displayMode: 'ti-grid'
},
// 编辑态使用
groupFactory: () => [
new TiDynamicInputModel({
id: 'protocol',
value: '容许',
validators: {
required: null,
},
}),
new TiDynamicSelectModel({
id: 'type',
value: 'IPV4',
validators: {
required: null,
},
options: [
{
label: 'IPV4',
value: 'IPV4',
},
{
label: 'IPV6',
value: 'IPV6',
},
],
relations: [
{
match: MATCH_DISABLED,
when: [
{
id: 'protocol',
value: '',
},
],
},
],
}),
// 协议端口
new TiDynamicFormGroupModel({
id: 'protocolPort',
group: [
new TiDynamicSelectModel({
id: 'protocol',
options: [
{
value: 'HTTP',
label: 'HTTP',
},
{
value: 'TCP',
label: 'TCP',
},
],
defaultValueIndex: 0,
}),
new TiDynamicInputModel({
id: 'sessionTime',
placeholder: '请输入保持会话时间',
descriptions: 'HTTP保持会话时间区域是1-1440,TCP保持会话的时间是1-60',
// 根据表单值动态添加的校验器
dynamicValidators: {
// 如果relation匹配成功,使用该校验规则
match: {
rangeValue: {
validator: TiValidators.rangeValue(1, 1440),
},
},
// 如果relation匹配不成功,使用该校验规则
notMatch: {
rangeValue: {
validator: TiValidators.rangeValue(1, 60),
},
},
},
// 校验规则,始终需要的校验规则
validators: {
// 设置为null,会调用TiValidators.required进行校验;
required: null,
},
// 报错信息配置,可选项
errorMessages: {
rangeValue: '当前协议的会话时间范围是:{0} - {1}',
},
relations: [
{
// 如果匹配成功,动态添加相关校验规则
match: MATCH_VALIDATOR_ADD,
// 当是layer是L7的后端协议,启用相关校验规则
when: [
{
id: 'protocol',
value: 'HTTP',
},
],
},
{
match: MATCH_HIDDEN,
when: [
{
value: 'IPV4',
/**
* 13.0.180 版本新增,用在formarray和dynamic-grid组件中和本行的formGroup其他control建立关联关系
* 解决之前通过rootPath(id.index.id)中行新增删除时index未同步变化时(groupFactory只能在初始化时调用一次)引起的关联关系不正确的问题
*
* 注意:如果存在formarray里面嵌套formarry类,可能会存在查找到的行group不准确的场景
* 查找逻辑:当前control往上递归,如果其父元素是个formArray,则认为该group为要找的行group
*/
currentRowPath: 'type',
},
],
},
],
}),
],
}),
],
// 行数据限制
limit: new BehaviorSubject({
min: 1,
}),
initialCount: 5,
// 配置触发表格刷新和添加删除事件的流: 组件内部会监听这个流去触发对应的行为。
actionEvents: {
// 行操作流
rowAction: new Subject(),
// 编辑态流
editAction: new Subject(),
// 刷新单元格流
refreshCells: new Subject(),
},
};
})(),
}),
];
dynamicDisplayed: Array<TiTableRowData> = [];
staticDisplayed: Array<TiTableRowData> = [];
enableEditStatic = false;
columns: Array<TiTableColumns> = [
{
label: this.i18n.get('common_config_name'),
width: '32%',
show: true,
},
{
label: this.i18n.get('common_config_range'),
width: '17%',
show: true,
},
{
label: this.i18n.get('common_config_default'),
width: '17%',
show: true,
},
{
label: this.i18n.get('common_config_running'),
width: '17%',
show: true,
},
{
label: this.i18n.get('common_config_option'),
width: '17%',
show: true,
},
];
dynamicSrcData: TiTableSrcData;
staticSrcData: TiTableSrcData;
staticItems: Array<TiActionmenuItem> = [
{
label: this.i18n.get('common_term_edit_label'),
},
];
dynamicItems: Array<TiActionmenuItem> = [
{
label: this.i18n.get('common_term_edit_label'),
},
];
editingItems: Array<TiActionmenuItem> = [
{
label: this.i18n.get('common_term_save_btn'),
},
{
label: this.i18n.get('common_term_cancel_button'),
},
{
label: this.i18n.get('common_config_rest'),
},
];
restItems: Array<TiActionmenuItem> = [
{
label: this.i18n.get('common_config_rest'),
},
];
levelOptions = [{ label: 'true' }, { label: 'false' }];
editingRow: TiTableRowData;
isTaskChanging = false;
isInstanceRunning = false;
hasConfig = false;
isLoading = true;
isReadOnly = true;
isInstanceErrorCode = false;
roles: Array<string>;
taskJobId: string;
tipMessage: string;
instanceName: string;
showStaticDefaultBtn = false;
isConfigEditing = { dynamic: false, static: false };
private instanceId: string;
private instanceStatusInterval = null;
private taskStatusInterval = null;
private finishSetConfig: Subject<boolean> = new Subject<boolean>();
private supportFeatures = '';
private instanceDetailData: IInstanceDetail;
customEvent = customEventObj => {
if (customEventObj?.type === 'gridRowSelected') {
}
};
constructor(
private fb: FormBuilder,
private stateService: StateService,
private tiMessage: TiMessageService,
private tiModal: TiModalService,
private cloudBIService: CloudBIService,
private dmsGridKitService: DMSGridKitService,
public i18n: I18nService,
private messageService: MessageService,
private kafkaConfigManagerService: KafkaConfigManagerService
) {}
ngOnInit() {
this.instanceId = this.stateService.params.instanceId;
this.handleInstance(this.instanceId);
}
ngAfterViewInit() {
DMSCommonService.getUserRoles$().subscribe(roles => {
this.roles = roles;
this.isReadOnly = DMSCommonService.isReadOnly(roles);
if (this.isReadOnly) {
this.columns[4].show = false;
} else {
this.columns[4].show = true;
}
});
}
ngOnDestroy(): void {
this.closeInterval();
}
onBatchEditClick(configType: string): void {
this.isConfigEditing[configType] = true;
}
onSaveClick(configType: string) {
let srcData: Array<TiTableRowData> = [];
srcData = configType === ConfigType.DYNAMIC ? this.dynamicSrcData.data : this.staticSrcData.data;
this.isConfigEditing[configType] = false;
const changedConfigs = this.kafkaConfigManagerService.fetchEditAllConfig(srcData);
// Open the modal if user has changed some configs
if (changedConfigs.length) {
this.openModal(changedConfigs, configType);
}
}
openModal(changedConfigs: IEditConfig[], configType: string): void {
this.tiModal.open(BatchEditModalComponent, {
id: 'batchEditModal',
context: { config: changedConfigs, configType },
beforeClose: (modalRef: TiModalRef, reason: Boolean): void => {
if (reason) {
this.handleSetConfigs(changedConfigs);
this.statusCheckToDestroyModal(modalRef);
} else {
modalRef.destroy(true);
}
},
close: (modalRef: TiModalRef): void => {},
dismiss: (modalRef: TiModalRef): void => {},
});
}
handleSetConfigs(changedConfigs: IEditConfig[]): void {
const requestConfigs = this.kafkaConfigManagerService.getRequestConfigs(changedConfigs);
this.setConfigs(requestConfigs);
}
statusCheckToDestroyModal(modalRef: TiModalRef) {
this.configSetFinished().subscribe(() => {
modalRef.destroy(true);
});
}
onCancelClick(configType: string): void {
this.isConfigEditing[configType] = false;
const srcData = configType === ConfigType.DYNAMIC ? this.dynamicSrcData.data : this.staticSrcData.data;
srcData.forEach(item => {
if (item.type === ConfigValueType.enum) {
item.currentValue = item.originValue;
}
});
}
handleConfigs(instanceId: string): void {
this.isLoading = true;
this.kafkaConfigManagerService.configsRequest$(instanceId).subscribe(
res => {
this.isLoading = false;
// RTTI
this.cloudBIService.onRTTIEvent();
this.hasConfig = true;
// 支持 "开启创建kafka消费组功能"
const isSupportKafkaCreateConsumerGroup = this.supportFeatures?.includes(
SupportFeature.kafkaCreateConsumerGroupEnable
);
const srcData = res?.kafka_configs?.filter(
item => isSupportKafkaCreateConsumerGroup || item.name !== SupportFeature.kafkaCreateConsumerGroupEnable
);
const fetchedData = this.kafkaConfigManagerService.fetchSrcData(this.fb, srcData);
this.dynamicSrcData = this.kafkaConfigManagerService.getDynamicSrcData(fetchedData);
this.staticSrcData = this.kafkaConfigManagerService.getStaticSrcData(fetchedData);
},
err => {
this.hasConfig = false;
this.isLoading = false;
}
);
}
onReloadConfig(): void {
this.handleConfigs(this.instanceId);
}
setConfigs(instanceConfigs: IInstanceConfig[]): void {
this.kafkaConfigManagerService.setConfigRequest$(this.instanceId, instanceConfigs).subscribe(
res => {
this.taskJobId = res.job_id;
this.finishSetConfig.next(true);
this.messageService.showSuccessMessage(
this.i18n.get('common_config_save_success').replace('{0}', this.instanceName)
);
setTimeout(() => {
if (this.taskJobId) {
this.getTaskStatus(this.instanceId, this.taskJobId);
} else {
this.handleInstance(this.instanceId);
}
}, 800);
},
err => {
this.finishSetConfig.next(true);
this.messageService.showException(err);
}
);
}
onSelect(item: TiActionmenuItem, row: TiTableRowData): void {
this.editingRow = { ...row };
this.editableRows(false, this.i18n.get('common_config_save_alert'));
}
onSelectEditing(item: TiActionmenuItem, row: TiTableRowData, configType: string): void {
const isSave: boolean = item.label === this.i18n.get('common_term_save_btn');
const isReset: boolean = item.label === this.i18n.get('common_config_rest');
let isConfigChanged = true;
isConfigChanged = this.editingRow.currentValue !== row.currentValue || isReset;
let isSendConfig = true;
isSendConfig = isConfigChanged && (isSave || isReset) && row.formGroupCtrl.status === FormValidation.VALID;
if (isSendConfig) {
const IEditConfig: IEditConfig = {
name: '',
value: '',
param_original: row.currentValue,
};
IEditConfig.name = this.editingRow.configName;
IEditConfig.value = isSave ? this.editingRow.currentValue : row.defaultValue;
const editConfigs = [IEditConfig];
this.openModal(editConfigs, configType);
}
this.editableRows(true);
this.editingRow = undefined;
}
onResetClick(item: TiActionmenuItem, row: TiTableRowData): void {
row.formGroupCtrl.patchValue({ newValue: row.defaultValue });
}
toTaskQuery(): void {
const params = { instanceId: this.instanceId };
this.stateService.go('queue.newKafkaDetail.taskQuery', params);
}
showResetConfirmPrompt(): void {
this.tiMessage.open({
type: 'prompt',
content: this.i18n.get('common_config_auto_restart_notice'),
close: (modalRef: TiModalRef): void => {
this.resetConfig();
},
});
}
resetConfigClick(): void {
this.showResetConfirmPrompt();
}
resetConfig(): void {
this.kafkaConfigManagerService.resetConfigRequest$(this.instanceId).subscribe(
res => {
this.taskJobId = res.job_id;
this.toTaskQuery();
},
err => {
this.messageService.showException(err);
}
);
}
private editableRows(editable: boolean, message = ''): void {
this.dynamicItems = this.kafkaConfigManagerService.getConfigTips(this.dynamicItems, editable, message);
if (this.enableEditStatic) {
this.staticItems = this.kafkaConfigManagerService.getConfigTips(this.staticItems, editable, message);
} else {
this.handleDisableStaticMsgTip();
}
}
private handleDisableStaticMsgTip(): void {
this.staticItems = this.staticItems.map(
(item: TiActionmenuItem): TiActionmenuItem => ({
...item,
disabled: true,
tip: this.i18n.get('common_config_disable_edit_static_config'),
})
);
}
private getTaskStatus(instanceId: string, jobId: string): void {
this.kafkaConfigManagerService.taskStatusRequest$(instanceId, jobId).subscribe(
res => {
const taskStatus = res.tasks[0].status;
this.handleTaskStatus(taskStatus, jobId);
},
err => {
this.messageService.showException(err);
}
);
}
private handleTaskStatus(taskStatus: string, jobId: string) {
if (taskStatus !== TaskStatus.SUCCESS) {
this.isTaskChanging = true;
this.isInstanceRunning = false;
this.editableRows(false, this.i18n.get('common_config_changing'));
if (this.taskStatusInterval === null) {
this.taskStatusInterval = setInterval(() => {
this.getTaskStatus(this.instanceId, jobId);
}, 5000);
}
} else {
this.isTaskChanging = false;
this.handleInstance(this.instanceId);
this.closeInterval();
}
}
private handleInstance(instanceId: string): void {
this.kafkaConfigManagerService.instanceStatusRequest$(instanceId).subscribe(
res => {
this.instanceDetailData = res;
this.instanceName = res.name;
this.supportFeatures = res?.support_features;
this.checkStaticFeature(res?.support_features);
this.handleRunningInstanceErrCode(res?.error_code);
if (res?.status) {
// 根据实例冻结和用户限制提示不可编辑原因
this.handleInstanceStatus(res?.error_code, res.status);
}
},
err => {
this.messageService.showException(err);
}
);
}
private checkStaticFeature(supportFeatures = '') {
this.enableEditStatic = supportFeatures.includes(SupportFeatures.KafkaConfigStaticModify);
}
private handleRunningInstanceErrCode(error_code = null) {
if (error_code) {
this.isInstanceErrorCode = true;
// 实例在运行,instance返回error_code时,实例不支持参数变更
this.editableRows(false, this.i18n.get(error_code));
}
}
private handleInstanceStatus(error_code: string, status: string): void {
// 根据用户权限和实例冻结显示tips
this.tipMessage = this.dmsGridKitService.getInstanceStatus(this.instanceDetailData).tip;
this.checkGetConfigs(status);
if (this.tipMessage !== '') {
this.editableRows(false, this.tipMessage);
}
// 只有实例在Running的时候才可以编辑,其他状态不可以。如果Extending可以去任务界面查看
if (status === InstanceStatus.RUNNING) {
this.isTaskChanging = false;
this.isInstanceRunning = true;
if (!error_code && !this.tipMessage) {
this.editableRows(true);
}
this.closeInterval();
} else {
this.isInstanceRunning = false;
this.editableRows(false, this.i18n.get('common_config_cannot_change'));
this.loopInstanceStatus();
}
}
private loopInstanceStatus() {
if (status === InstanceStatus.EXTENDING) {
this.isTaskChanging = true;
if (this.instanceStatusInterval === null) {
this.instanceStatusInterval = setInterval(() => {
this.handleInstance(this.instanceId);
}, 5000);
}
}
}
private checkGetConfigs(status: string) {
// 特定状态的实例无法调用config接口 -- 404, 不调用get config,直接返回显示空表格
const noConfigList: Array<string> = [
InstanceStatus.CREATING,
InstanceStatus.RESTARTING,
InstanceStatus.STARTING,
InstanceStatus.CREATEFAILED,
InstanceStatus.UPGRADING,
InstanceStatus.ROLLBACK,
InstanceStatus.FREEZING,
];
if (noConfigList.includes(status)) {
return;
}
this.handleConfigs(this.instanceId);
}
private configSetFinished(): Observable<boolean> {
return this.finishSetConfig.asObservable();
}
private closeInterval() {
if (this.instanceStatusInterval) {
clearInterval(this.instanceStatusInterval);
this.instanceStatusInterval = null;
}
if (this.taskStatusInterval) {
clearInterval(this.taskStatusInterval);
this.taskStatusInterval = null;
}
}
}
报错index.js:493
[webpack-dev-server] ERROR
src/ng2app/instance-config/kafka-config-manager/kafka-config-manager.component.ts:151:31 - error TS2339: Property 'tipStr' does not exist on type '{ label: string; key: string; actionCallback: (obj: any) => void; }'.
151 deleteBtn.tipStr = '只有一条记录,不能删除。';
~~~~~~
index.js:493
[webpack-dev-server] Errors while compiling. Reload prevented.
index.js:493
[webpack-dev-server] ERROR
src/ng2app/instance-config/kafka-config-manager/kafka-config-manager.component.ts:127:24 - error TS2339: Property 'disabled' does not exist on type '{ label: string; key: string; actionCallback: (obj: any) => void; }'.
127 }).disabled = hasEditRows;
~~~~~~~~
index.js:493
[webpack-dev-server] ERROR
src/ng2app/instance-config/kafka-config-manager/kafka-config-manager.component.ts:143:24 - error TS2339: Property 'tipStr' does not exist on type '{ label: string; key: string; actionCallback: (obj: any) => void; }'.
143 }).tipStr = editDisabledTip;
~~~~~~
index.js:493
[webpack-dev-server] ERROR
src/ng2app/instance-config/kafka-config-manager/kafka-config-manager.component.ts:145:29 - error TS2339: Property 'disabled' does not exist on type '{ label: string; key: string; actionCallback: (obj: any) => void; }'.
145 deleteBtn.disabled = hasEditRows;
~~~~~~~~
index.js:493
[webpack-dev-server] ERROR
src/ng2app/instance-config/kafka-config-manager/kafka-config-manager.component.ts:146:29 - error TS2339: Property 'tipStr' does not exist on type '{ label: string; key: string; actionCallback: (obj: any) => void; }'.
146 deleteBtn.tipStr = editDisabledTip;
~~~~~~
index.js:493
[webpack-dev-server] ERROR
src/ng2app/instance-config/kafka-config-manager/kafka-config-manager.component.ts:150:31 - error TS2339: Property 'disabled' does not exist on type '{ label: string; key: string; actionCallback: (obj: any) => void; }'.
150 deleteBtn.disabled = true;
~~~~~~~~
index.js:493
[webpack-dev-server] ERROR
src/ng2app/instance-config/kafka-config-manager/kafka-config-manager.component.ts:151:31 - error TS2339: Property 'tipStr' does not exist on type '{ label: string; key: string; actionCallback: (obj: any) => void; }'.
151 deleteBtn.tipStr = '只有一条记录,不能删除。';