Repository: cloudspannerecosystem/harbourbridge
Pull request #389 (2022-10-20 13:19:56)

Phase2 ux changes
Fixes #<issue_number_goes_here>

> It's a good idea to open an issue first for discussion.

- [x] Remove “Configure schema” as title of page as it is repetitive with breadcrumbs.
- [x] Configure schema should be bold and black in text . This pattern should be reflected everywhere we use breadcrumbs where the page you are on is Bold and black.
- [x] Spacing between the icon and the text should be consistent with the spacing with the object viewer icon and text .
- [x] “Issues and Suggestions (0)” should be blue in text when the underline is blue .
- [x] “Spanner draft” should be solid blue bc it is the view we are in .
- [x] “Columns” should be solid blue bc it is the view we are in .
- [x] “Pencil” icon should be the same blue as the edit .
- [x] Spacing of the icon and text is incorrect. It should be the same as the spacing we have in the empty state for object viewer.
- [ ] There is a feedback mechanism missing when a user “edits” once they save we should give them a success/failure message to acknowledge their action.
- [ ] Icons for the status in object explorer updated


Patch:
diff --git a/sources/common/toddl.go b/sources/common/toddl.go
index 2dffae0..deba9b8 100644
--- a/sources/common/toddl.go
+++ b/sources/common/toddl.go
@@ -189,41 +189,8 @@ func cvtForeignKeysHelper(conv *internal.Conv, spTableName string, srcTable stri
 func cvtIndexes(conv *internal.Conv, spTableName string, srcTable string, srcIndexes []schema.Index) []ddl.CreateIndex {
 	var spIndexes []ddl.CreateIndex
 	for _, srcIndex := range srcIndexes {
-		var spKeys []ddl.IndexKey
-		var spStoredColumns []string
-
-		for _, k := range srcIndex.Keys {
-			spCol, err := internal.GetSpannerCol(conv, srcTable, k.Column, true)
-			if err != nil {
-				conv.Unexpected(fmt.Sprintf("Can't map index key column name for table %s column %s", srcTable, k.Column))
-				continue
-			}
-			spKeys = append(spKeys, ddl.IndexKey{Col: spCol, Desc: k.Desc})
-		}
-		for _, k := range srcIndex.StoredColumns {
-			spCol, err := internal.GetSpannerCol(conv, srcTable, k, true)
-			if err != nil {
-				conv.Unexpected(fmt.Sprintf("Can't map index column name for table %s column %s", srcTable, k))
-				continue
-			}
-			spStoredColumns = append(spStoredColumns, spCol)
-		}
-		if srcIndex.Name == "" {
-			// Generate a name if index name is empty in MySQL.
-			// Collision of index name will be handled by ToSpannerIndexName.
-			srcIndex.Name = fmt.Sprintf("Index_%s", srcTable)
-		}
-		spIndexName := internal.ToSpannerIndexName(conv, srcIndex.Name)
-		spIndex := ddl.CreateIndex{
-			Name:          spIndexName,
-			Table:         spTableName,
-			Unique:        srcIndex.Unique,
-			Keys:          spKeys,
-			StoredColumns: spStoredColumns,
-		}
+		spIndex := CvtIndexHelper(conv, spTableName, srcTable, srcIndex)
 		spIndexes = append(spIndexes, spIndex)
-		conv.Audit.ToSpannerFkIdx[srcTable].Index[srcIndex.Name] = spIndexName
-		conv.Audit.ToSourceFkIdx[spTableName].Index[spIndexName] = srcIndex.Name
 	}
 	return spIndexes
 }
@@ -263,3 +230,41 @@ func cvtForeignKeysForAReferenceTable(conv *internal.Conv, spTableName string, s
 	}
 	return spKeys
 }
+
+func CvtIndexHelper(conv *internal.Conv, spTableName string, srcTable string, srcIndex schema.Index) ddl.CreateIndex {
+	var spKeys []ddl.IndexKey
+	var spStoredColumns []string
+
+	for _, k := range srcIndex.Keys {
+		spCol, err := internal.GetSpannerCol(conv, srcTable, k.Column, true)
+		if err != nil {
+			conv.Unexpected(fmt.Sprintf("Can't map index key column name for table %s column %s", srcTable, k.Column))
+			continue
+		}
+		spKeys = append(spKeys, ddl.IndexKey{Col: spCol, Desc: k.Desc})
+	}
+	for _, k := range srcIndex.StoredColumns {
+		spCol, err := internal.GetSpannerCol(conv, srcTable, k, true)
+		if err != nil {
+			conv.Unexpected(fmt.Sprintf("Can't map index column name for table %s column %s", srcTable, k))
+			continue
+		}
+		spStoredColumns = append(spStoredColumns, spCol)
+	}
+	if srcIndex.Name == "" {
+		// Generate a name if index name is empty in MySQL.
+		// Collision of index name will be handled by ToSpannerIndexName.
+		srcIndex.Name = fmt.Sprintf("Index_%s", srcTable)
+	}
+	spIndexName := internal.ToSpannerIndexName(conv, srcIndex.Name)
+	spIndex := ddl.CreateIndex{
+		Name:          spIndexName,
+		Table:         spTableName,
+		Unique:        srcIndex.Unique,
+		Keys:          spKeys,
+		StoredColumns: spStoredColumns,
+	}
+	conv.Audit.ToSpannerFkIdx[srcTable].Index[srcIndex.Name] = spIndexName
+	conv.Audit.ToSourceFkIdx[spTableName].Index[spIndexName] = srcIndex.Name
+	return spIndex
+}
diff --git a/ui/src/app/components/header/header.component.scss b/ui/src/app/components/header/header.component.scss
index 9498e79..7fe529f 100644
--- a/ui/src/app/components/header/header.component.scss
+++ b/ui/src/app/components/header/header.component.scss
@@ -31,3 +31,6 @@
     }
   }
 }
