feat: [AH-307]: GC interface Integration, event framework; schema changes; UI changes (#2688)

* fix
* Merge branch 'main' into AH-307-plus-url-support-2_no_rbac
* resolve PR comments
* resolve PR comments
* resolve PR comments
* feat: [AH-346]: new api changes for version list and digest list (#2726)

* feat: [AH-346]: new api changes for version list and digest list
* resolve pr comments
* resolve pr comments
* feat: [AH-346]: new api yaml integration (#2716)

* feat: [AH-346]: new api yaml integration
* Merge branch 'main' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into AH-307-plus-url-support-2_no_rbac
* fix wire check
* fix lint issues
* fix: [AH-357]: migrations
* changes for global artifact listing (#2708)

* changes for global artifact listing
* Merge branch 'main' into AH-307-plus-url-support-2_no_rbac
* merged main
* Merge branch 'main' into AH-307-plus-url-support-2_no_rbac
* [AH-307]: Updated lint
* fix comment
* add new method to spacestore
* feat: [AH-307]: fix after rebase with main
* [AH-307]: Removing comments
* [AH-307]: linting fixes
* feat: [AH-286]: define proto, interface and no-op reporter implementation to publish artifact events (#2657)

* feat: [AH-286]: publish artifact event - no row found is not error
* feat: [AH-286]: publish artifact event - no row found is not error
* feat: [AH-286]: publish artifact event - lint errors, move publishing event outside DB transaction
* feat: [AH-286]: publish artifact event - review comments
* feat: [AH-286]: publish artifact event - address review comments
* feat: [AH-286]: publish artifact event - keep payload generic
* feat: [AH-286]: publish artifact event - as sqlite locks DB, perform db operation outside goroutine publishing of events
* feat: [AH-286]: publish artifact event - make publishing event async
* feat: [AH-286]: publish artifact event - use api types
* feat: [AH-286]: Publish event for SSCA to trigger scans - no need to export spacePathStore
* feat: [AH-286]: Publish event for SSCA to trigger scans - send spacePath instead of parentID
* feat: [AH-286]: Publish event for SSCA to trigger scans - rename scanner as generic reporter
* feat: [AH-286]: Publish event for SSCA to trigger scans - rename scanner as generic reporter
* feat: [AH-286]: publish artifact event - reuse redis.Send()
* feat: [AH-286]: Publish event for SSCA to trigger scans - review comments
* feat: [AH-286]: Publish event for SSCA to trigger scans - remove unused interface
* feat: [AH-286]: Publish event for SSCA to trigger scans - update msg format
* feat: [AH-286]: define proto, interface and no-op reporter implementation to publish artifact events
* feat: [AH-286]: Publish event for SSCA to trigger scans - extract acctID/orgID/projectID from spacepathStore
* feat: [AH-286]: publish artifact event - remove protobuf reference, fix lint errors
* feat: [AH-286]: publish artifact event - fix msg format
* feat: [AH-286]: define proto, interface and no-op reporter implementation to publish artifact events
* feat: [AH-286]: define proto, interface and no-op reporter implementation to publish artifact events
* feat: [AH-321]: make repo form disabled for rbac (#2687)

* feat: [AH-321]: make repo form disabled for rbac
* fix wire-gen
* GC refactoring
* feat: [AH-340]: update UI as per the product feedbacks (#2685)

* feat: [AH-340]: update UI as per the product feedbacks
* feat: [AH-44]: add module data while redirecting to pipeline execution page
* feat: [AH-44]: add build pipeline details in overview cards
* feat: [AH-44]: update view for prod and non prod tag
* feat: [AH-44]: rearrange filters on artifact list apge
* feat: [AH-10]: add schema for overview cards, update artifact list, add ai search input, update api for registry artifact list and update mapping for deployments table
* feat: [AH-307]: add secretSpacePath in upstream password field while sending to BE (#2631)

* feat: [AH-307]: add secretSpacePath in upstream password field while sending to BE
* feat: [AH-299]: support new changes for artifact list page (#2630)

* feat: update har service api version
* feat: [AH-30]: integrate API schema for deployments list content
* feat: [AH-300]: update tag colors for prod and non prod tags
* feat: [AH-300]: Add Deployments table in artiface version details page
* feat: [AH-299]: support new changes for artifact list page
* feat: [AH-299]: support new changes for artifact list page
* feat: [AH-321]: support artifact registry rbac permission on UI (#2671)

* feat: [AH-321]: support artifact registry rbac permission on UI
* enable rbac (#2664)

* fix scope
* enable rbac
* feat: [AH-307]: hide code tab from version details page for both docker and helm
* feat: [AH-240]: add custom handling for enterprise auth type field
* Merge branch 'AH-307-plus-url-support-2_no_rbac' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into AH-307-plus-url-support-2_no_rbac
* feat: [AH-307]: send space_ref in query param while creating registries
* lowercase rootRef
* [AH-307]: updated route
* [AH-307]: Added logs
* [AH-307]: Added logs
* feat: [AH-317]: add space_ref query param
* local
* Merge commit
* Merge commit
* Merge commit
* Added comments
* Revert changes
* Merge commit
* Merge branch 'main' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into AH-307-plus-url-support-2
* Merge branch 'AH-306d' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into AH-307-plus-url-support-2
* fix space path handling
* Merge branch 'main' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into AH-307-plus-url-support-2
* Updated URLs to support slashes with + separator
* fix: [AH-306c]: fix anonymous flow
* fix: [AH-306c]: fix anonymous flow
* feat: [AH-307]: plus url support on UI

(cherry picked from commit 3fb6add3ce03498b6668b5f8f6d547e1acedaec4)
* [AH-307]: Added examples

(cherry picked from commit e83e41303da536f421be333be04aed09fbf75f5f)
* [AH-307]: Added Regex request rewrite support

(cherry picked from commit ed7b155256bdcd1134bc228b5705556a1233add6)
* fix: [AH-306c]: fix anonymous flow
CODE-2402
Arvind Choudhary 2024-09-24 12:47:53 +00:00 committed by Harness
parent dcc85d5545
commit 479c9b9fe7
136 changed files with 3853 additions and 980 deletions

View File

@ -13,4 +13,4 @@ GITNESS_SSH_HOST=localhost
GITNESS_SSH_PORT=2222 GITNESS_SSH_PORT=2222
GITNESS_REGISTRY_STORAGE_TYPE=filesystem GITNESS_REGISTRY_STORAGE_TYPE=filesystem
GITNESS_REGISTRY_FILESYSTEM_ROOT_DIRECTORY=/tmp GITNESS_REGISTRY_FILESYSTEM_ROOT_DIRECTORY=/tmp

View File

@ -24,7 +24,7 @@ import (
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
) )
// pathCacheGetter is used to hook a SpacePathStore as source of a PathCache. // pathCacheGetter is used to hook a spacePathStore as source of a PathCache.
// IMPORTANT: It assumes that the pathCache already transformed the key. // IMPORTANT: It assumes that the pathCache already transformed the key.
type pathCacheGetter struct { type pathCacheGetter struct {
spacePathStore store.SpacePathStore spacePathStore store.SpacePathStore

View File

@ -94,12 +94,16 @@ type (
DeleteServiceAccount(ctx context.Context, id int64) error DeleteServiceAccount(ctx context.Context, id int64) error
// ListServiceAccounts returns a list of service accounts for a specific parent. // ListServiceAccounts returns a list of service accounts for a specific parent.
ListServiceAccounts(ctx context.Context, ListServiceAccounts(
parentType enum.ParentResourceType, parentID int64) ([]*types.ServiceAccount, error) ctx context.Context,
parentType enum.ParentResourceType, parentID int64,
) ([]*types.ServiceAccount, error)
// CountServiceAccounts returns a count of service accounts for a specific parent. // CountServiceAccounts returns a count of service accounts for a specific parent.
CountServiceAccounts(ctx context.Context, CountServiceAccounts(
parentType enum.ParentResourceType, parentID int64) (int64, error) ctx context.Context,
parentType enum.ParentResourceType, parentID int64,
) (int64, error)
/* /*
* SERVICE RELATED OPERATIONS. * SERVICE RELATED OPERATIONS.
@ -167,6 +171,9 @@ type (
// FindByRef finds the space using the spaceRef as either the id or the space path. // FindByRef finds the space using the spaceRef as either the id or the space path.
FindByRef(ctx context.Context, spaceRef string) (*types.Space, error) FindByRef(ctx context.Context, spaceRef string) (*types.Space, error)
// FindByRefCaseInsensitive finds the space using the spaceRef.
FindByRefCaseInsensitive(ctx context.Context, spaceRef string) (*types.Space, error)
// FindByRefAndDeletedAt finds the space using the spaceRef and deleted timestamp. // FindByRefAndDeletedAt finds the space using the spaceRef and deleted timestamp.
FindByRefAndDeletedAt(ctx context.Context, spaceRef string, deletedAt int64) (*types.Space, error) FindByRefAndDeletedAt(ctx context.Context, spaceRef string, deletedAt int64) (*types.Space, error)
@ -192,8 +199,10 @@ type (
Update(ctx context.Context, space *types.Space) error Update(ctx context.Context, space *types.Space) error
// UpdateOptLock updates the space using the optimistic locking mechanism. // UpdateOptLock updates the space using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, space *types.Space, UpdateOptLock(
mutateFn func(space *types.Space) error) (*types.Space, error) ctx context.Context, space *types.Space,
mutateFn func(space *types.Space) error,
) (*types.Space, error)
// FindForUpdate finds the space and locks it for an update. // FindForUpdate finds the space and locks it for an update.
FindForUpdate(ctx context.Context, id int64) (*types.Space, error) FindForUpdate(ctx context.Context, id int64) (*types.Space, error)
@ -205,8 +214,10 @@ type (
Purge(ctx context.Context, id int64, deletedAt *int64) error Purge(ctx context.Context, id int64, deletedAt *int64) error
// Restore restores a soft deleted space. // Restore restores a soft deleted space.
Restore(ctx context.Context, space *types.Space, Restore(
newIdentifier *string, newParentID *int64) (*types.Space, error) ctx context.Context, space *types.Space,
newIdentifier *string, newParentID *int64,
) (*types.Space, error)
// Count the child spaces of a space. // Count the child spaces of a space.
Count(ctx context.Context, id int64, opts *types.SpaceFilter) (int64, error) Count(ctx context.Context, id int64, opts *types.SpaceFilter) (int64, error)
@ -239,8 +250,10 @@ type (
GetSize(ctx context.Context, id int64) (int64, error) GetSize(ctx context.Context, id int64) (int64, error)
// UpdateOptLock the repo details using the optimistic locking mechanism. // UpdateOptLock the repo details using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, repo *types.Repository, UpdateOptLock(
mutateFn func(repository *types.Repository) error) (*types.Repository, error) ctx context.Context, repo *types.Repository,
mutateFn func(repository *types.Repository) error,
) (*types.Repository, error)
// SoftDelete a repo. // SoftDelete a repo.
SoftDelete(ctx context.Context, repo *types.Repository, deletedAt int64) error SoftDelete(ctx context.Context, repo *types.Repository, deletedAt int64) error
@ -249,8 +262,10 @@ type (
Purge(ctx context.Context, id int64, deletedAt *int64) error Purge(ctx context.Context, id int64, deletedAt *int64) error
// Restore a deleted repo using the optimistic locking mechanism. // Restore a deleted repo using the optimistic locking mechanism.
Restore(ctx context.Context, repo *types.Repository, Restore(
newIdentifier *string, newParentID *int64) (*types.Repository, error) ctx context.Context, repo *types.Repository,
newIdentifier *string, newParentID *int64,
) (*types.Repository, error)
// Count of active repos in a space. With "DeletedBeforeOrAt" filter, counts deleted repos. // Count of active repos in a space. With "DeletedBeforeOrAt" filter, counts deleted repos.
Count(ctx context.Context, parentID int64, opts *types.RepoFilter) (int64, error) Count(ctx context.Context, parentID int64, opts *types.RepoFilter) (int64, error)
@ -308,7 +323,11 @@ type (
CountUsers(ctx context.Context, spaceID int64, filter types.MembershipUserFilter) (int64, error) CountUsers(ctx context.Context, spaceID int64, filter types.MembershipUserFilter) (int64, error)
ListUsers(ctx context.Context, spaceID int64, filter types.MembershipUserFilter) ([]types.MembershipUser, error) ListUsers(ctx context.Context, spaceID int64, filter types.MembershipUserFilter) ([]types.MembershipUser, error)
CountSpaces(ctx context.Context, userID int64, filter types.MembershipSpaceFilter) (int64, error) CountSpaces(ctx context.Context, userID int64, filter types.MembershipSpaceFilter) (int64, error)
ListSpaces(ctx context.Context, userID int64, filter types.MembershipSpaceFilter) ([]types.MembershipSpace, error) ListSpaces(
ctx context.Context,
userID int64,
filter types.MembershipSpaceFilter,
) ([]types.MembershipSpace, error)
} }
// PublicAccessStore defines the publicly accessible resources data storage. // PublicAccessStore defines the publicly accessible resources data storage.
@ -362,8 +381,10 @@ type (
Update(ctx context.Context, pr *types.PullReq) error Update(ctx context.Context, pr *types.PullReq) error
// UpdateOptLock the pull request details using the optimistic locking mechanism. // UpdateOptLock the pull request details using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, pr *types.PullReq, UpdateOptLock(
mutateFn func(pr *types.PullReq) error) (*types.PullReq, error) ctx context.Context, pr *types.PullReq,
mutateFn func(pr *types.PullReq) error,
) (*types.PullReq, error)
// UpdateActivitySeq the pull request's activity sequence number. // UpdateActivitySeq the pull request's activity sequence number.
// It will set new values to the ActivitySeq, Version and Updated fields. // It will set new values to the ActivitySeq, Version and Updated fields.
@ -395,7 +416,8 @@ type (
Create(ctx context.Context, act *types.PullReqActivity) error Create(ctx context.Context, act *types.PullReqActivity) error
// CreateWithPayload create a new system activity from the provided payload. // CreateWithPayload create a new system activity from the provided payload.
CreateWithPayload(ctx context.Context, CreateWithPayload(
ctx context.Context,
pr *types.PullReq, pr *types.PullReq,
principalID int64, principalID int64,
payload types.PullReqActivityPayload, payload types.PullReqActivityPayload,
@ -406,7 +428,8 @@ type (
Update(ctx context.Context, act *types.PullReqActivity) error Update(ctx context.Context, act *types.PullReqActivity) error
// UpdateOptLock updates the pull request activity using the optimistic locking mechanism. // UpdateOptLock updates the pull request activity using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, UpdateOptLock(
ctx context.Context,
act *types.PullReqActivity, act *types.PullReqActivity,
mutateFn func(act *types.PullReqActivity) error, mutateFn func(act *types.PullReqActivity) error,
) (*types.PullReqActivity, error) ) (*types.PullReqActivity, error)
@ -543,8 +566,10 @@ type (
Update(ctx context.Context, hook *types.Webhook) error Update(ctx context.Context, hook *types.Webhook) error
// UpdateOptLock updates the webhook using the optimistic locking mechanism. // UpdateOptLock updates the webhook using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, hook *types.Webhook, UpdateOptLock(
mutateFn func(hook *types.Webhook) error) (*types.Webhook, error) ctx context.Context, hook *types.Webhook,
mutateFn func(hook *types.Webhook) error,
) (*types.Webhook, error)
// Delete deletes the webhook for the given id. // Delete deletes the webhook for the given id.
Delete(ctx context.Context, id int64) error Delete(ctx context.Context, id int64) error
@ -553,12 +578,16 @@ type (
DeleteByIdentifier(ctx context.Context, parentType enum.WebhookParent, parentID int64, identifier string) error DeleteByIdentifier(ctx context.Context, parentType enum.WebhookParent, parentID int64, identifier string) error
// Count counts the webhooks for a given parent type and id. // Count counts the webhooks for a given parent type and id.
Count(ctx context.Context, parentType enum.WebhookParent, parentID int64, Count(
opts *types.WebhookFilter) (int64, error) ctx context.Context, parentType enum.WebhookParent, parentID int64,
opts *types.WebhookFilter,
) (int64, error)
// List lists the webhooks for a given parent type and id. // List lists the webhooks for a given parent type and id.
List(ctx context.Context, parentType enum.WebhookParent, parentID int64, List(
opts *types.WebhookFilter) ([]*types.Webhook, error) ctx context.Context, parentType enum.WebhookParent, parentID int64,
opts *types.WebhookFilter,
) ([]*types.Webhook, error)
} }
// WebhookExecutionStore defines the webhook execution data storage. // WebhookExecutionStore defines the webhook execution data storage.
@ -573,8 +602,10 @@ type (
DeleteOld(ctx context.Context, olderThan time.Time) (int64, error) DeleteOld(ctx context.Context, olderThan time.Time) (int64, error)
// ListForWebhook lists the webhook executions for a given webhook id. // ListForWebhook lists the webhook executions for a given webhook id.
ListForWebhook(ctx context.Context, webhookID int64, ListForWebhook(
opts *types.WebhookExecutionFilter) ([]*types.WebhookExecution, error) ctx context.Context, webhookID int64,
opts *types.WebhookExecutionFilter,
) ([]*types.WebhookExecution, error)
// ListForTrigger lists the webhook executions for a given trigger id. // ListForTrigger lists the webhook executions for a given trigger id.
ListForTrigger(ctx context.Context, triggerID string) ([]*types.WebhookExecution, error) ListForTrigger(ctx context.Context, triggerID string) ([]*types.WebhookExecution, error)
@ -646,7 +677,10 @@ type (
List(ctx context.Context, filter *types.GitspaceFilter) ([]*types.GitspaceInstance, error) List(ctx context.Context, filter *types.GitspaceFilter) ([]*types.GitspaceInstance, error)
// List lists the latest gitspace instance present for the gitspace configs in the datastore. // List lists the latest gitspace instance present for the gitspace configs in the datastore.
FindAllLatestByGitspaceConfigID(ctx context.Context, gitspaceConfigIDs []int64) ([]*types.GitspaceInstance, error) FindAllLatestByGitspaceConfigID(
ctx context.Context,
gitspaceConfigIDs []int64,
) ([]*types.GitspaceInstance, error)
} }
InfraProviderConfigStore interface { InfraProviderConfigStore interface {
@ -677,7 +711,8 @@ type (
Update(ctx context.Context, infraProviderResource *types.InfraProviderResource) error Update(ctx context.Context, infraProviderResource *types.InfraProviderResource) error
// List lists the infra provider resource present for the gitspace config in a parent space ID in the datastore. // List lists the infra provider resource present for the gitspace config in a parent space ID in the datastore.
List(ctx context.Context, List(
ctx context.Context,
infraProviderConfigID int64, infraProviderConfigID int64,
filter types.ListQueryFilter, filter types.ListQueryFilter,
) ([]*types.InfraProviderResource, error) ) ([]*types.InfraProviderResource, error)
@ -707,8 +742,10 @@ type (
ListLatest(ctx context.Context, repoID int64, pagination types.ListQueryFilter) ([]*types.Pipeline, error) ListLatest(ctx context.Context, repoID int64, pagination types.ListQueryFilter) ([]*types.Pipeline, error)
// UpdateOptLock updates the pipeline using the optimistic locking mechanism. // UpdateOptLock updates the pipeline using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, pipeline *types.Pipeline, UpdateOptLock(
mutateFn func(pipeline *types.Pipeline) error) (*types.Pipeline, error) ctx context.Context, pipeline *types.Pipeline,
mutateFn func(pipeline *types.Pipeline) error,
) (*types.Pipeline, error)
// Delete deletes a pipeline ID from the datastore. // Delete deletes a pipeline ID from the datastore.
Delete(ctx context.Context, id int64) error Delete(ctx context.Context, id int64) error
@ -743,8 +780,10 @@ type (
Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error) Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error)
// UpdateOptLock updates the secret using the optimistic locking mechanism. // UpdateOptLock updates the secret using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, secret *types.Secret, UpdateOptLock(
mutateFn func(secret *types.Secret) error) (*types.Secret, error) ctx context.Context, secret *types.Secret,
mutateFn func(secret *types.Secret) error,
) (*types.Secret, error)
// Update tries to update a secret. // Update tries to update a secret.
Update(ctx context.Context, secret *types.Secret) error Update(ctx context.Context, secret *types.Secret) error
@ -837,8 +876,10 @@ type (
Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error) Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error)
// UpdateOptLock updates the connector using the optimistic locking mechanism. // UpdateOptLock updates the connector using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, connector *types.Connector, UpdateOptLock(
mutateFn func(connector *types.Connector) error) (*types.Connector, error) ctx context.Context, connector *types.Connector,
mutateFn func(connector *types.Connector) error,
) (*types.Connector, error)
// Update tries to update a connector. // Update tries to update a connector.
Update(ctx context.Context, connector *types.Connector) error Update(ctx context.Context, connector *types.Connector) error
@ -858,8 +899,10 @@ type (
Find(ctx context.Context, id int64) (*types.Template, error) Find(ctx context.Context, id int64) (*types.Template, error)
// FindByIdentifierAndType returns a template given a space ID, identifier and a type // FindByIdentifierAndType returns a template given a space ID, identifier and a type
FindByIdentifierAndType(ctx context.Context, spaceID int64, FindByIdentifierAndType(
identifier string, resolverType enum.ResolverType) (*types.Template, error) ctx context.Context, spaceID int64,
identifier string, resolverType enum.ResolverType,
) (*types.Template, error)
// Create creates a new template. // Create creates a new template.
Create(ctx context.Context, template *types.Template) error Create(ctx context.Context, template *types.Template) error
@ -868,8 +911,10 @@ type (
Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error) Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error)
// UpdateOptLock updates the template using the optimistic locking mechanism. // UpdateOptLock updates the template using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, template *types.Template, UpdateOptLock(
mutateFn func(template *types.Template) error) (*types.Template, error) ctx context.Context, template *types.Template,
mutateFn func(template *types.Template) error,
) (*types.Template, error)
// Update tries to update a template. // Update tries to update a template.
Update(ctx context.Context, template *types.Template) error Update(ctx context.Context, template *types.Template) error
@ -878,7 +923,12 @@ type (
Delete(ctx context.Context, id int64) error Delete(ctx context.Context, id int64) error
// DeleteByIdentifierAndType deletes a template given a space ID, identifier and a type. // DeleteByIdentifierAndType deletes a template given a space ID, identifier and a type.
DeleteByIdentifierAndType(ctx context.Context, spaceID int64, identifier string, resolverType enum.ResolverType) error DeleteByIdentifierAndType(
ctx context.Context,
spaceID int64,
identifier string,
resolverType enum.ResolverType,
) error
// List lists the templates in a given space. // List lists the templates in a given space.
List(ctx context.Context, spaceID int64, filter types.ListQueryFilter) ([]*types.Template, error) List(ctx context.Context, spaceID int64, filter types.ListQueryFilter) ([]*types.Template, error)
@ -895,8 +945,10 @@ type (
Update(ctx context.Context, trigger *types.Trigger) error Update(ctx context.Context, trigger *types.Trigger) error
// UpdateOptLock updates the trigger using the optimistic locking mechanism. // UpdateOptLock updates the trigger using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, trigger *types.Trigger, UpdateOptLock(
mutateFn func(trigger *types.Trigger) error) (*types.Trigger, error) ctx context.Context, trigger *types.Trigger,
mutateFn func(trigger *types.Trigger) error,
) (*types.Trigger, error)
// List lists the triggers for a given pipeline ID. // List lists the triggers for a given pipeline ID.
List(ctx context.Context, pipelineID int64, filter types.ListQueryFilter) ([]*types.Trigger, error) List(ctx context.Context, pipelineID int64, filter types.ListQueryFilter) ([]*types.Trigger, error)
@ -941,7 +993,11 @@ type (
Map(ctx context.Context, ids []int64) (map[int64]*types.UserGroup, error) Map(ctx context.Context, ids []int64) (map[int64]*types.UserGroup, error)
// FindManyByIdentifiersAndSpaceID returns a list of usergroups // FindManyByIdentifiersAndSpaceID returns a list of usergroups
FindManyByIdentifiersAndSpaceID(ctx context.Context, identifiers []string, spaceID int64) ([]*types.UserGroup, error) FindManyByIdentifiersAndSpaceID(
ctx context.Context,
identifiers []string,
spaceID int64,
) ([]*types.UserGroup, error)
// FindManyByIDs returns a list of usergroups searching them via ids // FindManyByIDs returns a list of usergroups searching them via ids
FindManyByIDs(ctx context.Context, ids []int64) ([]*types.UserGroup, error) FindManyByIDs(ctx context.Context, ids []int64) ([]*types.UserGroup, error)
@ -1145,7 +1201,8 @@ type (
spaceID int64, spaceID int64,
gitspaceInstanceID int64, gitspaceInstanceID int64,
) (*types.InfraProvisioned, error) ) (*types.InfraProvisioned, error)
FindLatestByGitspaceInstanceIdentifier(ctx context.Context, FindLatestByGitspaceInstanceIdentifier(
ctx context.Context,
spaceID int64, spaceID int64,
gitspaceInstanceIdentifier string, gitspaceInstanceIdentifier string,
) (*types.InfraProvisioned, error) ) (*types.InfraProvisioned, error)

View File

@ -125,6 +125,40 @@ func (s *SpaceStore) FindByRef(ctx context.Context, spaceRef string) (*types.Spa
return s.findByRef(ctx, spaceRef, nil) return s.findByRef(ctx, spaceRef, nil)
} }
// FindByRefCaseInsensitive finds the space using the spaceRef.
func (s *SpaceStore) FindByRefCaseInsensitive(ctx context.Context, spaceRef string) (*types.Space, error) {
segments := paths.Segments(spaceRef)
if len(segments) < 1 {
return nil, fmt.Errorf("invalid space reference provided")
}
var stmt squirrel.SelectBuilder
switch {
case len(segments) == 1:
stmt = database.Builder.
Select("space_id").
From("spaces").
Where("LOWER(space_uid) = LOWER(?) ", segments[0])
case len(segments) > 1:
stmt = buildRecursiveSelectQueryUsingCaseInsensitivePath(segments)
}
sql, args, err := stmt.ToSql()
if err != nil {
return nil, fmt.Errorf("failed to create sql query: %w", err)
}
db := dbtx.GetAccessor(ctx, s.db)
var spaceID int64
if err = db.GetContext(ctx, &spaceID, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom select query")
}
return s.find(ctx, spaceID, nil)
}
// FindByRefAndDeletedAt finds the space using the spaceRef as either the id or the space path and deleted timestamp. // FindByRefAndDeletedAt finds the space using the spaceRef as either the id or the space path and deleted timestamp.
func (s *SpaceStore) FindByRefAndDeletedAt( func (s *SpaceStore) FindByRefAndDeletedAt(
ctx context.Context, ctx context.Context,
@ -701,7 +735,8 @@ func (s *SpaceStore) list(
return s.mapToSpaces(ctx, s.db, dst) return s.mapToSpaces(ctx, s.db, dst)
} }
func (s *SpaceStore) listAll(ctx context.Context, func (s *SpaceStore) listAll(
ctx context.Context,
id int64, id int64,
opts *types.SpaceFilter, opts *types.SpaceFilter,
) ([]*types.Space, error) { ) ([]*types.Space, error) {
@ -901,7 +936,7 @@ func mapToInternalSpace(s *types.Space) *space {
// buildRecursiveSelectQueryUsingPath builds the recursive select query using path among active or soft deleted spaces. // buildRecursiveSelectQueryUsingPath builds the recursive select query using path among active or soft deleted spaces.
func buildRecursiveSelectQueryUsingPath(segments []string, deletedAt int64) squirrel.SelectBuilder { func buildRecursiveSelectQueryUsingPath(segments []string, deletedAt int64) squirrel.SelectBuilder {
leaf := "s" + fmt.Sprint(len(segments)-1) leaf := "s" + strconv.Itoa(len(segments)-1)
// add the current space (leaf) // add the current space (leaf)
stmt := database.Builder. stmt := database.Builder.
@ -910,10 +945,37 @@ func buildRecursiveSelectQueryUsingPath(segments []string, deletedAt int64) squi
Where(leaf+".space_uid = ? AND "+leaf+".space_deleted = ?", segments[len(segments)-1], deletedAt) Where(leaf+".space_uid = ? AND "+leaf+".space_deleted = ?", segments[len(segments)-1], deletedAt)
for i := len(segments) - 2; i >= 0; i-- { for i := len(segments) - 2; i >= 0; i-- {
parentAlias := "s" + fmt.Sprint(i) parentAlias := "s" + strconv.Itoa(i)
alias := "s" + fmt.Sprint(i+1) alias := "s" + strconv.Itoa(i+1)
stmt = stmt.InnerJoin(fmt.Sprintf("spaces %s ON %s.space_id = %s.space_parent_id", parentAlias, parentAlias, alias)). stmt = stmt.InnerJoin(fmt.Sprintf("spaces %s ON %s.space_id = %s.space_parent_id", parentAlias, parentAlias,
alias)).
Where(parentAlias+".space_uid = ?", segments[i])
}
// add parent check for root
stmt = stmt.Where("s0.space_parent_id IS NULL")
return stmt
}
// buildRecursiveSelectQueryUsingCaseInsensitivePath builds the recursive select query using path among active or soft
// deleted spaces.
func buildRecursiveSelectQueryUsingCaseInsensitivePath(segments []string) squirrel.SelectBuilder {
leaf := "s" + strconv.Itoa(len(segments)-1)
// add the current space (leaf)
stmt := database.Builder.
Select(leaf+".space_id").
From("spaces "+leaf).
Where("LOWER("+leaf+".space_uid) = LOWER(?)", segments[len(segments)-1])
for i := len(segments) - 2; i >= 0; i-- {
parentAlias := "s" + strconv.Itoa(i)
alias := "s" + strconv.Itoa(i+1)
stmt = stmt.InnerJoin(fmt.Sprintf("spaces %s ON %s.space_id = %s.space_parent_id", parentAlias, parentAlias,
alias)).
Where(parentAlias+".space_uid = ?", segments[i]) Where(parentAlias+".space_uid = ?", segments[i])
} }

View File

@ -129,6 +129,7 @@ import (
"github.com/harness/gitness/livelog" "github.com/harness/gitness/livelog"
"github.com/harness/gitness/lock" "github.com/harness/gitness/lock"
"github.com/harness/gitness/pubsub" "github.com/harness/gitness/pubsub"
"github.com/harness/gitness/registry/app/pkg/docker"
"github.com/harness/gitness/ssh" "github.com/harness/gitness/ssh"
"github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
@ -270,6 +271,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
aiagent.WireSet, aiagent.WireSet,
capabilities.WireSet, capabilities.WireSet,
capabilitiesservice.WireSet, capabilitiesservice.WireSet,
docker.ProvideReporter,
secretservice.WireSet, secretservice.WireSet,
containerGit.WireSet, containerGit.WireSet,
containerUser.WireSet, containerUser.WireSet,

View File

@ -432,16 +432,17 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
mediaTypesRepository := database2.ProvideMediaTypeDao(db) mediaTypesRepository := database2.ProvideMediaTypeDao(db)
blobRepository := database2.ProvideBlobDao(db, mediaTypesRepository) blobRepository := database2.ProvideBlobDao(db, mediaTypesRepository)
storageService := docker.StorageServiceProvider(config, storageDriver) storageService := docker.StorageServiceProvider(config, storageDriver)
manifestRepository := database2.ProvideManifestDao(db, mediaTypesRepository)
gcService := gc.ServiceProvider() gcService := gc.ServiceProvider()
app := docker.NewApp(ctx, db, storageDeleter, blobRepository, spaceStore, config, storageService, mediaTypesRepository, manifestRepository, gcService) app := docker.NewApp(ctx, db, storageDeleter, blobRepository, spaceStore, config, storageService, gcService)
registryRepository := database2.ProvideRepoDao(db, mediaTypesRepository) registryRepository := database2.ProvideRepoDao(db, mediaTypesRepository)
manifestRepository := database2.ProvideManifestDao(db, mediaTypesRepository)
manifestReferenceRepository := database2.ProvideManifestRefDao(db) manifestReferenceRepository := database2.ProvideManifestRefDao(db)
tagRepository := database2.ProvideTagDao(db) tagRepository := database2.ProvideTagDao(db)
imageRepository := database2.ProvideImageDao(db) imageRepository := database2.ProvideImageDao(db)
artifactRepository := database2.ProvideArtifactDao(db) artifactRepository := database2.ProvideArtifactDao(db)
layerRepository := database2.ProvideLayerDao(db, mediaTypesRepository) layerRepository := database2.ProvideLayerDao(db, mediaTypesRepository)
manifestService := docker.ManifestServiceProvider(registryRepository, manifestRepository, blobRepository, mediaTypesRepository, manifestReferenceRepository, tagRepository, imageRepository, artifactRepository, layerRepository, gcService, transactor) eventReporter := docker.ProvideReporter()
manifestService := docker.ManifestServiceProvider(registryRepository, manifestRepository, blobRepository, mediaTypesRepository, manifestReferenceRepository, tagRepository, imageRepository, artifactRepository, layerRepository, gcService, transactor, eventReporter, spacePathStore)
registryBlobRepository := database2.ProvideRegistryBlobDao(db) registryBlobRepository := database2.ProvideRegistryBlobDao(db)
bandwidthStatRepository := database2.ProvideBandwidthStatDao(db) bandwidthStatRepository := database2.ProvideBandwidthStatDao(db)
downloadStatRepository := database2.ProvideDownloadStatDao(db) downloadStatRepository := database2.ProvideDownloadStatDao(db)

View File

@ -25,19 +25,53 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func GetArtifactMetadata(artifacts *[]types.ArtifactMetadata) []artifactapi.ArtifactMetadata { func GetArtifactMetadata(
artifactMetadataList := make([]artifactapi.ArtifactMetadata, 0, len(*artifacts)) artifacts []types.ArtifactMetadata,
for _, artifact := range *artifacts { rootIdentifier string,
artifactMetadata := mapToArtifactMetadata(artifact) registryURL string,
) []artifactapi.ArtifactMetadata {
artifactMetadataList := make([]artifactapi.ArtifactMetadata, 0, len(artifacts))
for _, artifact := range artifacts {
artifactMetadata := mapToArtifactMetadata(artifact, rootIdentifier, registryURL)
artifactMetadataList = append(artifactMetadataList, *artifactMetadata) artifactMetadataList = append(artifactMetadataList, *artifactMetadata)
} }
return artifactMetadataList return artifactMetadataList
} }
func mapToArtifactMetadata(artifact types.ArtifactMetadata) *artifactapi.ArtifactMetadata { func GetRegistryArtifactMetadata(artifacts []types.ArtifactMetadata) []artifactapi.RegistryArtifactMetadata {
artifactMetadataList := make([]artifactapi.RegistryArtifactMetadata, 0, len(artifacts))
for _, artifact := range artifacts {
artifactMetadata := mapToRegistryArtifactMetadata(artifact)
artifactMetadataList = append(artifactMetadataList, *artifactMetadata)
}
return artifactMetadataList
}
func mapToArtifactMetadata(
artifact types.ArtifactMetadata,
rootIdentifier string,
registryURL string,
) *artifactapi.ArtifactMetadata {
lastModified := GetTimeInMs(artifact.ModifiedAt) lastModified := GetTimeInMs(artifact.ModifiedAt)
packageType := artifact.PackageType packageType := artifact.PackageType
pullCommand := GetPullCommand(rootIdentifier, artifact.RepoName, artifact.Name, artifact.Version,
string(packageType), registryURL)
return &artifactapi.ArtifactMetadata{ return &artifactapi.ArtifactMetadata{
RegistryIdentifier: artifact.RepoName,
Name: artifact.Name,
Version: &artifact.Version,
Labels: &artifact.Labels,
LastModified: &lastModified,
PackageType: &packageType,
DownloadsCount: &artifact.DownloadCount,
PullCommand: &pullCommand,
}
}
func mapToRegistryArtifactMetadata(artifact types.ArtifactMetadata) *artifactapi.RegistryArtifactMetadata {
lastModified := GetTimeInMs(artifact.ModifiedAt)
packageType := artifact.PackageType
return &artifactapi.RegistryArtifactMetadata{
RegistryIdentifier: artifact.RepoName, RegistryIdentifier: artifact.RepoName,
Name: artifact.Name, Name: artifact.Name,
LatestVersion: artifact.LatestVersion, LatestVersion: artifact.LatestVersion,
@ -103,8 +137,15 @@ func GetAllArtifactResponse(
count int64, count int64,
pageNumber int64, pageNumber int64,
pageSize int, pageSize int,
rootIdentifier string,
registryURL string,
) *artifactapi.ListArtifactResponseJSONResponse { ) *artifactapi.ListArtifactResponseJSONResponse {
artifactMetadataList := GetArtifactMetadata(artifacts) var artifactMetadataList []artifactapi.ArtifactMetadata
if artifacts == nil {
artifactMetadataList = make([]artifactapi.ArtifactMetadata, 0)
} else {
artifactMetadataList = GetArtifactMetadata(*artifacts, rootIdentifier, registryURL)
}
pageCount := GetPageCount(count, pageSize) pageCount := GetPageCount(count, pageSize)
listArtifact := &artifactapi.ListArtifact{ listArtifact := &artifactapi.ListArtifact{
ItemCount: &count, ItemCount: &count,
@ -120,6 +161,33 @@ func GetAllArtifactResponse(
return response return response
} }
func GetAllArtifactByRegistryResponse(
artifacts *[]types.ArtifactMetadata,
count int64,
pageNumber int64,
pageSize int,
) *artifactapi.ListRegistryArtifactResponseJSONResponse {
var artifactMetadataList []artifactapi.RegistryArtifactMetadata
if artifacts == nil {
artifactMetadataList = make([]artifactapi.RegistryArtifactMetadata, 0)
} else {
artifactMetadataList = GetRegistryArtifactMetadata(*artifacts)
}
pageCount := GetPageCount(count, pageSize)
listArtifact := &artifactapi.ListRegistryArtifact{
ItemCount: &count,
PageCount: &pageCount,
PageIndex: &pageNumber,
PageSize: &pageSize,
Artifacts: artifactMetadataList,
}
response := &artifactapi.ListRegistryArtifactResponseJSONResponse{
Data: *listArtifact,
Status: artifactapi.StatusSUCCESS,
}
return response
}
func GetAllArtifactLabelsResponse( func GetAllArtifactLabelsResponse(
artifactLabels *[]string, artifactLabels *[]string,
count int64, count int64,

View File

@ -57,6 +57,21 @@ type RegistryRequestInfo struct {
pageNumber int64 pageNumber int64
searchTerm string searchTerm string
labels []string labels []string
registryIDs []string
}
type RegistryRequestParams struct {
packageTypesParam *api.PackageTypeParam
page *api.PageNumber
size *api.PageSize
search *api.SearchTerm
resource string
parentRef string
regRef string
labelsParam *api.LabelsParam
sortOrder *api.SortOrder
sortField *api.SortField
registryIDsParam *api.RegistryIdentifierParam
} }
// GetRegistryRequestBaseInfo returns the base info for the registry request // GetRegistryRequestBaseInfo returns the base info for the registry request
@ -117,50 +132,45 @@ func (c *APIController) GetRegistryRequestBaseInfo(
func (c *APIController) GetRegistryRequestInfo( func (c *APIController) GetRegistryRequestInfo(
ctx context.Context, ctx context.Context,
packageTypesParam *api.PackageTypeParam, registryRequestParams RegistryRequestParams,
page *api.PageNumber,
size *api.PageSize,
search *api.SearchTerm,
resource string,
parentRef string,
regRef string,
labelsParam *api.LabelsParam,
sortOrder *api.SortOrder,
sortField *api.SortField,
) (*RegistryRequestInfo, error) { ) (*RegistryRequestInfo, error) {
packageTypes := []string{} packageTypes := []string{}
if packageTypesParam != nil { if registryRequestParams.packageTypesParam != nil {
packageTypes = *packageTypesParam packageTypes = *registryRequestParams.packageTypesParam
}
registryIDs := []string{}
if registryRequestParams.registryIDsParam != nil {
registryIDs = *registryRequestParams.registryIDsParam
} }
sortByField := "" sortByField := ""
sortByOrder := "" sortByOrder := ""
if sortOrder != nil { if registryRequestParams.sortOrder != nil {
sortByOrder = string(*sortOrder) sortByOrder = string(*registryRequestParams.sortOrder)
} }
if sortField != nil { if registryRequestParams.sortField != nil {
sortByField = string(*sortField) sortByField = string(*registryRequestParams.sortField)
} }
labels := []string{} labels := []string{}
if labelsParam != nil { if registryRequestParams.labelsParam != nil {
labels = *labelsParam labels = *registryRequestParams.labelsParam
} }
sortByField = GetSortByField(sortByField, resource) sortByField = GetSortByField(sortByField, registryRequestParams.resource)
sortByOrder = GetSortByOrder(sortByOrder) sortByOrder = GetSortByOrder(sortByOrder)
offset := GetOffset(size, page) offset := GetOffset(registryRequestParams.size, registryRequestParams.page)
limit := GetPageLimit(size) limit := GetPageLimit(registryRequestParams.size)
pageNumber := GetPageNumber(page) pageNumber := GetPageNumber(registryRequestParams.page)
searchTerm := "" searchTerm := ""
if search != nil { if registryRequestParams.search != nil {
searchTerm = string(*search) searchTerm = string(*registryRequestParams.search)
} }
baseInfo, err := c.GetRegistryRequestBaseInfo(ctx, parentRef, regRef) baseInfo, err := c.GetRegistryRequestBaseInfo(ctx, registryRequestParams.parentRef, registryRequestParams.regRef)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -175,6 +185,7 @@ func (c *APIController) GetRegistryRequestInfo(
pageNumber: pageNumber, pageNumber: pageNumber,
searchTerm: searchTerm, searchTerm: searchTerm,
labels: labels, labels: labels,
registryIDs: registryIDs,
}, nil }, nil
} }

View File

@ -21,7 +21,6 @@ import (
apiauth "github.com/harness/gitness/app/api/auth" apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/request" "github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/registry/app/api/openapi/contracts/artifact" "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/registry/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )
@ -29,20 +28,21 @@ func (c *APIController) GetAllArtifacts(
ctx context.Context, ctx context.Context,
r artifact.GetAllArtifactsRequestObject, r artifact.GetAllArtifactsRequestObject,
) (artifact.GetAllArtifactsResponseObject, error) { ) (artifact.GetAllArtifactsResponseObject, error) {
ref := "" registryRequestParams := &RegistryRequestParams{
if r.Params.RegIdentifier != nil { packageTypesParam: nil,
ref2, err2 := GetRegRef(string(r.SpaceRef), string(*r.Params.RegIdentifier)) page: r.Params.Page,
if err2 != nil { size: r.Params.Size,
return c.getAllArtifacts400JsonResponse(err2) search: r.Params.SearchTerm,
} resource: ArtifactResource,
ref = ref2 parentRef: string(r.SpaceRef),
regRef: "",
labelsParam: nil,
sortOrder: r.Params.SortOrder,
sortField: r.Params.SortField,
registryIDsParam: r.Params.RegIdentifier,
} }
regInfo, err := c.GetRegistryRequestInfo( regInfo, err := c.GetRegistryRequestInfo(ctx, *registryRequestParams)
ctx, r.Params.PackageType, r.Params.Page, r.Params.Size,
r.Params.SearchTerm, ArtifactResource, string(r.SpaceRef), ref, r.Params.Label,
r.Params.SortOrder, r.Params.SortField,
)
if err != nil { if err != nil {
return c.getAllArtifacts400JsonResponse(err) return c.getAllArtifacts400JsonResponse(err)
} }
@ -70,28 +70,16 @@ func (c *APIController) GetAllArtifacts(
), ),
}, nil }, nil
} }
latestVersion := false
var artifacts *[]types.ArtifactMetadata if r.Params.LatestVersion != nil {
var count int64 latestVersion = bool(*r.Params.LatestVersion)
if len(regInfo.RegistryIdentifier) == 0 {
artifacts, err = c.TagStore.GetAllArtifactsByParentID(
ctx, regInfo.parentID, &regInfo.packageTypes,
regInfo.sortByField, regInfo.sortByOrder, regInfo.limit, regInfo.offset, regInfo.searchTerm, regInfo.labels,
)
count, _ = c.TagStore.CountAllArtifactsByParentID(
ctx, regInfo.parentID, &regInfo.packageTypes,
regInfo.searchTerm, regInfo.labels,
)
} else {
artifacts, err = c.TagStore.GetAllArtifactsByRepo(
ctx, regInfo.parentID, regInfo.RegistryIdentifier,
regInfo.sortByField, regInfo.sortByOrder, regInfo.limit, regInfo.offset, regInfo.searchTerm, regInfo.labels,
)
count, _ = c.TagStore.CountAllArtifactsByRepo(
ctx, regInfo.parentID, regInfo.RegistryIdentifier,
regInfo.searchTerm, regInfo.labels,
)
} }
artifacts, err := c.TagStore.GetAllArtifactsByParentID(
ctx, regInfo.parentID, &regInfo.registryIDs,
regInfo.sortByField, regInfo.sortByOrder, regInfo.limit, regInfo.offset, regInfo.searchTerm, latestVersion)
count, _ := c.TagStore.CountAllArtifactsByParentID(
ctx, regInfo.parentID, &regInfo.registryIDs,
regInfo.searchTerm, latestVersion)
if err != nil { if err != nil {
return artifact.GetAllArtifacts500JSONResponse{ return artifact.GetAllArtifacts500JSONResponse{
InternalServerErrorJSONResponse: artifact.InternalServerErrorJSONResponse( InternalServerErrorJSONResponse: artifact.InternalServerErrorJSONResponse(
@ -100,7 +88,8 @@ func (c *APIController) GetAllArtifacts(
}, nil }, nil
} }
return artifact.GetAllArtifacts200JSONResponse{ return artifact.GetAllArtifacts200JSONResponse{
ListArtifactResponseJSONResponse: *GetAllArtifactResponse(artifacts, count, regInfo.pageNumber, regInfo.limit), ListArtifactResponseJSONResponse: *GetAllArtifactResponse(artifacts, count, regInfo.pageNumber, regInfo.limit,
regInfo.RootIdentifier, c.URLProvider.RegistryURL()),
}, nil }, nil
} }

View File

@ -28,11 +28,21 @@ func (c *APIController) ListArtifactLabels(
ctx context.Context, ctx context.Context,
r artifact.ListArtifactLabelsRequestObject, r artifact.ListArtifactLabelsRequestObject,
) (artifact.ListArtifactLabelsResponseObject, error) { ) (artifact.ListArtifactLabelsResponseObject, error) {
registryRequestParams := &RegistryRequestParams{
packageTypesParam: nil,
page: r.Params.Page,
size: r.Params.Size,
search: r.Params.SearchTerm,
resource: ArtifactResource,
parentRef: "",
regRef: string(r.RegistryRef),
labelsParam: nil,
sortOrder: nil,
sortField: nil,
registryIDsParam: nil,
}
regInfo, _ := c.GetRegistryRequestInfo( regInfo, _ := c.GetRegistryRequestInfo(
ctx, nil, r.Params.Page, r.Params.Size, ctx, *registryRequestParams)
r.Params.SearchTerm, ArtifactResource, "", string(r.RegistryRef),
nil, nil, nil,
)
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef) space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil { if err != nil {

View File

@ -36,11 +36,20 @@ func (c *APIController) GetAllArtifactVersions(
ctx context.Context, ctx context.Context,
r artifact.GetAllArtifactVersionsRequestObject, r artifact.GetAllArtifactVersionsRequestObject,
) (artifact.GetAllArtifactVersionsResponseObject, error) { ) (artifact.GetAllArtifactVersionsResponseObject, error) {
regInfo, _ := c.GetRegistryRequestInfo( registryRequestParams := &RegistryRequestParams{
ctx, nil, r.Params.Page, r.Params.Size, packageTypesParam: nil,
r.Params.SearchTerm, ArtifactVersionResource, "", string(r.RegistryRef), page: r.Params.Page,
nil, r.Params.SortOrder, r.Params.SortField, size: r.Params.Size,
) search: r.Params.SearchTerm,
resource: ArtifactVersionResource,
parentRef: "",
regRef: string(r.RegistryRef),
labelsParam: nil,
sortOrder: r.Params.SortOrder,
sortField: r.Params.SortField,
registryIDsParam: nil,
}
regInfo, _ := c.GetRegistryRequestInfo(ctx, *registryRequestParams)
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef) space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil { if err != nil {

View File

@ -32,11 +32,20 @@ func (c *APIController) GetAllRegistries(
ctx context.Context, ctx context.Context,
r artifact.GetAllRegistriesRequestObject, r artifact.GetAllRegistriesRequestObject,
) (artifact.GetAllRegistriesResponseObject, error) { ) (artifact.GetAllRegistriesResponseObject, error) {
regInfo, _ := c.GetRegistryRequestInfo( registryRequestParams := &RegistryRequestParams{
ctx, r.Params.PackageType, r.Params.Page, r.Params.Size, packageTypesParam: nil,
r.Params.SearchTerm, RepositoryResource, string(r.SpaceRef), "", nil, page: r.Params.Page,
r.Params.SortOrder, r.Params.SortField, size: r.Params.Size,
) search: r.Params.SearchTerm,
resource: RepositoryResource,
parentRef: string(r.SpaceRef),
regRef: "",
labelsParam: nil,
sortOrder: r.Params.SortOrder,
sortField: r.Params.SortField,
registryIDsParam: nil,
}
regInfo, _ := c.GetRegistryRequestInfo(ctx, *registryRequestParams)
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef) space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil { if err != nil {

View File

@ -0,0 +1,102 @@
// Copyright 2023 Harness, Inc.
//
// 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 metadata
import (
"context"
"net/http"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/types/enum"
)
func (c *APIController) GetAllArtifactsByRegistry(
ctx context.Context,
r artifact.GetAllArtifactsByRegistryRequestObject,
) (artifact.GetAllArtifactsByRegistryResponseObject, error) {
registryRequestParams := &RegistryRequestParams{
packageTypesParam: nil,
page: r.Params.Page,
size: r.Params.Size,
search: r.Params.SearchTerm,
resource: ArtifactResource,
parentRef: "",
regRef: string(r.RegistryRef),
labelsParam: nil,
sortOrder: r.Params.SortOrder,
sortField: r.Params.SortField,
registryIDsParam: nil,
}
regInfo, err := c.GetRegistryRequestInfo(ctx, *registryRequestParams)
if err != nil {
return c.getAllArtifactsByRegistry400JsonResponse(err)
}
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil {
return artifact.GetAllArtifactsByRegistry400JSONResponse{
BadRequestJSONResponse: artifact.BadRequestJSONResponse(
*GetErrorResponse(http.StatusBadRequest, err.Error()),
),
}, nil
}
session, _ := request.AuthSessionFrom(ctx)
permissionChecks := GetPermissionChecks(space, regInfo.RegistryIdentifier, enum.PermissionRegistryView)
if err = apiauth.CheckRegistry(
ctx,
c.Authorizer,
session,
permissionChecks...,
); err != nil {
return artifact.GetAllArtifactsByRegistry403JSONResponse{
UnauthorizedJSONResponse: artifact.UnauthorizedJSONResponse(
*GetErrorResponse(http.StatusForbidden, err.Error()),
),
}, nil
}
artifacts, err := c.TagStore.GetAllArtifactsByRepo(
ctx, regInfo.parentID, regInfo.RegistryIdentifier,
regInfo.sortByField, regInfo.sortByOrder, regInfo.limit, regInfo.offset, regInfo.searchTerm, regInfo.labels,
)
count, _ := c.TagStore.CountAllArtifactsByRepo(
ctx, regInfo.parentID, regInfo.RegistryIdentifier,
regInfo.searchTerm, regInfo.labels,
)
if err != nil {
return artifact.GetAllArtifactsByRegistry500JSONResponse{
InternalServerErrorJSONResponse: artifact.InternalServerErrorJSONResponse(
*GetErrorResponse(http.StatusInternalServerError, err.Error()),
),
}, nil
}
return artifact.GetAllArtifactsByRegistry200JSONResponse{
ListRegistryArtifactResponseJSONResponse: *GetAllArtifactByRegistryResponse(
artifacts, count, regInfo.pageNumber, regInfo.limit,
),
}, nil
}
func (c *APIController) getAllArtifactsByRegistry400JsonResponse(err error) (
artifact.GetAllArtifactsByRegistryResponseObject, error,
) {
return artifact.GetAllArtifactsByRegistry400JSONResponse{
BadRequestJSONResponse: artifact.BadRequestJSONResponse(
*GetErrorResponse(http.StatusBadRequest, err.Error()),
),
}, nil
}

View File

@ -29,10 +29,20 @@ func (c *APIController) UpdateArtifactLabels(
ctx context.Context, ctx context.Context,
r artifact.UpdateArtifactLabelsRequestObject, r artifact.UpdateArtifactLabelsRequestObject,
) (artifact.UpdateArtifactLabelsResponseObject, error) { ) (artifact.UpdateArtifactLabelsResponseObject, error) {
regInfo, _ := c.GetRegistryRequestInfo( registryRequestParams := &RegistryRequestParams{
ctx, nil, nil, nil, nil, packageTypesParam: nil,
ArtifactVersionResource, "", string(r.RegistryRef), nil, nil, nil, page: nil,
) size: nil,
search: nil,
resource: ArtifactVersionResource,
parentRef: "",
regRef: string(r.RegistryRef),
labelsParam: nil,
sortOrder: nil,
sortField: nil,
registryIDsParam: nil,
}
regInfo, _ := c.GetRegistryRequestInfo(ctx, *registryRequestParams)
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef) space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil { if err != nil {

View File

@ -400,7 +400,7 @@ func GetPermissionChecks(
) []types.PermissionCheck { ) []types.PermissionCheck {
var permissionChecks []types.PermissionCheck var permissionChecks []types.PermissionCheck
permissionCheck := &types.PermissionCheck{ permissionCheck := &types.PermissionCheck{
Scope: types.Scope{SpacePath: space.Identifier}, Scope: types.Scope{SpacePath: space.Path},
Resource: types.Resource{Type: enum.ResourceTypeRegistry, Identifier: registryIdentifier}, Resource: types.Resource{Type: enum.ResourceTypeRegistry, Identifier: registryIdentifier},
Permission: permission, Permission: permission,
} }

View File

@ -16,6 +16,7 @@ package oci
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -34,6 +35,10 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type TokenResponseOCI struct {
Token string `json:"token"`
}
func (h *Handler) GetToken(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetToken(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
session, ok := request.AuthSessionFrom(ctx) session, ok := request.AuthSessionFrom(ctx)
@ -79,8 +84,12 @@ func (h *Handler) GetToken(w http.ResponseWriter, r *http.Request) {
} }
if jwtToken != "" { if jwtToken != "" {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(fmt.Sprintf("{\"token\":\"%s\"}", jwtToken))) enc := json.NewEncoder(w)
if err != nil { if err := enc.Encode(
TokenResponseOCI{
Token: jwtToken,
},
); err != nil {
log.Error().Msgf("failed to write token response: %v", err) log.Error().Msgf("failed to write token response: %v", err)
} }
return return

View File

@ -173,17 +173,44 @@ paths:
- Spaces - Spaces
parameters: parameters:
- $ref: "#/components/parameters/spaceRefPathParam" - $ref: "#/components/parameters/spaceRefPathParam"
- $ref: "#/components/parameters/LabelsParam"
- $ref: "#/components/parameters/packageTypeParam"
- $ref: "#/components/parameters/RegistryIdentifierParam" - $ref: "#/components/parameters/RegistryIdentifierParam"
- $ref: "#/components/parameters/pageNumber" - $ref: "#/components/parameters/pageNumber"
- $ref: "#/components/parameters/pageSize" - $ref: "#/components/parameters/pageSize"
- $ref: "#/components/parameters/sortOrder" - $ref: "#/components/parameters/sortOrder"
- $ref: "#/components/parameters/sortField" - $ref: "#/components/parameters/sortField"
- $ref: "#/components/parameters/searchTerm" - $ref: "#/components/parameters/searchTerm"
- $ref: "#/components/parameters/latestVersion"
responses:
200:
$ref: "#/components/responses/ListArtifactResponse"
400:
$ref: "#/components/responses/BadRequest"
401:
$ref: "#/components/responses/Unauthenticated"
403:
$ref: "#/components/responses/Unauthorized"
404:
$ref: "#/components/responses/NotFound"
500:
$ref: "#/components/responses/InternalServerError"
/registry/{registry_ref}/artifacts:
get:
summary: List Artifacts for Registry
description: Lists all the Artifacts for Registry
operationId: GetAllArtifactsByRegistry
tags:
- Registries
parameters:
- $ref: "#/components/parameters/registryRefPathParam"
- $ref: "#/components/parameters/LabelsParam"
- $ref: "#/components/parameters/pageNumber"
- $ref: "#/components/parameters/pageSize"
- $ref: "#/components/parameters/sortOrder"
- $ref: "#/components/parameters/sortField"
- $ref: "#/components/parameters/searchTerm"
responses: responses:
200: 200:
$ref: "#/components/responses/ListArtifactResponse" $ref: "#/components/responses/ListRegistryArtifactResponse"
400: 400:
$ref: "#/components/responses/BadRequest" $ref: "#/components/responses/BadRequest"
401: 401:
@ -764,6 +791,20 @@ components:
required: required:
- status - status
- data - data
ListRegistryArtifactResponse:
description: response for list artifact
content:
application/json:
schema:
type: object
properties:
status:
$ref: "#/components/schemas/Status"
data:
$ref: "#/components/schemas/ListRegistryArtifact"
required:
- status
- data
ListArtifactVersionResponse: ListArtifactVersionResponse:
description: response for list versions of artifact description: response for list versions of artifact
content: content:
@ -896,6 +937,36 @@ components:
$ref: "#/components/schemas/ArtifactMetadata" $ref: "#/components/schemas/ArtifactMetadata"
required: required:
- artifacts - artifacts
ListRegistryArtifact:
type: object
description: A list of Artifacts
properties:
pageCount:
type: integer
format: int64
description: The total number of pages
example: 100
itemCount:
type: integer
format: int64
description: The total number of items
example: 1
pageSize:
type: integer
description: The number of items per page
example: 1
pageIndex:
type: integer
format: int64
description: The current page
example: 0
artifacts:
type: array
description: A list of Artifact
items:
$ref: "#/components/schemas/RegistryArtifactMetadata"
required:
- artifacts
ListArtifactVersion: ListArtifactVersion:
type: object type: object
description: A list of Artifact versions description: A list of Artifact versions
@ -1029,6 +1100,36 @@ components:
type: type:
$ref: "#/components/schemas/ClientSetupStepType" $ref: "#/components/schemas/ClientSetupStepType"
ArtifactMetadata: ArtifactMetadata:
type: object
description: Artifact Metadata
properties:
name:
type: string
version:
type: string
registryIdentifier:
type: string
registryPath:
type: string
labels:
type: array
items:
type: string
downloadsCount:
type: integer
format: int64
lastModified:
type: string
pullCommand:
type: string
packageType:
$ref: "#/components/schemas/PackageType"
required:
- name
- registryIdentifier
- latestVersion
- registryPath
RegistryArtifactMetadata:
type: object type: object
description: Artifact Metadata description: Artifact Metadata
properties: properties:
@ -1397,7 +1498,7 @@ components:
description: "ClientSetupStepType type" description: "ClientSetupStepType type"
enum: enum:
- Static - Static
- GenerateToken - GenerateToken
Error: Error:
type: object type: object
properties: properties:
@ -1503,7 +1604,9 @@ components:
required: false required: false
description: Registry Identifier description: Registry Identifier
schema: schema:
type: string type: array
items:
type: string
spaceRefPathParam: spaceRefPathParam:
name: space_ref name: space_ref
in: path in: path
@ -1601,6 +1704,13 @@ components:
description: sortField description: sortField
schema: schema:
type: string type: string
latestVersion:
name: latest_version
in: query
required: false
description: Latest Version Filter.
schema:
type: boolean
fromDateParam: fromDateParam:
name: from name: from
in: query in: query
@ -1614,4 +1724,4 @@ components:
required: false required: false
description: Date. Format - MM/DD/YYYY description: Date. Format - MM/DD/YYYY
schema: schema:
type: string type: string

View File

@ -74,6 +74,9 @@ type ServerInterface interface {
// List Artifact Versions // List Artifact Versions
// (GET /registry/{registry_ref}/artifact/{artifact}/versions) // (GET /registry/{registry_ref}/artifact/{artifact}/versions)
GetAllArtifactVersions(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, artifact ArtifactPathParam, params GetAllArtifactVersionsParams) GetAllArtifactVersions(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, artifact ArtifactPathParam, params GetAllArtifactVersionsParams)
// List Artifacts for Registry
// (GET /registry/{registry_ref}/artifacts)
GetAllArtifactsByRegistry(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetAllArtifactsByRegistryParams)
// Returns CLI Client Setup Details // Returns CLI Client Setup Details
// (GET /registry/{registry_ref}/client-setup-details) // (GET /registry/{registry_ref}/client-setup-details)
GetClientSetupDetails(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetClientSetupDetailsParams) GetClientSetupDetails(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetClientSetupDetailsParams)
@ -194,6 +197,12 @@ func (_ Unimplemented) GetAllArtifactVersions(w http.ResponseWriter, r *http.Req
w.WriteHeader(http.StatusNotImplemented) w.WriteHeader(http.StatusNotImplemented)
} }
// List Artifacts for Registry
// (GET /registry/{registry_ref}/artifacts)
func (_ Unimplemented) GetAllArtifactsByRegistry(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetAllArtifactsByRegistryParams) {
w.WriteHeader(http.StatusNotImplemented)
}
// Returns CLI Client Setup Details // Returns CLI Client Setup Details
// (GET /registry/{registry_ref}/client-setup-details) // (GET /registry/{registry_ref}/client-setup-details)
func (_ Unimplemented) GetClientSetupDetails(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetClientSetupDetailsParams) { func (_ Unimplemented) GetClientSetupDetails(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetClientSetupDetailsParams) {
@ -995,6 +1004,83 @@ func (siw *ServerInterfaceWrapper) GetAllArtifactVersions(w http.ResponseWriter,
handler.ServeHTTP(w, r.WithContext(ctx)) handler.ServeHTTP(w, r.WithContext(ctx))
} }
// GetAllArtifactsByRegistry operation middleware
func (siw *ServerInterfaceWrapper) GetAllArtifactsByRegistry(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// ------------- Path parameter "registry_ref" -------------
var registryRef RegistryRefPathParam
err = runtime.BindStyledParameterWithOptions("simple", "registry_ref", chi.URLParam(r, "registry_ref"), &registryRef, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "registry_ref", Err: err})
return
}
// Parameter object where we will unmarshal all parameters from the context
var params GetAllArtifactsByRegistryParams
// ------------- Optional query parameter "label" -------------
err = runtime.BindQueryParameter("form", true, false, "label", r.URL.Query(), &params.Label)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "label", Err: err})
return
}
// ------------- Optional query parameter "page" -------------
err = runtime.BindQueryParameter("form", true, false, "page", r.URL.Query(), &params.Page)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err})
return
}
// ------------- Optional query parameter "size" -------------
err = runtime.BindQueryParameter("form", true, false, "size", r.URL.Query(), &params.Size)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "size", Err: err})
return
}
// ------------- Optional query parameter "sort_order" -------------
err = runtime.BindQueryParameter("form", true, false, "sort_order", r.URL.Query(), &params.SortOrder)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort_order", Err: err})
return
}
// ------------- Optional query parameter "sort_field" -------------
err = runtime.BindQueryParameter("form", true, false, "sort_field", r.URL.Query(), &params.SortField)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort_field", Err: err})
return
}
// ------------- Optional query parameter "search_term" -------------
err = runtime.BindQueryParameter("form", true, false, "search_term", r.URL.Query(), &params.SearchTerm)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "search_term", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetAllArtifactsByRegistry(w, r, registryRef, params)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetClientSetupDetails operation middleware // GetClientSetupDetails operation middleware
func (siw *ServerInterfaceWrapper) GetClientSetupDetails(w http.ResponseWriter, r *http.Request) { func (siw *ServerInterfaceWrapper) GetClientSetupDetails(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -1103,22 +1189,6 @@ func (siw *ServerInterfaceWrapper) GetAllArtifacts(w http.ResponseWriter, r *htt
// Parameter object where we will unmarshal all parameters from the context // Parameter object where we will unmarshal all parameters from the context
var params GetAllArtifactsParams var params GetAllArtifactsParams
// ------------- Optional query parameter "label" -------------
err = runtime.BindQueryParameter("form", true, false, "label", r.URL.Query(), &params.Label)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "label", Err: err})
return
}
// ------------- Optional query parameter "package_type" -------------
err = runtime.BindQueryParameter("form", true, false, "package_type", r.URL.Query(), &params.PackageType)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "package_type", Err: err})
return
}
// ------------- Optional query parameter "reg_identifier" ------------- // ------------- Optional query parameter "reg_identifier" -------------
err = runtime.BindQueryParameter("form", true, false, "reg_identifier", r.URL.Query(), &params.RegIdentifier) err = runtime.BindQueryParameter("form", true, false, "reg_identifier", r.URL.Query(), &params.RegIdentifier)
@ -1167,6 +1237,14 @@ func (siw *ServerInterfaceWrapper) GetAllArtifacts(w http.ResponseWriter, r *htt
return return
} }
// ------------- Optional query parameter "latest_version" -------------
err = runtime.BindQueryParameter("form", true, false, "latest_version", r.URL.Query(), &params.LatestVersion)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "latest_version", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetAllArtifacts(w, r, spaceRef, params) siw.Handler.GetAllArtifacts(w, r, spaceRef, params)
})) }))
@ -1427,6 +1505,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/registry/{registry_ref}/artifact/{artifact}/versions", wrapper.GetAllArtifactVersions) r.Get(options.BaseURL+"/registry/{registry_ref}/artifact/{artifact}/versions", wrapper.GetAllArtifactVersions)
}) })
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/registry/{registry_ref}/artifacts", wrapper.GetAllArtifactsByRegistry)
})
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/registry/{registry_ref}/client-setup-details", wrapper.GetClientSetupDetails) r.Get(options.BaseURL+"/registry/{registry_ref}/client-setup-details", wrapper.GetClientSetupDetails)
}) })
@ -1559,6 +1640,14 @@ type ListArtifactVersionResponseJSONResponse struct {
Status Status `json:"status"` Status Status `json:"status"`
} }
type ListRegistryArtifactResponseJSONResponse struct {
// Data A list of Artifacts
Data ListRegistryArtifact `json:"data"`
// Status Indicates if the request was successful or not
Status Status `json:"status"`
}
type ListRegistryResponseJSONResponse struct { type ListRegistryResponseJSONResponse struct {
// Data A list of Harness Artifact Registries // Data A list of Harness Artifact Registries
Data ListRegistry `json:"data"` Data ListRegistry `json:"data"`
@ -2719,6 +2808,73 @@ func (response GetAllArtifactVersions500JSONResponse) VisitGetAllArtifactVersion
return json.NewEncoder(w).Encode(response) return json.NewEncoder(w).Encode(response)
} }
type GetAllArtifactsByRegistryRequestObject struct {
RegistryRef RegistryRefPathParam `json:"registry_ref"`
Params GetAllArtifactsByRegistryParams
}
type GetAllArtifactsByRegistryResponseObject interface {
VisitGetAllArtifactsByRegistryResponse(w http.ResponseWriter) error
}
type GetAllArtifactsByRegistry200JSONResponse struct {
ListRegistryArtifactResponseJSONResponse
}
func (response GetAllArtifactsByRegistry200JSONResponse) VisitGetAllArtifactsByRegistryResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type GetAllArtifactsByRegistry400JSONResponse struct{ BadRequestJSONResponse }
func (response GetAllArtifactsByRegistry400JSONResponse) VisitGetAllArtifactsByRegistryResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
return json.NewEncoder(w).Encode(response)
}
type GetAllArtifactsByRegistry401JSONResponse struct{ UnauthenticatedJSONResponse }
func (response GetAllArtifactsByRegistry401JSONResponse) VisitGetAllArtifactsByRegistryResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(401)
return json.NewEncoder(w).Encode(response)
}
type GetAllArtifactsByRegistry403JSONResponse struct{ UnauthorizedJSONResponse }
func (response GetAllArtifactsByRegistry403JSONResponse) VisitGetAllArtifactsByRegistryResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(403)
return json.NewEncoder(w).Encode(response)
}
type GetAllArtifactsByRegistry404JSONResponse struct{ NotFoundJSONResponse }
func (response GetAllArtifactsByRegistry404JSONResponse) VisitGetAllArtifactsByRegistryResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(404)
return json.NewEncoder(w).Encode(response)
}
type GetAllArtifactsByRegistry500JSONResponse struct {
InternalServerErrorJSONResponse
}
func (response GetAllArtifactsByRegistry500JSONResponse) VisitGetAllArtifactsByRegistryResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
return json.NewEncoder(w).Encode(response)
}
type GetClientSetupDetailsRequestObject struct { type GetClientSetupDetailsRequestObject struct {
RegistryRef RegistryRefPathParam `json:"registry_ref"` RegistryRef RegistryRefPathParam `json:"registry_ref"`
Params GetClientSetupDetailsParams Params GetClientSetupDetailsParams
@ -3040,6 +3196,9 @@ type StrictServerInterface interface {
// List Artifact Versions // List Artifact Versions
// (GET /registry/{registry_ref}/artifact/{artifact}/versions) // (GET /registry/{registry_ref}/artifact/{artifact}/versions)
GetAllArtifactVersions(ctx context.Context, request GetAllArtifactVersionsRequestObject) (GetAllArtifactVersionsResponseObject, error) GetAllArtifactVersions(ctx context.Context, request GetAllArtifactVersionsRequestObject) (GetAllArtifactVersionsResponseObject, error)
// List Artifacts for Registry
// (GET /registry/{registry_ref}/artifacts)
GetAllArtifactsByRegistry(ctx context.Context, request GetAllArtifactsByRegistryRequestObject) (GetAllArtifactsByRegistryResponseObject, error)
// Returns CLI Client Setup Details // Returns CLI Client Setup Details
// (GET /registry/{registry_ref}/client-setup-details) // (GET /registry/{registry_ref}/client-setup-details)
GetClientSetupDetails(ctx context.Context, request GetClientSetupDetailsRequestObject) (GetClientSetupDetailsResponseObject, error) GetClientSetupDetails(ctx context.Context, request GetClientSetupDetailsRequestObject) (GetClientSetupDetailsResponseObject, error)
@ -3571,6 +3730,33 @@ func (sh *strictHandler) GetAllArtifactVersions(w http.ResponseWriter, r *http.R
} }
} }
// GetAllArtifactsByRegistry operation middleware
func (sh *strictHandler) GetAllArtifactsByRegistry(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetAllArtifactsByRegistryParams) {
var request GetAllArtifactsByRegistryRequestObject
request.RegistryRef = registryRef
request.Params = params
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.GetAllArtifactsByRegistry(ctx, request.(GetAllArtifactsByRegistryRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetAllArtifactsByRegistry")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(GetAllArtifactsByRegistryResponseObject); ok {
if err := validResponse.VisitGetAllArtifactsByRegistryResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// GetClientSetupDetails operation middleware // GetClientSetupDetails operation middleware
func (sh *strictHandler) GetClientSetupDetails(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetClientSetupDetailsParams) { func (sh *strictHandler) GetClientSetupDetails(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetClientSetupDetailsParams) {
var request GetClientSetupDetailsRequestObject var request GetClientSetupDetailsRequestObject
@ -3682,69 +3868,70 @@ func (sh *strictHandler) GetAllRegistries(w http.ResponseWriter, r *http.Request
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
"y6/L/wUAAP//B0v/XHmOAAA=", "S3S0xIYJdE1RVKHgHUijaL+GVO87spbCDOfR7P6l0F/ZV6cW3NW5qPCivocrv4PrN75cLIUpyzloAnIQ",
"mXtbAPObH4PWeqidTW8H1ecVsrknsw9MnXVOeJ37bHy9X+uxdWiw+rL6XwAAAP//+v5kDP2XAAA=",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View File

@ -75,13 +75,14 @@ type ArtifactMetadata struct {
DownloadsCount *int64 `json:"downloadsCount,omitempty"` DownloadsCount *int64 `json:"downloadsCount,omitempty"`
Labels *[]string `json:"labels,omitempty"` Labels *[]string `json:"labels,omitempty"`
LastModified *string `json:"lastModified,omitempty"` LastModified *string `json:"lastModified,omitempty"`
LatestVersion string `json:"latestVersion"`
Name string `json:"name"` Name string `json:"name"`
// PackageType refers to package // PackageType refers to package
PackageType *PackageType `json:"packageType,omitempty"` PackageType *PackageType `json:"packageType,omitempty"`
PullCommand *string `json:"pullCommand,omitempty"`
RegistryIdentifier string `json:"registryIdentifier"` RegistryIdentifier string `json:"registryIdentifier"`
RegistryPath string `json:"registryPath"` RegistryPath string `json:"registryPath"`
Version *string `json:"version,omitempty"`
} }
// ArtifactStats Harness Artifact Stats // ArtifactStats Harness Artifact Stats
@ -321,6 +322,24 @@ type ListRegistry struct {
Registries []RegistryMetadata `json:"registries"` Registries []RegistryMetadata `json:"registries"`
} }
// ListRegistryArtifact A list of Artifacts
type ListRegistryArtifact struct {
// Artifacts A list of Artifact
Artifacts []RegistryArtifactMetadata `json:"artifacts"`
// ItemCount The total number of items
ItemCount *int64 `json:"itemCount,omitempty"`
// PageCount The total number of pages
PageCount *int64 `json:"pageCount,omitempty"`
// PageIndex The current page
PageIndex *int64 `json:"pageIndex,omitempty"`
// PageSize The number of items per page
PageSize *int `json:"pageSize,omitempty"`
}
// PackageType refers to package // PackageType refers to package
type PackageType string type PackageType string
@ -343,6 +362,20 @@ type Registry struct {
Url string `json:"url"` Url string `json:"url"`
} }
// RegistryArtifactMetadata Artifact Metadata
type RegistryArtifactMetadata struct {
DownloadsCount *int64 `json:"downloadsCount,omitempty"`
Labels *[]string `json:"labels,omitempty"`
LastModified *string `json:"lastModified,omitempty"`
LatestVersion string `json:"latestVersion"`
Name string `json:"name"`
// PackageType refers to package
PackageType *PackageType `json:"packageType,omitempty"`
RegistryIdentifier string `json:"registryIdentifier"`
RegistryPath string `json:"registryPath"`
}
// RegistryConfig SubConfig specific for Virtual or Upstream Registry // RegistryConfig SubConfig specific for Virtual or Upstream Registry
type RegistryConfig struct { type RegistryConfig struct {
// Type refers to type of registry i.e virtual or upstream // Type refers to type of registry i.e virtual or upstream
@ -427,7 +460,7 @@ type VirtualConfig struct {
type LabelsParam []string type LabelsParam []string
// RegistryIdentifierParam defines model for RegistryIdentifierParam. // RegistryIdentifierParam defines model for RegistryIdentifierParam.
type RegistryIdentifierParam string type RegistryIdentifierParam []string
// RegistryTypeParam defines model for RegistryTypeParam. // RegistryTypeParam defines model for RegistryTypeParam.
type RegistryTypeParam string type RegistryTypeParam string
@ -444,6 +477,9 @@ type DigestParam string
// FromDateParam defines model for fromDateParam. // FromDateParam defines model for fromDateParam.
type FromDateParam string type FromDateParam string
// LatestVersion defines model for latestVersion.
type LatestVersion bool
// PackageTypeParam defines model for packageTypeParam. // PackageTypeParam defines model for packageTypeParam.
type PackageTypeParam []string type PackageTypeParam []string
@ -612,6 +648,15 @@ type ListArtifactVersionResponse struct {
Status Status `json:"status"` Status Status `json:"status"`
} }
// ListRegistryArtifactResponse defines model for ListRegistryArtifactResponse.
type ListRegistryArtifactResponse struct {
// Data A list of Artifacts
Data ListRegistryArtifact `json:"data"`
// Status Indicates if the request was successful or not
Status Status `json:"status"`
}
// ListRegistryResponse defines model for ListRegistryResponse. // ListRegistryResponse defines model for ListRegistryResponse.
type ListRegistryResponse struct { type ListRegistryResponse struct {
// Data A list of Harness Artifact Registries // Data A list of Harness Artifact Registries
@ -717,6 +762,27 @@ type GetAllArtifactVersionsParams struct {
SearchTerm *SearchTerm `form:"search_term,omitempty" json:"search_term,omitempty"` SearchTerm *SearchTerm `form:"search_term,omitempty" json:"search_term,omitempty"`
} }
// GetAllArtifactsByRegistryParams defines parameters for GetAllArtifactsByRegistry.
type GetAllArtifactsByRegistryParams struct {
// Label Label.
Label *LabelsParam `form:"label,omitempty" json:"label,omitempty"`
// Page Current page number
Page *PageNumber `form:"page,omitempty" json:"page,omitempty"`
// Size Number of items per page
Size *PageSize `form:"size,omitempty" json:"size,omitempty"`
// SortOrder sortOrder
SortOrder *SortOrder `form:"sort_order,omitempty" json:"sort_order,omitempty"`
// SortField sortField
SortField *SortField `form:"sort_field,omitempty" json:"sort_field,omitempty"`
// SearchTerm search Term.
SearchTerm *SearchTerm `form:"search_term,omitempty" json:"search_term,omitempty"`
}
// GetClientSetupDetailsParams defines parameters for GetClientSetupDetails. // GetClientSetupDetailsParams defines parameters for GetClientSetupDetails.
type GetClientSetupDetailsParams struct { type GetClientSetupDetailsParams struct {
// Artifact Artifat // Artifact Artifat
@ -737,12 +803,6 @@ type GetArtifactStatsForSpaceParams struct {
// GetAllArtifactsParams defines parameters for GetAllArtifacts. // GetAllArtifactsParams defines parameters for GetAllArtifacts.
type GetAllArtifactsParams struct { type GetAllArtifactsParams struct {
// Label Label.
Label *LabelsParam `form:"label,omitempty" json:"label,omitempty"`
// PackageType Registry Package Type
PackageType *PackageTypeParam `form:"package_type,omitempty" json:"package_type,omitempty"`
// RegIdentifier Registry Identifier // RegIdentifier Registry Identifier
RegIdentifier *RegistryIdentifierParam `form:"reg_identifier,omitempty" json:"reg_identifier,omitempty"` RegIdentifier *RegistryIdentifierParam `form:"reg_identifier,omitempty" json:"reg_identifier,omitempty"`
@ -760,6 +820,9 @@ type GetAllArtifactsParams struct {
// SearchTerm search Term. // SearchTerm search Term.
SearchTerm *SearchTerm `form:"search_term,omitempty" json:"search_term,omitempty"` SearchTerm *SearchTerm `form:"search_term,omitempty" json:"search_term,omitempty"`
// LatestVersion Latest Version Filter.
LatestVersion *LatestVersion `form:"latest_version,omitempty" json:"latest_version,omitempty"`
} }
// GetAllRegistriesParams defines parameters for GetAllRegistries. // GetAllRegistriesParams defines parameters for GetAllRegistries.

View File

@ -30,6 +30,7 @@ import (
"github.com/harness/gitness/registry/app/pkg/docker" "github.com/harness/gitness/registry/app/pkg/docker"
"github.com/harness/gitness/registry/app/store/database" "github.com/harness/gitness/registry/app/store/database"
"github.com/harness/gitness/registry/config" "github.com/harness/gitness/registry/config"
"github.com/harness/gitness/registry/gc"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/google/wire" "github.com/google/wire"
@ -64,9 +65,11 @@ func BlobStorageProvider(c *types.Config) (storagedriver.StorageDriver, error) {
return d, err return d, err
} }
func NewHandlerProvider(controller *docker.Controller, spaceStore corestore.SpaceStore, func NewHandlerProvider(
controller *docker.Controller, spaceStore corestore.SpaceStore,
tokenStore corestore.TokenStore, userCtrl *usercontroller.Controller, authenticator authn.Authenticator, tokenStore corestore.TokenStore, userCtrl *usercontroller.Controller, authenticator authn.Authenticator,
urlProvider urlprovider.Provider, authorizer authz.Authorizer) *ocihandler.Handler { urlProvider urlprovider.Provider, authorizer authz.Authorizer,
) *ocihandler.Handler {
return ocihandler.NewHandler(controller, spaceStore, tokenStore, userCtrl, authenticator, urlProvider, authorizer) return ocihandler.NewHandler(controller, spaceStore, tokenStore, userCtrl, authenticator, urlProvider, authorizer)
} }
@ -77,6 +80,7 @@ var WireSet = wire.NewSet(
pkg.WireSet, pkg.WireSet,
docker.WireSet, docker.WireSet,
router.WireSet, router.WireSet,
gc.WireSet,
) )
func Wire(_ *types.Config) (RegistryApp, error) { func Wire(_ *types.Config) (RegistryApp, error) {

View File

@ -177,8 +177,9 @@ func (d *driver) PutContent(ctx context.Context, subPath string, contents []byte
// Reader retrieves an io.ReadCloser for the content stored at "path" with a // Reader retrieves an io.ReadCloser for the content stored at "path" with a
// given byte offset. // given byte offset.
func (d *driver) Reader(_ context.Context, path string, offset int64) (io.ReadCloser, error) { func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
file, err := os.OpenFile(d.fullPath(path), os.O_RDONLY, 0o644) file, err := os.OpenFile(d.fullPath(path), os.O_RDONLY, 0o644)
log.Ctx(ctx).Info().Msgf("Opening file %s %s", d.fullPath(path), d.rootDirectory)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, storagedriver.PathNotFoundError{Path: path} return nil, storagedriver.PathNotFoundError{Path: path}

View File

@ -0,0 +1,66 @@
// Copyright 2023 Harness, Inc.
//
// 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 event
import (
"context"
"fmt"
"github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
)
type PackageType int32
type ArtifactDetails struct {
RegistryID string `json:"registry_id,omitempty"`
ImagePath string `json:"image_path,omitempty"` // format = image:tag
PackageType PackageType `json:"package_type,omitempty"`
}
// PackageType constants using iota.
const (
PackageTypeDOCKER = iota
PackageTypeGENERIC
PackageTypeHELM
PackageTypeMAVEN
)
var PackageTypeValue = map[string]PackageType{
string(artifact.PackageTypeDOCKER): PackageTypeDOCKER,
string(artifact.PackageTypeGENERIC): PackageTypeGENERIC,
string(artifact.PackageTypeHELM): PackageTypeHELM,
string(artifact.PackageTypeMAVEN): PackageTypeMAVEN,
}
// GetPackageTypeFromString returns the PackageType constant corresponding to the given string value.
func GetPackageTypeFromString(value string) (PackageType, error) {
if val, ok := PackageTypeValue[value]; ok {
return val, nil
}
return 0, fmt.Errorf("invalid PackageType string value: %s", value)
}
type Reporter interface {
ReportEvent(
ctx context.Context, payload interface{}, spacePath string,
) // format of spacePath = acctID/orgID/projectID
}
type Noop struct {
}
func (*Noop) ReportEvent(_ context.Context, _ interface{}, _ string) {
// no implementation
}

View File

@ -55,7 +55,6 @@ func NewApp(
ctx context.Context, sqlDB *sqlx.DB, storageDeleter storagedriver.StorageDeleter, ctx context.Context, sqlDB *sqlx.DB, storageDeleter storagedriver.StorageDeleter,
blobRepo store.BlobRepository, spaceStore corestore.SpaceStore, blobRepo store.BlobRepository, spaceStore corestore.SpaceStore,
cfg *types.Config, storageService *registrystorage.Service, cfg *types.Config, storageService *registrystorage.Service,
mtRepository store.MediaTypesRepository, manifestRepository store.ManifestRepository,
gcService gc.Service, gcService gc.Service,
) *App { ) *App {
app := &App{ app := &App{
@ -64,7 +63,7 @@ func NewApp(
storageService: storageService, storageService: storageService,
} }
app.configureSecret(cfg) app.configureSecret(cfg)
gcService.Start(ctx, sqlDB, spaceStore, blobRepo, storageDeleter, cfg, mtRepository, manifestRepository) gcService.Start(ctx, sqlDB, spaceStore, blobRepo, storageDeleter, cfg)
return app return app
} }

View File

@ -52,7 +52,7 @@ func GetRegistryCheckAccess(
for i := range reqPermissions { for i := range reqPermissions {
permissionCheck := types.PermissionCheck{ permissionCheck := types.PermissionCheck{
Permission: reqPermissions[i], Permission: reqPermissions[i],
Scope: types.Scope{SpacePath: space.Identifier}, Scope: types.Scope{SpacePath: space.Path},
Resource: types.Resource{ Resource: types.Resource{
Type: enum.ResourceTypeRegistry, Type: enum.ResourceTypeRegistry,
Identifier: registry.Name, Identifier: registry.Name,

View File

@ -23,6 +23,8 @@ import (
"fmt" "fmt"
"time" "time"
gitnessappstore "github.com/harness/gitness/app/store"
"github.com/harness/gitness/registry/app/event"
"github.com/harness/gitness/registry/app/manifest" "github.com/harness/gitness/registry/app/manifest"
"github.com/harness/gitness/registry/app/manifest/manifestlist" "github.com/harness/gitness/registry/app/manifest/manifestlist"
"github.com/harness/gitness/registry/app/manifest/ocischema" "github.com/harness/gitness/registry/app/manifest/ocischema"
@ -33,7 +35,8 @@ import (
"github.com/harness/gitness/registry/app/store/database/util" "github.com/harness/gitness/registry/app/store/database/util"
"github.com/harness/gitness/registry/gc" "github.com/harness/gitness/registry/gc"
"github.com/harness/gitness/registry/types" "github.com/harness/gitness/registry/types"
store2 "github.com/harness/gitness/store" gitnessstore "github.com/harness/gitness/store"
db "github.com/harness/gitness/store/database"
"github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/store/database/dbtx"
"github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3"
@ -53,8 +56,10 @@ type manifestService struct {
imageDao store.ImageRepository imageDao store.ImageRepository
artifactDao store.ArtifactRepository artifactDao store.ArtifactRepository
manifestRefDao store.ManifestReferenceRepository manifestRefDao store.ManifestReferenceRepository
spacePathStore gitnessappstore.SpacePathStore
gcService gc.Service gcService gc.Service
tx dbtx.Transactor tx dbtx.Transactor
reporter event.Reporter
} }
func NewManifestService( func NewManifestService(
@ -62,7 +67,7 @@ func NewManifestService(
blobRepo store.BlobRepository, mtRepository store.MediaTypesRepository, tagDao store.TagRepository, blobRepo store.BlobRepository, mtRepository store.MediaTypesRepository, tagDao store.TagRepository,
imageDao store.ImageRepository, artifactDao store.ArtifactRepository, imageDao store.ImageRepository, artifactDao store.ArtifactRepository,
layerDao store.LayerRepository, manifestRefDao store.ManifestReferenceRepository, layerDao store.LayerRepository, manifestRefDao store.ManifestReferenceRepository,
tx dbtx.Transactor, gcService gc.Service, tx dbtx.Transactor, gcService gc.Service, reporter event.Reporter, spacePathStore gitnessappstore.SpacePathStore,
) ManifestService { ) ManifestService {
return &manifestService{ return &manifestService{
registryDao: registryDao, registryDao: registryDao,
@ -76,6 +81,8 @@ func NewManifestService(
manifestRefDao: manifestRefDao, manifestRefDao: manifestRefDao,
gcService: gcService, gcService: gcService,
tx: tx, tx: tx,
reporter: reporter,
spacePathStore: spacePathStore,
} }
} }
@ -166,78 +173,154 @@ func (l *manifestService) dbTagManifest(
tagName, imageName string, tagName, imageName string,
info pkg.RegistryInfo, info pkg.RegistryInfo,
) error { ) error {
dbRepo, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier) dbRegistry, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
if err != nil { if err != nil {
return err return formatFailedToTagErr(err)
} }
newDigest, err := types.NewDigest(dgst) newDigest, err := types.NewDigest(dgst)
if err != nil { if err != nil {
return err return formatFailedToTagErr(err)
}
dbManifest, err := l.manifestDao.FindManifestByDigest(ctx, dbRegistry.ID, info.Image, newDigest)
if errors.Is(err, gitnessstore.ErrResourceNotFound) {
return fmt.Errorf("manifest %s not found in database", dgst)
} }
dbManifest, err := l.manifestDao.FindManifestByDigest(ctx, dbRepo.ID, info.Image, newDigest)
if err != nil { if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) { return formatFailedToTagErr(err)
return fmt.Errorf("manifest %s not found in database", dgst) }
err = l.tx.WithTx(ctx, func(ctx context.Context) error {
// Prevent long running transactions by setting an upper limit of manifestTagGCLockTimeout. If the GC is holding
// the lock of a related review record, the processing there should be fast enough to avoid this. Regardless, we
// should not let transactions open (and clients waiting) for too long. If this sensible timeout is exceeded, abort
// the tag creation and let the client retry. This will bubble up and lead to a 503 Service Unavailable response.
// Set timeout for the transaction to prevent long-running operations
ctx, cancel := context.WithTimeout(ctx, manifestTagGCLockTimeout)
defer cancel()
// Attempt to find and lock the manifest for GC review
if err := l.lockManifestForGC(ctx, dbRegistry.ID, dbManifest.ID); err != nil {
return formatFailedToTagErr(err)
} }
// Create or update artifact and tag records
if err := l.createOrUpdateArtifactAndTag(ctx, dbRegistry.ID, dbManifest.ID, imageName, tagName, dgst); err != nil {
return formatFailedToTagErr(err)
}
return nil
})
if err != nil {
return formatFailedToTagErr(err)
}
spacePath, packageType, err := l.getSpacePathAndPackageType(ctx, dbRegistry)
if err == nil {
l.reportEventAsync(ctx, info.RegIdentifier, imageName, tagName, packageType, spacePath)
} else {
log.Ctx(ctx).Err(err).Msg("Failed to find spacePath, not publishing event")
}
return nil
}
func formatFailedToTagErr(err error) error {
return fmt.Errorf("failed to tag manifest: %w", err)
}
// Locks the manifest for GC review.
func (l *manifestService) lockManifestForGC(ctx context.Context, repoID, manifestID int64) error {
_, err := l.gcService.ManifestFindAndLockBefore(
ctx, repoID, manifestID,
time.Now().Add(manifestTagGCReviewWindow),
)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
// Use ProcessSQLErrorf for handling the SQL error abstraction
return db.ProcessSQLErrorf(
ctx,
err,
"failed to lock manifest for GC review [repoID: %d, manifestID: %d]", repoID, manifestID,
)
}
return nil
}
// Creates or updates artifact and tag records.
func (l *manifestService) createOrUpdateArtifactAndTag(
ctx context.Context,
registryID,
manifestID int64,
imageName,
tagName string,
dgst digest.Digest,
) error {
image := &types.Image{
Name: imageName,
RegistryID: registryID,
Enabled: true,
}
if err := l.imageDao.CreateOrUpdate(ctx, image); err != nil {
return err return err
} }
// We need to find and lock a GC manifest task that is related with the manifest that we're about to tag. This digest, err := types.NewDigest(dgst)
// is needed to ensure we lock any related online GC tasks to prevent race conditions around the tag creation. See: if err != nil {
return err
}
artifact := &types.Artifact{
ImageID: image.ID,
Version: digest.String(),
}
return l.tx.WithTx( if err := l.artifactDao.CreateOrUpdate(ctx, artifact); err != nil {
ctx, func(ctx context.Context) error { return err
// Prevent long running transactions by setting an upper limit of manifestTagGCLockTimeout. If the GC is holding }
// the lock of a related review record, the processing there should be fast enough to avoid this. Regardless, we
// should not let transactions open (and clients waiting) for too long. If this sensible timeout is exceeded, abort
// the tag creation and let the client retry. This will bubble up and lead to a 503 Service Unavailable response.
ctx, cancel := context.WithTimeout(ctx, manifestTagGCLockTimeout)
defer cancel()
if _, err := l.gcService.ManifestFindAndLockBefore( tag := &types.Tag{
ctx, dbRepo.ID, dbManifest.ID, Name: tagName,
time.Now().Add(manifestTagGCReviewWindow), ImageName: imageName,
); err != nil && !errors.Is(err, sql.ErrNoRows) { RegistryID: registryID,
return err ManifestID: manifestID,
} }
image := &types.Image{ return l.tagDao.CreateOrUpdate(ctx, tag)
Name: imageName, }
RegistryID: dbRepo.ID,
Enabled: true,
}
if err := l.imageDao.CreateOrUpdate(ctx, image); err != nil { // Retrieves the spacePath and packageType.
return err func (l *manifestService) getSpacePathAndPackageType(
} ctx context.Context,
dbRepo *types.Registry,
) (string, event.PackageType, error) {
spacePath, err := l.spacePathStore.FindPrimaryBySpaceID(ctx, dbRepo.ParentID)
if err != nil {
log.Ctx(ctx).Err(err).Msg("Failed to find spacePath")
return "", event.PackageType(0), err
}
digest, err := types.NewDigest(dgst) packageType, err := event.GetPackageTypeFromString(string(dbRepo.PackageType))
if err != nil { if err != nil {
return err log.Ctx(ctx).Err(err).Msg("Failed to find packageType")
} return "", event.PackageType(0), err
artifact := &types.Artifact{ }
ImageID: image.ID,
Version: digest.String(),
}
if err := l.artifactDao.CreateOrUpdate(ctx, artifact); err != nil { return spacePath.Value, packageType, nil
return err }
}
tag := &types.Tag{ // Reports event asynchronously.
Name: tagName, func (l *manifestService) reportEventAsync(
ImageName: imageName, ctx context.Context,
RegistryID: dbRepo.ID, regID,
ManifestID: dbManifest.ID, imageName,
} tagName string,
packageType event.PackageType,
if err := l.tagDao.CreateOrUpdate(ctx, tag); err != nil { spacePath string,
return err ) {
} go l.reporter.ReportEvent(ctx, &event.ArtifactDetails{
RegistryID: regID,
return nil ImagePath: imageName + ":" + tagName,
}, PackageType: packageType,
) }, spacePath)
} }
func (l *manifestService) DBPut( func (l *manifestService) DBPut(
@ -327,7 +410,7 @@ func (l *manifestService) dbPutManifestV2(
} }
dbManifest, err := l.manifestDao.FindManifestByDigest(ctx, dbRepo.ID, info.Image, dgst) dbManifest, err := l.manifestDao.FindManifestByDigest(ctx, dbRepo.ID, info.Image, dgst)
if err != nil && !errors.Is(err, store2.ErrResourceNotFound) { if err != nil && !errors.Is(err, gitnessstore.ErrResourceNotFound) {
return err return err
} }
@ -433,7 +516,7 @@ func (l *manifestService) DBFindRepositoryBlob(
image := info.Image image := info.Image
b, err := l.blobRepo.FindByDigestAndRepoID(ctx, desc.Digest, repoID, image) b, err := l.blobRepo.FindByDigestAndRepoID(ctx, desc.Digest, repoID, image)
if err != nil { if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) { if errors.Is(err, gitnessstore.ErrResourceNotFound) {
return nil, fmt.Errorf("blob not found in database") return nil, fmt.Errorf("blob not found in database")
} }
return nil, err return nil, err
@ -453,11 +536,11 @@ func (l *manifestService) handleSubject(
return err return err
} }
dbSubject, err := l.manifestDao.FindManifestByDigest(ctx, dbRepo.ID, info.Image, subjectDigest) dbSubject, err := l.manifestDao.FindManifestByDigest(ctx, dbRepo.ID, info.Image, subjectDigest)
if err != nil && !errors.Is(err, store2.ErrResourceNotFound) { if err != nil && !errors.Is(err, gitnessstore.ErrResourceNotFound) {
return err return err
} }
if errors.Is(err, store2.ErrResourceNotFound) { if errors.Is(err, gitnessstore.ErrResourceNotFound) {
// in case something happened to the referenced manifest after validation // in case something happened to the referenced manifest after validation
// return distribution.ManifestBlobUnknownError{Digest: subject.Digest} // return distribution.ManifestBlobUnknownError{Digest: subject.Digest}
log.Ctx(ctx).Warn().Msgf("subject manifest not found in database") log.Ctx(ctx).Warn().Msgf("subject manifest not found in database")
@ -516,7 +599,7 @@ func (l *manifestService) dbPutManifestList(
} }
ml, err := l.manifestDao.FindManifestByDigest(ctx, r.ID, info.Image, dgst) ml, err := l.manifestDao.FindManifestByDigest(ctx, r.ID, info.Image, dgst)
if err != nil && !errors.Is(err, store2.ErrResourceNotFound) { if err != nil && !errors.Is(err, gitnessstore.ErrResourceNotFound) {
return err return err
} }
@ -625,7 +708,7 @@ func (l *manifestService) dbPutImageIndex(
} }
mi, err := l.manifestDao.FindManifestByDigest(ctx, r.ID, info.Image, dgst) mi, err := l.manifestDao.FindManifestByDigest(ctx, r.ID, info.Image, dgst)
if err != nil && !errors.Is(err, store2.ErrResourceNotFound) { if err != nil && !errors.Is(err, gitnessstore.ErrResourceNotFound) {
return err return err
} }
@ -758,7 +841,7 @@ func (l *manifestService) dbFindManifestListManifest(
imageName, dgst, imageName, dgst,
) )
if err != nil { if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) { if errors.Is(err, gitnessstore.ErrResourceNotFound) {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"manifest %s not found for %s/%s", digest.String(), "manifest %s not found for %s/%s", digest.String(),
repository.Name, imageName, repository.Name, imageName,
@ -849,7 +932,7 @@ func (l *manifestService) DeleteManifest(
} }
m, err := l.manifestDao.FindManifestByDigest(ctx, registry.ID, imageName, newDigest) m, err := l.manifestDao.FindManifestByDigest(ctx, registry.ID, imageName, newDigest)
if err != nil { if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) { if errors.Is(err, gitnessstore.ErrResourceNotFound) {
return util.ErrManifestNotFound return util.ErrManifestNotFound
} }
return err return err

View File

@ -16,8 +16,9 @@ package docker
import ( import (
"github.com/harness/gitness/app/auth/authz" "github.com/harness/gitness/app/auth/authz"
corestore "github.com/harness/gitness/app/store" gitnessstore "github.com/harness/gitness/app/store"
storagedriver "github.com/harness/gitness/registry/app/driver" storagedriver "github.com/harness/gitness/registry/app/driver"
"github.com/harness/gitness/registry/app/event"
"github.com/harness/gitness/registry/app/pkg" "github.com/harness/gitness/registry/app/pkg"
"github.com/harness/gitness/registry/app/storage" "github.com/harness/gitness/registry/app/storage"
"github.com/harness/gitness/registry/app/store" "github.com/harness/gitness/registry/app/store"
@ -49,17 +50,17 @@ func ManifestServiceProvider(
manifestDao store.ManifestRepository, blobRepo store.BlobRepository, mtRepository store.MediaTypesRepository, manifestDao store.ManifestRepository, blobRepo store.BlobRepository, mtRepository store.MediaTypesRepository,
manifestRefDao store.ManifestReferenceRepository, tagDao store.TagRepository, imageDao store.ImageRepository, manifestRefDao store.ManifestReferenceRepository, tagDao store.TagRepository, imageDao store.ImageRepository,
artifactDao store.ArtifactRepository, layerDao store.LayerRepository, artifactDao store.ArtifactRepository, layerDao store.LayerRepository,
gcService gc.Service, tx dbtx.Transactor, gcService gc.Service, tx dbtx.Transactor, reporter event.Reporter, spacePathStore gitnessstore.SpacePathStore,
) ManifestService { ) ManifestService {
return NewManifestService( return NewManifestService(
registryDao, manifestDao, blobRepo, mtRepository, tagDao, imageDao, registryDao, manifestDao, blobRepo, mtRepository, tagDao, imageDao,
artifactDao, layerDao, manifestRefDao, tx, gcService, artifactDao, layerDao, manifestRefDao, tx, gcService, reporter, spacePathStore,
) )
} }
func RemoteRegistryProvider( func RemoteRegistryProvider(
local *LocalRegistry, app *App, upstreamProxyConfigRepo store.UpstreamProxyConfigRepository, local *LocalRegistry, app *App, upstreamProxyConfigRepo store.UpstreamProxyConfigRepository,
spacePathStore corestore.SpacePathStore, secretService secret.Service, spacePathStore gitnessstore.SpacePathStore, secretService secret.Service,
) *RemoteRegistry { ) *RemoteRegistry {
return NewRemoteRegistry(local, app, upstreamProxyConfigRepo, spacePathStore, secretService).(*RemoteRegistry) return NewRemoteRegistry(local, app, upstreamProxyConfigRepo, spacePathStore, secretService).(*RemoteRegistry)
} }
@ -68,7 +69,7 @@ func ControllerProvider(
local *LocalRegistry, local *LocalRegistry,
remote *RemoteRegistry, remote *RemoteRegistry,
controller *pkg.CoreController, controller *pkg.CoreController,
spaceStore corestore.SpaceStore, spaceStore gitnessstore.SpaceStore,
authorizer authz.Authorizer, authorizer authz.Authorizer,
) *Controller { ) *Controller {
return NewController(local, remote, controller, spaceStore, authorizer) return NewController(local, remote, controller, spaceStore, authorizer)
@ -78,8 +79,12 @@ func StorageServiceProvider(cfg *types.Config, driver storagedriver.StorageDrive
return GetStorageService(cfg, driver) return GetStorageService(cfg, driver)
} }
func ProvideReporter() event.Reporter {
return &event.Noop{}
}
var ControllerSet = wire.NewSet(ControllerProvider) var ControllerSet = wire.NewSet(ControllerProvider)
var RegistrySet = wire.NewSet(LocalRegistryProvider, ManifestServiceProvider, RemoteRegistryProvider) var RegistrySet = wire.NewSet(LocalRegistryProvider, ManifestServiceProvider, RemoteRegistryProvider)
var StorageServiceSet = wire.NewSet(StorageServiceProvider) var StorageServiceSet = wire.NewSet(StorageServiceProvider)
var AppSet = wire.NewSet(NewApp) var AppSet = wire.NewSet(NewApp)
var WireSet = wire.NewSet(ControllerSet, RegistrySet, StorageServiceSet, AppSet, gc.WireSet) var WireSet = wire.NewSet(ControllerSet, RegistrySet, StorageServiceSet, AppSet)

View File

@ -178,15 +178,15 @@ type TagRepository interface {
GetAllArtifactsByParentID( GetAllArtifactsByParentID(
ctx context.Context, parentID int64, ctx context.Context, parentID int64,
packageTypes *[]string, sortByField string, registryIDs *[]string, sortByField string,
sortByOrder string, limit int, offset int, search string, sortByOrder string, limit int, offset int, search string,
labels []string, latestVersion bool,
) (*[]types.ArtifactMetadata, error) ) (*[]types.ArtifactMetadata, error)
CountAllArtifactsByParentID( CountAllArtifactsByParentID(
ctx context.Context, parentID int64, ctx context.Context, parentID int64,
packageTypes *[]string, search string, registryIDs *[]string, search string,
labels []string, latestVersion bool,
) (int64, error) ) (int64, error)
GetAllArtifactsByRepo( GetAllArtifactsByRepo(
@ -460,4 +460,5 @@ type GCManifestTaskRepository interface {
Postpone(ctx context.Context, b *types.GCManifestTask, d time.Duration) error Postpone(ctx context.Context, b *types.GCManifestTask, d time.Duration) error
IsDangling(ctx context.Context, b *types.GCManifestTask) (bool, error) IsDangling(ctx context.Context, b *types.GCManifestTask) (bool, error)
Delete(ctx context.Context, b *types.GCManifestTask) error Delete(ctx context.Context, b *types.GCManifestTask) error
DeleteManifest(ctx context.Context, registryID, id int64) (*digest.Digest, error)
} }

View File

@ -77,6 +77,7 @@ type artifactMetadataDB struct {
LatestVersion string `db:"latest_version"` LatestVersion string `db:"latest_version"`
CreatedAt int64 `db:"created_at"` CreatedAt int64 `db:"created_at"`
ModifiedAt int64 `db:"modified_at"` ModifiedAt int64 `db:"modified_at"`
Version string `db:"version"`
} }
type tagMetadataDB struct { type tagMetadataDB struct {
@ -337,54 +338,54 @@ func sqlPartialMatch(value string) string {
func (t tagDao) GetAllArtifactsByParentID( func (t tagDao) GetAllArtifactsByParentID(
ctx context.Context, ctx context.Context,
parentID int64, parentID int64,
packageTypes *[]string, registryIDs *[]string,
sortByField string, sortByField string,
sortByOrder string, sortByOrder string,
limit int, limit int,
offset int, offset int,
search string, search string,
labels []string, latestVersion bool,
) (*[]types.ArtifactMetadata, error) { ) (*[]types.ArtifactMetadata, error) {
q := databaseg.Builder.Select( q := databaseg.Builder.Select(
"r.registry_name as repo_name, t.tag_image_name as name,"+ `r.registry_name as repo_name,
" r.registry_package_type as package_type, t.tag_name as latest_version,"+ t.tag_image_name as name,
" t.tag_updated_at as modified_at, ar.image_labels as labels, t2.download_count as download_count ", r.registry_package_type as package_type,
t.tag_name as version,
t.tag_updated_at as modified_at,
ar.image_labels as labels,
COALESCE(t2.download_count,0) as download_count `,
). ).
From("tags t"). From("tags t").
Join(
"(SELECT t.tag_id as id, ROW_NUMBER() OVER "+
" (PARTITION BY t.tag_registry_id, t.tag_image_name ORDER BY t.tag_updated_at DESC) AS rank "+
" FROM tags t JOIN registries r ON t.tag_registry_id = r.registry_id "+
" WHERE r.registry_parent_id = ? ) AS a ON t.tag_id = a.id", parentID,
).
Join("registries r ON t.tag_registry_id = r.registry_id"). Join("registries r ON t.tag_registry_id = r.registry_id").
Where("r.registry_parent_id = ?", parentID).
Join( Join(
"images ar ON ar.image_registry_id = t.tag_registry_id AND"+ "images ar ON ar.image_registry_id = t.tag_registry_id AND"+
" ar.image_name = t.tag_image_name", " ar.image_name = t.tag_image_name",
). ).
LeftJoin( LeftJoin(
"(SELECT i.image_name, t1.download_count FROM"+ `( SELECT i.image_name, SUM(COALESCE(t1.download_count, 0)) as download_count FROM
" ( SELECT a.artifact_image_id, COUNT(d.download_stat_id) as download_count"+ ( SELECT a.artifact_image_id, COUNT(d.download_stat_id) as download_count
" FROM artifacts a "+ FROM artifacts a JOIN download_stats d ON d.download_stat_artifact_id = a.artifact_id
" LEFT JOIN download_stats d ON d.download_stat_artifact_id = a.artifact_id GROUP BY"+ GROUP BY a.artifact_image_id ) as t1
" a.artifact_image_id ) as t1 "+ JOIN images i ON i.image_id = t1.artifact_image_id
" JOIN images ON i.image_id = t1.artifact_image_id "+ JOIN registries r ON r.registry_id = i.image_registry_id
" JOIN registries r ON r.registry_id = i.image_registry_id "+ WHERE r.registry_parent_id = ? GROUP BY i.image_name) as t2
" WHERE r.registry_parent_id = ? GROUP BY i.image_name) as t2"+ ON t.tag_image_name = t2.image_name`, parentID,
" ON t.tag_image_name = t2.image_name", parentID, )
).
Where("a.rank = 1")
if len(*packageTypes) > 0 { if latestVersion {
q = q.Where(sq.Eq{"r.registry_package_type": packageTypes}) q = q.Join(
`(SELECT t.tag_id as id, ROW_NUMBER() OVER (PARTITION BY t.tag_registry_id, t.tag_image_name
ORDER BY t.tag_updated_at DESC) AS rank FROM tags t
JOIN registries r ON t.tag_registry_id = r.registry_id
WHERE r.registry_parent_id = ? ) AS a
ON t.tag_id = a.id`, parentID, // nolint:goconst
).
Where("a.rank = 1")
} }
if len(labels) > 0 { if len(*registryIDs) > 0 {
sort.Strings(labels) q = q.Where(sq.Eq{"r.registry_name": registryIDs})
labelsVal := util.GetEmptySQLString(util.ArrToString(labels))
labelsVal.String = labelSeparatorStart + labelsVal.String + labelSeparatorEnd
q = q.Where("'^_' || ar.image_labels || '^_' LIKE ?", labelsVal)
} }
if search != "" { if search != "" {
@ -409,39 +410,35 @@ func (t tagDao) GetAllArtifactsByParentID(
func (t tagDao) CountAllArtifactsByParentID( func (t tagDao) CountAllArtifactsByParentID(
ctx context.Context, parentID int64, ctx context.Context, parentID int64,
packageTypes *[]string, search string, labels []string, registryIDs *[]string, search string, latestVersion bool,
) (int64, error) { ) (int64, error) {
// nolint:goconst // nolint:goconst
q := databaseg.Builder.Select("COUNT(*)"). q := databaseg.Builder.Select("COUNT(*)").
From("tags t"). From("tags t").
Join( Join("registries r ON t.tag_registry_id = r.registry_id"). // nolint:goconst
"(SELECT t.tag_id as id, ROW_NUMBER() OVER "+ Where("r.registry_parent_id = ?", parentID).
" (PARTITION BY t.tag_registry_id, t.tag_image_name ORDER BY t.tag_updated_at DESC) AS rank FROM tags t "+
" JOIN registries r ON t.tag_registry_id = r.registry_id "+
" WHERE r.registry_parent_id = ?) AS a ON t.tag_id = a.id", parentID,
).
Join("registries r ON t.tag_registry_id = r.registry_id").
Join( Join(
"images ar ON ar.image_registry_id = t.tag_registry_id" + "images ar ON ar.image_registry_id = t.tag_registry_id" +
" AND ar.image_name = t.tag_image_name", " AND ar.image_name = t.tag_image_name",
). )
Where("a.rank = 1 ")
if len(*packageTypes) > 0 { if latestVersion {
q = q.Where(sq.Eq{"r.registry_package_type": packageTypes}) q = q.Join(
`(SELECT t.tag_id as id, ROW_NUMBER() OVER (PARTITION BY t.tag_registry_id, t.tag_image_name
ORDER BY t.tag_updated_at DESC) AS rank FROM tags t
JOIN registries r ON t.tag_registry_id = r.registry_id
WHERE r.registry_parent_id = ? ) AS a
ON t.tag_id = a.id`, parentID, // nolint:goconst
).Where("a.rank = 1")
}
if len(*registryIDs) > 0 {
q = q.Where(sq.Eq{"r.registry_name": registryIDs})
} }
if search != "" { if search != "" {
q = q.Where("tag_image_name LIKE ?", sqlPartialMatch(search)) q = q.Where("tag_image_name LIKE ?", sqlPartialMatch(search))
} }
if len(labels) > 0 {
sort.Strings(labels)
labelsVal := util.GetEmptySQLString(util.ArrToString(labels))
labelsVal.String = labelSeparatorStart + labelsVal.String + labelSeparatorEnd
q = q.Where("'^_' || ar.image_labels || '^_' LIKE ?", labelsVal)
}
sql, args, err := q.ToSql() sql, args, err := q.ToSql()
if err != nil { if err != nil {
return -1, errors.Wrap(err, "Failed to convert query to sql") return -1, errors.Wrap(err, "Failed to convert query to sql")
@ -461,9 +458,9 @@ func (t tagDao) GetTagDetail(
name string, name string,
) (*types.TagDetail, error) { ) (*types.TagDetail, error) {
q := databaseg.Builder.Select( q := databaseg.Builder.Select(
"tag_id as id, tag_name as name ,"+ `tag_id as id, tag_name as name,
" tag_image_name as image_name, tag_created_at as created_at, "+ tag_image_name as image_name, tag_created_at as created_at,
" tag_updated_at as updated_at, manifest_total_size as size", tag_updated_at as updated_at, manifest_total_size as size`,
). ).
From("tags"). From("tags").
Join("manifests ON manifest_id = tag_manifest_id"). Join("manifests ON manifest_id = tag_manifest_id").
@ -494,13 +491,13 @@ func (t tagDao) GetLatestTagMetadata(
imageName string, imageName string,
) (*types.ArtifactMetadata, error) { ) (*types.ArtifactMetadata, error) {
q := databaseg.Builder.Select( q := databaseg.Builder.Select(
"r.registry_name as repo_name,"+ `r.registry_name as repo_name,
" r.registry_package_type as package_type, t.tag_image_name as name, "+ r.registry_package_type as package_type, t.tag_image_name as name,
"t.tag_name as latest_version, t.tag_created_at as created_at,"+ t.tag_name as latest_version, t.tag_created_at as created_at,
" t.tag_updated_at as modified_at, ar.image_labels as labels", t.tag_updated_at as modified_at, ar.image_labels as labels`,
). ).
From("tags t"). From("tags t").
Join("registries r ON t.tag_registry_id = r.registry_id"). Join("registries r ON t.tag_registry_id = r.registry_id"). // nolint:goconst
Join( Join(
"images ar ON ar.image_registry_id = t.tag_registry_id "+ "images ar ON ar.image_registry_id = t.tag_registry_id "+
"AND ar.image_name = t.tag_image_name", "AND ar.image_name = t.tag_image_name",
@ -618,18 +615,18 @@ func (t tagDao) GetAllArtifactsByRepo(
labels []string, labels []string,
) (*[]types.ArtifactMetadata, error) { ) (*[]types.ArtifactMetadata, error) {
q := databaseg.Builder.Select( q := databaseg.Builder.Select(
"r.registry_name as repo_name, t.tag_image_name as name,"+ `r.registry_name as repo_name, t.tag_image_name as name,
" r.registry_package_type as package_type, t.tag_name as latest_version,"+ r.registry_package_type as package_type, t.tag_name as latest_version,
" t.tag_updated_at as modified_at, ar.image_labels as labels, "+ t.tag_updated_at as modified_at, ar.image_labels as labels,
" COALESCE(t2.download_count, 0) as download_count ", COALESCE(t2.download_count, 0) as download_count `,
). ).
From("tags t"). From("tags t").
Join( Join(
"(SELECT t.tag_id as id, ROW_NUMBER() OVER (PARTITION BY t.tag_registry_id, t.tag_image_name"+ `(SELECT t.tag_id as id, ROW_NUMBER() OVER (PARTITION BY t.tag_registry_id, t.tag_image_name
" ORDER BY t.tag_updated_at DESC) AS rank FROM tags t "+ ORDER BY t.tag_updated_at DESC) AS rank FROM tags t
" JOIN registries r ON t.tag_registry_id = r.registry_id "+ JOIN registries r ON t.tag_registry_id = r.registry_id
" WHERE r.registry_parent_id = ? AND r.registry_name = ? ) AS a"+ WHERE r.registry_parent_id = ? AND r.registry_name = ? ) AS a
" ON t.tag_id = a.id", parentID, repoKey, ON t.tag_id = a.id`, parentID, repoKey, // nolint:goconst
). ).
Join("registries r ON t.tag_registry_id = r.registry_id"). Join("registries r ON t.tag_registry_id = r.registry_id").
Join( Join(
@ -637,15 +634,15 @@ func (t tagDao) GetAllArtifactsByRepo(
" AND ar.image_name = t.tag_image_name", " AND ar.image_name = t.tag_image_name",
). ).
LeftJoin( LeftJoin(
"( SELECT i.image_name, SUM(COALESCE(t1.download_count, 0)) as download_count FROM"+ `( SELECT i.image_name, SUM(COALESCE(t1.download_count, 0)) as download_count FROM
" ( SELECT a.artifact_image_id, COUNT(d.download_stat_id) as download_count"+ ( SELECT a.artifact_image_id, COUNT(d.download_stat_id) as download_count
" FROM artifacts a "+ FROM artifacts a
" JOIN download_stats d ON d.download_stat_artifact_id = a.artifact_id GROUP BY"+ JOIN download_stats d ON d.download_stat_artifact_id = a.artifact_id GROUP BY
" a.artifact_image_id ) as t1 "+ a.artifact_image_id ) as t1
" JOIN images i ON i.image_id = t1.artifact_image_id "+ JOIN images i ON i.image_id = t1.artifact_image_id
" JOIN registries r ON r.registry_id = i.image_registry_id "+ JOIN registries r ON r.registry_id = i.image_registry_id
" WHERE r.registry_parent_id = ? AND r.registry_name = ? GROUP BY i.image_name) as t2"+ WHERE r.registry_parent_id = ? AND r.registry_name = ? GROUP BY i.image_name) as t2
" ON t.tag_image_name = t2.image_name", parentID, repoKey, ON t.tag_image_name = t2.image_name`, parentID, repoKey,
). ).
Where("a.rank = 1 ") Where("a.rank = 1 ")
@ -676,6 +673,7 @@ func (t tagDao) GetAllArtifactsByRepo(
return t.mapToArtifactMetadataList(ctx, dst) return t.mapToArtifactMetadataList(ctx, dst)
} }
// nolint:goconst
func (t tagDao) CountAllArtifactsByRepo( func (t tagDao) CountAllArtifactsByRepo(
ctx context.Context, parentID int64, repoKey string, ctx context.Context, parentID int64, repoKey string,
search string, labels []string, search string, labels []string,
@ -683,10 +681,10 @@ func (t tagDao) CountAllArtifactsByRepo(
q := databaseg.Builder.Select("COUNT(*)"). q := databaseg.Builder.Select("COUNT(*)").
From("tags t"). From("tags t").
Join( Join(
"(SELECT t.tag_id as id, ROW_NUMBER() OVER (PARTITION BY t.tag_registry_id, t.tag_image_name"+ `(SELECT t.tag_id as id, ROW_NUMBER() OVER (PARTITION BY t.tag_registry_id, t.tag_image_name
" ORDER BY t.tag_updated_at DESC) AS rank FROM tags t "+ ORDER BY t.tag_updated_at DESC) AS rank FROM tags t
" JOIN registries r ON t.tag_registry_id = r.registry_id "+ JOIN registries r ON t.tag_registry_id = r.registry_id
" WHERE r.registry_parent_id = ? AND r.registry_name = ? ) AS a ON t.tag_id = a.id", parentID, repoKey, WHERE r.registry_parent_id = ? AND r.registry_name = ? ) AS a ON t.tag_id = a.id`, parentID, repoKey,
). ).
Join("registries r ON t.tag_registry_id = r.registry_id"). Join("registries r ON t.tag_registry_id = r.registry_id").
Join( Join(
@ -726,10 +724,10 @@ func (t tagDao) GetAllTagsByRepoAndImage(
search string, search string,
) (*[]types.TagMetadata, error) { ) (*[]types.TagMetadata, error) {
q := databaseg.Builder.Select( q := databaseg.Builder.Select(
"t.tag_name as name, m.manifest_total_size as size,"+ `t.tag_name as name, m.manifest_total_size as size,
" r.registry_package_type as package_type, t.tag_updated_at as modified_at, "+ r.registry_package_type as package_type, t.tag_updated_at as modified_at,
" m.manifest_schema_version, m.manifest_non_conformant, m.manifest_payload, "+ m.manifest_schema_version, m.manifest_non_conformant, m.manifest_payload,
" mt.mt_media_type ", mt.mt_media_type `,
). ).
From("tags t"). From("tags t").
Join("registries r ON t.tag_registry_id = r.registry_id"). Join("registries r ON t.tag_registry_id = r.registry_id").
@ -903,6 +901,7 @@ func (t tagDao) mapToArtifactMetadata(
Labels: util.StringToArr(dst.Labels.String), Labels: util.StringToArr(dst.Labels.String),
CreatedAt: time.UnixMilli(dst.CreatedAt), CreatedAt: time.UnixMilli(dst.CreatedAt),
ModifiedAt: time.UnixMilli(dst.ModifiedAt), ModifiedAt: time.UnixMilli(dst.ModifiedAt),
Version: dst.Version,
}, nil }, nil
} }

View File

@ -36,7 +36,7 @@ func New() Service {
func (s *Noop) Start( func (s *Noop) Start(
_ context.Context, _ *sqlx.DB, _ corestore.SpaceStore, _ context.Context, _ *sqlx.DB, _ corestore.SpaceStore,
_ store.BlobRepository, _ storagedriver.StorageDeleter, _ store.BlobRepository, _ storagedriver.StorageDeleter,
_ *types.Config, _ store.MediaTypesRepository, _ store.ManifestRepository, _ *types.Config,
) { ) {
// NOOP // NOOP
} }

View File

@ -31,7 +31,7 @@ type Service interface {
Start( Start(
ctx context.Context, sqlDB *sqlx.DB, spaceStore corestore.SpaceStore, ctx context.Context, sqlDB *sqlx.DB, spaceStore corestore.SpaceStore,
blobRepo store.BlobRepository, storageDeleter storagedriver.StorageDeleter, blobRepo store.BlobRepository, storageDeleter storagedriver.StorageDeleter,
config *types.Config, mtRepository store.MediaTypesRepository, manifestRepository store.ManifestRepository, config *types.Config,
) )
BlobFindAndLockBefore(ctx context.Context, blobID int64, date time.Time) (*registrytypes.GCBlobTask, error) BlobFindAndLockBefore(ctx context.Context, blobID int64, date time.Time) (*registrytypes.GCBlobTask, error)
BlobReschedule(ctx context.Context, b *registrytypes.GCBlobTask, d time.Duration) error BlobReschedule(ctx context.Context, b *registrytypes.GCBlobTask, d time.Duration) error

View File

@ -14,15 +14,11 @@
package types package types
import (
"time"
)
type GCBlobTask struct { type GCBlobTask struct {
BlobID int64 BlobID int64
ReviewAfter time.Time ReviewAfter int64
ReviewCount int ReviewCount int
CreatedAt time.Time CreatedAt int64
Event string Event string
} }
@ -30,14 +26,8 @@ type GCBlobTask struct {
type GCManifestTask struct { type GCManifestTask struct {
RegistryID int64 RegistryID int64
ManifestID int64 ManifestID int64
ReviewAfter time.Time ReviewAfter int64
ReviewCount int ReviewCount int
CreatedAt time.Time CreatedAt int64
Event string Event string
} }
// GCReviewAfterDefault represents a row in the gc_review_after_defaults table.
type GCReviewAfterDefault struct {
Event string
Value time.Duration
}

View File

@ -42,6 +42,7 @@ type ArtifactMetadata struct {
LatestVersion string LatestVersion string
CreatedAt time.Time CreatedAt time.Time
ModifiedAt time.Time ModifiedAt time.Time
Version string
} }
type TagMetadata struct { type TagMetadata struct {

View File

@ -51,7 +51,7 @@
"@codemirror/view": "^6.9.6", "@codemirror/view": "^6.9.6",
"@harnessio/design-system": "^2.1.1", "@harnessio/design-system": "^2.1.1",
"@harnessio/icons": "^2.1.7", "@harnessio/icons": "^2.1.7",
"@harnessio/react-har-service-client": "^0.0.15", "@harnessio/react-har-service-client": "^0.0.21",
"@harnessio/uicore": "^4.1.2", "@harnessio/uicore": "^4.1.2",
"@tanstack/react-query": "4.20.4", "@tanstack/react-query": "4.20.4",
"@types/dompurify": "^3.0.2", "@types/dompurify": "^3.0.2",

View File

@ -15,9 +15,9 @@
*/ */
import type React from 'react' import type React from 'react'
import type { Button } from '@harnessio/uicore'
import type { PermissionIdentifier, ResourceType } from '@ar/common/permissionTypes' import type { PermissionIdentifier, ResourceType } from '@ar/common/permissionTypes'
import type RbacButton from '@ar/__mocks__/components/RbacButton'
import type RbacMenuItem from '@ar/__mocks__/components/RbacMenuItem' import type RbacMenuItem from '@ar/__mocks__/components/RbacMenuItem'
import type NGBreadcrumbs from '@ar/__mocks__/components/NGBreadcrumbs' import type NGBreadcrumbs from '@ar/__mocks__/components/NGBreadcrumbs'
import type DependencyView from '@ar/__mocks__/components/DependencyView' import type DependencyView from '@ar/__mocks__/components/DependencyView'
@ -43,6 +43,16 @@ export interface Scope {
space?: string space?: string
} }
export interface PipelineExecutionPathProps {
executionIdentifier: string
pipelineIdentifier: string
module: 'ci' | 'cd'
}
export interface ServiceDetailsPathProps {
serviceId: string
}
export interface PermissionsRequest { export interface PermissionsRequest {
resource: { resourceType: ResourceType; resourceIdentifier?: string } resource: { resourceType: ResourceType; resourceIdentifier?: string }
permissions: PermissionIdentifier[] permissions: PermissionIdentifier[]
@ -63,7 +73,7 @@ export interface ParentContextObj {
} }
export interface Components { export interface Components {
RbacButton: typeof Button RbacButton: typeof RbacButton
NGBreadcrumbs: typeof NGBreadcrumbs NGBreadcrumbs: typeof NGBreadcrumbs
RbacMenuItem: typeof RbacMenuItem RbacMenuItem: typeof RbacMenuItem
} }
@ -96,6 +106,8 @@ export interface CustomUtils {
getCustomHeaders: () => Record<string, string> getCustomHeaders: () => Record<string, string>
getApiBaseUrl: (url: string) => string getApiBaseUrl: (url: string) => string
getRouteDefinitions?: (routeParams: Record<string, string>) => ARRouteDefinitionsReturn getRouteDefinitions?: (routeParams: Record<string, string>) => ARRouteDefinitionsReturn
getRouteToPipelineExecutionView?: (params: Scope & PipelineExecutionPathProps) => string
getRouteToServiceDetailsView?: (params: Scope & ServiceDetailsPathProps) => string
} }
export interface MFEAppProps { export interface MFEAppProps {

View File

@ -0,0 +1,29 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import React from 'react'
import { Button, ButtonProps } from '@harnessio/uicore'
import type { PermissionsRequest } from '@ar/MFEAppTypes'
import type { PermissionIdentifier } from '@ar/common/permissionTypes'
export interface RbacButtonProps extends ButtonProps {
permission?: Omit<PermissionsRequest, 'permissions'> & { permission: PermissionIdentifier }
}
export default function RbacButton(props: RbacButtonProps) {
return <Button {...props} />
}

View File

@ -16,7 +16,7 @@
import React, { createContext } from 'react' import React, { createContext } from 'react'
import { defaultTo, noop } from 'lodash-es' import { defaultTo, noop } from 'lodash-es'
import { Button, Container } from '@harnessio/uicore' import { Container } from '@harnessio/uicore'
import type { MFEAppProps } from '@ar/MFEAppTypes' import type { MFEAppProps } from '@ar/MFEAppTypes'
@ -29,6 +29,7 @@ import {
useQueryParamsOptions, useQueryParamsOptions,
useUpdateQueryParams useUpdateQueryParams
} from '@ar/__mocks__/hooks' } from '@ar/__mocks__/hooks'
import RbacButton from '@ar/__mocks__/components/RbacButton'
import RbacMenuItem from '@ar/__mocks__/components/RbacMenuItem' import RbacMenuItem from '@ar/__mocks__/components/RbacMenuItem'
import NGBreadcrumbs from '@ar/__mocks__/components/NGBreadcrumbs' import NGBreadcrumbs from '@ar/__mocks__/components/NGBreadcrumbs'
import DependencyView from '@ar/__mocks__/components/DependencyView' import DependencyView from '@ar/__mocks__/components/DependencyView'
@ -76,7 +77,7 @@ const GitnessApp = (props: Partial<MFEAppProps>): JSX.Element => {
scope={defaultTo(scope, {})} scope={defaultTo(scope, {})}
customScope={defaultTo(customScope, {})} customScope={defaultTo(customScope, {})}
components={Object.assign( components={Object.assign(
{ RbacButton: Button, NGBreadcrumbs, RbacMenuItem, SecretFormInput, VulnerabilityView, DependencyView }, { RbacButton, NGBreadcrumbs, RbacMenuItem, SecretFormInput, VulnerabilityView, DependencyView },
components components
)} )}
NavComponent={NavComponent} NavComponent={NavComponent}

View File

@ -16,7 +16,7 @@
import type { IconName } from '@harnessio/icons' import type { IconName } from '@harnessio/icons'
import type { StringsMap } from '@ar/frameworks/strings' import type { StringsMap } from '@ar/frameworks/strings'
import { RepositoryPackageType } from './types' import { EnvironmentType, RepositoryPackageType } from './types'
export interface RepositoryTypeListItem { export interface RepositoryTypeListItem {
label: keyof StringsMap label: keyof StringsMap
@ -80,3 +80,20 @@ export const RepositoryTypes: RepositoryTypeListItem[] = [
disabled: true disabled: true
} }
] ]
interface EnvironmentTypeListItem {
label: keyof StringsMap
value: EnvironmentType
disabled?: boolean
}
export const EnvironmentTypeList: EnvironmentTypeListItem[] = [
{
label: 'prod',
value: EnvironmentType.Prod
},
{
label: 'nonProd',
value: EnvironmentType.NonProd
}
]

View File

@ -15,10 +15,14 @@
*/ */
export enum PermissionIdentifier { export enum PermissionIdentifier {
DELETE_SERVICE = 'core_service_delete', VIEW_ARTIFACT_REGISTRY = 'artifact_artregistry_view',
EDIT_SERVICE = 'core_service_edit' EDIT_ARTIFACT_REGISTRY = 'artifact_artregistry_edit',
DELETE_ARTIFACT_REGISTRY = 'artifact_artregistry_delete',
DOWNLOAD_ARTIFACT = 'artifact_artregistry_downloadartifact',
UPLOAD_ARTIFACT = 'artifact_artregistry_uploadartifact',
DELETE_ARTIFACT = 'artifact_artregistry_deleteartifact'
} }
export enum ResourceType { export enum ResourceType {
SERVICE = 'SERVICE' ARTIFACT_REGISTRY = 'ARTIFACT_REGISTRY'
} }

View File

@ -29,8 +29,8 @@ export type FormikFowardRef<T = unknown> =
| null | null
export enum EnvironmentType { export enum EnvironmentType {
Production = 'Production', Prod = 'Production',
PreProduction = 'PreProduction' NonProd = 'PreProduction'
} }
export enum RepositoryPackageType { export enum RepositoryPackageType {

View File

@ -0,0 +1,55 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
.textInputWrapper {
margin-bottom: 0;
width: 600px;
.iconClass {
margin-top: 11px !important;
}
.rightIconClass {
width: 130px;
display: flex;
justify-content: center;
align-items: center;
height: 32px;
}
}
.popoverContainer {
position: relative;
.popoverContent {
position: absolute;
left: 0px;
top: 40px;
border: 1px solid var(--grey-200);
border-radius: 0 0 5px 5px;
}
.aiButton {
position: absolute;
top: var(--spacing-xsmall);
right: var(--spacing-xsmall);
--background-color: var(--white) !important;
--background-color-hover: var(--white) !important;
--background-color-active: var(--white) !important;
--text-color: var(--ai-purple-800) !important;
--border: 1px solid var(--ai-purple-800) !important;
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2023 Harness, Inc.
*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const aiButton: string
export declare const iconClass: string
export declare const popoverContainer: string
export declare const popoverContent: string
export declare const rightIconClass: string
export declare const textInputWrapper: string

View File

@ -0,0 +1,103 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import React, { useEffect, useState } from 'react'
import { debounce, isEmpty } from 'lodash-es'
import { Menu, MenuItem } from '@blueprintjs/core'
import { Button, ButtonSize, ButtonVariation, Container, TextInput } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import type { AIOption } from './types'
import css from './AISearchInput.module.scss'
interface AISearchInputProps {
searchTerm: string
onChange: (val: string) => void
placeholder?: string
options: AIOption[]
}
function AISearchInput({ searchTerm, onChange, placeholder, options }: AISearchInputProps) {
const [query, setQuery] = useState(searchTerm)
const [focus, setFocus] = useState(false)
const { getString } = useStrings()
useEffect(() => {
if (isEmpty(searchTerm)) {
setQuery('')
}
}, [searchTerm])
const deboucedOnChange = debounce(onChange, 500)
const deboucedSetFocus = debounce(setFocus, 400)
const handleChange = (value: string) => {
setQuery(value)
deboucedOnChange(value)
}
return (
<Container className={css.popoverContainer}>
<TextInput
value={query}
wrapperClassName={css.textInputWrapper}
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => handleChange(evt.currentTarget.value as string)}
placeholder={placeholder ?? getString('search')}
leftIcon="search"
onFocus={() => {
setFocus(true)
}}
onBlur={() => {
deboucedSetFocus(false)
}}
leftIconProps={{
name: 'search',
className: css.iconClass
}}
rightElement="harness-copilot"
rightElementProps={{
className: css.rightIconClass
}}
/>
<Button
className={css.aiButton}
icon="harness-copilot"
variation={ButtonVariation.AI}
text={getString('harnessAI')}
size={ButtonSize.SMALL}
/>
{focus && (
<Container width={600} className={css.popoverContent}>
<Menu>
{options.map(each => (
<MenuItem
key={each.value}
text={each.label}
disabled={each.disabled}
onClick={() => {
handleChange(each.value)
setFocus(false)
}}
/>
))}
</Menu>
</Container>
)}
</Container>
)
}
export default AISearchInput

View File

@ -0,0 +1,21 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
export interface AIOption {
label: string
value: string
disabled?: boolean
}

View File

@ -40,13 +40,14 @@ interface ButtonTabsProps<T> {
children: React.ReactElement<ButtonTabProps<T>, typeof ButtonTab>[] children: React.ReactElement<ButtonTabProps<T>, typeof ButtonTab>[]
small?: boolean small?: boolean
bold?: boolean bold?: boolean
className?: string
} }
export function ButtonTabs<T>(props: ButtonTabsProps<T>): JSX.Element { export function ButtonTabs<T>(props: ButtonTabsProps<T>): JSX.Element {
const { children: tabs, small, id, selectedTabId, onChange, bold } = props const { children: tabs, small, id, selectedTabId, onChange, bold, className } = props
const selectedTabPannel = tabs.find(each => each.props.id === selectedTabId) const selectedTabPannel = tabs.find(each => each.props.id === selectedTabId)
return ( return (
<Container data-testid={id}> <Container className={className} data-testid={id}>
<ButtonGroup className={css.btnGroup}> <ButtonGroup className={css.btnGroup}>
{tabs.map(each => ( {tabs.map(each => (
<Button <Button

View File

@ -0,0 +1,45 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import React from 'react'
import { useStrings } from '@ar/frameworks/strings'
import type { EnvironmentType } from '@ar/common/types'
import { EnvironmentTypeList } from '@ar/common/constants'
import MultiSelectDropdownList from '@ar/components/MultiDropdownSelect/MultiDropdownSelect'
interface EnvironmentTypeSelectorProps {
value: EnvironmentType[]
onChange: (val: EnvironmentType[]) => void
}
export default function EnvironmentTypeSelector(props: EnvironmentTypeSelectorProps): JSX.Element {
const { value, onChange } = props
const { getString } = useStrings()
return (
<MultiSelectDropdownList
buttonTestId="environment-type-select"
items={EnvironmentTypeList.filter(each => !each.disabled).map(each => ({
...each,
label: getString(each.label)
}))}
value={value}
onSelect={onChange}
placeholder={getString('repositoryList.selectEnvironments')}
allowSearch
/>
)
}

View File

@ -31,7 +31,7 @@ interface PatternInputProps extends StyledProps {
} }
function PatternInput<T>(props: PatternInputProps & { formik: FormikContextType<T> }): JSX.Element { function PatternInput<T>(props: PatternInputProps & { formik: FormikContextType<T> }): JSX.Element {
const { label, name, placeholder, formik } = props const { label, name, placeholder, formik, disabled } = props
const formValue = get(formik.values, name, []) as string[] const formValue = get(formik.values, name, []) as string[]
@ -53,6 +53,7 @@ function PatternInput<T>(props: PatternInputProps & { formik: FormikContextType<
onChange={selectedItems => { onChange={selectedItems => {
formik.setFieldValue(name, selectedItems) formik.setFieldValue(name, selectedItems)
}} }}
readonly={disabled}
/> />
</FormGroup> </FormGroup>
) )

View File

@ -83,11 +83,13 @@ export default function IncludeExcludePatterns<T>(props: IncludeExcludePatternsP
label={includePatternListProps.label} label={includePatternListProps.label}
name={includePatternListProps.name} name={includePatternListProps.name}
placeholder={includePatternListProps.placeholder} placeholder={includePatternListProps.placeholder}
disabled={disabled}
/> />
<PatternInput <PatternInput
label={excludePatternListProps.label} label={excludePatternListProps.label}
name={excludePatternListProps.name} name={excludePatternListProps.name}
placeholder={excludePatternListProps.placeholder} placeholder={excludePatternListProps.placeholder}
disabled={disabled}
/> />
</Container> </Container>
) )

View File

@ -70,7 +70,7 @@ export const Description = (props: DescriptionComponentProps): JSX.Element => {
} }
export const Tags = (props: TagsComponentProps): JSX.Element => { export const Tags = (props: TagsComponentProps): JSX.Element => {
const { tagsProps, hasValue, isOptional = true, disabled, name } = props const { tagsProps = {}, hasValue, isOptional = true, disabled, name } = props
const { getString } = useStrings() const { getString } = useStrings()
const [isTagsOpen, setTagsOpen] = useState<boolean>(hasValue || false) const [isTagsOpen, setTagsOpen] = useState<boolean>(hasValue || false)
@ -96,7 +96,17 @@ export const Tags = (props: TagsComponentProps): JSX.Element => {
/> />
)} )}
</Label> </Label>
{isTagsOpen && <FormInput.KVTagInput name={name} isArray tagsProps={tagsProps} disabled={disabled} />} {isTagsOpen && (
<FormInput.KVTagInput
name={name}
isArray
tagsProps={{
...tagsProps,
disabled
}}
disabled={disabled}
/>
)}
</Container> </Container>
) )
} }

View File

@ -18,7 +18,9 @@ import React, { useState } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { Button, ButtonVariation, Layout, useToggleOpen } from '@harnessio/uicore' import { Button, ButtonVariation, Layout, useToggleOpen } from '@harnessio/uicore'
import { useParentComponents } from '@ar/hooks'
import TagIcon from '@ar/components/MultiTagsInput/TagIcon' import TagIcon from '@ar/components/MultiTagsInput/TagIcon'
import type { RbacButtonProps } from '@ar/__mocks__/components/RbacButton'
import MultiTagsInput from '@ar/components/MultiTagsInput/MultiTagsInput' import MultiTagsInput from '@ar/components/MultiTagsInput/MultiTagsInput'
import css from './PageTitle.module.scss' import css from './PageTitle.module.scss'
@ -27,14 +29,16 @@ interface ArtifactTagsProps {
labels: string[] labels: string[]
placeholder?: string placeholder?: string
onChange: (items: string[]) => Promise<boolean> onChange: (items: string[]) => Promise<boolean>
permission?: RbacButtonProps['permission']
} }
const EMPTY_TAG_VALUE = '+ Labels' const EMPTY_TAG_VALUE = '+ Labels'
export default function ArtifactTags(props: ArtifactTagsProps): JSX.Element | null { export default function ArtifactTags(props: ArtifactTagsProps): JSX.Element | null {
const { labels, onChange, placeholder } = props const { labels, onChange, placeholder, permission } = props
const [selectedItems, setSelectedItems] = useState(labels) const [selectedItems, setSelectedItems] = useState(labels)
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const { RbacButton } = useParentComponents()
const { isOpen: isEdit, open, close } = useToggleOpen(false) const { isOpen: isEdit, open, close } = useToggleOpen(false)
const handleOnSubmit = async () => { const handleOnSubmit = async () => {
@ -83,13 +87,14 @@ export default function ArtifactTags(props: ArtifactTagsProps): JSX.Element | nu
}} }}
/> />
{!isEdit && !!selectedItems.length && ( {!isEdit && !!selectedItems.length && (
<Button <RbacButton
className={css.iconBtn} className={css.iconBtn}
minimal minimal
iconProps={{ size: 20 }} iconProps={{ size: 20 }}
variation={ButtonVariation.ICON} variation={ButtonVariation.ICON}
icon="code-edit" icon="code-edit"
onClick={() => open()} onClick={() => open()}
permission={permission}
/> />
)} )}
{isEdit && ( {isEdit && (
@ -107,13 +112,14 @@ export default function ArtifactTags(props: ArtifactTagsProps): JSX.Element | nu
setQuery('') setQuery('')
}} }}
/> />
<Button <RbacButton
className={css.iconBtn} className={css.iconBtn}
minimal minimal
variation={ButtonVariation.ICON} variation={ButtonVariation.ICON}
iconProps={{ size: 20 }} iconProps={{ size: 20 }}
icon="small-tick" icon="small-tick"
onClick={handleOnSubmit} onClick={handleOnSubmit}
permission={permission}
/> />
</> </>
)} )}

View File

@ -20,16 +20,35 @@
} }
.nameCellContainer { .nameCellContainer {
align-items: center; display: flex;
justify-content: flex-start;
align-items: flex-start;
gap: var(--spacing-small);
} }
.copyButton { .copyUrlBtn {
& :global(.bp3-button-text) { & :global(.bp3-button-text) {
margin-left: var(--spacing-small) !important; padding-left: var(--spacing-xsmall);
font-size: 12px;
} }
& .copyUrlIcon { & .copyUrlIcon {
--icon-padding: 0px !important;
rotate: 45deg; rotate: 45deg;
} }
} }
.copyButton {
--font-size: 12px !important;
--font-weight: 500 !important;
--padding: 0px !important;
}
.deploymentsRow {
--typography-size: 11px;
--typography-weight: 700;
&.prod {
--intent-color: #2e2d96;
}
&.nonProd {
--intent-color: #07a0ab;
}
}

View File

@ -17,6 +17,10 @@
/* eslint-disable */ /* eslint-disable */
// This is an auto-generated file // This is an auto-generated file
export declare const copyButton: string export declare const copyButton: string
export declare const copyUrlBtn: string
export declare const copyUrlIcon: string export declare const copyUrlIcon: string
export declare const deploymentsRow: string
export declare const nameCellContainer: string export declare const nameCellContainer: string
export declare const nonProd: string
export declare const prod: string
export declare const toggleAccordion: string export declare const toggleAccordion: string

View File

@ -15,13 +15,14 @@
*/ */
import React, { FC, PropsWithChildren, useState } from 'react' import React, { FC, PropsWithChildren, useState } from 'react'
import classNames from 'classnames'
import { defaultTo } from 'lodash-es' import { defaultTo } from 'lodash-es'
import copy from 'clipboard-copy' import copy from 'clipboard-copy'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import type { TableExpandedToggleProps } from 'react-table' import type { TableExpandedToggleProps } from 'react-table'
import { Button, ButtonVariation, Layout, Text } from '@harnessio/uicore' import { Button, ButtonProps, ButtonVariation, Layout, Text } from '@harnessio/uicore'
import type { IconName, IconProps } from '@harnessio/icons' import type { IconName, IconProps } from '@harnessio/icons'
import { Color } from '@harnessio/design-system' import { Color, FontVariation } from '@harnessio/design-system'
import { killEvent } from '@ar/common/utils' import { killEvent } from '@ar/common/utils'
import { useStrings } from '@ar/frameworks/strings/String' import { useStrings } from '@ar/frameworks/strings/String'
@ -31,6 +32,8 @@ import RepositoryLocationBadge from '@ar/components/Badge/RepositoryLocationBadg
import { DefaultIconProps } from './constants' import { DefaultIconProps } from './constants'
import { handleToggleExpandableRow } from './utils' import { handleToggleExpandableRow } from './utils'
import CommandBlock from '../CommandBlock/CommandBlock'
import { NonProdTag, ProdTag } from '../Tag/Tags'
import css from './TableCells.module.scss' import css from './TableCells.module.scss'
@ -67,10 +70,11 @@ export const CopyUrlCell: FC<PropsWithChildren<CopyUrlCellProps>> = ({ value, ch
} }
return ( return (
<Button <Button
className={css.copyButton} className={classNames(css.copyButton, css.copyUrlBtn)}
intent="primary" intent="primary"
minimal minimal
icon="link" icon="link"
variation={ButtonVariation.LINK}
iconProps={{ size: 12, className: css.copyUrlIcon }} iconProps={{ size: 12, className: css.copyUrlIcon }}
onClick={evt => { onClick={evt => {
killEvent(evt) killEvent(evt)
@ -84,6 +88,46 @@ export const CopyUrlCell: FC<PropsWithChildren<CopyUrlCellProps>> = ({ value, ch
) )
} }
interface CopyTextCellProps {
value: string
icon?: ButtonProps['rightIcon']
iconProps?: ButtonProps['iconProps']
}
export const CopyTextCell: FC<PropsWithChildren<CopyTextCellProps>> = ({
value,
icon,
iconProps,
children
}): JSX.Element => {
const { getString } = useStrings()
const [openTooltip, setOpenTooltip] = useState(false)
const showCopySuccess = () => {
setOpenTooltip(true)
setTimeout(() => {
setOpenTooltip(false)
}, 1000)
}
return (
<Button
className={css.copyButton}
intent="primary"
minimal
variation={ButtonVariation.LINK}
rightIcon={defaultTo(icon, 'code-copy')}
iconProps={iconProps}
onClick={evt => {
killEvent(evt)
copy(value)
showCopySuccess()
}}
tooltip={getString('copied')}
tooltipProps={{ isOpen: openTooltip, isDark: true }}>
{children}
</Button>
)
}
interface RepositoryLocationBadgeProps { interface RepositoryLocationBadgeProps {
value: RepositoryConfigType value: RepositoryConfigType
} }
@ -158,26 +202,58 @@ const ToggleAccordionCell = (props: ToggleAccordionCellProps): JSX.Element => {
interface LinkCellProps { interface LinkCellProps {
label: string label: string
subLabel?: string
prefix?: React.ReactElement prefix?: React.ReactElement
postfix?: React.ReactElement postfix?: React.ReactElement
linkTo: string linkTo: string
} }
const LinkCell = (props: LinkCellProps): JSX.Element => { const LinkCell = (props: LinkCellProps): JSX.Element => {
const { prefix, postfix, label, linkTo } = props const { prefix, postfix, label, linkTo, subLabel } = props
return ( return (
<Layout.Horizontal className={css.nameCellContainer} spacing="small"> <Layout.Horizontal
className={css.nameCellContainer}
flex={{ justifyContent: 'flex-start', alignItems: 'flex-start' }}>
{prefix} {prefix}
<Link to={linkTo}> <Layout.Vertical>
<Text color={Color.PRIMARY_7} lineClamp={1}> <Link to={linkTo}>
{label} <Text color={Color.PRIMARY_7} lineClamp={1}>
</Text> {label}
</Link> </Text>
</Link>
{subLabel && <Text lineClamp={1}>{subLabel}</Text>}
</Layout.Vertical>
{postfix} {postfix}
</Layout.Horizontal> </Layout.Horizontal>
) )
} }
interface DeploymentsCellProps {
prodCount?: number
nonProdCount?: number
}
export const DeploymentsCell = ({ prodCount, nonProdCount }: DeploymentsCellProps) => {
return (
<Layout.Horizontal spacing="xsmall">
<Layout.Horizontal spacing="small">
<Text font={{ variation: FontVariation.BODY }}>{defaultTo(prodCount, 0)}</Text>
<ProdTag />
</Layout.Horizontal>
<Layout.Horizontal spacing="small">
<Text font={{ variation: FontVariation.BODY }}>{defaultTo(nonProdCount, 0)}</Text>
<NonProdTag />
</Layout.Horizontal>
</Layout.Horizontal>
)
}
export const PullCommandCell = ({ value }: CommonCellProps) => {
const { getString } = useStrings()
if (!value) return <>{getString('na')}</>
return <CommandBlock noWrap commandSnippet={value as string} allowCopy onCopy={killEvent} />
}
export default { export default {
UrlCell, UrlCell,
SizeCell, SizeCell,
@ -185,6 +261,9 @@ export default {
LinkCell, LinkCell,
TextCell, TextCell,
CopyUrlCell, CopyUrlCell,
CopyTextCell,
DeploymentsCell,
PullCommandCell,
LastModifiedCell, LastModifiedCell,
ToggleAccordionCell, ToggleAccordionCell,
RepositoryLocationBadgeCell RepositoryLocationBadgeCell

View File

@ -31,3 +31,19 @@
color: var(--grey-0) !important; color: var(--grey-0) !important;
} }
} }
.tag {
font-size: 10px;
font-weight: 700;
line-height: 15px;
}
.prodTag {
background-color: var(--blue-50) !important;
color: var(--blue-900) !important;
}
.nonProdTag {
background-color: var(--teal-50) !important;
color: var(--team-900) !important;
}

View File

@ -17,4 +17,7 @@
/* eslint-disable */ /* eslint-disable */
// This is an auto-generated file // This is an auto-generated file
export declare const artifactTag: string export declare const artifactTag: string
export declare const nonProdTag: string
export declare const prodTag: string
export declare const tag: string
export declare const versionTag: string export declare const versionTag: string

View File

@ -0,0 +1,41 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import React from 'react'
import classNames from 'classnames'
import { Tag } from '@blueprintjs/core'
import { useStrings } from '@ar/frameworks/strings'
import css from './Tag.module.scss'
export function ProdTag() {
const { getString } = useStrings()
return (
<Tag className={classNames(css.tag, css.prodTag)} round>
{getString('prod')}
</Tag>
)
}
export function NonProdTag() {
const { getString } = useStrings()
return (
<Tag className={classNames(css.tag, css.nonProdTag)} round>
{getString('nonProd')}
</Tag>
)
}

View File

@ -50,10 +50,10 @@ export default function DeleteRepositoryMenuItem({ repoKey }: ArtifactActionProp
onClick={handleDeleteService} onClick={handleDeleteService}
permission={{ permission={{
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: repoKey resourceIdentifier: repoKey
}, },
permission: PermissionIdentifier.DELETE_SERVICE permission: PermissionIdentifier.DELETE_ARTIFACT_REGISTRY
}} }}
/> />
) )

View File

@ -47,10 +47,10 @@ export default function EditRepositoryMenuItem({ repoKey }: ArtifactActionProps)
onClick={handleOpenRepository} onClick={handleOpenRepository}
permission={{ permission={{
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: repoKey resourceIdentifier: repoKey
}, },
permission: PermissionIdentifier.EDIT_SERVICE permission: PermissionIdentifier.EDIT_ARTIFACT_REGISTRY
}} }}
/> />
</> </>

View File

@ -43,10 +43,10 @@ export default function SetupClientMenuItem({ data, repoKey }: ArtifactActionPro
onClick={showSetupClientModal} onClick={showSetupClientModal}
permission={{ permission={{
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: defaultTo(repoKey, '') resourceIdentifier: defaultTo(repoKey, '')
}, },
permission: PermissionIdentifier.DELETE_SERVICE permission: PermissionIdentifier.VIEW_ARTIFACT_REGISTRY
}} }}
/> />
</> </>

View File

@ -30,6 +30,7 @@ import WeeklyDownloads from '@ar/components/PageTitle/WeeklyDownloads'
import CreatedAndModifiedAt from '@ar/components/PageTitle/CreatedAndModifiedAt' import CreatedAndModifiedAt from '@ar/components/PageTitle/CreatedAndModifiedAt'
import ArtifactTags from '@ar/components/PageTitle/ArtifactTags' import ArtifactTags from '@ar/components/PageTitle/ArtifactTags'
import NameAndDescription from '@ar/components/PageTitle/NameAndDescription' import NameAndDescription from '@ar/components/PageTitle/NameAndDescription'
import { PermissionIdentifier, ResourceType } from '@ar/common/permissionTypes'
import { useSetupClientModal } from '@ar/pages/repository-details/hooks/useSetupClientModal/useSetupClientModal' import { useSetupClientModal } from '@ar/pages/repository-details/hooks/useSetupClientModal/useSetupClientModal'
import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon' import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon'
@ -109,6 +110,13 @@ function ArtifactDetailsHeaderContent(props: ArtifactDetailsHeaderContentProps):
onChange={handleUpdateArtifactLabels} onChange={handleUpdateArtifactLabels}
labels={defaultTo(labels, [])} labels={defaultTo(labels, [])}
placeholder={getString('artifactDetails.artifactLabelInputPlaceholder')} placeholder={getString('artifactDetails.artifactLabelInputPlaceholder')}
permission={{
permission: PermissionIdentifier.EDIT_ARTIFACT_REGISTRY,
resource: {
resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: repositoryIdentifier
}
}}
/> />
</Layout.Vertical> </Layout.Vertical>
</Layout.Horizontal> </Layout.Horizontal>

View File

@ -35,3 +35,12 @@
width: 100%; width: 100%;
} }
} }
.filterTabContainer {
display: flex;
align-items: center;
& button {
--button-height: 32px !important;
}
}

View File

@ -16,6 +16,7 @@
/* eslint-disable */ /* eslint-disable */
// This is an auto-generated file // This is an auto-generated file
export declare const filterTabContainer: string
export declare const pageBody: string export declare const pageBody: string
export declare const subHeader: string export declare const subHeader: string
export declare const subHeaderItems: string export declare const subHeaderItems: string

View File

@ -14,47 +14,38 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useCallback, useMemo, useRef } from 'react' import React, { useMemo } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { flushSync } from 'react-dom'
import { defaultTo } from 'lodash-es'
import { Expander } from '@blueprintjs/core' import { Expander } from '@blueprintjs/core'
import { HarnessDocTooltip, Page, Button, ButtonVariation } from '@harnessio/uicore'
import { import {
ExpandingSearchInput, GetAllHarnessArtifactsQueryQueryParams,
HarnessDocTooltip, useGetAllHarnessArtifactsQuery
Page, } from '@harnessio/react-har-service-client'
type ExpandingSearchInputHandle,
Button,
ButtonVariation
} from '@harnessio/uicore'
import { useGetAllArtifactsQuery } from '@harnessio/react-har-service-client'
import { useStrings } from '@ar/frameworks/strings' import { useStrings } from '@ar/frameworks/strings'
import { DEFAULT_PAGE_INDEX, PreferenceScope } from '@ar/constants' import { DEFAULT_PAGE_INDEX, PreferenceScope } from '@ar/constants'
import { ButtonTab, ButtonTabs } from '@ar/components/ButtonTabs/ButtonTabs'
import { useGetSpaceRef, useParentComponents, useParentHooks } from '@ar/hooks' import { useGetSpaceRef, useParentComponents, useParentHooks } from '@ar/hooks'
import PackageTypeSelector from '@ar/components/PackageTypeSelector/PackageTypeSelector' import PackageTypeSelector from '@ar/components/PackageTypeSelector/PackageTypeSelector'
import RepositorySelector from './components/RepositorySelector/RepositorySelector'
import ArtifactListTable from './components/ArtifactListTable/ArtifactListTable' import { ArtifactListVersionFilter } from './constants'
import LabelsSelector from './components/LabelsSelector/LabelsSelector' import LabelsSelector from './components/LabelsSelector/LabelsSelector'
import ArtifactListTable from './components/ArtifactListTable/ArtifactListTable'
import RepositorySelector from './components/RepositorySelector/RepositorySelector'
import ArtifactSearchInput from './components/ArtifactSearchInput/ArtifactSearchInput'
import { useArtifactListQueryParamOptions, type ArtifactListPageQueryParams } from './utils' import { useArtifactListQueryParamOptions, type ArtifactListPageQueryParams } from './utils'
import css from './ArtifactListPage.module.scss' import css from './ArtifactListPage.module.scss'
interface ArtifactListPageProps { function ArtifactListPage(): JSX.Element {
withHeader?: boolean
parentRepoKey?: string
pageBodyClassName?: string
}
function ArtifactListPage({ withHeader = true, parentRepoKey, pageBodyClassName }: ArtifactListPageProps): JSX.Element {
const { getString } = useStrings() const { getString } = useStrings()
const { NGBreadcrumbs } = useParentComponents() const { NGBreadcrumbs } = useParentComponents()
const { useQueryParams, useUpdateQueryParams, usePreferenceStore } = useParentHooks() const { useQueryParams, useUpdateQueryParams, usePreferenceStore } = useParentHooks()
const searchRef = useRef({} as ExpandingSearchInputHandle)
const { updateQueryParams } = useUpdateQueryParams<Partial<ArtifactListPageQueryParams>>() const { updateQueryParams } = useUpdateQueryParams<Partial<ArtifactListPageQueryParams>>()
const queryParams = useQueryParams<ArtifactListPageQueryParams>(useArtifactListQueryParamOptions()) const queryParams = useQueryParams<ArtifactListPageQueryParams>(useArtifactListQueryParamOptions())
const { searchTerm, isDeployedArtifacts, packageTypes, repositoryKey, page, size, labels } = queryParams const { searchTerm, isDeployedArtifacts, repositoryKey, page, size, latestVersion, packageTypes, labels } =
const shouldRenderRepositorySelectFilter = !parentRepoKey queryParams
const shouldRenderPackageTypeSelectFilter = !parentRepoKey
const spaceRef = useGetSpaceRef('') const spaceRef = useGetSpaceRef('')
const { preference: sortingPreference, setPreference: setSortingPreference } = usePreferenceStore<string | undefined>( const { preference: sortingPreference, setPreference: setSortingPreference } = usePreferenceStore<string | undefined>(
@ -73,108 +64,108 @@ function ArtifactListPage({ withHeader = true, parentRepoKey, pageBodyClassName
refetch, refetch,
isLoading: loading, isLoading: loading,
error error
} = useGetAllArtifactsQuery({ } = useGetAllHarnessArtifactsQuery({
space_ref: spaceRef, space_ref: spaceRef,
queryParams: { queryParams: {
page, page,
size, size,
label: labels,
package_type: packageTypes,
search_term: searchTerm, search_term: searchTerm,
sort_field: sortField, sort_field: sortField,
sort_order: sortOrder, sort_order: sortOrder,
reg_identifier: defaultTo(parentRepoKey, repositoryKey) reg_identifier: repositoryKey ? [repositoryKey] : undefined,
}, latest_version: latestVersion,
deployed_artifact: isDeployedArtifacts,
package_type: packageTypes,
label: labels
} as GetAllHarnessArtifactsQueryQueryParams,
stringifyQueryParamsOptions: { stringifyQueryParamsOptions: {
arrayFormat: 'repeat' arrayFormat: 'repeat'
} }
}) })
const handleClearAllFilters = (): void => { const handleClearAllFilters = (): void => {
flushSync(searchRef.current.clear)
updateQueryParams({ updateQueryParams({
page: 0, page: 0,
searchTerm: '', searchTerm: '',
packageTypes: [], isDeployedArtifacts: false,
isDeployedArtifacts: false latestVersion: false
}) })
} }
const handleClickLabel = useCallback( const hasFilter = !!searchTerm || isDeployedArtifacts || latestVersion
(val: string) => {
if (labels.includes(val)) return
updateQueryParams({
labels: [...labels, val],
page: DEFAULT_PAGE_INDEX
})
},
[labels]
)
const hasFilter = !!searchTerm || packageTypes.length || isDeployedArtifacts
const responseData = data?.content?.data const responseData = data?.content?.data
return ( return (
<> <>
{withHeader && ( <Page.Header
<Page.Header title={
title={ <div className="ng-tooltip-native">
<div className="ng-tooltip-native"> <h2 data-tooltip-id="artifactsPageHeading">{getString('artifactList.pageHeading')}</h2>
<h2 data-tooltip-id="artifactsPageHeading">{getString('artifactList.pageHeading')}</h2> <HarnessDocTooltip tooltipId="artifactsPageHeading" useStandAlone={true} />
<HarnessDocTooltip tooltipId="artifactsPageHeading" useStandAlone={true} /> </div>
</div> }
} breadcrumbs={<NGBreadcrumbs links={[]} />}
breadcrumbs={<NGBreadcrumbs links={[]} />} />
/>
)}
<Page.SubHeader className={css.subHeader}> <Page.SubHeader className={css.subHeader}>
<div className={css.subHeaderItems}> <div className={css.subHeaderItems}>
{shouldRenderRepositorySelectFilter && ( <ArtifactSearchInput
<RepositorySelector searchTerm={searchTerm || ''}
value={repositoryKey} onChange={text => {
onChange={val => { updateQueryParams({ searchTerm: text || undefined, page: DEFAULT_PAGE_INDEX })
updateQueryParams({ repositoryKey: val, page: DEFAULT_PAGE_INDEX }) }}
}} placeholder={getString('search')}
/> />
)} <RepositorySelector
{shouldRenderPackageTypeSelectFilter && ( value={repositoryKey}
<PackageTypeSelector onChange={val => {
value={packageTypes} updateQueryParams({ repositoryKey: val, page: DEFAULT_PAGE_INDEX })
onChange={val => { }}
updateQueryParams({ packageTypes: val, page: DEFAULT_PAGE_INDEX }) />
}} <PackageTypeSelector
/> value={packageTypes}
)} onChange={val => {
updateQueryParams({ packageTypes: val, page: DEFAULT_PAGE_INDEX })
}}
/>
<LabelsSelector <LabelsSelector
value={labels} value={labels}
onChange={val => { onChange={val => {
updateQueryParams({ labels: val, page: DEFAULT_PAGE_INDEX }) updateQueryParams({ labels: val, page: DEFAULT_PAGE_INDEX })
}} }}
/> />
{/* TODO: removed till BE support this filter */}
{/* <TableFilterCheckbox
value={isDeployedArtifacts}
label={getString('artifactList.deployedArtifacts')}
disabled={false}
onChange={val => {
updateQueryParams({ isDeployedArtifacts: val, page: DEFAULT_PAGE_INDEX })
}}
/> */}
<Expander /> <Expander />
<ExpandingSearchInput <ButtonTabs
alwaysExpanded className={css.filterTabContainer}
width={200} small
placeholder={getString('search')} bold
onChange={text => { selectedTabId={
updateQueryParams({ searchTerm: text || undefined, page: DEFAULT_PAGE_INDEX }) latestVersion ? ArtifactListVersionFilter.LATEST_VERSION : ArtifactListVersionFilter.ALL_VERSION
}} }
defaultValue={searchTerm} onChange={newTab => {
ref={searchRef} updateQueryParams({
/> latestVersion: newTab === ArtifactListVersionFilter.LATEST_VERSION,
page: DEFAULT_PAGE_INDEX
})
}}>
<ButtonTab
id={ArtifactListVersionFilter.LATEST_VERSION}
icon="layers"
iconProps={{ size: 12 }}
panel={<></>}
title={getString('artifactList.table.latestVersions')}
/>
<ButtonTab
id={ArtifactListVersionFilter.ALL_VERSION}
icon="document"
iconProps={{ size: 12 }}
panel={<></>}
title={getString('artifactList.table.allVersions')}
/>
</ButtonTabs>
</div> </div>
</Page.SubHeader> </Page.SubHeader>
<Page.Body <Page.Body
className={classNames(css.pageBody, pageBodyClassName)} className={classNames(css.pageBody)}
loading={loading} loading={loading}
error={error?.message} error={error?.message}
retryOnError={() => refetch()} retryOnError={() => refetch()}
@ -199,7 +190,6 @@ function ArtifactListPage({ withHeader = true, parentRepoKey, pageBodyClassName
setSortingPreference(JSON.stringify(sortArray)) setSortingPreference(JSON.stringify(sortArray))
updateQueryParams({ sort: sortArray }) updateQueryParams({ sort: sortArray })
}} }}
onClickLabel={handleClickLabel}
sortBy={sort} sortBy={sort}
/> />
)} )}

View File

@ -0,0 +1,161 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import React, { useCallback, useMemo, useRef } from 'react'
import classNames from 'classnames'
import { flushSync } from 'react-dom'
import { Expander } from '@blueprintjs/core'
import { ExpandingSearchInput, Page, type ExpandingSearchInputHandle, Button, ButtonVariation } from '@harnessio/uicore'
import { useGetAllArtifactsByRegistryQuery } from '@harnessio/react-har-service-client'
import { useStrings } from '@ar/frameworks/strings'
import { DEFAULT_PAGE_INDEX, PreferenceScope } from '@ar/constants'
import { useGetSpaceRef, useParentHooks } from '@ar/hooks'
import LabelsSelector from './components/LabelsSelector/LabelsSelector'
import {
useRegistryArtifactListQueryParamOptions,
type RegistryArtifactListPageQueryParams
} from './components/RegistryArtifactListTable/utils'
import RegistryArtifactListTable from './components/RegistryArtifactListTable/RegistryArtifactListTable'
import css from './ArtifactListPage.module.scss'
interface RegistryArtifactListPageProps {
pageBodyClassName?: string
}
function RegistryArtifactListPage({ pageBodyClassName }: RegistryArtifactListPageProps): JSX.Element {
const { getString } = useStrings()
const { useQueryParams, useUpdateQueryParams, usePreferenceStore } = useParentHooks()
const searchRef = useRef({} as ExpandingSearchInputHandle)
const { updateQueryParams } = useUpdateQueryParams<Partial<RegistryArtifactListPageQueryParams>>()
const queryParams = useQueryParams<RegistryArtifactListPageQueryParams>(useRegistryArtifactListQueryParamOptions())
const { searchTerm, isDeployedArtifacts, packageTypes, page, size, labels } = queryParams
const registryRef = useGetSpaceRef()
const { preference: sortingPreference, setPreference: setSortingPreference } = usePreferenceStore<string | undefined>(
PreferenceScope.USER,
'ArtifactRepositorySortingPreference'
)
const sort = useMemo(
() => (sortingPreference ? JSON.parse(sortingPreference) : queryParams.sort),
[queryParams.sort, sortingPreference]
)
const [sortField, sortOrder] = sort || []
const {
data,
refetch,
isLoading: loading,
error
} = useGetAllArtifactsByRegistryQuery({
registry_ref: registryRef,
queryParams: {
page,
size,
search_term: searchTerm,
sort_field: sortField,
sort_order: sortOrder
},
stringifyQueryParamsOptions: {
arrayFormat: 'repeat'
}
})
const handleClearAllFilters = (): void => {
flushSync(searchRef.current.clear)
updateQueryParams({
page: 0,
searchTerm: '',
packageTypes: [],
isDeployedArtifacts: false
})
}
const handleClickLabel = useCallback(
(val: string) => {
if (labels.includes(val)) return
updateQueryParams({
labels: [...labels, val],
page: DEFAULT_PAGE_INDEX
})
},
[labels]
)
const hasFilter = !!searchTerm || packageTypes.length || isDeployedArtifacts
const responseData = data?.content?.data
return (
<>
<Page.SubHeader className={css.subHeader}>
<div className={css.subHeaderItems}>
<LabelsSelector
value={labels}
onChange={val => {
updateQueryParams({ labels: val, page: DEFAULT_PAGE_INDEX })
}}
/>
<Expander />
<ExpandingSearchInput
alwaysExpanded
width={200}
placeholder={getString('search')}
onChange={text => {
updateQueryParams({ searchTerm: text || undefined, page: DEFAULT_PAGE_INDEX })
}}
defaultValue={searchTerm}
ref={searchRef}
/>
</div>
</Page.SubHeader>
<Page.Body
className={classNames(css.pageBody, pageBodyClassName)}
loading={loading}
error={error?.message}
retryOnError={() => refetch()}
noData={{
when: () => !responseData?.artifacts?.length,
// image: getEmptyStateIllustration(hasFilter, module),
icon: 'container',
messageTitle: hasFilter ? getString('noResultsFound') : getString('artifactList.table.noArtifactsTitle'),
button: hasFilter ? (
<Button text={getString('clearFilters')} variation={ButtonVariation.LINK} onClick={handleClearAllFilters} />
) : undefined
}}>
{responseData && (
<RegistryArtifactListTable
data={responseData}
gotoPage={pageNumber => updateQueryParams({ page: pageNumber })}
onPageSizeChange={newSize => updateQueryParams({ size: newSize, page: DEFAULT_PAGE_INDEX })}
refetchList={() => {
refetch()
}}
setSortBy={sortArray => {
setSortingPreference(JSON.stringify(sortArray))
updateQueryParams({ sort: sortArray })
}}
onClickLabel={handleClickLabel}
sortBy={sort}
/>
)}
</Page.Body>
</>
)
}
export default RegistryArtifactListPage

View File

@ -34,7 +34,7 @@
div[class*='TableV2--cells'], div[class*='TableV2--cells'],
div[class*='TableV2--header'] { div[class*='TableV2--header'] {
display: grid !important; display: grid !important;
grid-template-columns: 1fr 15rem 20rem 15rem; grid-template-columns: 1fr 10rem 10rem 15rem 15rem 15rem 50px;
} }
} }
@ -45,3 +45,11 @@
.nameCellContainer { .nameCellContainer {
align-items: center; align-items: center;
} }
.cellBtn {
--padding: 0 !important;
}
.optionsMenu {
min-width: unset;
}

View File

@ -16,6 +16,8 @@
/* eslint-disable */ /* eslint-disable */
// This is an auto-generated file // This is an auto-generated file
export declare const cellBtn: string
export declare const nameCellContainer: string export declare const nameCellContainer: string
export declare const optionsMenu: string
export declare const table: string export declare const table: string
export declare const tableRow: string export declare const tableRow: string

View File

@ -23,10 +23,12 @@ import type { ArtifactMetadata, ListArtifact } from '@harnessio/react-har-servic
import { useStrings } from '@ar/frameworks/strings' import { useStrings } from '@ar/frameworks/strings'
import { useParentHooks } from '@ar/hooks' import { useParentHooks } from '@ar/hooks'
import { import {
ArtifactDeploymentsCell,
ArtifactDownloadsCell, ArtifactDownloadsCell,
ArtifactListPullCommandCell,
ArtifactListVulnerabilitiesCell,
ArtifactNameCell, ArtifactNameCell,
LatestArtifactCell, LatestArtifactCell
RepositoryNameCell
} from './ArtifactListTableCell' } from './ArtifactListTableCell'
import css from './ArtifactListTable.module.scss' import css from './ArtifactListTable.module.scss'
@ -40,11 +42,10 @@ export interface ArtifactListTableProps extends ArtifactListColumnActions {
setSortBy: (sortBy: string[]) => void setSortBy: (sortBy: string[]) => void
sortBy: string[] sortBy: string[]
minimal?: boolean minimal?: boolean
onClickLabel: (val: string) => void
} }
export default function ArtifactListTable(props: ArtifactListTableProps): JSX.Element { export default function ArtifactListTable(props: ArtifactListTableProps): JSX.Element {
const { data, gotoPage, onPageSizeChange, sortBy, setSortBy, onClickLabel } = props const { data, gotoPage, onPageSizeChange, sortBy, setSortBy } = props
const { useDefaultPaginationProps } = useParentHooks() const { useDefaultPaginationProps } = useParentHooks()
const { getString } = useStrings() const { getString } = useStrings()
@ -72,17 +73,16 @@ export default function ArtifactListTable(props: ArtifactListTableProps): JSX.El
} }
return [ return [
{ {
Header: getString('artifactList.table.columns.name'), Header: getString('artifactList.table.columns.artifactName'),
accessor: 'name', accessor: 'name',
Cell: ArtifactNameCell, Cell: ArtifactNameCell,
serverSortProps: getServerSortProps('name'), serverSortProps: getServerSortProps('name')
onClickLabel
}, },
{ {
Header: getString('artifactList.table.columns.repository'), Header: getString('artifactList.table.columns.pullCommand'),
accessor: 'registryIdentifier', accessor: 'pullCommand',
Cell: RepositoryNameCell, Cell: ArtifactListPullCommandCell,
serverSortProps: getServerSortProps('registryIdentifier') disableSortBy: true
}, },
{ {
Header: getString('artifactList.table.columns.downloads'), Header: getString('artifactList.table.columns.downloads'),
@ -91,13 +91,25 @@ export default function ArtifactListTable(props: ArtifactListTableProps): JSX.El
serverSortProps: getServerSortProps('downloadsCount') serverSortProps: getServerSortProps('downloadsCount')
}, },
{ {
Header: getString('artifactList.table.columns.latestVersion'), Header: getString('artifactList.table.columns.environments'),
accessor: 'latestVersion', accessor: 'environments',
Cell: ArtifactDeploymentsCell,
disableSortBy: true
},
{
Header: getString('artifactList.table.columns.sto'),
accessor: 'sto',
Cell: ArtifactListVulnerabilitiesCell,
disableSortBy: true
},
{
Header: getString('artifactList.table.columns.lastUpdated'),
accessor: 'lastUpdated',
Cell: LatestArtifactCell, Cell: LatestArtifactCell,
serverSortProps: getServerSortProps('latestVersion') serverSortProps: getServerSortProps('lastUpdated')
} }
].filter(Boolean) as unknown as Column<ArtifactMetadata>[] ].filter(Boolean) as unknown as Column<ArtifactMetadata>[]
}, [currentOrder, currentSort, getString, onClickLabel]) }, [currentOrder, currentSort, getString])
return ( return (
<TableV2<ArtifactMetadata> <TableV2<ArtifactMetadata>

View File

@ -14,22 +14,25 @@
* limitations under the License. * limitations under the License.
*/ */
import React from 'react' import React, { useState } from 'react'
import { defaultTo } from 'lodash-es' import { defaultTo } from 'lodash-es'
import { Position } from '@blueprintjs/core' import { Link, useHistory } from 'react-router-dom'
import { Menu, Position } from '@blueprintjs/core'
import { Color, FontVariation } from '@harnessio/design-system'
import { Button, ButtonVariation, Layout, Text } from '@harnessio/uicore'
import type { Cell, CellValue, ColumnInstance, Renderer, Row, TableInstance } from 'react-table' import type { Cell, CellValue, ColumnInstance, Renderer, Row, TableInstance } from 'react-table'
import { Layout, Text } from '@harnessio/uicore' import type { ArtifactMetadata, StoDigestMetadata } from '@harnessio/react-har-service-client'
import { Color } from '@harnessio/design-system'
import type { ArtifactMetadata } from '@harnessio/react-har-service-client'
import { useRoutes } from '@ar/hooks' import { useParentComponents, useRoutes } from '@ar/hooks'
import Tag from '@ar/components/Tag/Tag'
import { useStrings } from '@ar/frameworks/strings'
import TableCells from '@ar/components/TableCells/TableCells' import TableCells from '@ar/components/TableCells/TableCells'
import type { RepositoryPackageType } from '@ar/common/types'
import LabelsPopover from '@ar/components/LabelsPopover/LabelsPopover' import LabelsPopover from '@ar/components/LabelsPopover/LabelsPopover'
import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon' import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon'
import type { RepositoryPackageType } from '@ar/common/types' import { useStrings } from '@ar/frameworks/strings'
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants' import { getShortDigest } from '@ar/pages/digest-list/utils'
import { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
import css from './ArtifactListTable.module.scss'
type CellTypeWithActions<D extends Record<string, any>, V = any> = TableInstance<D> & { type CellTypeWithActions<D extends Record<string, any>, V = any> = TableInstance<D> & {
column: ColumnInstance<D> column: ColumnInstance<D>
@ -51,62 +54,37 @@ export const ArtifactNameCell: Renderer<{
const { original } = row const { original } = row
const { onClickLabel } = column const { onClickLabel } = column
const routes = useRoutes() const routes = useRoutes()
const value = original.name const { name: value, version } = original
return ( return (
<TableCells.LinkCell <Layout.Vertical>
prefix={<RepositoryIcon packageType={original.packageType as RepositoryPackageType} iconProps={{ size: 24 }} />} <TableCells.LinkCell
linkTo={routes.toARArtifactDetails({ prefix={<RepositoryIcon packageType={original.packageType as RepositoryPackageType} iconProps={{ size: 24 }} />}
repositoryIdentifier: original.registryIdentifier, linkTo={routes.toARVersionDetailsTab({
artifactIdentifier: value repositoryIdentifier: original.registryIdentifier,
})} artifactIdentifier: value,
label={value} versionIdentifier: defaultTo(version, ''),
postfix={ versionTab: VersionDetailsTab.OVERVIEW
<LabelsPopover })}
popoverProps={{ label={value}
position: Position.RIGHT subLabel={version}
}} postfix={
labels={defaultTo(original.labels, [])} <LabelsPopover
tagProps={{ popoverProps={{
interactive: true, position: Position.RIGHT
onClick: e => onClickLabel(e.currentTarget.ariaValueText as string) }}
}} labels={defaultTo(original.labels, [])}
/> tagProps={{
} interactive: true,
/> onClick: e => {
) if (typeof onClickLabel === 'function') {
} onClickLabel(e.currentTarget.ariaValueText as string)
}
export const ArtifactTagsCell: CellType = ({ value }) => { }
const { getString } = useStrings() }}
if (!Array.isArray(value) || !value.length) { />
return ( }
<Text color={Color.GREY_900} font={{ size: 'small' }}> />
{getString('na')} </Layout.Vertical>
</Text>
)
}
return (
<Layout.Horizontal spacing="small">
{Array.isArray(value) &&
value.map(each => (
<Tag key={each} isArtifactTag>
{each}
</Tag>
))}
</Layout.Horizontal>
)
}
export const RepositoryNameCell: CellType = ({ value }) => {
const routes = useRoutes()
return (
<TableCells.LinkCell
linkTo={routes.toARRepositoryDetails({
repositoryIdentifier: value,
tab: RepositoryDetailsTab.PACKAGES
})}
label={value}
/>
) )
} }
@ -114,23 +92,107 @@ export const ArtifactDownloadsCell: CellType = ({ value }) => {
return <TableCells.CountCell value={value} icon="download-box" iconProps={{ size: 12 }} /> return <TableCells.CountCell value={value} icon="download-box" iconProps={{ size: 12 }} />
} }
export const LatestArtifactCell: CellType = ({ row }) => { export const ArtifactDeploymentsCell: CellType = ({ row }) => {
const { getString } = useStrings()
const { original } = row const { original } = row
const { latestVersion, lastModified } = original || {} const { deploymentMetadata } = original
if (!latestVersion) { const { nonProdEnvCount, prodEnvCount } = deploymentMetadata || {}
return <TableCells.DeploymentsCell prodCount={prodEnvCount} nonProdCount={nonProdEnvCount} />
}
export const ArtifactListPullCommandCell: CellType = ({ value }) => {
const { getString } = useStrings()
return <TableCells.CopyTextCell value={value}>{getString('copy')}</TableCells.CopyTextCell>
}
export const ArtifactListVulnerabilitiesCell: CellType = ({ row }) => {
const { original } = row
const { stoMetadata, registryIdentifier, name, version } = original
const { scannedCount, totalCount, digestMetadata } = stoMetadata || {}
const [isOptionsOpen, setIsOptionsOpen] = useState(false)
const { getString } = useStrings()
const { RbacMenuItem } = useParentComponents()
const routes = useRoutes()
const history = useHistory()
const handleRenderDigestMenuItem = (digest: StoDigestMetadata) => {
return ( return (
<Text color={Color.GREY_900} font={{ size: 'small' }}> <RbacMenuItem
{getString('na')} text={getString('artifactList.table.actions.VulnerabilityStatus.digestMenuItemText', {
</Text> archName: digest.osArch,
digest: getShortDigest(digest.digest || '')
})}
onClick={() => {
const url = routes.toARVersionDetailsTab({
repositoryIdentifier: registryIdentifier,
artifactIdentifier: name,
versionIdentifier: version as string,
versionTab: VersionDetailsTab.SECURITY_TESTS,
pipelineIdentifier: digest.stoPipelineId,
executionIdentifier: digest.stoExecutionId
})
history.push(`${url}?digest=${digest.digest}`)
}}
/>
) )
} }
if (!scannedCount) {
return <Text>{getString('artifactList.table.actions.VulnerabilityStatus.nonScanned')}</Text>
}
return ( return (
<Layout.Vertical spacing="small"> <Button
<Text color={Color.PRIMARY_7} font={{ size: 'small' }}> className={css.cellBtn}
{latestVersion} tooltip={
<Menu
className={css.optionsMenu}
onClick={e => {
e.stopPropagation()
setIsOptionsOpen(false)
}}>
{digestMetadata?.map(handleRenderDigestMenuItem)}
</Menu>
}
tooltipProps={{
interactionKind: 'click',
onInteraction: nextOpenState => {
setIsOptionsOpen(nextOpenState)
},
isOpen: isOptionsOpen,
position: Position.BOTTOM
}}
variation={ButtonVariation.LINK}>
<Text font={{ variation: FontVariation.BODY }} color={Color.PRIMARY_7}>
{getString('artifactList.table.actions.VulnerabilityStatus.partiallyScanned', {
total: defaultTo(totalCount, 0),
scanned: defaultTo(scannedCount, 0)
})}
</Text> </Text>
<TableCells.LastModifiedCell value={defaultTo(lastModified, 0)} /> </Button>
</Layout.Vertical>
) )
} }
export const ScanStatusCell: CellType = ({ row }) => {
const { original } = row
const router = useRoutes()
const { version = '', name, registryIdentifier } = original
const { getString } = useStrings()
const linkTo = router.toARVersionDetailsTab({
repositoryIdentifier: registryIdentifier,
artifactIdentifier: name,
versionIdentifier: version,
versionTab: VersionDetailsTab.OVERVIEW
})
return (
<Link to={linkTo} target="_blank">
<Text color={Color.PRIMARY_7} rightIcon="main-share" rightIconProps={{ size: 12, color: Color.PRIMARY_7 }}>
{getString('artifactList.table.actions.VulnerabilityStatus.scanStatus')}
</Text>
</Link>
)
}
export const LatestArtifactCell: CellType = ({ row }) => {
const { original } = row
return <TableCells.LastModifiedCell value={defaultTo(original.lastModified, 0)} />
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import React from 'react'
import AISearchInput from '@ar/components/AISearchInput/AISearchInput'
import { ArtifactListAIOptions } from './constants'
interface ArtifactSearchInputProps {
searchTerm: string
onChange: (val: string) => void
placeholder?: string
}
export default function ArtifactSearchInput(props: ArtifactSearchInputProps) {
return <AISearchInput {...props} options={ArtifactListAIOptions} />
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import type { AIOption } from '@ar/components/AISearchInput/types'
export const ArtifactListAIOptions: AIOption[] = [
{
label: 'Need help? Here are some prompts example:',
value: '-1',
disabled: true
},
{
label: 'Show me artifacts not downloaded in last 30 days',
value: 'Show me artifacts not downloaded in last 30 days'
},
{
label: 'Find out artifacts in prod but not deployed in lower environments',
value: 'Find out artifacts in prod but not deployed in lower environments'
},
{
label: 'List artifacts in prod but with critical vulnerabilities',
value: 'List artifacts in prod but with critical vulnerabilities'
}
]

View File

@ -0,0 +1,47 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
.table {
--typography-size: 12px;
--typography-weight: 400;
--line-height: 20px;
padding: var(--spacing-large);
display: grid;
grid-template-rows: max-content 1fr max-content;
row-gap: var(--spacing-small);
min-height: inherit;
[role='cell'],
[role='columnheader'] {
width: auto !important;
padding-right: var(--spacing-small);
}
div[class*='TableV2--cells'],
div[class*='TableV2--header'] {
display: grid !important;
grid-template-columns: 1fr 15rem 20rem 15rem;
}
}
.tableRow {
position: relative;
}
.nameCellContainer {
align-items: center;
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2023 Harness, Inc.
*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const nameCellContainer: string
export declare const table: string
export declare const tableRow: string

View File

@ -0,0 +1,112 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import React from 'react'
import classNames from 'classnames'
import type { Column } from 'react-table'
import { PaginationProps, TableV2 } from '@harnessio/uicore'
import type { ArtifactMetadata, ListArtifact } from '@harnessio/react-har-service-client'
import { useStrings } from '@ar/frameworks/strings'
import { useParentHooks } from '@ar/hooks'
import {
RegistryArtifactDownloadsCell,
RegistryArtifactLatestUpdatedCell,
RegistryArtifactNameCell,
RepositoryNameCell
} from './RegistryArtifactListTableCell'
import css from './RegistryArtifactListTable.module.scss'
export interface RegistryArtifactListColumnActions {
refetchList?: () => void
}
export interface RegistryArtifactListTableProps extends RegistryArtifactListColumnActions {
data: ListArtifact
gotoPage: (pageNumber: number) => void
onPageSizeChange?: PaginationProps['onPageSizeChange']
setSortBy: (sortBy: string[]) => void
sortBy: string[]
minimal?: boolean
onClickLabel: (val: string) => void
}
export default function RegistryArtifactListTable(props: RegistryArtifactListTableProps): JSX.Element {
const { data, gotoPage, onPageSizeChange, sortBy, setSortBy, onClickLabel } = props
const { useDefaultPaginationProps } = useParentHooks()
const { getString } = useStrings()
const { artifacts = [], itemCount = 0, pageCount = 0, pageIndex, pageSize = 0 } = data || {}
const paginationProps = useDefaultPaginationProps({
itemCount,
pageSize,
pageCount,
pageIndex,
gotoPage,
onPageSizeChange
})
const [currentSort, currentOrder] = sortBy
const columns: Column<ArtifactMetadata>[] = React.useMemo(() => {
const getServerSortProps = (id: string) => {
return {
enableServerSort: true,
isServerSorted: currentSort === id,
isServerSortedDesc: currentOrder === 'DESC',
getSortedColumn: ({ sort }: any) => {
setSortBy([sort, currentOrder === 'DESC' ? 'ASC' : 'DESC'])
}
}
}
return [
{
Header: getString('artifactList.table.columns.name'),
accessor: 'name',
Cell: RegistryArtifactNameCell,
serverSortProps: getServerSortProps('name'),
onClickLabel
},
{
Header: getString('artifactList.table.columns.repository'),
accessor: 'registryIdentifier',
Cell: RepositoryNameCell,
serverSortProps: getServerSortProps('registryIdentifier')
},
{
Header: getString('artifactList.table.columns.downloads'),
accessor: 'downloadsCount',
Cell: RegistryArtifactDownloadsCell,
serverSortProps: getServerSortProps('downloadsCount')
},
{
Header: getString('artifactList.table.columns.latestVersion'),
accessor: 'latestVersion',
Cell: RegistryArtifactLatestUpdatedCell,
serverSortProps: getServerSortProps('latestVersion')
}
].filter(Boolean) as unknown as Column<ArtifactMetadata>[]
}, [currentOrder, currentSort, getString, onClickLabel])
return (
<TableV2<ArtifactMetadata>
className={classNames(css.table)}
columns={columns}
data={artifacts}
pagination={paginationProps}
sortable
getRowClassName={() => css.tableRow}
/>
)
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import React from 'react'
import { defaultTo } from 'lodash-es'
import { Position } from '@blueprintjs/core'
import type { Cell, CellValue, ColumnInstance, Renderer, Row, TableInstance } from 'react-table'
import { Layout, Text } from '@harnessio/uicore'
import { Color } from '@harnessio/design-system'
import type { RegistryArtifactMetadata } from '@harnessio/react-har-service-client'
import { useRoutes } from '@ar/hooks'
import Tag from '@ar/components/Tag/Tag'
import { useStrings } from '@ar/frameworks/strings'
import TableCells from '@ar/components/TableCells/TableCells'
import LabelsPopover from '@ar/components/LabelsPopover/LabelsPopover'
import RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon'
import type { RepositoryPackageType } from '@ar/common/types'
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
type CellTypeWithActions<D extends Record<string, any>, V = any> = TableInstance<D> & {
column: ColumnInstance<D>
row: Row<D>
cell: Cell<D, V>
value: CellValue<V>
}
type CellType = Renderer<CellTypeWithActions<RegistryArtifactMetadata>>
type RegistryArtifactNameCellActionProps = {
onClickLabel: (val: string) => void
}
export const RegistryArtifactNameCell: Renderer<{
row: Row<RegistryArtifactMetadata>
column: ColumnInstance<RegistryArtifactMetadata> & RegistryArtifactNameCellActionProps
}> = ({ row, column }) => {
const { original } = row
const { onClickLabel } = column
const routes = useRoutes()
const value = original.name
return (
<TableCells.LinkCell
prefix={<RepositoryIcon packageType={original.packageType as RepositoryPackageType} iconProps={{ size: 24 }} />}
linkTo={routes.toARArtifactDetails({
repositoryIdentifier: original.registryIdentifier,
artifactIdentifier: value
})}
label={value}
postfix={
<LabelsPopover
popoverProps={{
position: Position.RIGHT
}}
labels={defaultTo(original.labels, [])}
tagProps={{
interactive: true,
onClick: e => onClickLabel(e.currentTarget.ariaValueText as string)
}}
/>
}
/>
)
}
export const RegistryArtifactTagsCell: CellType = ({ value }) => {
const { getString } = useStrings()
if (!Array.isArray(value) || !value.length) {
return (
<Text color={Color.GREY_900} font={{ size: 'small' }}>
{getString('na')}
</Text>
)
}
return (
<Layout.Horizontal spacing="small">
{Array.isArray(value) &&
value.map(each => (
<Tag key={each} isArtifactTag>
{each}
</Tag>
))}
</Layout.Horizontal>
)
}
export const RepositoryNameCell: CellType = ({ value }) => {
const routes = useRoutes()
return (
<TableCells.LinkCell
linkTo={routes.toARRepositoryDetails({
repositoryIdentifier: value,
tab: RepositoryDetailsTab.PACKAGES
})}
label={value}
/>
)
}
export const RegistryArtifactDownloadsCell: CellType = ({ value }) => {
return <TableCells.CountCell value={value} icon="download-box" iconProps={{ size: 12 }} />
}
export const RegistryArtifactLatestUpdatedCell: CellType = ({ row }) => {
const { getString } = useStrings()
const { original } = row
const { latestVersion, lastModified } = original || {}
if (!latestVersion) {
return (
<Text color={Color.GREY_900} font={{ size: 'small' }}>
{getString('na')}
</Text>
)
}
return (
<Layout.Vertical spacing="small">
<Text color={Color.PRIMARY_7} font={{ size: 'small' }}>
{latestVersion}
</Text>
<TableCells.LastModifiedCell value={defaultTo(lastModified, 0)} />
</Layout.Vertical>
)
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
import { useMemo } from 'react'
import { useParentHooks } from '@ar/hooks'
import type { RepositoryPackageType } from '@ar/common/types'
import { DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE, DEFAULT_PIPELINE_LIST_TABLE_SORT } from '@ar/constants'
import type { UseQueryParamsOptions } from '@ar/__mocks__/hooks'
export type RegistryArtifactListPageQueryParams = {
page: number
size: number
sort: string[]
searchTerm?: string
isDeployedArtifacts: boolean
packageTypes: RepositoryPackageType[]
repositoryKey?: string
labels: string[]
}
export const useRegistryArtifactListQueryParamOptions =
(): UseQueryParamsOptions<RegistryArtifactListPageQueryParams> => {
const { useQueryParamsOptions } = useParentHooks()
const _options = useQueryParamsOptions(
{
page: DEFAULT_PAGE_INDEX,
size: DEFAULT_PAGE_SIZE,
sort: DEFAULT_PIPELINE_LIST_TABLE_SORT,
isDeployedArtifacts: false,
packageTypes: [],
labels: []
},
{ ignoreEmptyString: false }
)
const options = useMemo(() => ({ ..._options, strictNullHandling: true }), [_options])
return options
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2024 Harness, Inc.
*
* 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.
*/
export enum ArtifactListVersionFilter {
LATEST_VERSION = 'latest_version',
ALL_VERSION = 'all_version'
}

View File

@ -3,13 +3,26 @@ pageHeading: Artifacts
deployedArtifacts: Deployed Artifacts deployedArtifacts: Deployed Artifacts
table: table:
noArtifactsTitle: There are no artifact available. noArtifactsTitle: There are no artifact available.
allRepositories: All Registries allRepositories: Registries
latestVersions: Latest Versions
allVersions: All Versions
columns: columns:
name: Name name: Name
artifactName: Artifact Name
repository: Registry repository: Registry
tags: Labels tags: Labels
downloads: Downloads (In the last week) downloads: Downloads
latestVersion: Latest Version latestVersion: Latest Version
pullCommand: Pull Command
environments: Environments
sto: Security Vulnerabilities
lastUpdated: Uploaded At
actions: actions:
editRepository: Edit Registry editRepository: Edit Registry
deleteRepository: Delete Registry deleteRepository: Delete Registry
VulnerabilityStatus:
scanned: Scanned
nonScanned: Not Scanned
scanStatus: Scan Status
partiallyScanned: '{{ scanned }}/{{ total }} Scanned'
digestMenuItemText: '{{archName}} (digest: {{digest}})'

View File

@ -17,29 +17,22 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { useParentHooks } from '@ar/hooks' import { useParentHooks } from '@ar/hooks'
import type { RepositoryPackageType } from '@ar/common/types'
import { DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE, DEFAULT_PIPELINE_LIST_TABLE_SORT } from '@ar/constants' import { DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE, DEFAULT_PIPELINE_LIST_TABLE_SORT } from '@ar/constants'
import type { RepositoryPackageType } from '@ar/common/types'
import type { UseQueryParamsOptions } from '@ar/__mocks__/hooks' import type { UseQueryParamsOptions } from '@ar/__mocks__/hooks'
type GetArtifactListQueryParams = { export type ArtifactListPageQueryParams = {
accountIdentifier: string
orgIdentifier: string
projectIdentifier: string
page: number page: number
size: number size: number
sort: string[] sort: string[]
searchTerm?: string
isDeployedArtifacts: boolean
packageTypes: RepositoryPackageType[] packageTypes: RepositoryPackageType[]
repositoryKey?: string
labels: string[] labels: string[]
latestVersion: boolean
isDeployedArtifacts: boolean
searchTerm?: string
repositoryKey?: string
} }
export type ArtifactListPageQueryParams = Omit<
GetArtifactListQueryParams,
'accountIdentifier' | 'orgIdentifier' | 'projectIdentifier'
>
export const useArtifactListQueryParamOptions = (): UseQueryParamsOptions<ArtifactListPageQueryParams> => { export const useArtifactListQueryParamOptions = (): UseQueryParamsOptions<ArtifactListPageQueryParams> => {
const { useQueryParamsOptions } = useParentHooks() const { useQueryParamsOptions } = useParentHooks()
const _options = useQueryParamsOptions( const _options = useQueryParamsOptions(
@ -48,10 +41,11 @@ export const useArtifactListQueryParamOptions = (): UseQueryParamsOptions<Artifa
size: DEFAULT_PAGE_SIZE, size: DEFAULT_PAGE_SIZE,
sort: DEFAULT_PIPELINE_LIST_TABLE_SORT, sort: DEFAULT_PIPELINE_LIST_TABLE_SORT,
isDeployedArtifacts: false, isDeployedArtifacts: false,
latestVersion: false,
packageTypes: [], packageTypes: [],
labels: [] labels: []
}, },
{ ignoreEmptyString: false } { ignoreEmptyString: true }
) )
const options = useMemo(() => ({ ..._options, strictNullHandling: true }), [_options]) const options = useMemo(() => ({ ..._options, strictNullHandling: true }), [_options])

View File

@ -34,7 +34,7 @@
div[class*='TableV2--cells'], div[class*='TableV2--cells'],
div[class*='TableV2--header'] { div[class*='TableV2--header'] {
display: grid !important; display: grid !important;
grid-template-columns: 1fr 10rem 10rem 10rem 20rem 50px !important; grid-template-columns: 1fr 10rem 10rem 10rem 20rem 10rem 50px !important;
} }
div[class*='TableV2--header'] { div[class*='TableV2--header'] {

View File

@ -26,6 +26,7 @@ import {
DigestNameCell, DigestNameCell,
DownloadsCell, DownloadsCell,
OsArchCell, OsArchCell,
ScanStatusCell,
SizeCell, SizeCell,
UploadedByCell UploadedByCell
} from './DigestTableCells' } from './DigestTableCells'
@ -69,6 +70,12 @@ export default function DigestListTable(props: DigestListTableProps): JSX.Elemen
accessor: 'downloadsCount', accessor: 'downloadsCount',
Cell: DownloadsCell Cell: DownloadsCell
}, },
{
Header: getString('digestList.table.columns.scanStatus'),
accessor: 'scanStatus',
Cell: ScanStatusCell,
version
},
{ {
Header: '', Header: '',
accessor: 'menu', accessor: 'menu',

View File

@ -15,13 +15,18 @@
*/ */
import React from 'react' import React from 'react'
import { Link } from 'react-router-dom'
import type { Cell, CellValue, ColumnInstance, Renderer, Row, TableInstance } from 'react-table' import type { Cell, CellValue, ColumnInstance, Renderer, Row, TableInstance } from 'react-table'
import { Text } from '@harnessio/uicore'
import { Color } from '@harnessio/design-system'
import type { DockerManifestDetails } from '@harnessio/react-har-service-client' import type { DockerManifestDetails } from '@harnessio/react-har-service-client'
import { useStrings } from '@ar/frameworks/strings'
import { useDecodedParams, useRoutes } from '@ar/hooks' import { useDecodedParams, useRoutes } from '@ar/hooks'
import type { ArtifactDetailsPathParams } from '@ar/routes/types'
import TableCells from '@ar/components/TableCells/TableCells'
import { getShortDigest } from '@ar/pages/digest-list/utils' import { getShortDigest } from '@ar/pages/digest-list/utils'
import TableCells from '@ar/components/TableCells/TableCells'
import type { ArtifactDetailsPathParams } from '@ar/routes/types'
import { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants' import { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
type CellTypeWithActions<D extends Record<string, any>, V = any> = TableInstance<D> & { type CellTypeWithActions<D extends Record<string, any>, V = any> = TableInstance<D> & {
@ -72,6 +77,42 @@ export const DownloadsCell: CellType = ({ value }) => {
return <TableCells.CountCell value={value} icon="download-box" iconProps={{ size: 12 }} /> return <TableCells.CountCell value={value} icon="download-box" iconProps={{ size: 12 }} />
} }
export const ScanStatusCell: Renderer<{
row: Row<DockerManifestDetails>
column: ColumnInstance<DockerManifestDetails> & DigestNameColumnProps
}> = ({ row, column }) => {
const { original } = row
const { version } = column
const router = useRoutes()
const { stoExecutionId, stoPipelineId, digest } = original
const pathParams = useDecodedParams<ArtifactDetailsPathParams>()
const { getString } = useStrings()
if (!stoExecutionId || !stoPipelineId)
return <TableCells.TextCell value={getString('artifactList.table.actions.VulnerabilityStatus.nonScanned')} />
const linkTo = router.toARVersionDetailsTab({
repositoryIdentifier: pathParams.repositoryIdentifier,
artifactIdentifier: pathParams.artifactIdentifier,
versionIdentifier: version,
versionTab: VersionDetailsTab.SECURITY_TESTS,
pipelineIdentifier: stoPipelineId,
executionIdentifier: stoExecutionId
})
return (
<Link to={`${linkTo}?digest=${digest}`}>
<Text
color={Color.PRIMARY_7}
rightIcon="main-share"
rightIconProps={{
size: 12
}}
lineClamp={1}>
{getString('artifactList.table.actions.VulnerabilityStatus.scanned')}
</Text>
</Link>
)
}
export const DigestActionsCell: CellType = () => { export const DigestActionsCell: CellType = () => {
return <></> return <></>
} }

View File

@ -6,5 +6,6 @@ table:
digest: Digest digest: Digest
osArch: OS/Arch osArch: OS/Arch
size: Size size: Size
uploadedBy: Uploaded By uploadedBy: Uploaded At
downloads: Downloads/Last Download downloads: Downloads/Last Download
scanStatus: Scan Status

View File

@ -16,31 +16,33 @@
import React, { useCallback, useContext } from 'react' import React, { useCallback, useContext } from 'react'
import type { FormikProps } from 'formik' import type { FormikProps } from 'formik'
import { useParams } from 'react-router-dom'
import { Expander } from '@blueprintjs/core' import { Expander } from '@blueprintjs/core'
import { Button, ButtonVariation, Container, Layout, Tab, Tabs } from '@harnessio/uicore' import { Button, ButtonVariation, Container, Layout, Tab, Tabs } from '@harnessio/uicore'
import { useParentHooks } from '@ar/hooks'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryDetailsPathParams } from '@ar/routes/types' import type { RepositoryDetailsPathParams } from '@ar/routes/types'
import ArtifactListPage from '@ar/pages/artifact-list/ArtifactListPage' import { useDecodedParams, useParentComponents, useParentHooks } from '@ar/hooks'
import { PermissionIdentifier, ResourceType } from '@ar/common/permissionTypes'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryConfigType, RepositoryPackageType } from '@ar/common/types' import type { RepositoryConfigType, RepositoryPackageType } from '@ar/common/types'
import RepositoryDetailsHeaderWidget from '@ar/frameworks/RepositoryStep/RepositoryDetailsHeaderWidget' import RepositoryDetailsHeaderWidget from '@ar/frameworks/RepositoryStep/RepositoryDetailsHeaderWidget'
import RepositoryConfigurationFormWidget from '@ar/frameworks/RepositoryStep/RepositoryConfigurationFormWidget' import RepositoryConfigurationFormWidget from '@ar/frameworks/RepositoryStep/RepositoryConfigurationFormWidget'
import { RepositoryProviderContext } from './context/RepositoryProvider'
import type { Repository } from './types' import type { Repository } from './types'
import { RepositoryDetailsTab } from './constants' import { RepositoryDetailsTab } from './constants'
import { RepositoryProviderContext } from './context/RepositoryProvider'
import RegistryArtifactListPage from '../artifact-list/RegistryArtifactListPage'
import css from './RepositoryDetailsPage.module.scss' import css from './RepositoryDetailsPage.module.scss'
export default function RepositoryDetails(): JSX.Element | null { export default function RepositoryDetails(): JSX.Element | null {
const pathParams = useParams<RepositoryDetailsPathParams>()
const { useUpdateQueryParams, useQueryParams } = useParentHooks() const { useUpdateQueryParams, useQueryParams } = useParentHooks()
const { updateQueryParams } = useUpdateQueryParams() const { updateQueryParams } = useUpdateQueryParams()
const { RbacButton } = useParentComponents()
const { getString } = useStrings() const { getString } = useStrings()
const { repositoryIdentifier } = useDecodedParams<RepositoryDetailsPathParams>()
const { tab: selectedTabId = RepositoryDetailsTab.PACKAGES } = useQueryParams<{ tab: RepositoryDetailsTab }>() const { tab: selectedTabId = RepositoryDetailsTab.PACKAGES } = useQueryParams<{ tab: RepositoryDetailsTab }>()
const stepRef = React.useRef<FormikProps<unknown> | null>(null) const stepRef = React.useRef<FormikProps<unknown> | null>(null)
const { isDirty, data } = useContext(RepositoryProviderContext) const { isDirty, data, isReadonly, isUpdating } = useContext(RepositoryProviderContext)
const { config } = data as Repository const { config } = data as Repository
const { type } = config const { type } = config
@ -62,12 +64,19 @@ export default function RepositoryDetails(): JSX.Element | null {
const renderActionBtns = (): JSX.Element => ( const renderActionBtns = (): JSX.Element => (
<Layout.Horizontal className={css.btnContainer}> <Layout.Horizontal className={css.btnContainer}>
<Button <RbacButton
variation={ButtonVariation.PRIMARY}
text={getString('save')} text={getString('save')}
className={css.saveButton} className={css.saveButton}
variation={ButtonVariation.PRIMARY}
onClick={handleSubmitForm} onClick={handleSubmitForm}
disabled={!isDirty} disabled={!isDirty || isUpdating}
permission={{
permission: PermissionIdentifier.EDIT_ARTIFACT_REGISTRY,
resource: {
resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: repositoryIdentifier
}
}}
/> />
<Button <Button
className={css.discardBtn} className={css.discardBtn}
@ -95,11 +104,7 @@ export default function RepositoryDetails(): JSX.Element | null {
title={getString('repositoryDetails.tabs.packages')} title={getString('repositoryDetails.tabs.packages')}
panel={ panel={
<Container> <Container>
<ArtifactListPage <RegistryArtifactListPage pageBodyClassName={css.packagesPageBody} />
withHeader={false}
parentRepoKey={pathParams.repositoryIdentifier}
pageBodyClassName={css.packagesPageBody}
/>
</Container> </Container>
} }
/> />
@ -111,7 +116,7 @@ export default function RepositoryDetails(): JSX.Element | null {
packageType={data.packageType as RepositoryPackageType} packageType={data.packageType as RepositoryPackageType}
type={type as RepositoryConfigType} type={type as RepositoryConfigType}
ref={stepRef} ref={stepRef}
readonly={false} readonly={isReadonly}
/> />
} }
/> />

View File

@ -53,10 +53,10 @@ export default function DeleteRepositoryMenuItem({ data }: RepositoryActionsProp
onClick={handleDeleteService} onClick={handleDeleteService}
permission={{ permission={{
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: defaultTo(data?.identifier, '') resourceIdentifier: defaultTo(data?.identifier, '')
}, },
permission: PermissionIdentifier.DELETE_SERVICE permission: PermissionIdentifier.DELETE_ARTIFACT_REGISTRY
}} }}
/> />
) )

View File

@ -32,10 +32,10 @@ export default function QuarantineRepositoryMenuItem({ data }: RepositoryActions
onClick={noop} onClick={noop}
permission={{ permission={{
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: defaultTo(data?.identifier, '') resourceIdentifier: defaultTo(data?.identifier, '')
}, },
permission: PermissionIdentifier.DELETE_SERVICE permission: PermissionIdentifier.DELETE_ARTIFACT_REGISTRY
}} }}
/> />
) )

View File

@ -32,10 +32,10 @@ export default function RestoreRepositoryMenuItem({ data }: RepositoryActionsPro
onClick={noop} onClick={noop}
permission={{ permission={{
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: defaultTo(data?.identifier, '') resourceIdentifier: defaultTo(data?.identifier, '')
}, },
permission: PermissionIdentifier.DELETE_SERVICE permission: PermissionIdentifier.DELETE_ARTIFACT_REGISTRY
}} }}
/> />
) )

View File

@ -32,10 +32,10 @@ export default function ScanRepositoryMenuItem({ data }: RepositoryActionsProps)
onClick={noop} onClick={noop}
permission={{ permission={{
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: defaultTo(data?.identifier, '') resourceIdentifier: defaultTo(data?.identifier, '')
}, },
permission: PermissionIdentifier.DELETE_SERVICE permission: PermissionIdentifier.EDIT_ARTIFACT_REGISTRY
}} }}
/> />
) )

View File

@ -39,10 +39,10 @@ export default function SetupClientMenuItem({ data }: RepositoryActionsProps): J
onClick={showSetupClientModal} onClick={showSetupClientModal}
permission={{ permission={{
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: defaultTo(data?.identifier, '') resourceIdentifier: defaultTo(data?.identifier, '')
}, },
permission: PermissionIdentifier.DELETE_SERVICE permission: PermissionIdentifier.VIEW_ARTIFACT_REGISTRY
}} }}
/> />
</> </>

