mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-31 20:11:33 +01:00
Merge pull request #1399 from vmware/feature/harbor_shell
Feature/harbor shell
This commit is contained in:
commit
64acbd98a8
@ -0,0 +1,32 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop">
|
||||
<h3 class="modal-title">Accout Settings</h3>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="account_settings_username" class="col-md-4">Username</label>
|
||||
<input type="text" class="col-md-8" id="account_settings_username" size="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_email" class="col-md-4">Email</label>
|
||||
<input type="text" class="col-md-8" id="account_settings_email" size="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_full_name" class="col-md-4">Full name</label>
|
||||
<input type="text" class="col-md-8" id="account_settings_full_name" size="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_comments" class="col-md-4">Comments</label>
|
||||
<input type="text" class="col-md-8" id="account_settings_comments" size="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="button" class="col-md-4" class="btn btn-outline">Change Password</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="close()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="submit()">Ok</button>
|
||||
</div>
|
||||
</clr-modal>
|
@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: "account-settings-modal",
|
||||
templateUrl: "account-settings-modal.component.html"
|
||||
})
|
||||
|
||||
export class AccountSettingsModalComponent{
|
||||
opened:boolean = false;
|
||||
staticBackdrop: boolean = true;
|
||||
|
||||
open() {
|
||||
this.opened = true;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
submit() {
|
||||
console.info("ok here!");
|
||||
this.close();
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-right'">
|
||||
<button class="nav-text" clrDropdownToggle>
|
||||
<!--<clr-icon shape="user" class="is-inverse" size="24"></clr-icon>-->
|
||||
<span>Administrator</span>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem>Add User</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>Account Setting</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>About</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
@ -1,16 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: "base-settings",
|
||||
templateUrl: "base-settings.component.html"
|
||||
})
|
||||
|
||||
/**
|
||||
* Component to handle the account settings
|
||||
*/
|
||||
export class BaseSettingsComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
@ -9,7 +9,8 @@ import { NavigatorComponent } from './navigator/navigator.component';
|
||||
import { GlobalSearchComponent } from './global-search/global-search.component';
|
||||
import { FooterComponent } from './footer/footer.component';
|
||||
import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
|
||||
import { BaseSettingsComponent } from './base-settings/base-settings.component';
|
||||
import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component';
|
||||
import { SearchResultComponent } from './global-search/search-result.component';
|
||||
|
||||
import { BaseRoutingModule } from './base-routing.module';
|
||||
|
||||
@ -24,9 +25,10 @@ import { BaseRoutingModule } from './base-routing.module';
|
||||
declarations: [
|
||||
NavigatorComponent,
|
||||
GlobalSearchComponent,
|
||||
BaseSettingsComponent,
|
||||
FooterComponent,
|
||||
HarborShellComponent
|
||||
HarborShellComponent,
|
||||
AccountSettingsModalComponent,
|
||||
SearchResultComponent
|
||||
],
|
||||
exports: [ HarborShellComponent ]
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
<a class="nav-link">
|
||||
<span class="nav-text">
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
</span>
|
||||
</a>
|
||||
<form class="search">
|
||||
<label for="search_input">
|
||||
<input #globalSearchBox id="search_input" type="text" (keyup)="search(globalSearchBox.value)" placeholder="Search Harbor...">
|
||||
</label>
|
||||
</form>
|
@ -1,10 +1,44 @@
|
||||
import { Component} from '@angular/core';
|
||||
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { SearchEvent } from '../search-event';
|
||||
|
||||
import 'rxjs/add/operator/debounceTime';
|
||||
import 'rxjs/add/operator/distinctUntilChanged';
|
||||
|
||||
const deBounceTime = 500; //ms
|
||||
|
||||
@Component({
|
||||
selector: 'global-search',
|
||||
templateUrl: "global-search.component.html"
|
||||
})
|
||||
export class GlobalSearchComponent{
|
||||
// constructor(private router: Router){}
|
||||
export class GlobalSearchComponent implements OnInit {
|
||||
//Publish search event to parent
|
||||
@Output() searchEvt = new EventEmitter<SearchEvent>();
|
||||
|
||||
//Keep search term as Subject
|
||||
private searchTerms = new Subject<string>();
|
||||
|
||||
//Implement ngOnIni
|
||||
ngOnInit(): void {
|
||||
this.searchTerms
|
||||
.debounceTime(deBounceTime)
|
||||
.distinctUntilChanged()
|
||||
.subscribe(term => {
|
||||
this.searchEvt.emit({
|
||||
term: term
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//Handle the term inputting event
|
||||
search(term: string): void {
|
||||
//Send event only when term is not empty
|
||||
|
||||
let nextTerm = term.trim();
|
||||
if (nextTerm != "") {
|
||||
this.searchTerms.next(nextTerm);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Headers, Http, RequestOptions } from '@angular/http';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
import { SearchResults } from './search-results';
|
||||
|
||||
const searchEndpoint = "/api/search";
|
||||
/**
|
||||
* Declare service to handle the global search
|
||||
*
|
||||
*
|
||||
* @export
|
||||
* @class GlobalSearchService
|
||||
*/
|
||||
@Injectable()
|
||||
export class GlobalSearchService {
|
||||
private headers = new Headers({
|
||||
"Content-Type": 'application/json'
|
||||
});
|
||||
private options = new RequestOptions({
|
||||
headers: this.headers
|
||||
});
|
||||
|
||||
constructor(private http: Http) { }
|
||||
|
||||
/**
|
||||
* Search related artifacts with the provided keyword
|
||||
*
|
||||
* @param {string} keyword
|
||||
* @returns {Promise<SearchResults>}
|
||||
*
|
||||
* @memberOf GlobalSearchService
|
||||
*/
|
||||
doSearch(term: string): Promise<SearchResults> {
|
||||
let searchUrl = searchEndpoint + "?q=" + term;
|
||||
|
||||
return this.http.get(searchUrl, this.options).toPromise()
|
||||
.then(response => response.json() as SearchResults)
|
||||
.catch(error => error);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
.search-overlay {
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 94%;
|
||||
width: 97%;
|
||||
/*shoud be lesser than 1000 to aoivd override the popup menu*/
|
||||
z-index: 999;
|
||||
box-sizing: border-box;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-title {
|
||||
margin-top: 0px;
|
||||
font-size: 28px;
|
||||
letter-spacing: normal;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.search-close {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-parent-override {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.search-spinner {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<div class="search-overlay" *ngIf="state">
|
||||
<div class="search-header">
|
||||
<span class="search-title">Search results</span>
|
||||
<span class="search-close" (mouseover)="mouseAction(true)" (mouseout)="mouseAction(false)">
|
||||
<clr-icon shape="close" [class.is-highlight]="hover" size="36" (click)="close()"></clr-icon>
|
||||
</span>
|
||||
</div>
|
||||
<!-- spinner -->
|
||||
<div class="spinner spinner-lg search-spinner" [hidden]="done">Search...</div>
|
||||
<div>Results is showing here!</div>
|
||||
</div>
|
@ -0,0 +1,80 @@
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
import { GlobalSearchService } from './global-search.service';
|
||||
import { SearchResults } from './search-results';
|
||||
|
||||
@Component({
|
||||
selector: "search-result",
|
||||
templateUrl: "search-result.component.html",
|
||||
styleUrls: ["search-result.component.css"],
|
||||
|
||||
providers: [GlobalSearchService]
|
||||
})
|
||||
|
||||
export class SearchResultComponent {
|
||||
@Output() closeEvt = new EventEmitter<boolean>();
|
||||
|
||||
searchResults: SearchResults;
|
||||
|
||||
//Open or close
|
||||
private stateIndicator: boolean = false;
|
||||
//Search in progress
|
||||
private onGoing: boolean = true;
|
||||
|
||||
//Whether or not mouse point is onto the close indicator
|
||||
private mouseOn: boolean = false;
|
||||
|
||||
constructor(private search: GlobalSearchService) { }
|
||||
|
||||
public get state(): boolean {
|
||||
return this.stateIndicator;
|
||||
}
|
||||
|
||||
public get done(): boolean {
|
||||
return !this.onGoing;
|
||||
}
|
||||
|
||||
public get hover(): boolean {
|
||||
return this.mouseOn;
|
||||
}
|
||||
|
||||
//Handle mouse event of close indicator
|
||||
mouseAction(over: boolean): void {
|
||||
this.mouseOn = over;
|
||||
}
|
||||
|
||||
//Show the results
|
||||
show(): void {
|
||||
this.stateIndicator = true;
|
||||
}
|
||||
|
||||
//Close the result page
|
||||
close(): void {
|
||||
//Tell shell close
|
||||
this.closeEvt.emit(true);
|
||||
|
||||
this.stateIndicator = false;
|
||||
}
|
||||
|
||||
//Call search service to complete the search request
|
||||
doSearch(term: string): void {
|
||||
//Confirm page is displayed
|
||||
if (!this.stateIndicator) {
|
||||
this.show();
|
||||
}
|
||||
|
||||
//Show spinner
|
||||
this.onGoing = true;
|
||||
|
||||
this.search.doSearch(term)
|
||||
.then(searchResults => {
|
||||
this.onGoing = false;
|
||||
this.searchResults = searchResults;
|
||||
console.info(searchResults);
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
console.error(error);//TODO: Use general erro handler
|
||||
});
|
||||
}
|
||||
}
|
7
harbor-app/src/app/base/global-search/search-results.ts
Normal file
7
harbor-app/src/app/base/global-search/search-results.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Project } from '../../project/project';
|
||||
import { Repository } from '../../repository/repository';
|
||||
|
||||
export class SearchResults {
|
||||
projects: Project[];
|
||||
repositories: Repository[];
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
.side-nav-override {
|
||||
box-shadow: 6px 0px 0px 0px #ccc;
|
||||
}
|
||||
|
||||
.container-override {
|
||||
position: relative !important;
|
||||
}
|
@ -1,41 +1,29 @@
|
||||
<clr-main-container>
|
||||
<clr-modal [(clrModalOpen)]="account_settings_opened">
|
||||
<h3 class="modal-title">Accout Settings</h3>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="account_settings_username" class="col-md-4">Username</label>
|
||||
<input type="text" class="col-md-8" id="account_settings_username" size="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_email" class="col-md-4">Email</label>
|
||||
<input type="text" class="col-md-8" id="account_settings_email" size="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_full_name" class="col-md-4">Full name</label>
|
||||
<input type="text" class="col-md-8" id="account_settings_full_name" size="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_comments" class="col-md-4">Comments</label>
|
||||
<input type="text" class="col-md-8" id="account_settings_comments" size="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="button" class="col-md-4" class="btn btn-outline">Change Password</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
<navigator (showAccountSettingsModal)="openModal($event)" (searchEvt)="doSearch($event)"></navigator>
|
||||
<global-message></global-message>
|
||||
<div class="content-container">
|
||||
<div class="content-area" [class.container-override]="showSearch">
|
||||
<!-- Only appear when searching -->
|
||||
<search-result (closeEvt)="searchClose($event)"></search-result>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<nav class="sidenav" [class.side-nav-override]="showSearch">
|
||||
<section class="sidenav-content">
|
||||
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">
|
||||
Projects
|
||||
</a>
|
||||
<section class="nav-group collapsible">
|
||||
<input id="tabsystem" type="checkbox">
|
||||
<label for="tabsystem">System Managements</label>
|
||||
<ul class="nav-list">
|
||||
<li><a class="nav-link">Users</a></li>
|
||||
<li><a class="nav-link">Replications</a></li>
|
||||
<li><a class="nav-link">Quarantine[*]</a></li>
|
||||
<li><a class="nav-link">Configurations[*]</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="account_settings_opened = false">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="account_settings_opened = false">Ok</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
<navigator></navigator>
|
||||
<global-message></global-message>
|
||||
<div class="content-container">
|
||||
<div class="content-area">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
</clr-main-container>
|
||||
<account-settings-modal></account-settings-modal>
|
@ -1,10 +1,70 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { SessionService } from '../../shared/session.service';
|
||||
import { ModalEvent } from '../modal-event';
|
||||
import { SearchEvent } from '../search-event';
|
||||
|
||||
import { AccountSettingsModalComponent } from '../account-settings/account-settings-modal.component';
|
||||
import { SearchResultComponent } from '../global-search/search-result.component';
|
||||
|
||||
@Component({
|
||||
selector: 'harbor-shell',
|
||||
templateUrl: 'harbor-shell.component.html'
|
||||
templateUrl: 'harbor-shell.component.html',
|
||||
styleUrls: ["harbor-shell.component.css"]
|
||||
})
|
||||
export class HarborShellComponent {
|
||||
|
||||
|
||||
export class HarborShellComponent implements OnInit {
|
||||
|
||||
@ViewChild(AccountSettingsModalComponent)
|
||||
private accountSettingsModal: AccountSettingsModalComponent;
|
||||
|
||||
@ViewChild(SearchResultComponent)
|
||||
private searchResultComponet: SearchResultComponent;
|
||||
|
||||
//To indicator whwther or not the search results page is displayed
|
||||
//We need to use this property to do some overriding work
|
||||
private isSearchResultsOpened: boolean = false;
|
||||
|
||||
constructor(private session: SessionService) { }
|
||||
|
||||
ngOnInit() {
|
||||
let cUser = this.session.getCurrentUser();
|
||||
if (!cUser) {
|
||||
//Try to update the session
|
||||
this.session.retrieveUser();
|
||||
}
|
||||
}
|
||||
|
||||
public get showSearch(): boolean {
|
||||
return this.isSearchResultsOpened;
|
||||
}
|
||||
|
||||
//Open modal dialog
|
||||
openModal(event: ModalEvent): void {
|
||||
switch (event.modalName) {
|
||||
case "account-settings":
|
||||
this.accountSettingsModal.open();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Handle the global search event and then let the result page to trigger api
|
||||
doSearch(event: SearchEvent): void {
|
||||
//Once this method is called
|
||||
//the search results page must be opened
|
||||
this.isSearchResultsOpened = true;
|
||||
|
||||
//Call the child component to do the real work
|
||||
this.searchResultComponet.doSearch(event.term);
|
||||
}
|
||||
|
||||
//Search results page closed
|
||||
//remove the related ovevriding things
|
||||
searchClose(event: boolean): void {
|
||||
if(event){
|
||||
this.isSearchResultsOpened = false;
|
||||
}
|
||||
}
|
||||
}
|
5
harbor-app/src/app/base/modal-event.ts
Normal file
5
harbor-app/src/app/base/modal-event.ts
Normal file
@ -0,0 +1,5 @@
|
||||
//Define a object to store the modal event
|
||||
export class ModalEvent {
|
||||
modalName: string;
|
||||
modalFlag: boolean; //true for open, false for close
|
||||
}
|
@ -1,40 +1,44 @@
|
||||
<clr-header class="header-4 header">
|
||||
<clr-header class="header-5 header">
|
||||
<div class="branding">
|
||||
<a href="#" class="nav-link">
|
||||
<clr-icon shape="vm-bug"></clr-icon>
|
||||
<span class="title">Harbor</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="header-nav">
|
||||
<a class="nav-link" id="dashboard-link" [routerLink]="['/harbor', 'dashboard']" routerLinkActive="active">
|
||||
<span class="nav-text">Dashboard</span>
|
||||
</a>
|
||||
<a class="nav-link" id="dashboard-link" [routerLink]="['/harbor', 'projects']" routerLinkActive="active">
|
||||
<span class="nav-text">Project</span>
|
||||
</a>
|
||||
</div>
|
||||
<global-search (searchEvt)="transferSearchEvent($event)"></global-search>
|
||||
<div class="header-actions">
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-right'">
|
||||
<!--<clr-icon shape="user" class="is-inverse" size="24"></clr-icon>-->
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-left'" class="dropdown">
|
||||
<button class="nav-text" clrDropdownToggle>
|
||||
<clr-icon shape="user" class="is-inverse" size="24" style="left: -2px;"></clr-icon>
|
||||
<span>Administrator</span>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem>Add User</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>Account Setting</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>About</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="open()">Account Setting</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>Log out</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
<clr-dropdown class="dropdown bottom-left">
|
||||
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
|
||||
<clr-icon shape="world" style="left:-5px;"></clr-icon>
|
||||
<span>English</span>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem>English</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>中文简体</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>中文繁體</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
<global-search></global-search>
|
||||
<clr-dropdown class="dropdown bottom-right">
|
||||
<button class="nav-text" clrDropdownToggle>
|
||||
<button class="nav-icon" clrDropdownToggle>
|
||||
<clr-icon shape="cog"></clr-icon>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem>Preferences</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>Log out</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>Add User</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>About</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
|
@ -1,10 +1,28 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ModalEvent } from '../modal-event';
|
||||
import { SearchEvent } from '../search-event';
|
||||
|
||||
@Component({
|
||||
selector: 'navigator',
|
||||
templateUrl: "navigator.component.html"
|
||||
})
|
||||
export class NavigatorComponent {
|
||||
// constructor(private router: Router){}
|
||||
@Output() showAccountSettingsModal = new EventEmitter<ModalEvent>();
|
||||
@Output() searchEvt = new EventEmitter<SearchEvent>();
|
||||
|
||||
//Open the account setting dialog
|
||||
open():void {
|
||||
this.showAccountSettingsModal.emit({
|
||||
modalName:"account-settings",
|
||||
modalFlag: true
|
||||
});
|
||||
}
|
||||
|
||||
//Only transfer the search event to the parent shell
|
||||
transferSearchEvent(evt: SearchEvent): void {
|
||||
this.searchEvt.emit(evt);
|
||||
}
|
||||
}
|
4
harbor-app/src/app/base/search-event.ts
Normal file
4
harbor-app/src/app/base/search-event.ts
Normal file
@ -0,0 +1,4 @@
|
||||
//Define a object to store the search event
|
||||
export class SearchEvent {
|
||||
term: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user