+.cursor-pointer{
+  color: #3367d6;
+}
diff --git a/ui/src/app/components/object-detail/object-detail.component.html b/ui/src/app/components/object-detail/object-detail.component.html
index 43c65bd..67c86ad 100644
--- a/ui/src/app/components/object-detail/object-detail.component.html
+++ b/ui/src/app/components/object-detail/object-detail.component.html
@@ -67,11 +67,29 @@
         color="primary"
         class="icon drop"
         (click)="dropIndex()"
-        *ngIf="currentObject!.isSpannerNode && currentObject!.type === 'indexName'"
+        *ngIf="
+          currentObject!.isSpannerNode &&
+          !currentObject!.isDeleted &&
+          currentObject!.type === 'indexName'
+        "
       >
         <mat-icon>delete</mat-icon>
         <span>DROP INDEX</span>
       </button>
+      <button
+        mat-button
+        color="primary"
+        class="icon drop"
+        (click)="restoreIndex()"
+        *ngIf="
+          currentObject!.isSpannerNode &&
+          currentObject!.isDeleted &&
+          currentObject!.type == ObjectExplorerNodeType.Index
+        "
+      >
+        <mat-icon>undo</mat-icon>
+        <span> RESTORE INDEX</span>
+      </button>
       <button
         mat-button
         color="primary"
@@ -91,7 +109,11 @@
         color="primary"
         class="icon drop"
         (click)="restoreSpannerTable()"
-        *ngIf="currentObject!.isSpannerNode && currentObject!.isDeleted"
+        *ngIf="
+          currentObject!.isSpannerNode &&
+          currentObject!.isDeleted &&
+          currentObject!.type == ObjectExplorerNodeType.Table
+        "
       >
         <mat-icon>undo</mat-icon>
         <span> RESTORE TABLE</span>
@@ -110,7 +132,10 @@
     *ngIf="currentObject!.type === 'tableName'"
     (selectedTabChange)="tabChanged($event)"
   >
-    <mat-tab label="COLUMNS">
+    <mat-tab>
+      <ng-template mat-tab-label>
+        <span>COLUMNS </span>
+      </ng-template>
       <div class="column-tab-container">
         <div class="column-table-container">
           <table mat-table [dataSource]="srcDataSource">
@@ -352,7 +377,10 @@
       </div>
     </mat-tab>
 
-    <mat-tab label="PRIMARY KEY">
+    <mat-tab >
+      <ng-template mat-tab-label>
+        <span>PRIMARY KEY </span>
+      </ng-template>
       <div class="pk-tab-container">
         <table mat-table [dataSource]="pkDataSource">
           <ng-container matColumnDef="srcDatabase">
@@ -563,7 +591,10 @@
       </div>
     </mat-tab>
 
-    <mat-tab label="FOREIGN KEY">
+    <mat-tab>
+      <ng-template mat-tab-label>
+        <span>FOREIGN KEY</span>
+      </ng-template>
       <div class="fk-tab-container">
         <table mat-table [dataSource]="fkDataSource">
           <ng-container matColumnDef="srcDatabase">
@@ -738,7 +769,11 @@
       </div>
     </mat-tab>
 
-    <mat-tab label="SQL" *ngIf="currentObject!.isSpannerNode && !currentObject!.isDeleted">
+    <mat-tab>
+      <ng-template mat-tab-label>
+        <span  *ngIf="currentObject!.isSpannerNode && !currentObject!.isDeleted">SQL</span>
+      </ng-template>
+      
       <div class="ddl_container">
         <pre><code>{{ddlStmts[currentObject!.name]}}</code></pre>
       </div>
@@ -756,7 +791,9 @@
           <div class="spanner_edit-button">
             <span>Spanner</span>
 
-            <div *ngIf="!isIndexEditMode && currentObject!.isSpannerNode">
+            <div
+              *ngIf="!isIndexEditMode && !currentObject!.isDeleted && currentObject!.isSpannerNode"
+            >
               <button mat-stroked-button color="primary" (click)="toggleIndexEdit()">
                 <mat-icon class="edit-icon">edit</mat-icon>
                 EDIT