View File

@ -44,7 +44,7 @@ interface RepositoryConfigurationFormProps {
function RepositoryConfigurationForm(props: RepositoryConfigurationFormProps, formikRef: FormikFowardRef): JSX.Element { function RepositoryConfigurationForm(props: RepositoryConfigurationFormProps, formikRef: FormikFowardRef): JSX.Element {
const { readonly, factory = repositoryFactory } = props const { readonly, factory = repositoryFactory } = props
const { data } = useContext(RepositoryProviderContext) const { data, setIsUpdating } = useContext(RepositoryProviderContext)
const { showSuccess, showError, clear } = useToaster() const { showSuccess, showError, clear } = useToaster()
const { getString } = useStrings() const { getString } = useStrings()
const spaceRef = useGetSpaceRef() const spaceRef = useGetSpaceRef()
@ -54,6 +54,7 @@ function RepositoryConfigurationForm(props: RepositoryConfigurationFormProps, fo
const handleModifyRepository = async (values: VirtualRegistryRequest): Promise<void> => { const handleModifyRepository = async (values: VirtualRegistryRequest): Promise<void> => {
try { try {
setIsUpdating(true)
const response = await modifyRepository({ const response = await modifyRepository({
registry_ref: spaceRef, registry_ref: spaceRef,
body: values as unknown as RegistryRequestRequestBody body: values as unknown as RegistryRequestRequestBody
@ -65,6 +66,8 @@ function RepositoryConfigurationForm(props: RepositoryConfigurationFormProps, fo
} }
} catch (e: any) { } catch (e: any) {
showError(getErrorInfoFromErrorObject(e, true)) showError(getErrorInfoFromErrorObject(e, true))
} finally {
setIsUpdating(false)
} }
} }

View File

@ -136,6 +136,9 @@ function RepositoryCreateForm(props: RepositoryCreateFormProps, formikRef: Formi
const formattedValues = repositoryType.processRepositoryFormData(values) as VirtualRegistryRequest const formattedValues = repositoryType.processRepositoryFormData(values) as VirtualRegistryRequest
const formattedValuesForCleanupPolicy = getFormattedFormDataForCleanupPolicy(formattedValues) const formattedValuesForCleanupPolicy = getFormattedFormDataForCleanupPolicy(formattedValues)
const response = await createRepository({ const response = await createRepository({
queryParams: {
space_ref: decodeRef(parentRef)
},
body: { body: {
...formattedValuesForCleanupPolicy, ...formattedValuesForCleanupPolicy,
parentRef: decodeRef(parentRef) parentRef: decodeRef(parentRef)

View File

@ -29,8 +29,10 @@ import type { Repository } from '@ar/pages/repository-details/types'
export interface RepositoryProviderProps { export interface RepositoryProviderProps {
data: Repository | undefined data: Repository | undefined
isDirty: boolean isDirty: boolean
isUpdating: boolean
setIsDirty: (val: boolean) => void setIsDirty: (val: boolean) => void
setIsLoading: (val: boolean) => void setIsLoading: (val: boolean) => void
setIsUpdating: (val: boolean) => void
isReadonly: boolean isReadonly: boolean
refetch: () => void refetch: () => void
} }
@ -38,8 +40,10 @@ export interface RepositoryProviderProps {
export const RepositoryProviderContext = createContext<RepositoryProviderProps>({ export const RepositoryProviderContext = createContext<RepositoryProviderProps>({
data: undefined, data: undefined,
isDirty: false, isDirty: false,
isUpdating: false,
setIsDirty: noop, setIsDirty: noop,
setIsLoading: noop, setIsLoading: noop,
setIsUpdating: noop,
isReadonly: false, isReadonly: false,
refetch: noop refetch: noop
}) })
@ -48,6 +52,7 @@ const RepositoryProvider: FC<PropsWithChildren<{ className?: string }>> = ({ chi
const { repositoryIdentifier } = useDecodedParams<RepositoryDetailsPathParams>() const { repositoryIdentifier } = useDecodedParams<RepositoryDetailsPathParams>()
const [isDirty, setIsDirty] = useState(false) const [isDirty, setIsDirty] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isUpdating, setIsUpdating] = useState(false)
const { usePermission } = useParentHooks() const { usePermission } = useParentHooks()
const spaceRef = useGetSpaceRef() const spaceRef = useGetSpaceRef()
const { scope } = useAppStore() const { scope } = useAppStore()
@ -62,10 +67,10 @@ const RepositoryProvider: FC<PropsWithChildren<{ className?: string }>> = ({ chi
projectIdentifier projectIdentifier
}, },
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: repositoryIdentifier resourceIdentifier: repositoryIdentifier
}, },
permissions: [PermissionIdentifier.EDIT_SERVICE] permissions: [PermissionIdentifier.EDIT_ARTIFACT_REGISTRY]
}, },
[accountId, projectIdentifier, orgIdentifier, repositoryIdentifier] [accountId, projectIdentifier, orgIdentifier, repositoryIdentifier]
) )
@ -85,8 +90,10 @@ const RepositoryProvider: FC<PropsWithChildren<{ className?: string }>> = ({ chi
value={{ value={{
data: repositoryData?.content?.data as RepositoryProviderProps['data'], data: repositoryData?.content?.data as RepositoryProviderProps['data'],
isDirty, isDirty,
isUpdating,
setIsDirty, setIsDirty,
setIsLoading, setIsLoading,
setIsUpdating,
isReadonly: !isEdit, isReadonly: !isEdit,
refetch refetch
}}> }}>

View File

@ -40,43 +40,47 @@ export function useCreateRepositoryModal(props: useCreateRepositoryModalProps) {
stepRef.current?.submitForm() stepRef.current?.submitForm()
} }
const [showModal, hideModal] = useModalHook(() => ( const [showModal, hideModal] = useModalHook(
<ModalDialog () => (
isOpen={true} <ModalDialog
enforceFocus={false} isOpen={true}
canEscapeKeyClose enforceFocus={false}
canOutsideClickClose canEscapeKeyClose
onClose={() => { canOutsideClickClose
hideModal() onClose={() => {
}} hideModal()
title={ }}
<> title={
<Text font={{ variation: FontVariation.H3 }} margin={{ bottom: 'small' }}> <>
{getString('repositoryDetails.repositoryForm.modalTitle')} <Text font={{ variation: FontVariation.H3 }} margin={{ bottom: 'small' }}>
</Text> {getString('repositoryDetails.repositoryForm.modalTitle')}
<Text font={{ variation: FontVariation.SMALL }} color={Color.GREY_500}> </Text>
{getString('repositoryDetails.repositoryForm.modalSubTitle')} <Text font={{ variation: FontVariation.SMALL }} color={Color.GREY_500}>
</Text> {getString('repositoryDetails.repositoryForm.modalSubTitle')}
</> </Text>
} </>
isCloseButtonShown }
width={800} isCloseButtonShown
showOverlay={showOverlay} width={800}
footer={ showOverlay={showOverlay}
<Layout.Horizontal spacing="small"> footer={
<Button <Layout.Horizontal spacing="small">
variation={ButtonVariation.PRIMARY} <Button
type={'submit'} variation={ButtonVariation.PRIMARY}
text={getString('repositoryDetails.repositoryForm.create')} type={'submit'}
data-id="service-save" text={getString('repositoryDetails.repositoryForm.create')}
onClick={handleSubmitForm} data-id="service-save"
/> onClick={handleSubmitForm}
<Button variation={ButtonVariation.TERTIARY} text={getString('cancel')} onClick={hideModal} /> disabled={showOverlay}
</Layout.Horizontal> />
}> <Button variation={ButtonVariation.TERTIARY} text={getString('cancel')} onClick={hideModal} />
<RepositoryCreateForm onSuccess={onSuccess} setShowOverlay={setShowOverlay} ref={stepRef} /> </Layout.Horizontal>
</ModalDialog> }>
)) <RepositoryCreateForm onSuccess={onSuccess} setShowOverlay={setShowOverlay} ref={stepRef} />
</ModalDialog>
),
[showOverlay]
)
return [showModal, hideModal] return [showModal, hideModal]
} }

View File

@ -43,7 +43,7 @@ function RepositoryListPage(): JSX.Element {
const spaceRef = useGetSpaceRef() const spaceRef = useGetSpaceRef()
const queryParamOptions = useArtifactRepositoriesQueryParamOptions() const queryParamOptions = useArtifactRepositoriesQueryParamOptions()
const queryParams = useQueryParams<ArtifactRepositoryListPageQueryParams>(queryParamOptions) const queryParams = useQueryParams<ArtifactRepositoryListPageQueryParams>(queryParamOptions)
const { searchTerm, page, size, environmentTypes, repositoryTypes } = queryParams const { searchTerm, page, size, repositoryTypes } = queryParams
const { preference: sortingPreference, setPreference: setSortingPreference } = usePreferenceStore<string | undefined>( const { preference: sortingPreference, setPreference: setSortingPreference } = usePreferenceStore<string | undefined>(
PreferenceScope.USER, PreferenceScope.USER,
@ -85,7 +85,7 @@ function RepositoryListPage(): JSX.Element {
}) })
} }
const hasFilter = !!searchTerm || repositoryTypes.length || environmentTypes.length const hasFilter = !!searchTerm || repositoryTypes.length
const responseData = data?.content.data const responseData = data?.content.data

View File

@ -39,14 +39,14 @@ export default function CreateRepositoryButton(props: CreateRepositoryButtonProp
// CHANGE_ME: update permissions once we get actual premission for AR module // CHANGE_ME: update permissions once we get actual premission for AR module
const [canDoAction] = usePermission({ const [canDoAction] = usePermission({
permissions: [PermissionIdentifier.EDIT_SERVICE], permissions: [PermissionIdentifier.EDIT_ARTIFACT_REGISTRY],
resourceScope: { resourceScope: {
accountIdentifier: scope.accountId, accountIdentifier: scope.accountId,
orgIdentifier: scope.orgIdentifier, orgIdentifier: scope.orgIdentifier,
projectIdentifier: scope.projectIdentifier projectIdentifier: scope.projectIdentifier
}, },
resource: { resource: {
resourceType: ResourceType.SERVICE resourceType: ResourceType.ARTIFACT_REGISTRY
} }
}) })

View File

@ -18,7 +18,7 @@ import { useMemo } from 'react'
import { useParentHooks } from '@ar/hooks' import { useParentHooks } from '@ar/hooks'
import { DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE, DEFAULT_PIPELINE_LIST_TABLE_SORT } from '@ar/constants' import { DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE, DEFAULT_PIPELINE_LIST_TABLE_SORT } from '@ar/constants'
import type { EnvironmentType, RepositoryPackageType } from '@ar/common/types' import type { RepositoryPackageType } from '@ar/common/types'
import type { UseQueryParamsOptions } from '@ar/__mocks__/hooks' import type { UseQueryParamsOptions } from '@ar/__mocks__/hooks'
type GetArtifactRepositoryQueryParams = { type GetArtifactRepositoryQueryParams = {
@ -29,7 +29,6 @@ type GetArtifactRepositoryQueryParams = {
size: number size: number
sort: string[] sort: string[]
searchTerm?: string searchTerm?: string
environmentTypes: EnvironmentType[]
repositoryTypes: RepositoryPackageType[] repositoryTypes: RepositoryPackageType[]
} }
@ -46,7 +45,6 @@ export const useArtifactRepositoriesQueryParamOptions =
page: DEFAULT_PAGE_INDEX, page: DEFAULT_PAGE_INDEX,
size: DEFAULT_PAGE_SIZE, size: DEFAULT_PAGE_SIZE,
sort: DEFAULT_PIPELINE_LIST_TABLE_SORT, sort: DEFAULT_PIPELINE_LIST_TABLE_SORT,
environmentTypes: [],
repositoryTypes: [] repositoryTypes: []
}, },
{ ignoreEmptyString: false } { ignoreEmptyString: false }

View File

@ -38,6 +38,7 @@ function DockerRepositoryUrlInput(
<FormInput.RadioGroup <FormInput.RadioGroup
name="config.source" name="config.source"
radioGroup={{ inline: true }} radioGroup={{ inline: true }}
disabled={readonly}
label={getString('upstreamProxyDetails.createForm.source.title')} label={getString('upstreamProxyDetails.createForm.source.title')}
items={[ items={[
{ {

View File

@ -71,6 +71,7 @@ export default function UpstreamProxyAuthenticationFormContent({
value: UpstreamProxyAuthenticationMode.ANONYMOUS value: UpstreamProxyAuthenticationMode.ANONYMOUS
} }
]} ]}
disabled={readonly}
/> />
{selectedRadioValue === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD && ( {selectedRadioValue === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD && (
<Container className={css.authContainer}> <Container className={css.authContainer}>

View File

@ -20,7 +20,7 @@ import { Formik, FormikForm, getErrorInfoFromErrorObject, useToaster } from '@ha
import { Anonymous, UserPassword, useModifyRegistryMutation } from '@harnessio/react-har-service-client' import { Anonymous, UserPassword, useModifyRegistryMutation } from '@harnessio/react-har-service-client'
import { URL_REGEX } from '@ar/constants' import { URL_REGEX } from '@ar/constants'
import { useGetSpaceRef } from '@ar/hooks' import { useAppStore, useGetSpaceRef } from '@ar/hooks'
import { useStrings } from '@ar/frameworks/strings' import { useStrings } from '@ar/frameworks/strings'
import { queryClient } from '@ar/utils/queryClient' import { queryClient } from '@ar/utils/queryClient'
import type { FormikFowardRef } from '@ar/common/types' import type { FormikFowardRef } from '@ar/common/types'
@ -41,6 +41,7 @@ import {
type UpstreamRegistry, type UpstreamRegistry,
type UpstreamRegistryRequest type UpstreamRegistryRequest
} from '../../types' } from '../../types'
import { getFormattedFormDataForAuthType } from './utils'
import css from './Forms.module.scss' import css from './Forms.module.scss'
@ -54,9 +55,10 @@ function UpstreamProxyConfigurationForm(
formikRef: FormikFowardRef formikRef: FormikFowardRef
): JSX.Element { ): JSX.Element {
const { readonly, factory = repositoryFactory } = props const { readonly, factory = repositoryFactory } = props
const { data } = useContext(RepositoryProviderContext) const { data, setIsUpdating } = useContext(RepositoryProviderContext)
const { showSuccess, showError, clear } = useToaster() const { showSuccess, showError, clear } = useToaster()
const { getString } = useStrings() const { getString } = useStrings()
const { parent } = useAppStore()
const spaceRef = useGetSpaceRef() const spaceRef = useGetSpaceRef()
const { mutateAsync: modifyUpstreamProxy } = useModifyRegistryMutation() const { mutateAsync: modifyUpstreamProxy } = useModifyRegistryMutation()
@ -74,6 +76,7 @@ function UpstreamProxyConfigurationForm(
const handleModifyUpstreamProxy = async (values: UpstreamRegistryRequest): Promise<void> => { const handleModifyUpstreamProxy = async (values: UpstreamRegistryRequest): Promise<void> => {
try { try {
setIsUpdating(true)
const response = await modifyUpstreamProxy({ const response = await modifyUpstreamProxy({
registry_ref: spaceRef, registry_ref: spaceRef,
body: values body: values
@ -85,13 +88,16 @@ function UpstreamProxyConfigurationForm(
} }
} catch (e: any) { } catch (e: any) {
showError(getErrorInfoFromErrorObject(e, true)) showError(getErrorInfoFromErrorObject(e, true))
} finally {
setIsUpdating(false)
} }
} }
const handleSubmit = async (values: UpstreamRegistryRequest): Promise<void> => { const handleSubmit = async (values: UpstreamRegistryRequest): Promise<void> => {
const repositoryType = factory.getRepositoryType(values.packageType) const repositoryType = factory.getRepositoryType(values.packageType)
if (repositoryType) { if (repositoryType) {
const transformedCleanupPolicy = getFormattedFormDataForCleanupPolicy(values) const transfomedAuthType = getFormattedFormDataForAuthType(values, parent)
const transformedCleanupPolicy = getFormattedFormDataForCleanupPolicy(transfomedAuthType)
const transformedValues = repositoryType.processUpstreamProxyFormData( const transformedValues = repositoryType.processUpstreamProxyFormData(
transformedCleanupPolicy transformedCleanupPolicy
) as UpstreamRegistryRequest ) as UpstreamRegistryRequest
@ -111,16 +117,18 @@ function UpstreamProxyConfigurationForm(
authType: Yup.string() authType: Yup.string()
.required() .required()
.oneOf([UpstreamProxyAuthenticationMode.ANONYMOUS, UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD]), .oneOf([UpstreamProxyAuthenticationMode.ANONYMOUS, UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD]),
auth: Yup.object().when(['authType'], { auth: Yup.object()
is: (authType: UpstreamProxyAuthenticationMode) => .when(['authType'], {
authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD, is: (authType: UpstreamProxyAuthenticationMode) =>
then: (schema: Yup.ObjectSchema<UserPassword | Anonymous>) => authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
schema.shape({ then: (schema: Yup.ObjectSchema<UserPassword | Anonymous>) =>
userName: Yup.string().trim().required(getString('validationMessages.userNameRequired')), schema.shape({
secretIdentifier: Yup.string().trim().required(getString('validationMessages.passwordRequired')) userName: Yup.string().trim().required(getString('validationMessages.userNameRequired')),
}), secretIdentifier: Yup.string().trim().required(getString('validationMessages.passwordRequired'))
otherwise: Yup.object().optional().nullable() }),
}), otherwise: Yup.object().optional().nullable()
})
.nullable(),
url: Yup.string().when(['source'], { url: Yup.string().when(['source'], {
is: (source: DockerRepositoryURLInputSource) => source === DockerRepositoryURLInputSource.Custom, is: (source: DockerRepositoryURLInputSource) => source === DockerRepositoryURLInputSource.Custom,
then: (schema: Yup.StringSchema) => then: (schema: Yup.StringSchema) =>

View File

@ -29,7 +29,7 @@ import {
} from '@harnessio/uicore' } from '@harnessio/uicore'
import { Anonymous, UserPassword, useCreateRegistryMutation } from '@harnessio/react-har-service-client' import { Anonymous, UserPassword, useCreateRegistryMutation } from '@harnessio/react-har-service-client'
import { useGetSpaceRef } from '@ar/hooks' import { useAppStore, useGetSpaceRef } from '@ar/hooks'
import { useStrings } from '@ar/frameworks/strings' import { useStrings } from '@ar/frameworks/strings'
import { decodeRef } from '@ar/hooks/useGetSpaceRef' import { decodeRef } from '@ar/hooks/useGetSpaceRef'
import { setFormikRef } from '@ar/common/utils' import { setFormikRef } from '@ar/common/utils'
@ -127,6 +127,7 @@ function UpstreamProxyCreateForm(props: UpstreamProxyCreateFormProps, formikRef:
const { showSuccess, showError, clear } = useToaster() const { showSuccess, showError, clear } = useToaster()
const { getString } = useStrings() const { getString } = useStrings()
const spaceRef = useGetSpaceRef('') const spaceRef = useGetSpaceRef('')
const { parent, scope } = useAppStore()
const { isLoading: loading, mutateAsync: createUpstreamProxy } = useCreateRegistryMutation() const { isLoading: loading, mutateAsync: createUpstreamProxy } = useCreateRegistryMutation()
@ -137,6 +138,9 @@ function UpstreamProxyCreateForm(props: UpstreamProxyCreateFormProps, formikRef:
const handleCreateUpstreamProxy = async (values: UpstreamRegistryRequest): Promise<void> => { const handleCreateUpstreamProxy = async (values: UpstreamRegistryRequest): Promise<void> => {
try { try {
const response = await createUpstreamProxy({ const response = await createUpstreamProxy({
queryParams: {
space_ref: decodeRef(spaceRef)
},
body: { body: {
...values, ...values,
parentRef: decodeRef(spaceRef) parentRef: decodeRef(spaceRef)
@ -155,7 +159,7 @@ function UpstreamProxyCreateForm(props: UpstreamProxyCreateFormProps, formikRef:
const handleSubmit = (values: UpstreamRegistryRequest): void => { const handleSubmit = (values: UpstreamRegistryRequest): void => {
const repositoryType = factory.getRepositoryType(values.packageType) const repositoryType = factory.getRepositoryType(values.packageType)
if (repositoryType) { if (repositoryType) {
const transfomedAuthType = getFormattedFormDataForAuthType(values) const transfomedAuthType = getFormattedFormDataForAuthType(values, parent, scope)
const transformedCleanupPolicy = getFormattedFormDataForCleanupPolicy(transfomedAuthType) const transformedCleanupPolicy = getFormattedFormDataForCleanupPolicy(transfomedAuthType)
const transformedValues = repositoryType.processUpstreamProxyFormData( const transformedValues = repositoryType.processUpstreamProxyFormData(
transformedCleanupPolicy transformedCleanupPolicy
@ -192,15 +196,17 @@ function UpstreamProxyCreateForm(props: UpstreamProxyCreateFormProps, formikRef:
authType: Yup.string() authType: Yup.string()
.required() .required()
.oneOf([UpstreamProxyAuthenticationMode.ANONYMOUS, UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD]), .oneOf([UpstreamProxyAuthenticationMode.ANONYMOUS, UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD]),
auth: Yup.object().when(['authType'], { auth: Yup.object()
is: (authType: UpstreamProxyAuthenticationMode) => .when(['authType'], {
authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD, is: (authType: UpstreamProxyAuthenticationMode) =>
then: (schema: Yup.ObjectSchema<UserPassword | Anonymous>) => authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
schema.shape({ then: (schema: Yup.ObjectSchema<UserPassword | Anonymous>) =>
userName: Yup.string().trim().required(getString('validationMessages.userNameRequired')), schema.shape({
secretIdentifier: Yup.string().trim().required(getString('validationMessages.passwordRequired')) userName: Yup.string().trim().required(getString('validationMessages.userNameRequired')),
}) secretIdentifier: Yup.string().trim().required(getString('validationMessages.passwordRequired'))
}), })
})
.nullable(),
url: Yup.string().when(['source'], { url: Yup.string().when(['source'], {
is: (source: DockerRepositoryURLInputSource) => source === DockerRepositoryURLInputSource.Custom, is: (source: DockerRepositoryURLInputSource) => source === DockerRepositoryURLInputSource.Custom,
then: (schema: Yup.StringSchema) => then: (schema: Yup.StringSchema) =>

View File

@ -15,17 +15,47 @@
*/ */
import produce from 'immer' import produce from 'immer'
import { set } from 'lodash-es' import { compact, get, set } from 'lodash-es'
import type { Scope } from '@ar/MFEAppTypes'
import { Parent } from '@ar/common/types'
import { import {
DockerRepositoryURLInputSource, DockerRepositoryURLInputSource,
UpstreamProxyAuthenticationMode, UpstreamProxyAuthenticationMode,
type UpstreamRegistryRequest type UpstreamRegistryRequest
} from '../../types' } from '../../types'
export function getFormattedFormDataForAuthType(values: UpstreamRegistryRequest): UpstreamRegistryRequest { export function getSecretSpacePath(referenceString: string, scope?: Scope) {
if (!scope) return referenceString
if (referenceString.startsWith('account.')) {
return compact([scope.accountId]).join('/')
}
if (referenceString.startsWith('org.')) {
return compact([scope.accountId, scope.orgIdentifier]).join('/')
}
return compact([scope.accountId, scope.orgIdentifier, scope.projectIdentifier]).join('/')
}
export function getReferenceStringFromSecretSpacePath(identifier: string, secretSpacePath: string) {
const [accountId, orgIdentifier, projectIdentifier] = secretSpacePath.split('/')
if (projectIdentifier) return identifier
if (orgIdentifier) return `org.${identifier}`
if (accountId) return `account.${identifier}`
return identifier
}
export function getFormattedFormDataForAuthType(
values: UpstreamRegistryRequest,
parent?: Parent,
scope?: Scope
): UpstreamRegistryRequest {
return produce(values, (draft: UpstreamRegistryRequest) => { return produce(values, (draft: UpstreamRegistryRequest) => {
if (draft.config.authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD) { if (draft.config.authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD) {
set(draft, 'config.auth.authType', draft.config.authType) set(draft, 'config.auth.authType', draft.config.authType)
if (parent === Parent.Enterprise) {
const password = draft.config.auth?.secretIdentifier
set(draft, 'config.auth.secretSpacePath', getSecretSpacePath(get(password, 'referenceString', ''), scope))
set(draft, 'config.auth.secretIdentifier', get(password, 'identifier'))
}
} }
if (draft.config.source !== DockerRepositoryURLInputSource.Custom) { if (draft.config.source !== DockerRepositoryURLInputSource.Custom) {
set(draft, 'config.url', '') set(draft, 'config.url', '')

View File

@ -51,10 +51,10 @@ export default function DeleteUpstreamProxy({ data }: UpstreamProxyActionProps):
onClick={handleDeleteService} onClick={handleDeleteService}
permission={{ permission={{
resource: { resource: {
resourceType: ResourceType.SERVICE, resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: data.identifier resourceIdentifier: data.identifier
}, },
permission: PermissionIdentifier.DELETE_SERVICE permission: PermissionIdentifier.DELETE_ARTIFACT_REGISTRY
}} }}
/> />
) )

Some files were not shown because too many files have changed in this diff Show More