From 190a5f2ee6b2f072a3442520a4c78223888fb363 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 27 Dec 2019 16:04:04 +0800 Subject: [PATCH 1/8] Remove the manifest list middleware We'll support manifest list, so this commit removes the manifest list middleware who blocks the manifest list pushing Signed-off-by: Wenkai Yin --- src/core/middlewares/chain.go | 22 ++++----- src/core/middlewares/config.go | 23 +++++---- .../middlewares/multiplmanifest/handler.go | 48 ------------------- 3 files changed, 21 insertions(+), 72 deletions(-) delete mode 100644 src/core/middlewares/multiplmanifest/handler.go diff --git a/src/core/middlewares/chain.go b/src/core/middlewares/chain.go index 630a8e8d4..c58920ee6 100644 --- a/src/core/middlewares/chain.go +++ b/src/core/middlewares/chain.go @@ -23,7 +23,6 @@ import ( "github.com/goharbor/harbor/src/core/middlewares/countquota" "github.com/goharbor/harbor/src/core/middlewares/immutable" "github.com/goharbor/harbor/src/core/middlewares/listrepo" - "github.com/goharbor/harbor/src/core/middlewares/multiplmanifest" "github.com/goharbor/harbor/src/core/middlewares/readonly" "github.com/goharbor/harbor/src/core/middlewares/regtoken" "github.com/goharbor/harbor/src/core/middlewares/sizequota" @@ -63,17 +62,16 @@ func (b *DefaultCreator) Create() *alice.Chain { func (b *DefaultCreator) geMiddleware(mName string) alice.Constructor { middlewares := map[string]alice.Constructor{ - CHART: func(next http.Handler) http.Handler { return chart.New(next) }, - READONLY: func(next http.Handler) http.Handler { return readonly.New(next) }, - URL: func(next http.Handler) http.Handler { return url.New(next) }, - MUITIPLEMANIFEST: func(next http.Handler) http.Handler { return multiplmanifest.New(next) }, - LISTREPO: func(next http.Handler) http.Handler { return listrepo.New(next) }, - CONTENTTRUST: func(next http.Handler) http.Handler { return contenttrust.New(next) }, - VULNERABLE: func(next http.Handler) http.Handler { return vulnerable.New(next) }, - SIZEQUOTA: func(next http.Handler) http.Handler { return sizequota.New(next) }, - COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) }, - IMMUTABLE: func(next http.Handler) http.Handler { return immutable.New(next) }, - REGTOKEN: func(next http.Handler) http.Handler { return regtoken.New(next) }, + CHART: func(next http.Handler) http.Handler { return chart.New(next) }, + READONLY: func(next http.Handler) http.Handler { return readonly.New(next) }, + URL: func(next http.Handler) http.Handler { return url.New(next) }, + LISTREPO: func(next http.Handler) http.Handler { return listrepo.New(next) }, + CONTENTTRUST: func(next http.Handler) http.Handler { return contenttrust.New(next) }, + VULNERABLE: func(next http.Handler) http.Handler { return vulnerable.New(next) }, + SIZEQUOTA: func(next http.Handler) http.Handler { return sizequota.New(next) }, + COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) }, + IMMUTABLE: func(next http.Handler) http.Handler { return immutable.New(next) }, + REGTOKEN: func(next http.Handler) http.Handler { return regtoken.New(next) }, } return middlewares[mName] } diff --git a/src/core/middlewares/config.go b/src/core/middlewares/config.go index f8147b49d..49841b44d 100644 --- a/src/core/middlewares/config.go +++ b/src/core/middlewares/config.go @@ -16,24 +16,23 @@ package middlewares // const variables const ( - CHART = "chart" - READONLY = "readonly" - URL = "url" - MUITIPLEMANIFEST = "manifest" - LISTREPO = "listrepo" - CONTENTTRUST = "contenttrust" - VULNERABLE = "vulnerable" - SIZEQUOTA = "sizequota" - COUNTQUOTA = "countquota" - IMMUTABLE = "immutable" - REGTOKEN = "regtoken" + CHART = "chart" + READONLY = "readonly" + URL = "url" + LISTREPO = "listrepo" + CONTENTTRUST = "contenttrust" + VULNERABLE = "vulnerable" + SIZEQUOTA = "sizequota" + COUNTQUOTA = "countquota" + IMMUTABLE = "immutable" + REGTOKEN = "regtoken" ) // ChartMiddlewares middlewares for chart server var ChartMiddlewares = []string{CHART} // Middlewares with sequential organization -var Middlewares = []string{READONLY, URL, REGTOKEN, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA} +var Middlewares = []string{READONLY, URL, REGTOKEN, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA} // MiddlewaresLocal ... var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA} diff --git a/src/core/middlewares/multiplmanifest/handler.go b/src/core/middlewares/multiplmanifest/handler.go deleted file mode 100644 index d0126696c..000000000 --- a/src/core/middlewares/multiplmanifest/handler.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package multiplmanifest - -import ( - "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/core/middlewares/util" - "net/http" - "strings" -) - -type multipleManifestHandler struct { - next http.Handler -} - -// New ... -func New(next http.Handler) http.Handler { - return &multipleManifestHandler{ - next: next, - } -} - -// ServeHTTP The handler is responsible for blocking request to upload manifest list by docker client, which is not supported so far by Harbor. -func (mh multipleManifestHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - match, _, _ := util.MatchPushManifest(req) - if match { - contentType := req.Header.Get("Content-type") - // application/vnd.docker.distribution.manifest.list.v2+json - if strings.Contains(contentType, "manifest.list.v2") { - log.Debugf("Content-type: %s is not supported, failing the response.", contentType) - http.Error(rw, util.MarshalError("UNSUPPORTED_MEDIA_TYPE", "Manifest.list is not supported."), http.StatusUnsupportedMediaType) - return - } - } - mh.next.ServeHTTP(rw, req) -} From de2ec7d06387368c225b0f7675c0aa947738d0fa Mon Sep 17 00:00:00 2001 From: danfengliu Date: Tue, 24 Dec 2019 11:40:10 +0800 Subject: [PATCH 2/8] Replace keyword without waiting to one which had waiting 1. Replace all keywords without using waiting; 2. Add a debug line in Go Into Repo for UI hung trouble shooting. Signed-off-by: danfengliu --- .../Harbor-Pages/Administration-Users.robot | 14 +++++++------- tests/resources/Harbor-Pages/Configuration.robot | 4 ++-- tests/resources/Harbor-Pages/HomePage.robot | 2 +- tests/resources/Harbor-Pages/LDAP-Mode.robot | 4 ++-- .../resources/Harbor-Pages/Project-Config.robot | 2 +- .../resources/Harbor-Pages/Project-Members.robot | 16 ++++++++-------- .../Project-Repository_Elements.robot | 3 +++ tests/resources/Harbor-Pages/Project.robot | 2 +- tests/resources/Harbor-Pages/UserProfile.robot | 2 +- tests/robot-cases/Group1-Nightly/Common.robot | 4 ++-- tests/robot-cases/Group1-Nightly/LDAP.robot | 3 +++ tests/robot-cases/Group1-Nightly/Nightly.robot | 2 +- tests/robot-cases/Group1-Nightly/UAA.robot | 2 +- 13 files changed, 33 insertions(+), 27 deletions(-) diff --git a/tests/resources/Harbor-Pages/Administration-Users.robot b/tests/resources/Harbor-Pages/Administration-Users.robot index cd15d0af5..be3a67571 100644 --- a/tests/resources/Harbor-Pages/Administration-Users.robot +++ b/tests/resources/Harbor-Pages/Administration-Users.robot @@ -21,28 +21,28 @@ Resource ../../resources/Util.robot *** Keywords *** Assign User Admin [Arguments] ${user} - Click Element xpath=//harbor-user//hbr-filter//clr-icon + Retry Element Click xpath=//harbor-user//hbr-filter//clr-icon Input Text xpath=//harbor-user//hbr-filter//input ${user} Sleep 2 #select checkbox - Click Element //clr-dg-row[contains(.,'${user}')]//label + Retry Element Click //clr-dg-row[contains(.,'${user}')]//label #click assign admin - Click Element //*[@id='set-admin'] + Retry Element Click //*[@id='set-admin'] Sleep 1 Switch to User Tag - Click Element xpath=${administration_user_tag_xpath} + Retry Element Click xpath=${administration_user_tag_xpath} Sleep 1 Administration Tag Should Display - Page Should Contain Element xpath=${administration_tag_xpath} + Retry Wait Until Page Contains Element xpath=${administration_tag_xpath} User Email Should Exist [Arguments] ${email} Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD} Switch to User Tag - Page Should Contain Element xpath=//clr-dg-cell[contains(., '${email}')] + Retry Wait Until Page Contains Element xpath=//clr-dg-cell[contains(., '${email}')] Add User Button Should Be Disabled Sleep 1 - Page Should Contain Element //button[contains(.,'New') and @disabled=''] + Retry Wait Until Page Contains Element //button[contains(.,'New') and @disabled=''] diff --git a/tests/resources/Harbor-Pages/Configuration.robot b/tests/resources/Harbor-Pages/Configuration.robot index d619698a1..c59dff789 100644 --- a/tests/resources/Harbor-Pages/Configuration.robot +++ b/tests/resources/Harbor-Pages/Configuration.robot @@ -140,10 +140,10 @@ Self Reg Should Be Enabled Checkbox Should Be Selected xpath=${self_reg_xpath} Project Creation Should Display - Page Should Contain Element xpath=${project_create_xpath} + Retry Wait Until Page Contains Element xpath=${project_create_xpath} Project Creation Should Not Display - Page Should Not Contain Element xpath=${project_create_xpath} + Retry Wait Until Page Not Contains Element xpath=${project_create_xpath} ## System settings Switch To System Settings diff --git a/tests/resources/Harbor-Pages/HomePage.robot b/tests/resources/Harbor-Pages/HomePage.robot index c5f8ad83c..0486efdc1 100644 --- a/tests/resources/Harbor-Pages/HomePage.robot +++ b/tests/resources/Harbor-Pages/HomePage.robot @@ -37,7 +37,7 @@ Capture Screenshot And Source Log Source Sign Up Should Not Display - Page Should Not Contain Element xpath=${sign_up_button_xpath} + Retry Wait Until Page Not Contains Element xpath=${sign_up_button_xpath} Create An New User [Arguments] ${url} ${username} ${email} ${realname} ${newPassword} ${comment} diff --git a/tests/resources/Harbor-Pages/LDAP-Mode.robot b/tests/resources/Harbor-Pages/LDAP-Mode.robot index d14b53430..fa0cf4c43 100644 --- a/tests/resources/Harbor-Pages/LDAP-Mode.robot +++ b/tests/resources/Harbor-Pages/LDAP-Mode.robot @@ -21,8 +21,8 @@ Resource ../../resources/Util.robot *** Keywords *** Ldap User Should Not See Change Password - Click Element //clr-header//clr-dropdown[2]//button - Sleep 1 + Retry Element Click //clr-header//clr-dropdown[2]//button + Sleep 2 Page Should Not Contain Password diff --git a/tests/resources/Harbor-Pages/Project-Config.robot b/tests/resources/Harbor-Pages/Project-Config.robot index 96eb835a9..c0e6c4871 100644 --- a/tests/resources/Harbor-Pages/Project-Config.robot +++ b/tests/resources/Harbor-Pages/Project-Config.robot @@ -42,7 +42,7 @@ Public Should Be Selected Project Should Be Public [Arguments] ${projectName} - Page Should Contain Element //clr-dg-row[contains(.,'${projectName}')]//clr-dg-cell[contains(.,'Public')] + Retry Wait Until Page Contains Element //clr-dg-row[contains(.,'${projectName}')]//clr-dg-cell[contains(.,'Public')] Content Trust Should Be Selected Checkbox Should Be Selected //hbr-project-policy-config//input[@name='content-trust'] diff --git a/tests/resources/Harbor-Pages/Project-Members.robot b/tests/resources/Harbor-Pages/Project-Members.robot index 7d8ef63cb..e506e047e 100644 --- a/tests/resources/Harbor-Pages/Project-Members.robot +++ b/tests/resources/Harbor-Pages/Project-Members.robot @@ -70,13 +70,13 @@ User Can Change Role [arguments] ${username} Retry Element Click xpath=//clr-dg-row[contains(.,'${username}')]//input/../label Retry Element Click xpath=//*[@id='member-action'] - Page Should Not Contain Element xpath=//button[@disabled='' and contains(.,'Admin')] + Retry Wait Until Page Not Contains Element xpath=//button[@disabled='' and contains(.,'Admin')] User Can Not Change Role [arguments] ${username} Retry Element Click xpath=//clr-dg-row[contains(.,'${username}')]//input/../label Retry Element Click xpath=//*[@id='member-action'] - Page Should Contain Element xpath=//button[@disabled='' and contains(.,'Admin')] + Retry Wait Until Page Contains Element xpath=//button[@disabled='' and contains(.,'Admin')] #this keyworkd seems will not use any more, will delete in the future Non-admin View Member Account @@ -84,7 +84,7 @@ Non-admin View Member Account Xpath Should Match X Times //clr-dg-row-master ${times} User Can Not Add Member - Page Should Contain Element xpath=//button[@disabled='' and contains(.,'User')] + Retry Wait Until Page Contains Element xpath=//button[@disabled='' and contains(.,'User')] Add Guest Member To Project [arguments] ${member} @@ -153,7 +153,7 @@ User Should Be Guest Go Into Project ${project} Switch To Member User Can Not Add Member - Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Guest')] + Retry Wait Until Page Contains Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Guest')] Logout Harbor Pull image ${ip} ${user} ${password} ${project} hello-world Cannot Push image ${ip} ${user} ${password} ${project} hello-world @@ -168,7 +168,7 @@ User Should Be Developer Go Into Project ${project} Switch To Member User Can Not Add Member - Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Developer')] + Retry Wait Until Page Contains Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Developer')] Logout Harbor Push Image With Tag ${ip} ${user} ${password} ${project} hello-world v1 @@ -183,7 +183,7 @@ User Should Be Admin Switch To Member Add Guest Member To Project ${guest} User Can Change Role ${guest} - Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Admin')] + Retry Wait Until Page Contains Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Admin')] Logout Harbor Push Image With Tag ${ip} ${user} ${password} ${project} hello-world v2 @@ -197,7 +197,7 @@ User Should Be Master Go Into Project ${project} Delete Repo ${project} Switch To Member - Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Master')] + Retry Wait Until Page Contains Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Master')] Logout Harbor Push Image With Tag ${ip} ${user} ${password} ${project} hello-world v3 @@ -206,5 +206,5 @@ Project Should Have Member Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD} Go Into Project ${project} Switch To Member - Page Should Contain Element xpath=//clr-dg-cell[contains(., '${user}')] + Retry Wait Until Page Contains Element xpath=//clr-dg-cell[contains(., '${user}')] Logout Harbor diff --git a/tests/resources/Harbor-Pages/Project-Repository_Elements.robot b/tests/resources/Harbor-Pages/Project-Repository_Elements.robot index 52ab139d9..053133296 100644 --- a/tests/resources/Harbor-Pages/Project-Repository_Elements.robot +++ b/tests/resources/Harbor-Pages/Project-Repository_Elements.robot @@ -21,3 +21,6 @@ ${first_cve_xpath} //clr-dg-row[1]//clr-dg-cell//a ${view_log_xpath} //clr-dg-row[1]//clr-dg-cell[4]//a ${build_history_btn} //button[contains(.,'Build History')] ${build_history_data} //clr-dg-row +${push_image_command_btn} //hbr-push-image-button//button + + diff --git a/tests/resources/Harbor-Pages/Project.robot b/tests/resources/Harbor-Pages/Project.robot index 047e175cd..d09f3dafb 100644 --- a/tests/resources/Harbor-Pages/Project.robot +++ b/tests/resources/Harbor-Pages/Project.robot @@ -27,7 +27,7 @@ Create An New Project Capture Page Screenshot Retry Text Input xpath=${project_name_xpath} ${projectname} ${element_project_public}= Set Variable xpath=${project_public_xpath} - Run Keyword If '${public}' == 'true' Run Keywords Wait Until Element Is Visible And Enabled ${element_project_public} AND Click Element ${element_project_public} + Run Keyword If '${public}' == 'true' Run Keywords Wait Until Element Is Visible And Enabled ${element_project_public} AND Retry Element Click ${element_project_public} Run Keyword If '${count_quota}'!='${null}' Input Count Quota ${count_quota} Run Keyword If '${storage_quota}'!='${null}' Input Storage Quota ${storage_quota} ${storage_quota_unit} Capture Page Screenshot diff --git a/tests/resources/Harbor-Pages/UserProfile.robot b/tests/resources/Harbor-Pages/UserProfile.robot index fc7b3fca0..8525fcca7 100644 --- a/tests/resources/Harbor-Pages/UserProfile.robot +++ b/tests/resources/Harbor-Pages/UserProfile.robot @@ -43,4 +43,4 @@ Logout Harbor Retry Link Click Log Out Capture Page Screenshot Logout.png Sleep 2 - Wait Until Keyword Succeeds 5x 1 Page Should Contain Element ${sign_in_title_xpath} \ No newline at end of file + Wait Until Keyword Succeeds 5x 1 Retry Wait Until Page Contains Element ${sign_in_title_xpath} \ No newline at end of file diff --git a/tests/robot-cases/Group1-Nightly/Common.robot b/tests/robot-cases/Group1-Nightly/Common.robot index 350ed23a4..e7d993046 100644 --- a/tests/robot-cases/Group1-Nightly/Common.robot +++ b/tests/robot-cases/Group1-Nightly/Common.robot @@ -406,7 +406,7 @@ Test Case - Developer Operate Labels Sign In Harbor ${HARBOR_URL} user022 Test1@34 Go Into Project project${d} has_image=${false} Sleep 3 - Page Should Not Contain Element xpath=//a[contains(.,'Labels')] + Retry Wait Until Page Not Contains Element xpath=//a[contains(.,'Labels')] Close Browser Test Case - Retag A Image Tag @@ -431,7 +431,7 @@ Test Case - Retag A Image Tag Page Should Contain ${target_image_name} Go Into Repo project${random_num1}${random_num2}/${target_image_name} Sleep 1 - Page Should Contain Element xpath=${tag_value_xpath} + Retry Wait Until Page Contains Element xpath=${tag_value_xpath} Close Browser Test Case - Create An New Project With Quotas Set diff --git a/tests/robot-cases/Group1-Nightly/LDAP.robot b/tests/robot-cases/Group1-Nightly/LDAP.robot index c077fca48..307565a6d 100644 --- a/tests/robot-cases/Group1-Nightly/LDAP.robot +++ b/tests/robot-cases/Group1-Nightly/LDAP.robot @@ -44,6 +44,7 @@ Test Case - System Admin On-board New Member ${d}= Get Current Date result_format=%m%s Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} Switch To User Tag + Sleep 2 Page Should Not Contain mike02 Navigate To Projects Create An New Project project${d} @@ -60,8 +61,10 @@ Test Case - LDAP User On-borad New Member Create An New Project project${d} Go Into Project project${d} has_image=${false} Switch To Member + Sleep 2 Page Should Not Contain mike04 Add Guest Member To Project mike04 + Sleep 2 Page Should Contain mike04 Close Browser diff --git a/tests/robot-cases/Group1-Nightly/Nightly.robot b/tests/robot-cases/Group1-Nightly/Nightly.robot index 8648101a7..a4bb5ca3b 100644 --- a/tests/robot-cases/Group1-Nightly/Nightly.robot +++ b/tests/robot-cases/Group1-Nightly/Nightly.robot @@ -393,7 +393,7 @@ TestCase - Developer Operate Labels Sign In Harbor ${HARBOR_URL} bob${d} Test1@34 Go Into Project project${d} has_image=${false} Sleep 3 - Page Should Not Contain Element xpath=//a[contains(.,'Labels')] + Retry Wait Until Page Not Contains Element xpath=//a[contains(.,'Labels')] Close Browser Test Case - Scan A Tag In The Repo diff --git a/tests/robot-cases/Group1-Nightly/UAA.robot b/tests/robot-cases/Group1-Nightly/UAA.robot index 1d5ba63e3..11c38d901 100644 --- a/tests/robot-cases/Group1-Nightly/UAA.robot +++ b/tests/robot-cases/Group1-Nightly/UAA.robot @@ -36,7 +36,7 @@ Test Case - Home Page Differences With DB Mode Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} Logout Harbor Sleep 2 - Page Should Not Contain Sign up + Page Should Not Contain Sign up Page Should Not Contain Forgot password Close Browser From 9dbb04925cc69f73da70cd5f97e89a386b11f250 Mon Sep 17 00:00:00 2001 From: "Danfeng Liu (c)" Date: Mon, 30 Dec 2019 14:11:19 +0800 Subject: [PATCH 3/8] Refresh project quotas list at regular time Signed-off-by: Danfeng Liu (c) --- tests/resources/Harbor-Pages/Configuration.robot | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/resources/Harbor-Pages/Configuration.robot b/tests/resources/Harbor-Pages/Configuration.robot index d619698a1..3d23ab497 100644 --- a/tests/resources/Harbor-Pages/Configuration.robot +++ b/tests/resources/Harbor-Pages/Configuration.robot @@ -153,6 +153,8 @@ Switch To System Settings Sleep 1 Switch To Project Quotas + Sleep 1 + Retry Element Click xpath=${configuration_xpath} Sleep 1 Retry Element Click xpath=//clr-main-container//clr-vertical-nav//a[contains(.,'Project Quotas')] Sleep 1 From a8521fb62f941ced386ad6bbc5a456dc6396ebb1 Mon Sep 17 00:00:00 2001 From: AllForNothing Date: Mon, 30 Dec 2019 14:59:17 +0800 Subject: [PATCH 4/8] Add more UT for project-quota-component Signed-off-by: AllForNothing --- .../harbor-shell/harbor-shell.component.html | 4 +- .../edit-project-quotas.component.html | 4 +- .../edit-project-quotas.component.spec.ts | 34 ++++++-- .../project-quotas.component.html | 2 +- .../project-quotas.component.spec.ts | 82 ++++++++++++++++--- 5 files changed, 101 insertions(+), 25 deletions(-) diff --git a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html index bde51dffb..33b77e6a6 100644 --- a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html +++ b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html @@ -62,7 +62,7 @@ {{'CONFIG.LABEL' | translate }} - + {{'CONFIG.PROJECT_QUOTAS' | translate }} @@ -95,4 +95,4 @@ - \ No newline at end of file + diff --git a/src/portal/src/lib/components/config/project-quotas/edit-project-quotas/edit-project-quotas.component.html b/src/portal/src/lib/components/config/project-quotas/edit-project-quotas/edit-project-quotas.component.html index 3b41f4dc9..ccc679818 100644 --- a/src/portal/src/lib/components/config/project-quotas/edit-project-quotas/edit-project-quotas.component.html +++ b/src/portal/src/lib/components/config/project-quotas/edit-project-quotas/edit-project-quotas.component.html @@ -75,7 +75,7 @@ - \ No newline at end of file + diff --git a/src/portal/src/lib/components/config/project-quotas/edit-project-quotas/edit-project-quotas.component.spec.ts b/src/portal/src/lib/components/config/project-quotas/edit-project-quotas/edit-project-quotas.component.spec.ts index 0a6a1e725..2cfb9c0b4 100644 --- a/src/portal/src/lib/components/config/project-quotas/edit-project-quotas/edit-project-quotas.component.spec.ts +++ b/src/portal/src/lib/components/config/project-quotas/edit-project-quotas/edit-project-quotas.component.spec.ts @@ -1,10 +1,9 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { EditProjectQuotasComponent } from './edit-project-quotas.component'; -import { SharedModule } from '../../../../utils/shared/shared.module'; -import { InlineAlertComponent } from '../../../inline-alert/inline-alert.component'; import { SERVICE_CONFIG, IServiceConfig } from '../../../../entities/service.config'; -import { RouterModule } from '@angular/router'; +import { EditQuotaQuotaInterface } from '../../../../services'; +import { HarborLibraryModule } from '../../../../harbor-library.module'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; describe('EditProjectQuotasComponent', () => { let component: EditProjectQuotasComponent; @@ -12,13 +11,20 @@ describe('EditProjectQuotasComponent', () => { let config: IServiceConfig = { quotaUrl: "/api/quotas/testing" }; + const mockedEditQuota: EditQuotaQuotaInterface = { + editQuota: "Edit Default Project Quotas", + setQuota: "Set the default project quotas when creating new projects", + countQuota: "Default artifact count", + storageQuota: "Default storage consumption", + quotaHardLimitValue: {storageLimit: -1, storageUnit: "Byte", countLimit: -1}, + isSystemDefaultQuota: true + }; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - SharedModule, - RouterModule.forRoot([]) + HarborLibraryModule, + BrowserAnimationsModule ], - declarations: [ EditProjectQuotasComponent, InlineAlertComponent ], providers: [ { provide: SERVICE_CONFIG, useValue: config }, ] @@ -34,4 +40,18 @@ describe('EditProjectQuotasComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + it('should open', async () => { + component.openEditQuota = true; + fixture.detectChanges(); + await fixture.whenStable(); + component.openEditQuotaModal(mockedEditQuota); + fixture.detectChanges(); + await fixture.whenStable(); + let countInput: HTMLInputElement = fixture.nativeElement.querySelector('#count'); + countInput.value = "100"; + countInput.dispatchEvent(new Event("input")); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component.isValid).toBeTruthy(); + }); }); diff --git a/src/portal/src/lib/components/config/project-quotas/project-quotas.component.html b/src/portal/src/lib/components/config/project-quotas/project-quotas.component.html index 64b175c93..aa036572f 100644 --- a/src/portal/src/lib/components/config/project-quotas/project-quotas.component.html +++ b/src/portal/src/lib/components/config/project-quotas/project-quotas.component.html @@ -6,7 +6,7 @@
{{'QUOTA.PROJECT_QUOTA_DEFAULT_ARTIFACT' | translate}} {{ quotaHardLimitValue?.countLimit === -1 ? ('QUOTA.UNLIMITED'| translate) : quotaHardLimitValue?.countLimit }} -
diff --git a/src/portal/src/lib/components/config/project-quotas/project-quotas.component.spec.ts b/src/portal/src/lib/components/config/project-quotas/project-quotas.component.spec.ts index f70897232..51a3e2665 100644 --- a/src/portal/src/lib/components/config/project-quotas/project-quotas.component.spec.ts +++ b/src/portal/src/lib/components/config/project-quotas/project-quotas.component.spec.ts @@ -1,11 +1,7 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { async, ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; import { ProjectQuotasComponent } from './project-quotas.component'; import { IServiceConfig, SERVICE_CONFIG } from '../../../entities/service.config'; -import { SharedModule } from '../../../utils/shared/shared.module'; -import { RouterModule } from '@angular/router'; -import { EditProjectQuotasComponent } from './edit-project-quotas/edit-project-quotas.component'; -import { InlineAlertComponent } from '../../inline-alert/inline-alert.component'; +import { Router } from '@angular/router'; import { ConfigurationService, ConfigurationDefaultService, QuotaService , QuotaDefaultService, Quota, RequestQueryParams @@ -14,8 +10,12 @@ import { ErrorHandler } from '../../../utils/error-handler'; import { of } from 'rxjs'; import { delay } from 'rxjs/operators'; import {APP_BASE_HREF} from '@angular/common'; +import { HarborLibraryModule } from '../../../harbor-library.module'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; describe('ProjectQuotasComponent', () => { let spy: jasmine.Spy; + let spyUpdate: jasmine.Spy; + let spyRoute: jasmine.Spy; let quotaService: QuotaService; let component: ProjectQuotasComponent; @@ -43,20 +43,35 @@ describe('ProjectQuotasComponent', () => { }, } ]; + const fakedRouter = { + navigate() { + return undefined; + } + }; + const fakedErrorHandler = { + error() { + return undefined; + }, + info() { + return undefined; + } + }; + const timeout = (ms: number) => { + return new Promise(resolve => setTimeout(resolve, ms)); + }; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - SharedModule, - RouterModule.forRoot([]) + HarborLibraryModule, + BrowserAnimationsModule ], - declarations: [ProjectQuotasComponent, EditProjectQuotasComponent, InlineAlertComponent], providers: [ - ErrorHandler, + { provide: ErrorHandler, useValue: fakedErrorHandler }, { provide: SERVICE_CONFIG, useValue: config }, { provide: ConfigurationService, useClass: ConfigurationDefaultService }, { provide: QuotaService, useClass: QuotaDefaultService }, - { provide: APP_BASE_HREF, useValue : '/' } - + { provide: APP_BASE_HREF, useValue : '/' }, + { provide: Router, useValue: fakedRouter } ] }) .compileComponents(); @@ -83,11 +98,52 @@ describe('ProjectQuotasComponent', () => { }; return of(httpRes).pipe(delay(0)); }); - + spyUpdate = spyOn(quotaService, 'updateQuota').and.returnValue(of(null)); + spyRoute = spyOn(fixture.debugElement.injector.get(Router), 'navigate').and.returnValue(of(null)); fixture.detectChanges(); })); it('should create', () => { expect(component).toBeTruthy(); }); + it('should open edit quota modal', async () => { + // wait getting list and rendering + await timeout(10); + fixture.detectChanges(); + await fixture.whenStable(); + const openEditButton: HTMLButtonElement = fixture.nativeElement.querySelector("#open-edit"); + openEditButton.dispatchEvent(new Event("click")); + fixture.detectChanges(); + await fixture.whenStable(); + const modal: HTMLElement = fixture.nativeElement.querySelector("clr-modal"); + expect(modal).toBeTruthy(); + }); + it('edit quota', async () => { + // wait getting list and rendering + await timeout(10); + fixture.detectChanges(); + await fixture.whenStable(); + component.editQuota(component.quotaList[0]); + fixture.detectChanges(); + await fixture.whenStable(); + const countInput: HTMLInputElement = fixture.nativeElement.querySelector('#count'); + countInput.value = "100"; + countInput.dispatchEvent(new Event("input")); + fixture.detectChanges(); + await fixture.whenStable(); + const saveButton: HTMLInputElement = fixture.nativeElement.querySelector('#edit-quota-save'); + saveButton.dispatchEvent(new Event("click")); + fixture.detectChanges(); + await fixture.whenStable(); + expect(spyUpdate.calls.count()).toEqual(1); + }); + it('should call navigate function', async () => { + // wait getting list and rendering + await timeout(10); + fixture.detectChanges(); + await fixture.whenStable(); + const a: HTMLElement = fixture.nativeElement.querySelector('clr-dg-cell a'); + a.dispatchEvent(new Event("click")); + expect(spyRoute.calls.count()).toEqual(1); + }); }); From a087ba02e37eb29c88ce3c3e05a27908ebe8e4b0 Mon Sep 17 00:00:00 2001 From: Daniel Jiang Date: Wed, 25 Dec 2019 14:59:47 +0800 Subject: [PATCH 5/8] Populate basic auth information for registry This commit updates `prepare` and templates to populate the credential for registry for basic authentication. A temporary flag `registry_use_basic_auth` was added to avoid breakage. It MUST be removed before the release. Signed-off-by: Daniel Jiang --- make/harbor.yml.tmpl | 3 +++ make/photon/prepare/Dockerfile.base | 7 ++++--- make/photon/prepare/main.py | 2 +- make/photon/prepare/templates/core/env.jinja | 2 ++ .../templates/registry/config.yml.jinja | 6 ++++++ make/photon/prepare/utils/cert.py | 19 +++--------------- make/photon/prepare/utils/configs.py | 10 ++++++++++ make/photon/prepare/utils/misc.py | 20 ++++++++++++++++--- make/photon/prepare/utils/registry.py | 11 +++++++++- src/core/config/config.go | 5 +++++ 10 files changed, 61 insertions(+), 24 deletions(-) diff --git a/make/harbor.yml.tmpl b/make/harbor.yml.tmpl index b5df0bac9..2d6655c65 100644 --- a/make/harbor.yml.tmpl +++ b/make/harbor.yml.tmpl @@ -26,6 +26,9 @@ https: # Remember Change the admin password from UI after launching Harbor. harbor_admin_password: Harbor12345 +#TODO: remove this temporary flag before ships v1.11/v2, this should always be true +registry_use_basic_auth: false + # Harbor DB configuration database: # The password for the root user of Harbor DB. Change this before any production use. diff --git a/make/photon/prepare/Dockerfile.base b/make/photon/prepare/Dockerfile.base index 7ebe37c73..5c6840768 100644 --- a/make/photon/prepare/Dockerfile.base +++ b/make/photon/prepare/Dockerfile.base @@ -1,5 +1,6 @@ FROM photon:2.0 -RUN tdnf install -y python3 \ - && tdnf install -y python3-pip -RUN pip3 install pipenv==2018.11.26 \ No newline at end of file +RUN tdnf install -y python3 python3-pip httpd +RUN pip3 install pipenv==2018.11.26 + + diff --git a/make/photon/prepare/main.py b/make/photon/prepare/main.py index 626511fca..2439e6f33 100644 --- a/make/photon/prepare/main.py +++ b/make/photon/prepare/main.py @@ -35,7 +35,7 @@ def main(conf, with_notary, with_clair, with_chartmuseum): try: validate(config_dict, notary_mode=with_notary) except Exception as e: - logging.info('Error happend in config validation...') + logging.info('Error happened in config validation...') logging.error(e) sys.exit(-1) diff --git a/make/photon/prepare/templates/core/env.jinja b/make/photon/prepare/templates/core/env.jinja index c056a3f2b..c965a9dbb 100644 --- a/make/photon/prepare/templates/core/env.jinja +++ b/make/photon/prepare/templates/core/env.jinja @@ -44,6 +44,8 @@ RELOAD_KEY={{reload_key}} CHART_REPOSITORY_URL={{chart_repository_url}} REGISTRY_CONTROLLER_URL={{registry_controller_url}} WITH_CHARTMUSEUM={{with_chartmuseum}} +REGISTRY_CREDENTIAL_USERNAME={{registry_username}} +REGISTRY_CREDENTIAL_PASSWORD={{registry_password}} HTTP_PROXY={{core_http_proxy}} HTTPS_PROXY={{core_https_proxy}} diff --git a/make/photon/prepare/templates/registry/config.yml.jinja b/make/photon/prepare/templates/registry/config.yml.jinja index 54592063c..e99a27b5c 100644 --- a/make/photon/prepare/templates/registry/config.yml.jinja +++ b/make/photon/prepare/templates/registry/config.yml.jinja @@ -26,11 +26,17 @@ http: debug: addr: localhost:5001 auth: +{% if registry_use_basic_auth %} + htpasswd: + realm: harbor-registry-basic-realm + path: /etc/registry/passwd +{% else %} token: issuer: harbor-token-issuer realm: {{public_url}}/service/token rootcertbundle: /etc/registry/root.crt service: harbor-registry +{% endif %} validation: disabled: true notifications: diff --git a/make/photon/prepare/utils/cert.py b/make/photon/prepare/utils/cert.py index 3ba42861a..d7ed07c7c 100644 --- a/make/photon/prepare/utils/cert.py +++ b/make/photon/prepare/utils/cert.py @@ -1,14 +1,14 @@ # Get or generate private key -import os, sys, subprocess, shutil +import os, subprocess, shutil from pathlib import Path from subprocess import DEVNULL -from functools import wraps from g import DEFAULT_GID, DEFAULT_UID from .misc import ( mark_file, generate_random_string, - check_permission) + check_permission, + stat_decorator) SSL_CERT_PATH = os.path.join("/etc/cert", "server.crt") SSL_CERT_KEY_PATH = os.path.join("/etc/cert", "server.key") @@ -44,19 +44,6 @@ def get_alias(path): alias = _get_secret(path, "defaultalias", length=8) return alias -## decorator actions -def stat_decorator(func): - @wraps(func) - def check_wrapper(*args, **kw): - stat = func(*args, **kw) - if stat == 0: - print("Generated certificate, key file: {key_path}, cert file: {cert_path}".format(**kw)) - else: - print("Fail to generate key file: {key_path}, cert file: {cert_path}".format(**kw)) - sys.exit(1) - return check_wrapper - - @stat_decorator def create_root_cert(subj, key_path="./k.key", cert_path="./cert.crt"): rc = subprocess.call(["/usr/bin/openssl", "genrsa", "-out", key_path, "4096"], stdout=DEVNULL, stderr=subprocess.STDOUT) diff --git a/make/photon/prepare/utils/configs.py b/make/photon/prepare/utils/configs.py index d50132d19..e6e684da6 100644 --- a/make/photon/prepare/utils/configs.py +++ b/make/photon/prepare/utils/configs.py @@ -9,6 +9,8 @@ default_db_max_open_conns = 0 # NOTE: https://golang.org/pkg/database/sql/#DB.S default_https_cert_path = '/your/certificate/path' default_https_key_path = '/your/certificate/path' +REGISTRY_USER_NAME = 'harbor_registry_user' + def validate(conf: dict, **kwargs): # hostname validate @@ -83,6 +85,7 @@ def validate(conf: dict, **kwargs): # TODO: # If user enable trust cert dir, need check if the files in this dir is readable. + def parse_versions(): if not versions_file_path.is_file(): return {} @@ -90,6 +93,7 @@ def parse_versions(): versions = yaml.load(f) return versions + def parse_yaml_config(config_file_path, with_notary, with_clair, with_chartmuseum): ''' :param configs: config_parser object @@ -321,6 +325,12 @@ def parse_yaml_config(config_file_path, with_notary, with_clair, with_chartmuseu # UAA configs config_dict['uaa'] = configs.get('uaa') or {} + config_dict['registry_username'] = REGISTRY_USER_NAME + config_dict['registry_password'] = generate_random_string(32) + + # TODO: remove the flag before release + config_dict['registry_use_basic_auth'] = configs['registry_use_basic_auth'] + return config_dict diff --git a/make/photon/prepare/utils/misc.py b/make/photon/prepare/utils/misc.py index 0b4d0e66d..6280450ee 100644 --- a/make/photon/prepare/utils/misc.py +++ b/make/photon/prepare/utils/misc.py @@ -1,11 +1,10 @@ -import os -import string +import os, string, sys import secrets from pathlib import Path +from functools import wraps from g import DEFAULT_UID, DEFAULT_GID - # To meet security requirement # By default it will change file mode to 0600, and make the owner of the file to 10000:10000 def mark_file(path, mode=0o600, uid=DEFAULT_UID, gid=DEFAULT_GID): @@ -154,3 +153,18 @@ def other_can_read(st_mode: int) -> bool: Check if other user have the read permission of this st_mode """ return True if st_mode & 0o004 else False + + +# decorator actions +def stat_decorator(func): + @wraps(func) + def check_wrapper(*args, **kw): + stat = func(*args, **kw) + if stat == 0: + print("Successfully called func: %s" % func.__name__) + else: + print("Failed to call func: %s" % func.__name__) + sys.exit(1) + return check_wrapper + + diff --git a/make/photon/prepare/utils/registry.py b/make/photon/prepare/utils/registry.py index 564a39111..932ef563b 100644 --- a/make/photon/prepare/utils/registry.py +++ b/make/photon/prepare/utils/registry.py @@ -1,4 +1,4 @@ -import os, copy +import os, copy, subprocess from g import config_dir, templates_dir, DEFAULT_GID, DEFAULT_UID, data_dir from utils.misc import prepare_dir @@ -8,6 +8,7 @@ from utils.jinja import render_jinja registry_config_dir = os.path.join(config_dir, "registry") registry_config_template_path = os.path.join(templates_dir, "registry", "config.yml.jinja") registry_conf = os.path.join(config_dir, "registry", "config.yml") +registry_passwd_path = os.path.join(config_dir, "registry", "passwd") registry_data_dir = os.path.join(data_dir, 'registry') levels_map = { @@ -18,10 +19,13 @@ levels_map = { 'fatal': 'fatal' } + def prepare_registry(config_dict): prepare_dir(registry_data_dir, uid=DEFAULT_UID, gid=DEFAULT_GID) prepare_dir(registry_config_dir) + if config_dict['registry_use_basic_auth']: + gen_passwd_file(config_dict) storage_provider_info = get_storage_provider_info( config_dict['storage_provider_name'], config_dict['storage_provider_config']) @@ -55,3 +59,8 @@ def get_storage_provider_info(provider_name, provider_config): storage_provider_conf_list.append('{}: {}'.format(config[0], value)) storage_provider_info = ('\n' + ' ' * 4).join(storage_provider_conf_list) return storage_provider_info + + +def gen_passwd_file(config_dict): + return subprocess.call(["/usr/bin/htpasswd", "-bcB", registry_passwd_path, config_dict['registry_username'], + config_dict['registry_password']], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) diff --git a/src/core/config/config.go b/src/core/config/config.go index b95b84f73..2b5f945c3 100755 --- a/src/core/config/config.go +++ b/src/core/config/config.go @@ -299,6 +299,11 @@ func CoreSecret() string { return os.Getenv("CORE_SECRET") } +// RegistryCredential returns the username and password the core uses to access registry +func RegistryCredential() (string, string) { + return os.Getenv("REGISTRY_CREDENTIAL_USERNAME"), os.Getenv("REGISTRY_CREDENTIAL_PASSWORD") +} + // JobserviceSecret returns a secret to mark Jobservice when communicate with // other component // TODO replace it with method of SecretStore From 0c776076bd1b4d393d27827eb878ee845c61b63b Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Tue, 31 Dec 2019 17:11:34 +0800 Subject: [PATCH 6/8] Remove the workaround for blocking manifest list in replication Remove the workaround for blocking manifest list in replication Signed-off-by: Wenkai Yin --- src/replication/transfer/image/transfer.go | 44 +--------------------- 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/src/replication/transfer/image/transfer.go b/src/replication/transfer/image/transfer.go index 8a31b0ed0..497052d48 100644 --- a/src/replication/transfer/image/transfer.go +++ b/src/replication/transfer/image/transfer.go @@ -16,7 +16,6 @@ package image import ( "errors" - "fmt" "strings" "github.com/docker/distribution/manifest/manifestlist" @@ -259,6 +258,7 @@ func (t *transfer) pullManifest(repository, reference string) ( return nil, "", nil } t.logger.Infof("pulling the manifest of image %s:%s ...", repository, reference) + // TODO add OCI media types manifest, digest, err := t.src.PullManifest(repository, reference, []string{ schema1.MediaTypeManifest, schema1.MediaTypeSignedManifest, @@ -271,47 +271,7 @@ func (t *transfer) pullManifest(repository, reference string) ( } t.logger.Infof("the manifest of image %s:%s pulled", repository, reference) - // this is a solution to work around that harbor doesn't support manifest list - return t.handleManifest(manifest, repository, digest) -} - -// if the media type of the specified manifest is manifest list, just abstract one -// manifest from the list and return it -func (t *transfer) handleManifest(manifest distribution.Manifest, repository, digest string) ( - distribution.Manifest, string, error) { - mediaType, _, err := manifest.Payload() - if err != nil { - t.logger.Errorf("failed to call the payload method for manifest of %s:%s: %v", repository, digest, err) - return nil, "", err - } - // manifest - if mediaType == schema1.MediaTypeManifest || - mediaType == schema1.MediaTypeSignedManifest || - mediaType == schema2.MediaTypeManifest { - return manifest, digest, nil - } - // manifest list - t.logger.Info("trying abstract a manifest from the manifest list...") - manifestlist, ok := manifest.(*manifestlist.DeserializedManifestList) - if !ok { - err := fmt.Errorf("the object isn't a DeserializedManifestList") - t.logger.Errorf(err.Error()) - return nil, "", err - } - digest = "" - for _, reference := range manifestlist.Manifests { - if strings.ToLower(reference.Platform.Architecture) == "amd64" && - strings.ToLower(reference.Platform.OS) == "linux" { - digest = reference.Digest.String() - t.logger.Infof("a manifest(architecture: amd64, os: linux) found, using this one: %s", digest) - break - } - } - if len(digest) == 0 { - digest = manifest.References()[0].Digest.String() - t.logger.Infof("no manifest(architecture: amd64, os: linux) found, using the first one: %s", digest) - } - return t.pullManifest(repository, digest) + return manifest, digest, nil } func (t *transfer) exist(repository, tag string) (bool, string, error) { From 7a4cb174509acbad7bec94fc224fbea8a344ec84 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Tue, 31 Dec 2019 18:30:52 +0800 Subject: [PATCH 7/8] feat(orm): add orm support with context (#10337) 1. Get and set orm from context. 2. Add WithTransaction decorator make func run in transaction. 3. Support nested transaction by Savepoint. Signed-off-by: He Weiwei --- src/core/filter/orm.go | 30 ++++ src/core/main.go | 1 + src/internal/orm/orm.go | 68 ++++++++ src/internal/orm/orm_test.go | 315 +++++++++++++++++++++++++++++++++++ src/internal/orm/tx.go | 77 +++++++++ 5 files changed, 491 insertions(+) create mode 100644 src/core/filter/orm.go create mode 100644 src/internal/orm/orm.go create mode 100644 src/internal/orm/orm_test.go create mode 100644 src/internal/orm/tx.go diff --git a/src/core/filter/orm.go b/src/core/filter/orm.go new file mode 100644 index 000000000..1310db398 --- /dev/null +++ b/src/core/filter/orm.go @@ -0,0 +1,30 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package filter + +import ( + "github.com/astaxie/beego/context" + o "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/internal/orm" +) + +// OrmFilter set orm.Ormer instance to the context of the http.Request +func OrmFilter(ctx *context.Context) { + if ctx == nil || ctx.Request == nil { + return + } + + ctx.Request = ctx.Request.WithContext(orm.NewContext(ctx.Request.Context(), o.NewOrm())) +} diff --git a/src/core/main.go b/src/core/main.go index 53a9a7f65..50a3a34e8 100755 --- a/src/core/main.go +++ b/src/core/main.go @@ -247,6 +247,7 @@ func main() { filter.Init() beego.InsertFilter("/api/*", beego.BeforeStatic, filter.SessionCheck) + beego.InsertFilter("/*", beego.BeforeRouter, filter.OrmFilter) beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter) beego.InsertFilter("/*", beego.BeforeRouter, filter.ReadonlyFilter) diff --git a/src/internal/orm/orm.go b/src/internal/orm/orm.go new file mode 100644 index 000000000..8748128f2 --- /dev/null +++ b/src/internal/orm/orm.go @@ -0,0 +1,68 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package orm + +import ( + "context" + "fmt" + + "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/common/utils/log" +) + +type ormKey struct{} + +// FromContext returns orm from context +func FromContext(ctx context.Context) (orm.Ormer, bool) { + o, ok := ctx.Value(ormKey{}).(orm.Ormer) + return o, ok +} + +// NewContext returns new context with orm +func NewContext(ctx context.Context, o orm.Ormer) context.Context { + return context.WithValue(ctx, ormKey{}, o) +} + +// WithTransaction a decorator which make f run in transaction +func WithTransaction(f func(ctx context.Context) error) func(ctx context.Context) error { + return func(ctx context.Context) error { + o, ok := FromContext(ctx) + if !ok { + return fmt.Errorf("ormer value not found in context") + } + + tx := ormerTx{Ormer: o} + if err := tx.Begin(); err != nil { + log.Errorf("begin transaction failed: %v", err) + return err + } + + if err := f(ctx); err != nil { + if e := tx.Rollback(); e != nil { + log.Errorf("rollback transaction failed: %v", e) + return e + } + + return err + } + + if err := tx.Commit(); err != nil { + log.Errorf("commit transaction failed: %v", err) + return err + } + + return nil + } +} diff --git a/src/internal/orm/orm_test.go b/src/internal/orm/orm_test.go new file mode 100644 index 000000000..bc07178f6 --- /dev/null +++ b/src/internal/orm/orm_test.go @@ -0,0 +1,315 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package orm + +import ( + "context" + "errors" + "testing" + + "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" + "github.com/stretchr/testify/suite" +) + +func addProject(ctx context.Context, project models.Project) (int64, error) { + o, ok := FromContext(ctx) + if !ok { + return 0, errors.New("orm not found in context") + } + + return o.Insert(&project) +} + +func readProject(ctx context.Context, id int64) (*models.Project, error) { + o, ok := FromContext(ctx) + if !ok { + return nil, errors.New("orm not found in context") + } + + project := &models.Project{ + ProjectID: id, + } + + if err := o.Read(project, "project_id"); err != nil { + return nil, err + } + + return project, nil +} + +func deleteProject(ctx context.Context, id int64) error { + o, ok := FromContext(ctx) + if !ok { + return errors.New("orm not found in context") + } + + project := &models.Project{ + ProjectID: id, + } + + _, err := o.Delete(project, "project_id") + return err +} + +func existProject(ctx context.Context, id int64) bool { + o, ok := FromContext(ctx) + if !ok { + return false + } + + project := &models.Project{ + ProjectID: id, + } + + if err := o.Read(project, "project_id"); err != nil { + return false + } + + return true +} + +// Suite ... +type OrmSuite struct { + suite.Suite +} + +// SetupSuite ... +func (suite *OrmSuite) SetupSuite() { + dao.PrepareTestForPostgresSQL() +} + +func (suite *OrmSuite) TestContext() { + ctx := context.TODO() + + o, ok := FromContext(ctx) + suite.False(ok) + suite.Nil(o) + + o, ok = FromContext(NewContext(ctx, orm.NewOrm())) + suite.True(ok) + suite.NotNil(o) +} + +func (suite *OrmSuite) TestWithTransaction() { + ctx := NewContext(context.TODO(), orm.NewOrm()) + + var id int64 + t1 := WithTransaction(func(ctx context.Context) (err error) { + id, err = addProject(ctx, models.Project{Name: "t1", OwnerID: 1}) + return err + }) + + suite.Nil(t1(ctx)) + suite.True(existProject(ctx, id)) + suite.Nil(deleteProject(ctx, id)) +} + +func (suite *OrmSuite) TestSequentialTransactions() { + ctx := NewContext(context.TODO(), orm.NewOrm()) + + var id1, id2 int64 + t1 := func(ctx context.Context, retErr error) error { + return WithTransaction(func(ctx context.Context) (err error) { + id1, err = addProject(ctx, models.Project{Name: "t1", OwnerID: 1}) + if err != nil { + return err + } + + // Ensure t1 created success + suite.True(existProject(ctx, id1)) + + return retErr + })(ctx) + } + t2 := func(ctx context.Context, retErr error) error { + return WithTransaction(func(ctx context.Context) (err error) { + id2, _ = addProject(ctx, models.Project{Name: "t2", OwnerID: 1}) + if err != nil { + return err + } + + // Ensure t2 created success + suite.True(existProject(ctx, id2)) + + return retErr + })(ctx) + } + + if suite.Nil(t1(ctx, nil)) { + suite.True(existProject(ctx, id1)) + } + + if suite.Nil(t2(ctx, nil)) { + suite.True(existProject(ctx, id2)) + } + + // delete project t1 and t2 in db + suite.Nil(deleteProject(ctx, id1)) + suite.Nil(deleteProject(ctx, id2)) + + if suite.Error(t1(ctx, errors.New("oops"))) { + suite.False(existProject(ctx, id1)) + } + + if suite.Nil(t2(ctx, nil)) { + suite.True(existProject(ctx, id2)) + suite.Nil(deleteProject(ctx, id2)) + } +} + +func (suite *OrmSuite) TestNestedTransaction() { + ctx := NewContext(context.TODO(), orm.NewOrm()) + + var id1, id2 int64 + nt1 := WithTransaction(func(ctx context.Context) (err error) { + id1, err = addProject(ctx, models.Project{Name: "nt1", OwnerID: 1}) + return err + }) + nt2 := WithTransaction(func(ctx context.Context) (err error) { + id2, err = addProject(ctx, models.Project{Name: "nt2", OwnerID: 1}) + return err + }) + + nt := func(ctx context.Context, retErr error) error { + return WithTransaction(func(ctx context.Context) error { + if err := nt1(ctx); err != nil { + return err + } + + if err := nt2(ctx); err != nil { + return err + } + + // Ensure nt1 and nt2 created success + suite.True(existProject(ctx, id1)) + suite.True(existProject(ctx, id2)) + + return retErr + })(ctx) + } + + if suite.Nil(nt(ctx, nil)) { + suite.True(existProject(ctx, id1)) + suite.True(existProject(ctx, id2)) + + // delete project nt1 and nt2 in db + suite.Nil(deleteProject(ctx, id1)) + suite.Nil(deleteProject(ctx, id2)) + suite.False(existProject(ctx, id1)) + suite.False(existProject(ctx, id2)) + } + + if suite.Error(nt(ctx, errors.New("oops"))) { + suite.False(existProject(ctx, id1)) + suite.False(existProject(ctx, id2)) + } + + // test nt1 failed but we skip it and nt2 success + suite.Nil(nt1(ctx)) + suite.True(existProject(ctx, id1)) + + // delete nt1 here because id1 will overwrite in the following transaction + defer func(id int64) { + suite.Nil(deleteProject(ctx, id)) + }(id1) + + t := WithTransaction(func(ctx context.Context) error { + suite.Error(nt1(ctx)) + + if err := nt2(ctx); err != nil { + return err + } + + // Ensure t2 created success + suite.True(existProject(ctx, id2)) + + return nil + }) + + if suite.Nil(t(ctx)) { + suite.True(existProject(ctx, id2)) + + // delete project t2 in db + suite.Nil(deleteProject(ctx, id2)) + } +} + +func (suite *OrmSuite) TestNestedSavepoint() { + ctx := NewContext(context.TODO(), orm.NewOrm()) + + var id1, id2 int64 + ns1 := WithTransaction(func(ctx context.Context) (err error) { + id1, err = addProject(ctx, models.Project{Name: "ns1", OwnerID: 1}) + return err + }) + ns2 := WithTransaction(func(ctx context.Context) (err error) { + id2, err = addProject(ctx, models.Project{Name: "ns2", OwnerID: 1}) + return err + }) + + ns := func(ctx context.Context, retErr error) error { + return WithTransaction(func(ctx context.Context) error { + if err := ns1(ctx); err != nil { + return err + } + + if err := ns2(ctx); err != nil { + return err + } + + // Ensure nt1 and nt2 created success + suite.True(existProject(ctx, id1)) + suite.True(existProject(ctx, id2)) + + return retErr + })(ctx) + } + + t := func(ctx context.Context, tErr, pErr error) error { + return WithTransaction(func(c context.Context) error { + ns(c, pErr) + return tErr + })(ctx) + } + + // transaction commit and s1s2 commit + suite.Nil(t(ctx, nil, nil)) + // Ensure nt1 and nt2 created success + suite.True(existProject(ctx, id1)) + suite.True(existProject(ctx, id2)) + // delete project nt1 and nt2 in db + suite.Nil(deleteProject(ctx, id1)) + suite.Nil(deleteProject(ctx, id2)) + suite.False(existProject(ctx, id1)) + suite.False(existProject(ctx, id2)) + + // transaction commit and s1s2 rollback + suite.Nil(t(ctx, nil, errors.New("oops"))) + // Ensure nt1 and nt2 created failed + suite.False(existProject(ctx, id1)) + suite.False(existProject(ctx, id2)) + + // transaction rollback and s1s2 commit + suite.Error(t(ctx, errors.New("oops"), nil)) + // Ensure nt1 and nt2 created failed + suite.False(existProject(ctx, id1)) + suite.False(existProject(ctx, id2)) +} + +func TestRunOrmSuite(t *testing.T) { + suite.Run(t, new(OrmSuite)) +} diff --git a/src/internal/orm/tx.go b/src/internal/orm/tx.go new file mode 100644 index 000000000..9627c482f --- /dev/null +++ b/src/internal/orm/tx.go @@ -0,0 +1,77 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package orm + +import ( + "encoding/hex" + "fmt" + + "github.com/astaxie/beego/orm" + "github.com/google/uuid" +) + +// ormerTx transaction which support savepoint +type ormerTx struct { + orm.Ormer + savepoint string +} + +func (o *ormerTx) savepointMode() bool { + return o.savepoint != "" +} + +func (o *ormerTx) createSavepoint() error { + val := uuid.New() + o.savepoint = fmt.Sprintf("p%s", hex.EncodeToString(val[:])) + + _, err := o.Raw(fmt.Sprintf("SAVEPOINT %s", o.savepoint)).Exec() + return err +} + +func (o *ormerTx) releaseSavepoint() error { + _, err := o.Raw(fmt.Sprintf("RELEASE SAVEPOINT %s", o.savepoint)).Exec() + return err +} + +func (o *ormerTx) rollbackToSavepoint() error { + _, err := o.Raw(fmt.Sprintf("ROLLBACK TO SAVEPOINT %s", o.savepoint)).Exec() + return err +} + +func (o *ormerTx) Begin() error { + err := o.Ormer.Begin() + if err == orm.ErrTxHasBegan { + // transaction has began for the ormer, so begin nested transaction by savepoint + return o.createSavepoint() + } + + return err +} + +func (o *ormerTx) Commit() error { + if o.savepointMode() { + return o.releaseSavepoint() + } + + return o.Ormer.Commit() +} + +func (o *ormerTx) Rollback() error { + if o.savepointMode() { + return o.rollbackToSavepoint() + } + + return o.Ormer.Rollback() +} From ad67e459325d1bd2bdc0bd4a2901b7e0746718d6 Mon Sep 17 00:00:00 2001 From: Yogi_Wang Date: Fri, 27 Dec 2019 16:27:15 +0800 Subject: [PATCH 8/8] Refine the tabs in ellipsis code Signed-off-by: AllForNothing Signed-off-by: Yogi_Wang --- .../member/add-group/add-group.component.html | 2 +- .../project-detail.component.html | 2 +- .../project-detail.component.scss | 13 --- .../project-detail.component.ts | 95 ++++++++++++++++--- .../project-detail/project-detail.const.ts | 3 + src/portal/src/css/common.scss | 11 +++ src/portal/src/css/dark-theme.scss | 6 +- src/portal/src/css/light-theme.scss | 6 +- 8 files changed, 107 insertions(+), 31 deletions(-) create mode 100644 src/portal/src/app/project/project-detail/project-detail.const.ts create mode 100644 src/portal/src/css/common.scss diff --git a/src/portal/src/app/project/member/add-group/add-group.component.html b/src/portal/src/app/project/member/add-group/add-group.component.html index 0432901e5..cb265b8a4 100644 --- a/src/portal/src/app/project/member/add-group/add-group.component.html +++ b/src/portal/src/app/project/member/add-group/add-group.component.html @@ -105,4 +105,4 @@
- \ No newline at end of file + diff --git a/src/portal/src/app/project/project-detail/project-detail.component.html b/src/portal/src/app/project/project-detail/project-detail.component.html index 69b5bd842..248973f18 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.html +++ b/src/portal/src/app/project/project-detail/project-detail.component.html @@ -6,7 +6,7 @@ - +
diff --git a/src/portal/src/app/project/project-detail/project-detail.component.scss b/src/portal/src/app/project/project-detail/project-detail.component.scss index 8e771e1d4..0e7e8ab29 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.scss +++ b/src/portal/src/app/project/project-detail/project-detail.component.scss @@ -42,16 +42,3 @@ button { padding: 0; } } - -.in-overflow { - ::ng-deep { - .tabs-overflow { - > .nav-item { - > button { - box-shadow: 0 -3px 0 #0077b8 inset; - color: 0077b8; - } - } - } - } -} diff --git a/src/portal/src/app/project/project-detail/project-detail.component.ts b/src/portal/src/app/project/project-detail/project-detail.component.ts index 2422f8041..e6ea1cd1c 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.ts +++ b/src/portal/src/app/project/project-detail/project-detail.component.ts @@ -11,21 +11,24 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, HostListener, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Project } from '../project'; import { SessionService } from '../../shared/session.service'; import { AppConfigService } from "../../app-config.service"; -import { forkJoin } from "rxjs"; -import { ProjectService, UserPermissionService, USERSTATICPERMISSION } from "../../../lib/services"; +import { forkJoin, Subject, Subscription } from "rxjs"; +import { UserPermissionService, USERSTATICPERMISSION } from "../../../lib/services"; import { ErrorHandler } from "../../../lib/utils/error-handler"; +import { debounceTime } from 'rxjs/operators'; +import { DOWN, SHOW_ELLIPSIS_WIDTH, UP } from './project-detail.const'; + + @Component({ selector: 'project-detail', templateUrl: 'project-detail.component.html', styleUrls: ['project-detail.component.scss'] }) -export class ProjectDetailComponent implements OnInit { - +export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy { hasSignedIn: boolean; currentProject: Project; @@ -89,29 +92,32 @@ export class ProjectDetailComponent implements OnInit { }, { linkName: "tag-strategy", - tabLinkInOverflow: true, + tabLinkInOverflow: false, showTabName: "PROJECT_DETAIL.TAG_STRATEGY", permissions: () => this.hasTagRetentionPermission }, { linkName: "robot-account", - tabLinkInOverflow: true, + tabLinkInOverflow: false, showTabName: "PROJECT_DETAIL.ROBOT_ACCOUNTS", permissions: () => this.hasRobotListPermission }, { linkName: "webhook", - tabLinkInOverflow: true, + tabLinkInOverflow: false, showTabName: "PROJECT_DETAIL.WEBHOOKS", permissions: () => this.hasWebhookListPermission }, { linkName: "logs", - tabLinkInOverflow: true, + tabLinkInOverflow: false, showTabName: "PROJECT_DETAIL.LOGS", permissions: () => this.hasLogListPermission } ]; + previousWindowWidth: number; + private _subject = new Subject(); + private _subscription: Subscription; constructor( private route: ActivatedRoute, private router: Router, @@ -119,8 +125,7 @@ export class ProjectDetailComponent implements OnInit { private appConfigService: AppConfigService, private userPermissionService: UserPermissionService, private errorHandler: ErrorHandler, - private projectService: ProjectService) { - + private cdf: ChangeDetectorRef) { this.hasSignedIn = this.sessionService.getCurrentUser() !== null; this.route.data.subscribe(data => { this.currentProject = data['projectResolver']; @@ -131,6 +136,32 @@ export class ProjectDetailComponent implements OnInit { ngOnInit() { this.projectId = this.route.snapshot.params['id']; this.getPermissionsList(this.projectId); + if (!this._subscription) { + this._subscription = this._subject.pipe( + debounceTime(100) + ).subscribe( + type => { + if (type === DOWN) { + this.resetTabsForDownSize(); + } else { + this.resetTabsForUpSize(); + } + } + ); + } + } + + ngAfterViewInit() { + this.previousWindowWidth = window.innerWidth; + setTimeout(() => { + this.resetTabsForDownSize(); + }, 0); + } + ngOnDestroy() { + if (this._subscription) { + this._subscription.unsubscribe(); + this._subscription = null; + } } getPermissionsList(projectId: number): void { let permissionsList = []; @@ -153,17 +184,17 @@ export class ProjectDetailComponent implements OnInit { permissionsList.push(this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.LABEL.KEY, USERSTATICPERMISSION.LABEL.VALUE.CREATE)); permissionsList.push(this.userPermissionService.getPermission(projectId, - USERSTATICPERMISSION.TAG_RETENTION.KEY, USERSTATICPERMISSION.TAG_RETENTION.VALUE.READ)); + USERSTATICPERMISSION.TAG_RETENTION.KEY, USERSTATICPERMISSION.TAG_RETENTION.VALUE.READ)); permissionsList.push(this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.WEBHOOK.KEY, USERSTATICPERMISSION.WEBHOOK.VALUE.LIST)); permissionsList.push(this.userPermissionService.getPermission(projectId, - USERSTATICPERMISSION.SCANNER.KEY, USERSTATICPERMISSION.SCANNER.VALUE.READ)); + USERSTATICPERMISSION.SCANNER.KEY, USERSTATICPERMISSION.SCANNER.VALUE.READ)); forkJoin(...permissionsList).subscribe(Rules => { [this.hasProjectReadPermission, this.hasLogListPermission, this.hasConfigurationListPermission, this.hasMemberListPermission , this.hasLabelListPermission, this.hasRepositoryListPermission, this.hasHelmChartsListPermission, this.hasRobotListPermission , this.hasLabelCreatePermission, this.hasTagRetentionPermission, this.hasWebhookListPermission, - this.hasScannerReadPermission] = Rules; + this.hasScannerReadPermission] = Rules; }, error => this.errorHandler.error(error)); } @@ -188,10 +219,46 @@ export class ProjectDetailComponent implements OnInit { isDefaultTab(tab, index) { return this.route.snapshot.children[0].routeConfig.path !== tab.linkName && index === 0; } + isTabLinkInOverFlow() { return this.tabLinkNavList.some(tab => { return tab.tabLinkInOverflow && this.route.snapshot.children[0].routeConfig.path === tab.linkName; }); } + @HostListener('window:resize', ['$event']) + onResize(event) { + if (this.previousWindowWidth) { + // down size + if (this.previousWindowWidth > event.target.innerWidth) { + this._subject.next(DOWN); + } else { // up size + this._subject.next(UP); + } + } + this.previousWindowWidth = event.target.innerWidth; + } + + resetTabsForDownSize(): void { + this.tabLinkNavList.filter(item => !item.tabLinkInOverflow).forEach((item, index) => { + const tabEle: HTMLElement = document.getElementById('project-' + item.linkName); + // strengthen code + if (tabEle && tabEle.getBoundingClientRect) { + const right: number = window.innerWidth - document.getElementById('project-' + item.linkName).getBoundingClientRect().right; + if (right < SHOW_ELLIPSIS_WIDTH) { + this.tabLinkNavList[index].tabLinkInOverflow = true; + } + } + }); + } + resetTabsForUpSize() { + // 1.Set tabLinkInOverflow to false for all tabs(show all tabs) + for ( let i = 0; i < this.tabLinkNavList.length; i++) { + this.tabLinkNavList[i].tabLinkInOverflow = false; + } + // 2.Manually detect changes to rerender dom + this.cdf.detectChanges(); + // 3. Hide overflowed tabs + this.resetTabsForDownSize(); + } } diff --git a/src/portal/src/app/project/project-detail/project-detail.const.ts b/src/portal/src/app/project/project-detail/project-detail.const.ts new file mode 100644 index 000000000..9d195ecb5 --- /dev/null +++ b/src/portal/src/app/project/project-detail/project-detail.const.ts @@ -0,0 +1,3 @@ +export const SHOW_ELLIPSIS_WIDTH = 80; +export const DOWN: string = "down"; +export const UP: string = "up"; diff --git a/src/portal/src/css/common.scss b/src/portal/src/css/common.scss new file mode 100644 index 000000000..06c40fdf5 --- /dev/null +++ b/src/portal/src/css/common.scss @@ -0,0 +1,11 @@ +// styles for dark and light theme should be defined here. +.in-overflow { + .tabs-overflow { + > .nav-item { + > button { + box-shadow: 0 -3px 0 $dark-active-tab-color inset; + color: 0077b8; + } + } + } +} \ No newline at end of file diff --git a/src/portal/src/css/dark-theme.scss b/src/portal/src/css/dark-theme.scss index 53999bf2e..2ecf4eb2c 100644 --- a/src/portal/src/css/dark-theme.scss +++ b/src/portal/src/css/dark-theme.scss @@ -1,7 +1,9 @@ +// Variables for dark theme should be defined here. @import "../../node_modules/@clr/ui/clr-ui-dark.min.css"; $dark-background-color: rgb(27, 42, 50) !important; $dark-font-color1: #acbac3 !important; $dark-font-color-title1: #eaedf0 !important; +$dark-active-tab-color: #4aaed9; .label-form { background-color: #212129 !important; @@ -110,4 +112,6 @@ clr-dg-action-overflow { color: #1b2a32; } } -} \ No newline at end of file +} + +@import "./common.scss"; \ No newline at end of file diff --git a/src/portal/src/css/light-theme.scss b/src/portal/src/css/light-theme.scss index b09e7465a..8d93ad912 100644 --- a/src/portal/src/css/light-theme.scss +++ b/src/portal/src/css/light-theme.scss @@ -1 +1,5 @@ -@import "../../node_modules/@clr/ui/clr-ui.min.css"; \ No newline at end of file +// Variables for dark theme should be defined here. +@import "../../node_modules/@clr/ui/clr-ui.min.css"; + +$dark-active-tab-color: #0077b8; +@import "./common.scss"; \ No newline at end of file