diff --git a/ui/src/app/components/object-detail/object-detail.component.scss b/ui/src/app/components/object-detail/object-detail.component.scss
index 7d20c32..c24baab 100644
--- a/ui/src/app/components/object-detail/object-detail.component.scss
+++ b/ui/src/app/components/object-detail/object-detail.component.scss
@@ -156,7 +156,7 @@
       background-color: inherit;
       border: none;
       .edit-icon {
-        color: black;
+        color: #3367d6;
         font-size: 1.1rem;
       }
     }
diff --git a/ui/src/app/components/object-detail/object-detail.component.ts b/ui/src/app/components/object-detail/object-detail.component.ts
index 5f08d3b..b1e7d07 100644
--- a/ui/src/app/components/object-detail/object-detail.component.ts
+++ b/ui/src/app/components/object-detail/object-detail.component.ts
@@ -41,7 +41,7 @@ export class ObjectDetailComponent implements OnInit {
   @Input() tableData: IColumnTabData[] = []
   @Input() currentDatabase: string = 'spanner'
   @Input() indexData: IIndexData[] = []
-  @Input() srcDbName: String = ''
+  @Input() srcDbName: String = localStorage.getItem(StorageKeys.SourceDbName) as string
   @Output() updateSidebar = new EventEmitter<boolean>()
   ObjectExplorerNodeType = ObjectExplorerNodeType
   conv: IConv = {} as IConv
@@ -56,7 +56,6 @@ export class ObjectDetailComponent implements OnInit {
         this.conv = res
       },
     })
-    this.srcDbName = extractSourceDbName(this.conv.DatabaseType)
   }
 
   srcDisplayedColumns = ['srcOrder', 'srcColName', 'srcDataType', 'srcIsPk', 'srcIsNotNull']
@@ -967,6 +966,21 @@ export class ObjectDetailComponent implements OnInit {
       }
     })
   }
