Commit 8bff170c authored by AllForNothing's avatar AllForNothing
Browse files

Improve routing and UI for artifact pages


Signed-off-by: default avatarAllForNothing <sshijun@vmware.com>
parent 86b3e47f
......@@ -13,11 +13,8 @@
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CoreModule } from '../core/core.module';
import { SharedModule } from '../shared/shared.module';
import { RepositoryModule } from '../repository/repository.module';
import { PasswordSettingComponent } from './password-setting/password-setting.component';
import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component';
import { SignUpComponent } from './sign-up/sign-up.component';
......@@ -33,7 +30,6 @@ import { AccountSettingsModalService } from './account-settings/account-settings
CoreModule,
RouterModule,
SharedModule,
RepositoryModule
],
declarations: [
PasswordSettingComponent,
......
......@@ -14,13 +14,10 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { RouterModule } from '@angular/router';
import { ProjectModule } from '../project/project.module';
import { UserModule } from '../user/user.module';
import { AccountModule } from '../account/account.module';
import { RepositoryModule } from '../repository/repository.module';
import { GroupModule } from '../group/group.module';
import { NavigatorComponent } from './navigator/navigator.component';
import { GlobalSearchComponent } from './global-search/global-search.component';
import { FooterComponent } from './footer/footer.component';
......@@ -36,7 +33,6 @@ import { SearchTriggerService } from './global-search/search-trigger.service';
UserModule,
AccountModule,
RouterModule,
RepositoryModule,
GroupModule
],
declarations: [
......
......@@ -13,40 +13,26 @@
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SystemAdminGuard } from './shared/route/system-admin-activate.service';
import { AuthCheckGuard } from './shared/route/auth-user-activate.service';
import { SignInGuard } from './shared/route/sign-in-guard-activate.service';
import { MemberGuard } from './shared/route/member-guard-activate.service';
import { ArtifactGuard } from './shared/route/artifact-guard-activate.service';
import { MemberPermissionGuard } from './shared/route/member-permission-guard-activate.service';
import { OidcGuard } from './shared/route/oidc-guard-active.service';
import { PageNotFoundComponent } from './shared/not-found/not-found.component';
import { HarborShellComponent } from './base/harbor-shell/harbor-shell.component';
import { ConfigurationComponent } from './config/config.component';
import { DevCenterComponent } from './dev-center/dev-center.component';
import { GcPageComponent } from './gc-page/gc-page.component';
import { VulnerabilityPageComponent } from './vulnerability-page/vulnerability-page.component';
import { UserComponent } from './user/user.component';
import { SignInComponent } from './sign-in/sign-in.component';
import { ResetPasswordComponent } from './account/password-setting/reset-password/reset-password.component';
import { GroupComponent } from './group/group.component';
import { TotalReplicationPageComponent } from './replication/total-replication/total-replication-page.component';
import { ReplicationTasksPageComponent } from './replication/replication-tasks-page/replication-tasks-page.component';
import { DestinationPageComponent } from './replication/destination/destination-page.component';
import { AuditLogComponent } from './log/audit-log.component';
import { LogPageComponent } from './log/log-page.component';
import { RepositoryPageComponent } from './repository/repository-page.component';
import { ArtifactListPageComponent } from './repository/artifact-list-page/artifact-list-page.component';
import { ArtifactSummaryPageComponent } from './repository/artifact-summary-page/artifact-summary-page.component';
import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-repository-deactivate.service';
import { ProjectComponent } from './project/project.component';
import { ProjectDetailComponent } from './project/project-detail/project-detail.component';
import { MemberComponent } from './project/member/member.component';
......@@ -61,7 +47,6 @@ import { HelmChartDetailComponent } from './project/helm-chart/helm-chart-detail
import { OidcOnboardComponent } from './oidc-onboard/oidc-onboard.component';
import { LicenseComponent } from './license/license.component';
import { SummaryComponent } from './project/summary/summary.component';
import { TagFeatureIntegrationComponent } from './project/tag-feature-integration/tag-feature-integration.component';
import { TagRetentionComponent } from './project/tag-feature-integration/tag-retention/tag-retention.component';
import { ImmutableTagComponent } from './project/tag-feature-integration/immutable-tag/immutable-tag.component';
......@@ -72,8 +57,9 @@ import { LabelsComponent } from "./labels/labels.component";
import { ProjectQuotasComponent } from "./project-quotas/project-quotas.component";
import { VulnerabilityConfigComponent } from "../lib/components/config/vulnerability/vulnerability-config.component";
import { USERSTATICPERMISSION } from "../lib/services";
import { LeavingArtifactSummaryRouteDeactivate } from './shared/route/leaving-artifact-summary-deactivate.service';
import { RepositoryGridviewComponent } from "./project/repository/repository-gridview.component";
import { ArtifactListPageComponent } from "./project/repository/artifact-list-page/artifact-list-page.component";
import { ArtifactSummaryComponent } from "./project/repository/artifact/artifact-summary.component";
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
......@@ -99,7 +85,6 @@ const harborRoutes: Routes = [
{
path: 'harbor',
component: HarborShellComponent,
// canActivate: [AuthCheckGuard],
canActivateChild: [AuthCheckGuard],
children: [
{ path: '', redirectTo: 'projects', pathMatch: 'full' },
......@@ -169,41 +154,6 @@ const harborRoutes: Routes = [
canActivate: [SystemAdminGuard],
canActivateChild: [SystemAdminGuard]
},
{
path: 'tags/:id/:repo',
component: ArtifactListPageComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
}
},
{
path: 'projects/:id/repositories/:repo',
component: ArtifactListPageComponent,
canActivate: [MemberGuard],
canDeactivate: [LeavingRepositoryRouteDeactivate],
resolve: {
projectResolver: ProjectRoutingResolver
},
},
{
path: 'projects/:id/repositories/:repo/depth/:depth',
component: ArtifactListPageComponent,
canActivate: [MemberGuard],
canDeactivate: [LeavingRepositoryRouteDeactivate],
resolve: {
projectResolver: ProjectRoutingResolver
},
},
{
path: 'projects/:id/repositories/:repo/artifacts/:digest',
component: ArtifactSummaryPageComponent,
canActivate: [MemberGuard, ArtifactGuard],
canDeactivate: [LeavingArtifactSummaryRouteDeactivate],
resolve: {
projectResolver: ProjectRoutingResolver
}
},
{
path: 'projects/:id/helm-charts/:chart/versions',
component: ListChartVersionsComponent,
......@@ -248,7 +198,7 @@ const harborRoutes: Routes = [
action: USERSTATICPERMISSION.REPOSITORY.VALUE.LIST
}
},
component: RepositoryPageComponent,
component: RepositoryGridviewComponent
},
{
path: 'helm-charts',
......@@ -261,17 +211,6 @@ const harborRoutes: Routes = [
},
component: ListChartsComponent
},
{
path: 'repositories/:repo/tags',
canActivate: [MemberPermissionGuard],
data: {
permissionParam: {
resource: USERSTATICPERMISSION.REPOSITORY.KEY,
action: USERSTATICPERMISSION.REPOSITORY.VALUE.LIST
}
},
component: ArtifactListPageComponent
},
{
path: 'members',
canActivate: [MemberPermissionGuard],
......@@ -374,6 +313,38 @@ const harborRoutes: Routes = [
}
]
},
{
path: 'projects/:id/repositories/:repo',
component: ArtifactListPageComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
}
},
{
path: 'projects/:id/repositories/:repo/depth/:depth',
component: ArtifactListPageComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
},
},
{
path: 'projects/:id/repositories/:repo/artifacts/:digest',
component: ArtifactSummaryComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
}
},
{
path: 'projects/:id/repositories/:repo/depth/:depth/artifacts/:digest',
component: ArtifactSummaryComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
}
},
{
path: 'configs',
component: ConfigurationComponent,
......
......@@ -4,8 +4,8 @@ import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { HelmChartVersion } from '../helm-chart.interface.service';
import { ResourceType } from '../../../shared/shared.const';
import { Label, Tag } from "../../../../lib/services";
import { Artifact } from '../../../../lib/components/artifact/artifact';
import { Label } from "../../../../lib/services";
import { Artifact } from "../../../../../ng-swagger-gen/models/artifact";
@Component({
selector: "hbr-chart-version-label-filter",
......
......@@ -12,31 +12,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { RepositoryModule } from '../repository/repository.module';
import { ReplicationModule } from '../replication/replication.module';
import { SummaryModule } from './summary/summary.module';
import { TagFeatureIntegrationModule } from './tag-feature-integration/tag-feature-integration.module';
import { LogModule } from '../log/log.module';
import { ProjectComponent } from './project.component';
import { CreateProjectComponent } from './create-project/create-project.component';
import { ListProjectComponent } from './list-project/list-project.component';
import { ProjectDetailComponent } from './project-detail/project-detail.component';
import { MemberComponent } from './member/member.component';
import { AddMemberComponent } from './member/add-member/add-member.component';
import { AddGroupComponent } from './member/add-group/add-group.component';
// import { ProjectService } from '@harbor/ui';
import { MemberService } from './member/member.service';
import { RobotService } from './robot-account/robot-account.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service';
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
import { ProjectLabelComponent } from "../project/project-label/project-label.component";
import { HelmChartModule } from './helm-chart/helm-chart.module';
import { RobotAccountComponent } from './robot-account/robot-account.component';
import { AddRobotComponent } from './robot-account/add-robot/add-robot.component';
......@@ -47,11 +39,34 @@ import { AddWebhookComponent } from './webhook/add-webhook/add-webhook.component
import { AddWebhookFormComponent } from './webhook/add-webhook-form/add-webhook-form.component';
import { ScannerComponent } from "./scanner/scanner.component";
import { ConfigScannerService } from "../config/scanner/config-scanner.service";
import { RepositoryGridviewComponent } from "./repository/repository-gridview.component";
import { ResultTipHistogramComponent } from "./repository/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component";
import { ResultGridComponent } from "./repository/vulnerability-scanning/result-grid.component";
import { ResultBarChartComponent } from "./repository/vulnerability-scanning/result-bar-chart.component";
import { HistogramChartComponent } from "./repository/vulnerability-scanning/histogram-chart/histogram-chart.component";
import { ResultTipComponent } from "./repository/vulnerability-scanning/result-tip.component";
import { ArtifactListPageComponent } from "./repository/artifact-list-page/artifact-list-page.component";
import { ProjectLabelComponent } from "./project-label/project-label.component";
import { ArtifactListComponent } from "./repository/artifact-list-page/artifact-list/artifact-list.component";
import { ArtifactTagComponent } from "./repository/artifact/artifact-tag/artifact-tag.component";
import { ArtifactCommonPropertiesComponent } from "./repository/artifact/artifact-common-properties/artifact-common-properties.component";
import { ArtifactAdditionsComponent } from "./repository/artifact/artifact-additions/artifact-additions.component";
import { ArtifactSummaryComponent } from "./repository/artifact/artifact-summary.component";
import { ArtifactListTabComponent } from "./repository/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component";
import { BuildHistoryComponent } from "./repository/artifact/artifact-additions/build-history/build-history.component";
import { DependenciesComponent } from "./repository/artifact/artifact-additions/dependencies/dependencies.component";
import { SummaryComponent } from "./repository/artifact/artifact-additions/summary/summary.component";
import { ValuesComponent } from "./repository/artifact/artifact-additions/values/values.component";
import {
ArtifactVulnerabilitiesComponent
} from "./repository/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component";
import { RepositoryDefaultService, RepositoryService } from "./repository/repository.service";
import { ArtifactDefaultService, ArtifactService } from "./repository/artifact/artifact.service";
import { GridViewComponent } from "./repository/gridview/grid-view.component";
@NgModule({
imports: [
SharedModule,
RepositoryModule,
ReplicationModule,
LogModule,
RouterModule,
......@@ -76,9 +91,38 @@ import { ConfigScannerService } from "../config/scanner/config-scanner.service";
AddWebhookComponent,
AddWebhookFormComponent,
ScannerComponent,
RepositoryGridviewComponent,
HistogramChartComponent,
ResultTipHistogramComponent,
ResultBarChartComponent,
ResultGridComponent,
ResultTipComponent,
ArtifactListPageComponent,
ArtifactListComponent,
ArtifactListTabComponent,
ArtifactSummaryComponent,
ArtifactCommonPropertiesComponent,
ArtifactTagComponent,
ArtifactAdditionsComponent,
BuildHistoryComponent,
DependenciesComponent,
SummaryComponent,
ValuesComponent,
ArtifactVulnerabilitiesComponent,
GridViewComponent,
],
exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, MemberService, RobotService, WebhookService, ConfigScannerService]
providers: [
ProjectRoutingResolver,
MemberService,
RobotService,
WebhookService,
ConfigScannerService,
RepositoryDefaultService,
ArtifactDefaultService,
{ provide: RepositoryService, useClass: RepositoryDefaultService },
{ provide: ArtifactService, useClass: ArtifactDefaultService },
]
})
export class ProjectModule {
......
......@@ -3,13 +3,12 @@
<a (click)="goProBack()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="watchGoBackEvt(projectId)">{{'REPOSITORY.REPOSITORIES'| translate}}</a>
<span *ngIf="referArtifactNameArray.length===1">&lt;<a (click)="backInitRepo()">{{repoName}}</a></span>
<span *ngIf="referArtifactNameArray.length>=2" >
<span *ngIf="depth">&lt;<a (click)="backInitRepo()">{{repoName}}</a></span>
<span *ngIf="referArtifactNameArray?.length>=1" >
<span *ngFor="let digest of referArtifactNameArray;let i = index">
&lt;<a (click)="jumpDigest(referArtifactNameArray,i)" >{{digest | slice:0:15}}</a></span>
&lt;<a (click)="jumpDigest(i)" >{{digest | slice:0:15}}</a></span>
</span>
</div>
<artifact-list [repoName]="repoName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole"
[projectId]="projectId" [memberRoleID]="projectMemberRoleId" [isGuest]="isGuest"
(tagClickEvent)="watchTagClickEvt($event)" (backEvt)="watchGoBackEvt($event)" (putArtifactReferenceArr)="putArtifactReferenceArr($event)"></artifact-list>
<artifact-list [repoName]="repoName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole"
[projectId]="projectId" [memberRoleID]="projectMemberRoleId" [isGuest]="isGuest"></artifact-list>
</div>
......@@ -5,14 +5,15 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ClarityModule } from '@clr/angular';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { SessionService } from "../../../shared/session.service";
import { AppConfigService } from "../../../app-config.service";
import { ArtifactService } from "../../../../../ng-swagger-gen/services/artifact.service";
import { ArtifactDefaultService } from "../artifact/artifact.service";
import { IServiceConfig, SERVICE_CONFIG } from "../../../../lib/entities/service.config";
import { AppConfigService } from '../../app-config.service';
import { SessionService } from '../../shared/session.service';
import { ArtifactService } from '../../../lib/services';
describe('ArtifactListPageComponent', () => {
let component: ArtifactListPageComponent;
let fixture: ComponentFixture<ArtifactListPageComponent>;
......@@ -42,7 +43,9 @@ describe('ArtifactListPageComponent', () => {
const mockActivatedRoute = {
RouterparamMap: of({ get: (key) => 'value' }),
snapshot: {
params: { id: 1 },
params: {
id: 1,
},
parent: {
params: { id: 1 },
......@@ -59,7 +62,15 @@ describe('ArtifactListPageComponent', () => {
ismember: true,
role_name: 'master',
}
})
}),
params: {
subscribe: () => {
return of(null);
}
}
};
const config: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories/testing"
};
beforeEach(async(() => {
TestBed.configureTestingModule({
......@@ -71,13 +82,14 @@ describe('ArtifactListPageComponent', () => {
ClarityModule,
TranslateModule.forRoot(),
FormsModule,
RouterTestingModule,
NoopAnimationsModule,
HttpClientTestingModule
],
declarations: [ArtifactListPageComponent],
providers: [
TranslateService,
ArtifactDefaultService,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: SessionService, useValue: mockSessionService },
{ provide: AppConfigService, useValue: mockAppConfigService },
{ provide: Router, useValue: mockRouter },
......
......@@ -13,12 +13,12 @@
// limitations under the License.
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AppConfigService } from '../../app-config.service';
import { SessionService } from '../../shared/session.service';
import { Project } from '../../project/project';
import { ArtifactListComponent } from "../../../lib/components/artifact-list/artifact-list.component";
import { ArtifactClickEvent, ArtifactService } from "../../../lib/services";
import { clone } from '../../../lib/utils/utils';
import { ArtifactListComponent } from "./artifact-list/artifact-list.component";
import { ArtifactDefaultService } from "../artifact/artifact.service";
import { AppConfigService } from "../../../app-config.service";
import { SessionService } from "../../../shared/session.service";
import { ArtifactClickEvent } from "../../../../lib/services";
import { Project } from "../../project";
@Component({
selector: 'artifact-list-page',
......@@ -37,13 +37,20 @@ export class ArtifactListPageComponent implements OnInit {
@ViewChild(ArtifactListComponent, {static: false})
repositoryComponent: ArtifactListComponent;
depth: string;
constructor(
private route: ActivatedRoute,
private router: Router,
private artifactService: ArtifactService,
private artifactService: ArtifactDefaultService,
private appConfigService: AppConfigService,
private session: SessionService) {
this.route.params.subscribe(params => {
this.depth = this.route.snapshot.params['depth'];
if (this.depth) {
const arr: string[] = this.depth.split('-');
this.referArtifactNameArray = arr.slice(0, arr.length - 1);
}
});
}
ngOnInit() {
......@@ -51,9 +58,7 @@ export class ArtifactListPageComponent implements OnInit {
if (!this.projectId) {
this.projectId = this.route.snapshot.parent.params['id'];
}
let resolverData = this.route.snapshot.data;
if (resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
this.isGuest = (<Project>resolverData['projectResolver']).current_user_role_id === 3;
......@@ -61,11 +66,6 @@ export class ArtifactListPageComponent implements OnInit {
}
this.repoName = this.route.snapshot.params['repo'];
this.registryUrl = this.appConfigService.getConfig().registry_url;
let refer = JSON.parse(sessionStorage.getItem('reference'));
if (refer && refer.projectId === this.projectId && refer.repo === this.repoName) {
this.referArtifactNameArray = refer.referArray;
}
}
get withNotary(): boolean {
......@@ -82,17 +82,6 @@ export class ArtifactListPageComponent implements OnInit {
hasChanges(): boolean {
return this.repositoryComponent.hasChanges();
}
watchTagClickEvt(artifactEvt: ArtifactClickEvent): void {
//
sessionStorage.setItem('referenceSummary', JSON.stringify({ projectId: this.projectId, repo: this.repoName,
"digest": artifactEvt.digest, referArray: this.referArtifactNameArray}));
let linkUrl = ['harbor', 'projects', artifactEvt.project_id, 'repositories'
, artifactEvt.repository_name, 'artifacts', artifactEvt.digest];
this.router.navigate(linkUrl);
}
watchGoBackEvt(projectId: string| number): void {
this.router.navigate(["harbor", "projects", projectId, "repositories"]);
}
......@@ -100,25 +89,14 @@ export class ArtifactListPageComponent implements OnInit {
this.router.navigate(["harbor", "projects"]);
}
backInitRepo() {
this.referArtifactNameArray = [];
sessionStorage.removeItem('reference');
this.updateArtifactList('repoName');
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repoName]);
}
jumpDigest(referArtifactNameArray: string[], index: number) {
this.referArtifactNameArray = referArtifactNameArray.slice(index);
this.referArtifactNameArray.pop();
this.referArtifactNameArray = referArtifactNameArray.slice(index);
sessionStorage.setItem('reference', JSON.stringify({ projectId: this.projectId, repo: this.repoName,
referArray: referArtifactNameArray.slice(index)}));
this.updateArtifactList(referArtifactNameArray.slice(index));
}
updateArtifactList(res): void {
this.artifactService.triggerUploadArtifact.next(res);
}
putArtifactReferenceArr(digestArray) {
this.referArtifactNameArray = digestArray;
jumpDigest(index: number) {
const arr: string[] = this.referArtifactNameArray.slice(0, index + 1 );
if ( arr && arr.length) {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repoName, "depth", arr.join('-')]);
} else {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repoName]);
}
}
}
......@@ -67,7 +67,7 @@
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="clrLoad($event)" class="datagrid-top" [class.embeded-datagrid]="isEmbedded"
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="clrDgRefresh($event)" class="datagrid-top" [class.embeded-datagrid]="isEmbedded"
[(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<button [clrLoading]="scanBtnState" type="button" class="btn btn-secondary scan-btn"
......@@ -146,13 +146,13 @@
<img class="artifact-icon"
[src]="artifact.type==='IMAGE'||artifact.type==='CHART'||artifact.type ==='CNAB'?'images/artifact-'+artifact.type.toLowerCase()+'.svg':'images/artifact-default.svg'" />
&nbsp; &nbsp;
<a href="javascript:void(0)" class="max-width-100" (click)="onTagClick(artifact)"
<a href="javascript:void(0)" class="max-width-100" (click)="goIntoArtifactSummaryPage(artifact)"
title="{{artifact.digest}}">
{{ artifact.digest | slice:0:15}}</a>
<clr-tooltip *ngIf="artifact.references">
<clr-tooltip *ngIf="artifact?.references && artifact?.references?.length">
<div clrTooltipTrigger class="level-border">
<div class="inner truncated ">
<a href="javascript:void(0)" (click)="refer(artifact)">
<a href="javascript:void(0)" (click)="goIntoIndexArtifact(artifact)">
<clr-icon class="icon-folder" shape="folder"></clr-icon>
</a>
</div>
......