+
+  restoreIndex() {
+    let tableId = this.currentObject!.parentId
+    let indexId = this.currentObject!.id
+    this.data
+      .restoreIndex(tableId, indexId)
+      .pipe(take(1))
+      .subscribe((res: string) => {
+        if (res === '') {
+          this.isObjectSelected = false
+        }
+      })
+    this.currentObject = null
+  }
+
   dropIndexKey(index: number) {
     if (this.localIndexData[index].srcColName) {
       this.localIndexData[index].spColName = ''
diff --git a/ui/src/app/components/object-explorer/object-explorer.component.html b/ui/src/app/components/object-explorer/object-explorer.component.html
index af8535a..588edc2 100644
--- a/ui/src/app/components/object-explorer/object-explorer.component.html
+++ b/ui/src/app/components/object-explorer/object-explorer.component.html
@@ -28,29 +28,32 @@
           <ng-container matColumnDef="status">
             <th mat-header-cell *matHeaderCellDef>Status</th>
             <td mat-cell *matCellDef="let data">
-              <mat-icon
-                *ngIf="data.status === 'GREEN' || data.status === 'GRAY'"
-                class="icon success"
-                matTooltip="Can be converted automatically"
-                matTooltipPosition="above"
-              >
-                check_circle
-              </mat-icon>
+              <svg  class="icon success"
+              matTooltip="Can be converted automatically"
+              matTooltipPosition="above" 
+              *ngIf="data.status === 'GREEN' || data.status === 'GRAY'" 
+              xmlns="http://www.w3.org/2000/svg" 
+              width="24"
+              height="24"
+              fill="black"
+             >
+             <path d="m10.4 17.6-2-4.4-4.4-2 4.4-2 2-4.4 2 4.4 4.4 2-4.4 2Zm6.4 1.6-1-2.2-2.2-1 2.2-1 1-2.2 1 2.2 2.2 1-2.2 1Z"/>
+              </svg>
               <mat-icon
                 *ngIf="data.status === 'YELLOW' || data.status === 'BLUE'"
-                class="icon warning"
+                class="work"
                 matTooltip="Requires minimal conversion changes"
                 matTooltipPosition="above"
               >
-                warning
+              work
               </mat-icon>
               <mat-icon
                 *ngIf="data.status === 'ORANGE'"
-                class="icon danger"
+                class="work_history"
                 matTooltip="Requires high complexity conversion changes"
                 matTooltipPosition="above"
               >
-                error
+              work_history
               </mat-icon>
             </td>
           </ng-container>
@@ -195,32 +198,35 @@
           <ng-container matColumnDef="status">
             <th mat-header-cell *matHeaderCellDef>Status</th>
             <td mat-cell *matCellDef="let data">
-              <mat-icon
-                *ngIf="data.status === 'GREEN' || data.status === 'GRAY'"
-                class="icon success"
-                matTooltip="Can be converted automatically"
-                matTooltipPosition="above"
-              >
-                check_circle
-              </mat-icon>
+              <svg  class="icon success"
+              matTooltip="Can be converted automatically"
+              matTooltipPosition="above" 
+              *ngIf="data.status === 'GREEN' || data.status === 'GRAY'" 
+              xmlns="http://www.w3.org/2000/svg" 
+              width="24"
+              height="24"
+              fill="black"
+             >
+             <path d="m10.4 17.6-2-4.4-4.4-2 4.4-2 2-4.4 2 4.4 4.4 2-4.4 2Zm6.4 1.6-1-2.2-2.2-1 2.2-1 1-2.2 1 2.2 2.2 1-2.2 1Z"/>
+              </svg>
               <mat-icon
                 *ngIf="data.status === 'YELLOW' || data.status === 'BLUE'"
-                class="icon warning"
+                class="work"
                 matTooltip="Requires minimal conversion changes"
                 matTooltipPosition="above"
               >
-                warning
+              work
               </mat-icon>
               <mat-icon
                 *ngIf="data.status === 'ORANGE'"
-                class="icon danger"
+                class="work_history"
                 matTooltip="Requires high complexity conversion changes"
                 matTooltipPosition="above"
               >
-                error
+              work_history
               </mat-icon>
               <mat-icon
-                *ngIf="data.status === 'DARK'"
+                *ngIf="data.status === 'DARK' || data.isDeleted==true"
                 class="icon dark"
                 matTooltip="Deleted"
                 matTooltipPosition="above"
@@ -360,3 +366,4 @@
     <mat-icon [ngClass]="[isLeftColumnCollapse ? 'display' : 'hidden']">last_page</mat-icon>
   </button>
 </div>
+
diff --git a/ui/src/app/components/prepare-migration/prepare-migration.component.html b/ui/src/app/components/prepare-migration/prepare-migration.component.html
index f377c1c..636f24a 100644
--- a/ui/src/app/components/prepare-migration/prepare-migration.component.html
+++ b/ui/src/app/components/prepare-migration/prepare-migration.component.html
@@ -4,7 +4,7 @@
         <span>></span>
         <a class="breadcrumb_workspace" mat-button [routerLink]="'/workspace'">Configure Schema</a>
         <span>></span>
-        <a class="breadcrumb_prepare_migration" mat-button [routerLink]="'/prepare-migration'">Prepare Migration</a>
+        <a class="breadcrumb_prepare_migration" mat-button [routerLink]="'/prepare-migration'"><b>Prepare Migration</b></a>
     </div>
 </div>
 <br />
diff --git a/ui/src/app/components/sidenav-review-changes/sidenav-review-changes.component.ts b/ui/src/app/components/sidenav-review-changes/sidenav-review-changes.component.ts
index 14406fa..43bd367 100644
--- a/ui/src/app/components/sidenav-review-changes/sidenav-review-changes.component.ts
+++ b/ui/src/app/components/sidenav-review-changes/sidenav-review-changes.component.ts
@@ -61,7 +61,18 @@ export class SidenavReviewChangesComponent implements OnInit {
       .subscribe({
         next: (res: string) => {
           if (res == '') {
-            this.snackbar.openSnackBar('Table updated successfully', 'Close', 5)
+            this.snackbar.openSnackBar(
+              `Schema changes to table ${this.tableUpdateData.tableName} saved successfully`,
+              'Close',
+              5
+            )
+            if (this.showDdl == false && this.tableNames.length != 1) {
+              this.snackbar.openSnackBar(
+                `Schema changes to tables ${this.tableNames[0]} and  ${this.tableNames[1]}  saved successfully`,
+                'Close',
+                5
+              )
+            }
             this.closeSidenav()
           } else {
             this.snackbar.openSnackBar(res, 'Close', 5)
diff --git a/ui/src/app/components/summary/summary.component.scss b/ui/src/app/components/summary/summary.component.scss
index d77bf54..6d094e1 100644
--- a/ui/src/app/components/summary/summary.component.scss
+++ b/ui/src/app/components/summary/summary.component.scss
@@ -11,9 +11,10 @@
     justify-content: center;
     align-items: center;
     .no-object-icon-container {
-      padding: 15px;
+      padding: 10px;
       background-color: rgba(51, 103, 214, 0.12);
       border-radius: 3px;
+      margin:20px;
     }
     .no-object-message {
       width: 80%;
@@ -32,6 +33,7 @@
       padding: 10px;
       background-color: rgba(51, 103, 214, 0.12);
       border-radius: 3px;
+      margin : 20px;
     }
     .no-issue-message {
       width: 80%;
diff --git a/ui/src/app/components/workspace/workspace.component.html b/ui/src/app/components/workspace/workspace.component.html
index b93b3c9..963f457 100644
--- a/ui/src/app/components/workspace/workspace.component.html
+++ b/ui/src/app/components/workspace/workspace.component.html
@@ -3,7 +3,7 @@
     <div class="breadcrumb vertical-center">
       <a class="breadcrumb_source" mat-button [routerLink]="'/'">Select Source</a>
       <span>></span>
-      <a class="breadcrumb_workspace" mat-button [routerLink]="'/workspace'">Configure Schema</a>
+      <a class="breadcrumb_workspace" mat-button [routerLink]="'/workspace'"><b>Configure Schema</b></a>
     </div>
     <div class="header_action">
       <span matTooltip="Connect to a spanner instance to run migration" [matTooltipDisabled]="!isOfflineStatus">
@@ -17,7 +17,6 @@
     </div>
   </div>
   <div class="container">
-    <h2 class="vertical-center">Configure schema</h2>
     <div class="summary">
       Estimation for {{ srcDbName.toUpperCase() }} to Spanner conversion:
       {{ conversionRatePercentages.good }}% of tables can be converted automatically,
@@ -73,11 +72,11 @@
       }"
     >
       <mat-tab-group mat-dialog-close="">
-        <mat-tab [label]="issuesAndSuggestionsLabel">
-          <app-summary
+        <mat-tab [ngClass]="{ 'blue-font-color': [updateIssuesLabel]}" [label]="issuesAndSuggestionsLabel">
+          <app-summary 
             [currentObject]="currentObject"
             (changeIssuesLabel)="updateIssuesLabel($event)"
-          ></app-summary>
+            ></app-summary>
         </mat-tab>
         <mat-tab label="RULES(0)">
           <app-rule></app-rule>
diff --git a/ui/src/app/components/workspace/workspace.component.ts b/ui/src/app/components/workspace/workspace.component.ts
index 6e94906..b5fbbfd 100644
--- a/ui/src/app/components/workspace/workspace.component.ts
+++ b/ui/src/app/components/workspace/workspace.component.ts
@@ -85,7 +85,6 @@ export class WorkspaceComponent implements OnInit, OnDestroy {
       }
       this.conv = data
 
-      this.srcDbName = extractSourceDbName(this.conv.DatabaseType)
       if (indexAddedOrRemoved && this.conversionRates) this.reRenderObjectExplorerSpanner()
       if (!this.objectExplorerInitiallyRender && this.conversionRates) {
         this.reRenderObjectExplorerSpanner()
diff --git a/ui/src/app/services/conversion/conversion.service.ts b/ui/src/app/services/conversion/conversion.service.ts
index 1cf8208..32fc4e3 100644
--- a/ui/src/app/services/conversion/conversion.service.ts
+++ b/ui/src/app/services/conversion/conversion.service.ts
@@ -35,6 +35,8 @@ export class ConversionService {
       }
       return true
     })
+    let deletedIndexes = this.getDeletedIndexes(conv)
+
     let parentNode: ISchemaObjectNode = {
       name: `Tables (${srcTableNames.length})`,
       type: ObjectExplorerNodeType.Tables,
@@ -102,6 +104,25 @@ export class ConversionService {
         parentId: '',
       })
     })
+
+    // add deleted indexes
+    parentNode.children?.forEach((tableNode: ISchemaObjectNode, i: number) => {
+      if (deletedIndexes[tableNode.id]) {
+        deletedIndexes[tableNode.id].forEach((index: IIndex) => {
+          parentNode.children![i].children![0].children?.push({
+            name: index.Name.replace(/[^A-Za-z0-9_]/g, '_'),
+            type: ObjectExplorerNodeType.Index,
+            parent: conv.SpSchema[tableNode.name]?.Name,
+            pos: i,
+            isSpannerNode: true,
+            isDeleted: true,
+            id: index.Id,
+            parentId: tableNode.id,
+          })
+        })
+      }
+    })
+
     return [
       {
         name: conv.DatabaseName,
@@ -372,6 +393,29 @@ export class ConversionService {
     return res
   }
 
+  getDeletedIndexes(conv: IConv): Record<string, IIndex[]> {
+    let deletedIndexes: Record<string, IIndex[]> = {}
+    Object.keys(conv.SpSchema).map((spTableName: string) => {
+      let spTable = conv.SpSchema[spTableName]
+      let srcTableName = this.getSourceTableNameFromId(spTable.Id, conv)
+      let srcTable = conv.SrcSchema[srcTableName]
+      let spIndexIds = spTable.Indexes?.map((index: ICreateIndex) => {
+        return index.Id
+      })
+      let tableDeletedIndexes = srcTable.Indexes?.filter((index: IIndex) => {
+        if (!spIndexIds.includes(index.Id)) {
+          return true
+        }
+        return false
+      })
+      if (tableDeletedIndexes && tableDeletedIndexes.length > 0) {
+        deletedIndexes[spTable.Id] = tableDeletedIndexes
+      }
+    })
+
+    return deletedIndexes
+  }
+
   getSpannerColDefFromId(tableName: string, id: string, data: IConv): IColumnDef | null {
     let res: IColumnDef | null = null
     Object.keys(data.SpSchema[tableName].ColDefs).forEach((colName) => {
diff --git a/ui/src/app/services/data/data.service.ts b/ui/src/app/services/data/data.service.ts
index c9bcc6b..d852531 100644
--- a/ui/src/app/services/data/data.service.ts
+++ b/ui/src/app/services/data/data.service.ts
@@ -387,6 +387,25 @@ export class DataService {
     )
   }
 
+  restoreIndex(tableId: string, indexId: string): Observable<string> {
+    return this.fetch.restoreIndex(tableId, indexId).pipe(
+      catchError((e: any) => {
+        return of({ error: e.error })
+      }),
+      tap(console.log),
+      map((data) => {
+        if (data.error) {
+          this.snackbar.openSnackBar(data.error, 'Close')
+          return data.error
+        } else {
+          this.convSubject.next(data)
+          this.snackbar.openSnackBar('Index restored successfully', 'Close', 5)
+          return ''
+        }
+      })
+    )
+  }
+
   getInterleaveConversionForATable(tableName: string) {
     this.fetch.getInterleaveStatus(tableName).subscribe((res: IInterleaveStatus) => {
       this.tableInterleaveStatusSub.next(res)
diff --git a/ui/src/app/services/fetch/fetch.service.ts b/ui/src/app/services/fetch/fetch.service.ts
index e0e71f4..a1ae5e7 100644
--- a/ui/src/app/services/fetch/fetch.service.ts
+++ b/ui/src/app/services/fetch/fetch.service.ts
@@ -185,6 +185,13 @@ export class FetchService {
     })
   }
 
+  restoreIndex(tableId: string, indexId: string) {
+    return this.http.post<HttpResponse<IConv>>(
+      `${this.url}/restore/secondaryIndex?tableId=${tableId}&indexId=${indexId}`,
+      {}
+    )
+  }
+
   getInterleaveStatus(tableName: string) {
     return this.http.get<IInterleaveStatus>(`${this.url}/setparent?table=${tableName}`)
   }
diff --git a/ui/src/styles.scss b/ui/src/styles.scss
index 409693a..70bcdb8 100644
--- a/ui/src/styles.scss
+++ b/ui/src/styles.scss
@@ -44,6 +44,10 @@ body {
   color: #3367d6;
 }
 
+.mat-tab-label{
+  opacity: 1 !important;
+}
+
 .gray-out {
   opacity: 0.6;
 }
@@ -53,13 +57,18 @@ body {
 }
 
 .warning {
-  color: #f3b300;
+  color: #ff9800;
 }
 
 .success {
   color: green;
 }
 
+.suggestion {
+  color: #f6be00;
+  transform: rotate(180deg);
+}
+
 .orange {
   color: rgb(248, 116, 68);
 }
@@ -111,17 +120,17 @@ body {
     align-items: center;
     .breadcrumb_source {
       padding-left: 0;
-      color: rgba(0, 0, 0, 0.56);
+      color:  rgb(143, 143, 143);
       font-weight: 400;
       font-size: 14px;
     }
     .breadcrumb_workspace {
       font-weight: 400;
       font-size: 14px;
-    }
+      color :#000000;
+    }  
     span {
       font-size: 1.2rem;
-      color: rgb(143, 143, 143);
     }
   }
 }
@@ -189,6 +198,9 @@ body {
   padding-left: 20vw;
 }
 
+.mat-tab-label-active{
+  color: #3367d6 !important;
+}
 .object-display-flex {
   display: flex;
   flex-direction: row;
@@ -250,12 +262,11 @@ body {
   }
 }
 
-.sidenav-header-border{
+.sidenav-header-border {
   border-bottom-width: 1px;
   border-bottom-style: solid;
 }
 
-
 .sidenav-percentage-bar {
   height: 0.8rem;
   display: flex;
@@ -280,21 +291,23 @@ body {
   }
 }
 
-.sidenav-conversionByTable{
-  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);
+.sidenav-conversionByTable {
+  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%),
+    0px 1px 3px 0px rgb(0 0 0 / 12%);
 }
 
-.sidenav-conversionByPercentage{
-  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);
+.sidenav-conversionByPercentage {
+  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%),
+    0px 1px 3px 0px rgb(0 0 0 / 12%);
 }
 
-.sidenav-databaseDefinitions{
-  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);
+.sidenav-databaseDefinitions {
+  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%),
+    0px 1px 3px 0px rgb(0 0 0 / 12%);
 }
 
-
-.sidenav-title{
-  margin-top :10px;
+.sidenav-title {
+  margin-top: 10px;
   margin-bottom: 0px;
 }
 .radio-button-container mat-radio-button:hover {
@@ -328,4 +341,4 @@ body {
 .spanner-tab-link{
   color:  #1967d2;
   font-weight: 400;
-}
\ No newline at end of file
+}
diff --git a/webv2/config.json b/webv2/config.json
index ca5c424..8c5d5e5 100755
--- a/webv2/config.json
+++ b/webv2/config.json
@@ -1,4 +1,4 @@
 {
-    "GCPProjectID": "",
-    "SpannerInstanceID": ""
+ "GCPProjectID": "",
+ "SpannerInstanceID": ""
 }
\ No newline at end of file
diff --git a/webv2/routes.go b/webv2/routes.go
index 4f26165..ebbe58b 100644
--- a/webv2/routes.go
+++ b/webv2/routes.go
@@ -53,6 +53,7 @@ func getRoutes() *mux.Router {
 
 	// TODO:(searce) take constraint names themselves which are guaranteed to be unique for Spanner.
 	router.HandleFunc("/drop/secondaryindex", dropSecondaryIndex).Methods("POST")
+	router.HandleFunc("/restore/secondaryIndex", restoreSecondaryIndex).Methods("POST")
 
 	router.HandleFunc("/restore/table", restoreTable).Methods("POST")
 	router.HandleFunc("/drop/table", dropTable).Methods("POST")
diff --git a/webv2/web.go b/webv2/web.go
index 1e4bfb9..e180069 100644
--- a/webv2/web.go
+++ b/webv2/web.go
@@ -43,6 +43,7 @@ import (
 	"github.com/cloudspannerecosystem/harbourbridge/internal"
 	"github.com/cloudspannerecosystem/harbourbridge/profiles"
 	"github.com/cloudspannerecosystem/harbourbridge/proto/migration"
+	"github.com/cloudspannerecosystem/harbourbridge/schema"
 	"github.com/cloudspannerecosystem/harbourbridge/sources/common"
 	"github.com/cloudspannerecosystem/harbourbridge/sources/mysql"
 	"github.com/cloudspannerecosystem/harbourbridge/sources/oracle"
@@ -1082,6 +1083,84 @@ func dropForeignKey(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(convm)
 }
 
+func restoreSecondaryIndex(w http.ResponseWriter, r *http.Request) {
+	tableId := r.FormValue("tableId")
+	indexId := r.FormValue("indexId")
+	sessionState := session.GetSessionState()
+	if sessionState.Conv == nil || sessionState.Driver == "" {
+		http.Error(w, fmt.Sprintf("Schema is not converted or Driver is not configured properly. Please retry converting the database to Spanner."), http.StatusNotFound)
+		return
+	}
+	if tableId == "" {
+		http.Error(w, fmt.Sprintf("Table Id is empty"), http.StatusBadRequest)
+		return
+	}
+	if indexId == "" {
+		http.Error(w, fmt.Sprintf("Index Id is empty"), http.StatusBadRequest)
+		return
+	}
+
+	srcTableName := ""
+	for _, value := range sessionState.Conv.SrcSchema {
+		if value.Id == tableId {
+			srcTableName = value.Name
+			break
+		}
+	}
+	if srcTableName == "" {
+		http.Error(w, fmt.Sprintf("Source Table not found"), http.StatusBadRequest)
+		return
+	}
+
+	spTableName := ""
+	for _, value := range sessionState.Conv.SpSchema {
+		if value.Id == tableId {
+			spTableName = value.Name
+			break
+		}
+	}
+	if spTableName == "" {
+		http.Error(w, fmt.Sprintf("Spanner Table not found"), http.StatusBadRequest)
+		return
+	}
+
+	var srcIndex schema.Index
+	srcIndexFound := false
+	for _, index := range sessionState.Conv.SrcSchema[srcTableName].Indexes {
+		if index.Id == indexId {
+			srcIndex = index
+			srcIndexFound = true
+			break
+		}
+	}
+	if !srcIndexFound {
+		http.Error(w, fmt.Sprintf("Source index not found"), http.StatusBadRequest)
+		return
+	}
+
+	conv := sessionState.Conv
+
+	spIndex := common.CvtIndexHelper(conv, spTableName, srcTableName, srcIndex)
+	spIndexes := conv.SpSchema[spTableName].Indexes
+	spIndexes = append(spIndexes, spIndex)
+	spTable := conv.SpSchema[spTableName]
+	spTable.Indexes = spIndexes
+	conv.SpSchema[spTableName] = spTable
+
+	uniqueid.CopyUniqueIdToSpannerTable(conv, spTable.Name)
+	sessionState.Conv = conv
+	index.AssignInitialOrders()
+	index.IndexSuggestion()
+
+	convm := session.ConvWithMetadata{
+		SessionMetadata: sessionState.SessionMetadata,
+		Conv:            *sessionState.Conv,
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(convm)
+
+}
+
 // renameForeignKeys checks the new names for spanner name validity, ensures the new names are already not used by existing tables
 // secondary indexes or foreign key constraints. If above checks passed then foreignKey renaming reflected in the schema else appropriate
 // error thrown.
@@ -1627,6 +1706,8 @@ func dropSecondaryIndex(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	usedNames := sessionState.Conv.UsedNames
+	delete(usedNames, sp.Indexes[position].Name)
 	index.RemoveIndexIssues(table, sp.Indexes[position])
 
 	sp.Indexes = utilities.RemoveSecondaryIndex(sp.Indexes, position)


Test patch:
diff --git a/webv2/web_test.go b/webv2/web_test.go
index 1b29db2..4cf8371 100644
--- a/webv2/web_test.go
+++ b/webv2/web_test.go
@@ -1920,6 +1920,138 @@ func TestDropSecondaryIndex(t *testing.T) {
 	}
 }
 
+func TestRestoreSecondaryIndex(t *testing.T) {
+	tc := []struct {
+		name         string
+		tableId      string
+		indexId      string
+		statusCode   int64
+		conv         *internal.Conv
+		expectedConv *internal.Conv
+	}{
+		{
+			name:       "Test restore valid secondary index success",
+			tableId:    "t1",
+			indexId:    "i1",
+			statusCode: http.StatusOK,
+			conv: &internal.Conv{
+				SrcSchema: map[string]schema.Table{
+					"t1": {
+						Name: "t1",
+						Indexes: []schema.Index{
+							{Name: "idx1", Unique: false, Keys: []schema.Key{{Column: "b", Desc: false, Order: 1}}, Id: "i1"},
+							{Name: "idx2", Unique: false, Keys: []schema.Key{{Column: "c", Desc: false, Order: 1}}, Id: "i2"},
+						},
+						Id: "t1",
+					},
+				},
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": {
+						Name: "t1",
+						Indexes: []ddl.CreateIndex{
+							{Name: "idx2", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "c", Desc: false, Order: 1}}, Id: "i2"},
+						},
+						Id: "t1",
+					},
+				},
+				Audit: internal.Audit{
+					MigrationType: migration.MigrationData_SCHEMA_ONLY.Enum(),
+					ToSourceFkIdx: map[string]internal.FkeyAndIdxs{
+						"t1": {
+							Name:       "t1",
+							ForeignKey: map[string]string{},
+							Index:      map[string]string{"idx2": "idx2"},
+						},
+					},
+					ToSpannerFkIdx: map[string]internal.FkeyAndIdxs{
+						"t1": {
+							Name:       "t1",
+							ForeignKey: map[string]string{},
+							Index:      map[string]string{"idx2": "idx2"},
+						},
+					},
+				},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": {Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+				ToSpanner: map[string]internal.NameAndCols{
+					"t1": {Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+				UsedNames: map[string]bool{"t1": true, "idx2": true},
+			},
+			expectedConv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": {
+						Name: "t1",
+						Indexes: []ddl.CreateIndex{
+							{Name: "idx2", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "c", Desc: false, Order: 1}}, Id: "i2"},
+							{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "b", Desc: false, Order: 1}}, Id: "i1"},
+						},
+						Id: "t1",
+					},
+				},
+			},
+		},
+
+		{
+			name:       "Test restore secondary index invalid index id",
+			tableId:    "t1",
+			indexId:    "A",
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": {
+						Indexes: []ddl.CreateIndex{{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "b", Desc: false, Order: 1}}, Id: "i1"}},
+					}},
+				Audit: internal.Audit{
+					MigrationType: migration.MigrationData_SCHEMA_ONLY.Enum(),
+				},
+			},
+			expectedConv: &internal.Conv{},
+		},
+		{
+			name:       "Test drop secondary index invalid table id",
+			tableId:    "X",
+			indexId:    "i1",
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": {
+						Indexes: []ddl.CreateIndex{{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "b", Desc: false, Order: 1}}, Id: "i1"}},
+					}},
+				Audit: internal.Audit{
+					MigrationType: migration.MigrationData_SCHEMA_ONLY.Enum(),
+				},
+			},
+			expectedConv: &internal.Conv{},
+		},
+	}
+	for _, tc := range tc {
+		sessionState := session.GetSessionState()
+
+		sessionState.Driver = constants.MYSQL
+		sessionState.Conv = tc.conv
+		payload := `{}`
+		req, err := http.NewRequest("POST", "/restore/secondaryIndex?tableId="+tc.tableId+"&indexId="+tc.indexId, strings.NewReader(payload))
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Content-Type", "application/json")
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(restoreSecondaryIndex)
+		handler.ServeHTTP(rr, req)
+		var res *internal.Conv
+		json.Unmarshal(rr.Body.Bytes(), &res)
+		if status := rr.Code; int64(status) != tc.statusCode {
+			t.Errorf("handler returned wrong status code: got %v want %v",
+				status, tc.statusCode)
+		}
+		if tc.statusCode == http.StatusOK {
+			assert.Equal(t, tc.expectedConv.SpSchema, res.SpSchema)
+		}
+	}
+}
+
 func TestDropTable(t *testing.T) {
 	sessionState := session.GetSessionState()
 	sessionState.Driver = constants.MYSQL