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

@ -24,7 +24,7 @@ import (
"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.
type pathCacheGetter struct {
spacePathStore store.SpacePathStore

View File

@ -94,12 +94,16 @@ type (
DeleteServiceAccount(ctx context.Context, id int64) error
// ListServiceAccounts returns a list of service accounts for a specific parent.
ListServiceAccounts(ctx context.Context,
parentType enum.ParentResourceType, parentID int64) ([]*types.ServiceAccount, error)
ListServiceAccounts(
ctx context.Context,
parentType enum.ParentResourceType, parentID int64,
) ([]*types.ServiceAccount, error)
// CountServiceAccounts returns a count of service accounts for a specific parent.
CountServiceAccounts(ctx context.Context,
parentType enum.ParentResourceType, parentID int64) (int64, error)
CountServiceAccounts(
ctx context.Context,
parentType enum.ParentResourceType, parentID int64,
) (int64, error)
/*
* SERVICE RELATED OPERATIONS.
@ -167,6 +171,9 @@ type (
// FindByRef finds the space using the spaceRef as either the id or the space path.
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(ctx context.Context, spaceRef string, deletedAt int64) (*types.Space, error)
@ -192,8 +199,10 @@ type (
Update(ctx context.Context, space *types.Space) error
// UpdateOptLock updates the space using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, space *types.Space,
mutateFn func(space *types.Space) error) (*types.Space, error)
UpdateOptLock(
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(ctx context.Context, id int64) (*types.Space, error)
@ -205,8 +214,10 @@ type (
Purge(ctx context.Context, id int64, deletedAt *int64) error
// Restore restores a soft deleted space.
Restore(ctx context.Context, space *types.Space,
newIdentifier *string, newParentID *int64) (*types.Space, error)
Restore(
ctx context.Context, space *types.Space,
newIdentifier *string, newParentID *int64,
) (*types.Space, error)
// Count the child spaces of a space.
Count(ctx context.Context, id int64, opts *types.SpaceFilter) (int64, error)
@ -239,8 +250,10 @@ type (
GetSize(ctx context.Context, id int64) (int64, error)
// UpdateOptLock the repo details using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, repo *types.Repository,
mutateFn func(repository *types.Repository) error) (*types.Repository, error)
UpdateOptLock(
ctx context.Context, repo *types.Repository,
mutateFn func(repository *types.Repository) error,
) (*types.Repository, error)
// SoftDelete a repo.
SoftDelete(ctx context.Context, repo *types.Repository, deletedAt int64) error
@ -249,8 +262,10 @@ type (
Purge(ctx context.Context, id int64, deletedAt *int64) error
// Restore a deleted repo using the optimistic locking mechanism.
Restore(ctx context.Context, repo *types.Repository,
newIdentifier *string, newParentID *int64) (*types.Repository, error)
Restore(
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(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)
ListUsers(ctx context.Context, spaceID int64, filter types.MembershipUserFilter) ([]types.MembershipUser, 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.
@ -362,8 +381,10 @@ type (
Update(ctx context.Context, pr *types.PullReq) error
// UpdateOptLock the pull request details using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, pr *types.PullReq,
mutateFn func(pr *types.PullReq) error) (*types.PullReq, error)
UpdateOptLock(
ctx context.Context, pr *types.PullReq,
mutateFn func(pr *types.PullReq) error,
) (*types.PullReq, error)
// UpdateActivitySeq the pull request's activity sequence number.
// 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
// CreateWithPayload create a new system activity from the provided payload.
CreateWithPayload(ctx context.Context,
CreateWithPayload(
ctx context.Context,
pr *types.PullReq,
principalID int64,
payload types.PullReqActivityPayload,
@ -406,7 +428,8 @@ type (
Update(ctx context.Context, act *types.PullReqActivity) error
// UpdateOptLock updates the pull request activity using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context,
UpdateOptLock(
ctx context.Context,
act *types.PullReqActivity,
mutateFn func(act *types.PullReqActivity) error,
) (*types.PullReqActivity, error)
@ -543,8 +566,10 @@ type (
Update(ctx context.Context, hook *types.Webhook) error
// UpdateOptLock updates the webhook using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, hook *types.Webhook,
mutateFn func(hook *types.Webhook) error) (*types.Webhook, error)
UpdateOptLock(
ctx context.Context, hook *types.Webhook,
mutateFn func(hook *types.Webhook) error,
) (*types.Webhook, error)
// Delete deletes the webhook for the given id.
Delete(ctx context.Context, id int64) error
@ -553,12 +578,16 @@ type (
DeleteByIdentifier(ctx context.Context, parentType enum.WebhookParent, parentID int64, identifier string) error
// Count counts the webhooks for a given parent type and id.
Count(ctx context.Context, parentType enum.WebhookParent, parentID int64,
opts *types.WebhookFilter) (int64, error)
Count(
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(ctx context.Context, parentType enum.WebhookParent, parentID int64,
opts *types.WebhookFilter) ([]*types.Webhook, error)
List(
ctx context.Context, parentType enum.WebhookParent, parentID int64,
opts *types.WebhookFilter,
) ([]*types.Webhook, error)
}
// WebhookExecutionStore defines the webhook execution data storage.
@ -573,8 +602,10 @@ type (
DeleteOld(ctx context.Context, olderThan time.Time) (int64, error)
// ListForWebhook lists the webhook executions for a given webhook id.
ListForWebhook(ctx context.Context, webhookID int64,
opts *types.WebhookExecutionFilter) ([]*types.WebhookExecution, error)
ListForWebhook(
ctx context.Context, webhookID int64,
opts *types.WebhookExecutionFilter,
) ([]*types.WebhookExecution, error)
// ListForTrigger lists the webhook executions for a given trigger id.
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 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 {
@ -677,7 +711,8 @@ type (
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(ctx context.Context,
List(
ctx context.Context,
infraProviderConfigID int64,
filter types.ListQueryFilter,
) ([]*types.InfraProviderResource, error)
@ -707,8 +742,10 @@ type (
ListLatest(ctx context.Context, repoID int64, pagination types.ListQueryFilter) ([]*types.Pipeline, error)
// UpdateOptLock updates the pipeline using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, pipeline *types.Pipeline,
mutateFn func(pipeline *types.Pipeline) error) (*types.Pipeline, error)
UpdateOptLock(
ctx context.Context, pipeline *types.Pipeline,
mutateFn func(pipeline *types.Pipeline) error,
) (*types.Pipeline, error)
// Delete deletes a pipeline ID from the datastore.
Delete(ctx context.Context, id int64) error
@ -743,8 +780,10 @@ type (
Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error)
// UpdateOptLock updates the secret using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, secret *types.Secret,
mutateFn func(secret *types.Secret) error) (*types.Secret, error)
UpdateOptLock(
ctx context.Context, secret *types.Secret,
mutateFn func(secret *types.Secret) error,
) (*types.Secret, error)
// Update tries to update a secret.
Update(ctx context.Context, secret *types.Secret) error
@ -837,8 +876,10 @@ type (
Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error)
// UpdateOptLock updates the connector using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, connector *types.Connector,
mutateFn func(connector *types.Connector) error) (*types.Connector, error)
UpdateOptLock(
ctx context.Context, connector *types.Connector,
mutateFn func(connector *types.Connector) error,
) (*types.Connector, error)
// Update tries to update a connector.
Update(ctx context.Context, connector *types.Connector) error
@ -858,8 +899,10 @@ type (
Find(ctx context.Context, id int64) (*types.Template, error)
// FindByIdentifierAndType returns a template given a space ID, identifier and a type
FindByIdentifierAndType(ctx context.Context, spaceID int64,
identifier string, resolverType enum.ResolverType) (*types.Template, error)
FindByIdentifierAndType(
ctx context.Context, spaceID int64,
identifier string, resolverType enum.ResolverType,
) (*types.Template, error)
// Create creates a new template.
Create(ctx context.Context, template *types.Template) error
@ -868,8 +911,10 @@ type (
Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error)
// UpdateOptLock updates the template using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, template *types.Template,
mutateFn func(template *types.Template) error) (*types.Template, error)
UpdateOptLock(
ctx context.Context, template *types.Template,
mutateFn func(template *types.Template) error,
) (*types.Template, error)
// Update tries to update a template.
Update(ctx context.Context, template *types.Template) error
@ -878,7 +923,12 @@ type (
Delete(ctx context.Context, id int64) error
// 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(ctx context.Context, spaceID int64, filter types.ListQueryFilter) ([]*types.Template, error)
@ -895,8 +945,10 @@ type (
Update(ctx context.Context, trigger *types.Trigger) error
// UpdateOptLock updates the trigger using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, trigger *types.Trigger,
mutateFn func(trigger *types.Trigger) error) (*types.Trigger, error)
UpdateOptLock(
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(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)
// 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(ctx context.Context, ids []int64) ([]*types.UserGroup, error)
@ -1145,7 +1201,8 @@ type (
spaceID int64,
gitspaceInstanceID int64,
) (*types.InfraProvisioned, error)
FindLatestByGitspaceInstanceIdentifier(ctx context.Context,
FindLatestByGitspaceInstanceIdentifier(
ctx context.Context,
spaceID int64,
gitspaceInstanceIdentifier string,
) (*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)
}
// 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.
func (s *SpaceStore) FindByRefAndDeletedAt(
ctx context.Context,
@ -701,7 +735,8 @@ func (s *SpaceStore) list(
return s.mapToSpaces(ctx, s.db, dst)
}
func (s *SpaceStore) listAll(ctx context.Context,
func (s *SpaceStore) listAll(
ctx context.Context,
id int64,
opts *types.SpaceFilter,
) ([]*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.
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)
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)
for i := len(segments) - 2; i >= 0; i-- {
parentAlias := "s" + fmt.Sprint(i)
alias := "s" + fmt.Sprint(i+1)
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)).
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])
}

View File

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

View File

@ -432,16 +432,17 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
mediaTypesRepository := database2.ProvideMediaTypeDao(db)
blobRepository := database2.ProvideBlobDao(db, mediaTypesRepository)
storageService := docker.StorageServiceProvider(config, storageDriver)
manifestRepository := database2.ProvideManifestDao(db, mediaTypesRepository)
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)
manifestRepository := database2.ProvideManifestDao(db, mediaTypesRepository)
manifestReferenceRepository := database2.ProvideManifestRefDao(db)
tagRepository := database2.ProvideTagDao(db)
imageRepository := database2.ProvideImageDao(db)
artifactRepository := database2.ProvideArtifactDao(db)
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)
bandwidthStatRepository := database2.ProvideBandwidthStatDao(db)
downloadStatRepository := database2.ProvideDownloadStatDao(db)

View File

@ -25,19 +25,53 @@ import (
"github.com/rs/zerolog/log"
)
func GetArtifactMetadata(artifacts *[]types.ArtifactMetadata) []artifactapi.ArtifactMetadata {
artifactMetadataList := make([]artifactapi.ArtifactMetadata, 0, len(*artifacts))
for _, artifact := range *artifacts {
artifactMetadata := mapToArtifactMetadata(artifact)
func GetArtifactMetadata(
artifacts []types.ArtifactMetadata,
rootIdentifier string,
registryURL string,
) []artifactapi.ArtifactMetadata {
artifactMetadataList := make([]artifactapi.ArtifactMetadata, 0, len(artifacts))
for _, artifact := range artifacts {
artifactMetadata := mapToArtifactMetadata(artifact, rootIdentifier, registryURL)
artifactMetadataList = append(artifactMetadataList, *artifactMetadata)
}
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)
packageType := artifact.PackageType
pullCommand := GetPullCommand(rootIdentifier, artifact.RepoName, artifact.Name, artifact.Version,
string(packageType), registryURL)
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,
Name: artifact.Name,
LatestVersion: artifact.LatestVersion,
@ -103,8 +137,15 @@ func GetAllArtifactResponse(
count int64,
pageNumber int64,
pageSize int,
rootIdentifier string,
registryURL string,
) *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)
listArtifact := &artifactapi.ListArtifact{
ItemCount: &count,
@ -120,6 +161,33 @@ func GetAllArtifactResponse(
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(
artifactLabels *[]string,
count int64,

View File

@ -57,6 +57,21 @@ type RegistryRequestInfo struct {
pageNumber int64
searchTerm 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
@ -117,50 +132,45 @@ func (c *APIController) GetRegistryRequestBaseInfo(
func (c *APIController) GetRegistryRequestInfo(
ctx context.Context,
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,
registryRequestParams RegistryRequestParams,
) (*RegistryRequestInfo, error) {
packageTypes := []string{}
if packageTypesParam != nil {
packageTypes = *packageTypesParam
if registryRequestParams.packageTypesParam != nil {
packageTypes = *registryRequestParams.packageTypesParam
}
registryIDs := []string{}
if registryRequestParams.registryIDsParam != nil {
registryIDs = *registryRequestParams.registryIDsParam
}
sortByField := ""
sortByOrder := ""
if sortOrder != nil {
sortByOrder = string(*sortOrder)
if registryRequestParams.sortOrder != nil {
sortByOrder = string(*registryRequestParams.sortOrder)
}
if sortField != nil {
sortByField = string(*sortField)
if registryRequestParams.sortField != nil {
sortByField = string(*registryRequestParams.sortField)
}
labels := []string{}
if labelsParam != nil {
labels = *labelsParam
if registryRequestParams.labelsParam != nil {
labels = *registryRequestParams.labelsParam
}
sortByField = GetSortByField(sortByField, resource)
sortByField = GetSortByField(sortByField, registryRequestParams.resource)
sortByOrder = GetSortByOrder(sortByOrder)
offset := GetOffset(size, page)
limit := GetPageLimit(size)
pageNumber := GetPageNumber(page)
offset := GetOffset(registryRequestParams.size, registryRequestParams.page)
limit := GetPageLimit(registryRequestParams.size)
pageNumber := GetPageNumber(registryRequestParams.page)
searchTerm := ""
if search != nil {
searchTerm = string(*search)
if registryRequestParams.search != nil {
searchTerm = string(*registryRequestParams.search)
}
baseInfo, err := c.GetRegistryRequestBaseInfo(ctx, parentRef, regRef)
baseInfo, err := c.GetRegistryRequestBaseInfo(ctx, registryRequestParams.parentRef, registryRequestParams.regRef)
if err != nil {
return nil, err
}
@ -175,6 +185,7 @@ func (c *APIController) GetRegistryRequestInfo(
pageNumber: pageNumber,
searchTerm: searchTerm,
labels: labels,
registryIDs: registryIDs,
}, nil
}

View File

@ -21,7 +21,6 @@ import (
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/registry/types"
"github.com/harness/gitness/types/enum"
)
@ -29,20 +28,21 @@ func (c *APIController) GetAllArtifacts(
ctx context.Context,
r artifact.GetAllArtifactsRequestObject,
) (artifact.GetAllArtifactsResponseObject, error) {
ref := ""
if r.Params.RegIdentifier != nil {
ref2, err2 := GetRegRef(string(r.SpaceRef), string(*r.Params.RegIdentifier))
if err2 != nil {
return c.getAllArtifacts400JsonResponse(err2)
}
ref = ref2
registryRequestParams := &RegistryRequestParams{
packageTypesParam: nil,
page: r.Params.Page,
size: r.Params.Size,
search: r.Params.SearchTerm,
resource: ArtifactResource,
parentRef: string(r.SpaceRef),
regRef: "",
labelsParam: nil,
sortOrder: r.Params.SortOrder,
sortField: r.Params.SortField,
registryIDsParam: r.Params.RegIdentifier,
}
regInfo, err := c.GetRegistryRequestInfo(
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,
)
regInfo, err := c.GetRegistryRequestInfo(ctx, *registryRequestParams)
if err != nil {
return c.getAllArtifacts400JsonResponse(err)
}
@ -70,28 +70,16 @@ func (c *APIController) GetAllArtifacts(
),
}, nil
}
var artifacts *[]types.ArtifactMetadata
var count int64
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,
)
latestVersion := false
if r.Params.LatestVersion != nil {
latestVersion = bool(*r.Params.LatestVersion)
}
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 {
return artifact.GetAllArtifacts500JSONResponse{
InternalServerErrorJSONResponse: artifact.InternalServerErrorJSONResponse(
@ -100,7 +88,8 @@ func (c *APIController) GetAllArtifacts(
}, nil
}
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
}

View File

@ -28,11 +28,21 @@ func (c *APIController) ListArtifactLabels(
ctx context.Context,
r artifact.ListArtifactLabelsRequestObject,
) (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(
ctx, nil, r.Params.Page, r.Params.Size,
r.Params.SearchTerm, ArtifactResource, "", string(r.RegistryRef),
nil, nil, nil,
)
ctx, *registryRequestParams)
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil {

View File

@ -36,11 +36,20 @@ func (c *APIController) GetAllArtifactVersions(
ctx context.Context,
r artifact.GetAllArtifactVersionsRequestObject,
) (artifact.GetAllArtifactVersionsResponseObject, error) {
regInfo, _ := c.GetRegistryRequestInfo(
ctx, nil, r.Params.Page, r.Params.Size,
r.Params.SearchTerm, ArtifactVersionResource, "", string(r.RegistryRef),
nil, r.Params.SortOrder, r.Params.SortField,
)
registryRequestParams := &RegistryRequestParams{
packageTypesParam: nil,
page: r.Params.Page,
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)
if err != nil {

View File

@ -32,11 +32,20 @@ func (c *APIController) GetAllRegistries(
ctx context.Context,
r artifact.GetAllRegistriesRequestObject,
) (artifact.GetAllRegistriesResponseObject, error) {
regInfo, _ := c.GetRegistryRequestInfo(
ctx, r.Params.PackageType, r.Params.Page, r.Params.Size,
r.Params.SearchTerm, RepositoryResource, string(r.SpaceRef), "", nil,
r.Params.SortOrder, r.Params.SortField,
)
registryRequestParams := &RegistryRequestParams{
packageTypesParam: nil,
page: r.Params.Page,
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)
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,
r artifact.UpdateArtifactLabelsRequestObject,
) (artifact.UpdateArtifactLabelsResponseObject, error) {
regInfo, _ := c.GetRegistryRequestInfo(
ctx, nil, nil, nil, nil,
ArtifactVersionResource, "", string(r.RegistryRef), nil, nil, nil,
)
registryRequestParams := &RegistryRequestParams{
packageTypesParam: 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)
if err != nil {

View File

@ -400,7 +400,7 @@ func GetPermissionChecks(
) []types.PermissionCheck {
var permissionChecks []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},
Permission: permission,
}

View File

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

View File

@ -173,17 +173,44 @@ paths:
- Spaces
parameters:
- $ref: "#/components/parameters/spaceRefPathParam"
- $ref: "#/components/parameters/LabelsParam"
- $ref: "#/components/parameters/packageTypeParam"
- $ref: "#/components/parameters/RegistryIdentifierParam"
- $ref: "#/components/parameters/pageNumber"
- $ref: "#/components/parameters/pageSize"
- $ref: "#/components/parameters/sortOrder"
- $ref: "#/components/parameters/sortField"
- $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:
200:
$ref: "#/components/responses/ListArtifactResponse"
$ref: "#/components/responses/ListRegistryArtifactResponse"
400:
$ref: "#/components/responses/BadRequest"
401:
@ -764,6 +791,20 @@ components:
required:
- status
- 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:
description: response for list versions of artifact
content:
@ -896,6 +937,36 @@ components:
$ref: "#/components/schemas/ArtifactMetadata"
required:
- 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:
type: object
description: A list of Artifact versions
@ -1029,6 +1100,36 @@ components:
type:
$ref: "#/components/schemas/ClientSetupStepType"
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
description: Artifact Metadata
properties:
@ -1503,6 +1604,8 @@ components:
required: false
description: Registry Identifier
schema:
type: array
items:
type: string
spaceRefPathParam:
name: space_ref
@ -1601,6 +1704,13 @@ components:
description: sortField
schema:
type: string
latestVersion:
name: latest_version
in: query
required: false
description: Latest Version Filter.
schema:
type: boolean
fromDateParam:
name: from
in: query

View File

@ -74,6 +74,9 @@ type ServerInterface interface {
// List Artifact Versions
// (GET /registry/{registry_ref}/artifact/{artifact}/versions)
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
// (GET /registry/{registry_ref}/client-setup-details)
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)
}
// 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
// (GET /registry/{registry_ref}/client-setup-details)
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))
}
// 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
func (siw *ServerInterfaceWrapper) GetClientSetupDetails(w http.ResponseWriter, r *http.Request) {
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
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" -------------
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
}
// ------------- 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) {
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.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.Get(options.BaseURL+"/registry/{registry_ref}/client-setup-details", wrapper.GetClientSetupDetails)
})
@ -1559,6 +1640,14 @@ type ListArtifactVersionResponseJSONResponse struct {
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 {
// Data A list of Harness Artifact Registries
Data ListRegistry `json:"data"`
@ -2719,6 +2808,73 @@ func (response GetAllArtifactVersions500JSONResponse) VisitGetAllArtifactVersion
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 {
RegistryRef RegistryRefPathParam `json:"registry_ref"`
Params GetClientSetupDetailsParams
@ -3040,6 +3196,9 @@ type StrictServerInterface interface {
// List Artifact Versions
// (GET /registry/{registry_ref}/artifact/{artifact}/versions)
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
// (GET /registry/{registry_ref}/client-setup-details)
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
func (sh *strictHandler) GetClientSetupDetails(w http.ResponseWriter, r *http.Request, registryRef RegistryRefPathParam, params GetClientSetupDetailsParams) {
var request GetClientSetupDetailsRequestObject
@ -3682,69 +3868,70 @@ func (sh *strictHandler) GetAllRegistries(w http.ResponseWriter, r *http.Request
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"y6/L/wUAAP//B0v/XHmOAAA=",
"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",
}
// GetSwagger returns the content of the embedded swagger specification file

View File

@ -75,13 +75,14 @@ type ArtifactMetadata 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"`
PullCommand *string `json:"pullCommand,omitempty"`
RegistryIdentifier string `json:"registryIdentifier"`
RegistryPath string `json:"registryPath"`
Version *string `json:"version,omitempty"`
}
// ArtifactStats Harness Artifact Stats
@ -321,6 +322,24 @@ type ListRegistry struct {
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
type PackageType string
@ -343,6 +362,20 @@ type Registry struct {
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
type RegistryConfig struct {
// Type refers to type of registry i.e virtual or upstream
@ -427,7 +460,7 @@ type VirtualConfig struct {
type LabelsParam []string
// RegistryIdentifierParam defines model for RegistryIdentifierParam.
type RegistryIdentifierParam string
type RegistryIdentifierParam []string
// RegistryTypeParam defines model for RegistryTypeParam.
type RegistryTypeParam string
@ -444,6 +477,9 @@ type DigestParam string
// FromDateParam defines model for fromDateParam.
type FromDateParam string
// LatestVersion defines model for latestVersion.
type LatestVersion bool
// PackageTypeParam defines model for packageTypeParam.
type PackageTypeParam []string
@ -612,6 +648,15 @@ type ListArtifactVersionResponse struct {
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.
type ListRegistryResponse struct {
// Data A list of Harness Artifact Registries
@ -717,6 +762,27 @@ type GetAllArtifactVersionsParams struct {
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.
type GetClientSetupDetailsParams struct {
// Artifact Artifat
@ -737,12 +803,6 @@ type GetArtifactStatsForSpaceParams struct {
// GetAllArtifactsParams defines parameters for GetAllArtifacts.
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 *RegistryIdentifierParam `form:"reg_identifier,omitempty" json:"reg_identifier,omitempty"`
@ -760,6 +820,9 @@ type GetAllArtifactsParams struct {
// SearchTerm search Term.
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.

View File

@ -30,6 +30,7 @@ import (
"github.com/harness/gitness/registry/app/pkg/docker"
"github.com/harness/gitness/registry/app/store/database"
"github.com/harness/gitness/registry/config"
"github.com/harness/gitness/registry/gc"
"github.com/harness/gitness/types"
"github.com/google/wire"
@ -64,9 +65,11 @@ func BlobStorageProvider(c *types.Config) (storagedriver.StorageDriver, error) {
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,
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)
}
@ -77,6 +80,7 @@ var WireSet = wire.NewSet(
pkg.WireSet,
docker.WireSet,
router.WireSet,
gc.WireSet,
)
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
// 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)
log.Ctx(ctx).Info().Msgf("Opening file %s %s", d.fullPath(path), d.rootDirectory)
if err != nil {
if os.IsNotExist(err) {
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,
blobRepo store.BlobRepository, spaceStore corestore.SpaceStore,
cfg *types.Config, storageService *registrystorage.Service,
mtRepository store.MediaTypesRepository, manifestRepository store.ManifestRepository,
gcService gc.Service,
) *App {
app := &App{
@ -64,7 +63,7 @@ func NewApp(
storageService: storageService,
}
app.configureSecret(cfg)
gcService.Start(ctx, sqlDB, spaceStore, blobRepo, storageDeleter, cfg, mtRepository, manifestRepository)
gcService.Start(ctx, sqlDB, spaceStore, blobRepo, storageDeleter, cfg)
return app
}

View File

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

View File

@ -23,6 +23,8 @@ import (
"fmt"
"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/manifestlist"
"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/gc"
"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/distribution/distribution/v3"
@ -53,8 +56,10 @@ type manifestService struct {
imageDao store.ImageRepository
artifactDao store.ArtifactRepository
manifestRefDao store.ManifestReferenceRepository
spacePathStore gitnessappstore.SpacePathStore
gcService gc.Service
tx dbtx.Transactor
reporter event.Reporter
}
func NewManifestService(
@ -62,7 +67,7 @@ func NewManifestService(
blobRepo store.BlobRepository, mtRepository store.MediaTypesRepository, tagDao store.TagRepository,
imageDao store.ImageRepository, artifactDao store.ArtifactRepository,
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 {
return &manifestService{
registryDao: registryDao,
@ -76,6 +81,8 @@ func NewManifestService(
manifestRefDao: manifestRefDao,
gcService: gcService,
tx: tx,
reporter: reporter,
spacePathStore: spacePathStore,
}
}
@ -166,44 +173,90 @@ func (l *manifestService) dbTagManifest(
tagName, imageName string,
info pkg.RegistryInfo,
) error {
dbRepo, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
dbRegistry, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
if err != nil {
return err
return formatFailedToTagErr(err)
}
newDigest, err := types.NewDigest(dgst)
if err != nil {
return err
return formatFailedToTagErr(err)
}
dbManifest, err := l.manifestDao.FindManifestByDigest(ctx, dbRepo.ID, info.Image, newDigest)
if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) {
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)
}
return err
if err != nil {
return formatFailedToTagErr(err)
}
// We need to find and lock a GC manifest task that is related with the manifest that we're about to tag. This
// is needed to ensure we lock any related online GC tasks to prevent race conditions around the tag creation. See:
return l.tx.WithTx(
ctx, func(ctx context.Context) error {
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()
if _, err := l.gcService.ManifestFindAndLockBefore(
ctx, dbRepo.ID, dbManifest.ID,
time.Now().Add(manifestTagGCReviewWindow),
); err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
// 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: dbRepo.ID,
RegistryID: registryID,
Enabled: true,
}
@ -227,17 +280,47 @@ func (l *manifestService) dbTagManifest(
tag := &types.Tag{
Name: tagName,
ImageName: imageName,
RegistryID: dbRepo.ID,
ManifestID: dbManifest.ID,
RegistryID: registryID,
ManifestID: manifestID,
}
if err := l.tagDao.CreateOrUpdate(ctx, tag); err != nil {
return err
return l.tagDao.CreateOrUpdate(ctx, tag)
}
// Retrieves the spacePath and packageType.
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
}
return nil
},
)
packageType, err := event.GetPackageTypeFromString(string(dbRepo.PackageType))
if err != nil {
log.Ctx(ctx).Err(err).Msg("Failed to find packageType")
return "", event.PackageType(0), err
}
return spacePath.Value, packageType, nil
}
// Reports event asynchronously.
func (l *manifestService) reportEventAsync(
ctx context.Context,
regID,
imageName,
tagName string,
packageType event.PackageType,
spacePath string,
) {
go l.reporter.ReportEvent(ctx, &event.ArtifactDetails{
RegistryID: regID,
ImagePath: imageName + ":" + tagName,
PackageType: packageType,
}, spacePath)
}
func (l *manifestService) DBPut(
@ -327,7 +410,7 @@ func (l *manifestService) dbPutManifestV2(
}
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
}
@ -433,7 +516,7 @@ func (l *manifestService) DBFindRepositoryBlob(
image := info.Image
b, err := l.blobRepo.FindByDigestAndRepoID(ctx, desc.Digest, repoID, image)
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, err
@ -453,11 +536,11 @@ func (l *manifestService) handleSubject(
return err
}
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
}
if errors.Is(err, store2.ErrResourceNotFound) {
if errors.Is(err, gitnessstore.ErrResourceNotFound) {
// in case something happened to the referenced manifest after validation
// return distribution.ManifestBlobUnknownError{Digest: subject.Digest}
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)
if err != nil && !errors.Is(err, store2.ErrResourceNotFound) {
if err != nil && !errors.Is(err, gitnessstore.ErrResourceNotFound) {
return err
}
@ -625,7 +708,7 @@ func (l *manifestService) dbPutImageIndex(
}
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
}
@ -758,7 +841,7 @@ func (l *manifestService) dbFindManifestListManifest(
imageName, dgst,
)
if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) {
if errors.Is(err, gitnessstore.ErrResourceNotFound) {
return nil, fmt.Errorf(
"manifest %s not found for %s/%s", digest.String(),
repository.Name, imageName,
@ -849,7 +932,7 @@ func (l *manifestService) DeleteManifest(
}
m, err := l.manifestDao.FindManifestByDigest(ctx, registry.ID, imageName, newDigest)
if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) {
if errors.Is(err, gitnessstore.ErrResourceNotFound) {
return util.ErrManifestNotFound
}
return err

View File

@ -16,8 +16,9 @@ package docker
import (
"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"
"github.com/harness/gitness/registry/app/event"
"github.com/harness/gitness/registry/app/pkg"
"github.com/harness/gitness/registry/app/storage"
"github.com/harness/gitness/registry/app/store"
@ -49,17 +50,17 @@ func ManifestServiceProvider(
manifestDao store.ManifestRepository, blobRepo store.BlobRepository, mtRepository store.MediaTypesRepository,
manifestRefDao store.ManifestReferenceRepository, tagDao store.TagRepository, imageDao store.ImageRepository,
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 {
return NewManifestService(
registryDao, manifestDao, blobRepo, mtRepository, tagDao, imageDao,
artifactDao, layerDao, manifestRefDao, tx, gcService,
artifactDao, layerDao, manifestRefDao, tx, gcService, reporter, spacePathStore,
)
}
func RemoteRegistryProvider(
local *LocalRegistry, app *App, upstreamProxyConfigRepo store.UpstreamProxyConfigRepository,
spacePathStore corestore.SpacePathStore, secretService secret.Service,
spacePathStore gitnessstore.SpacePathStore, secretService secret.Service,
) *RemoteRegistry {
return NewRemoteRegistry(local, app, upstreamProxyConfigRepo, spacePathStore, secretService).(*RemoteRegistry)
}
@ -68,7 +69,7 @@ func ControllerProvider(
local *LocalRegistry,
remote *RemoteRegistry,
controller *pkg.CoreController,
spaceStore corestore.SpaceStore,
spaceStore gitnessstore.SpaceStore,
authorizer authz.Authorizer,
) *Controller {
return NewController(local, remote, controller, spaceStore, authorizer)
@ -78,8 +79,12 @@ func StorageServiceProvider(cfg *types.Config, driver storagedriver.StorageDrive
return GetStorageService(cfg, driver)
}
func ProvideReporter() event.Reporter {
return &event.Noop{}
}
var ControllerSet = wire.NewSet(ControllerProvider)
var RegistrySet = wire.NewSet(LocalRegistryProvider, ManifestServiceProvider, RemoteRegistryProvider)
var StorageServiceSet = wire.NewSet(StorageServiceProvider)
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(
ctx context.Context, parentID int64,
packageTypes *[]string, sortByField string,
registryIDs *[]string, sortByField string,
sortByOrder string, limit int, offset int, search string,
labels []string,
latestVersion bool,
) (*[]types.ArtifactMetadata, error)
CountAllArtifactsByParentID(
ctx context.Context, parentID int64,
packageTypes *[]string, search string,
labels []string,
registryIDs *[]string, search string,
latestVersion bool,
) (int64, error)
GetAllArtifactsByRepo(
@ -460,4 +460,5 @@ type GCManifestTaskRepository interface {
Postpone(ctx context.Context, b *types.GCManifestTask, d time.Duration) error
IsDangling(ctx context.Context, b *types.GCManifestTask) (bool, 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"`
CreatedAt int64 `db:"created_at"`
ModifiedAt int64 `db:"modified_at"`
Version string `db:"version"`
}
type tagMetadataDB struct {
@ -337,54 +338,54 @@ func sqlPartialMatch(value string) string {
func (t tagDao) GetAllArtifactsByParentID(
ctx context.Context,
parentID int64,
packageTypes *[]string,
registryIDs *[]string,
sortByField string,
sortByOrder string,
limit int,
offset int,
search string,
labels []string,
latestVersion bool,
) (*[]types.ArtifactMetadata, error) {
q := databaseg.Builder.Select(
"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,"+
" t.tag_updated_at as modified_at, ar.image_labels as labels, t2.download_count as download_count ",
`r.registry_name as repo_name,
t.tag_image_name as name,
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").
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").
Where("r.registry_parent_id = ?", parentID).
Join(
"images ar ON ar.image_registry_id = t.tag_registry_id AND"+
" ar.image_name = t.tag_image_name",
).
LeftJoin(
"(SELECT i.image_name, t1.download_count FROM"+
" ( SELECT a.artifact_image_id, COUNT(d.download_stat_id) as download_count"+
" FROM artifacts a "+
" LEFT JOIN download_stats d ON d.download_stat_artifact_id = a.artifact_id GROUP BY"+
" a.artifact_image_id ) as t1 "+
" JOIN images ON i.image_id = t1.artifact_image_id "+
" JOIN registries r ON r.registry_id = i.image_registry_id "+
" WHERE r.registry_parent_id = ? GROUP BY i.image_name) as t2"+
" ON t.tag_image_name = t2.image_name", parentID,
`( 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
FROM artifacts a JOIN download_stats d ON d.download_stat_artifact_id = a.artifact_id
GROUP BY a.artifact_image_id ) as t1
JOIN images i ON i.image_id = t1.artifact_image_id
JOIN registries r ON r.registry_id = i.image_registry_id
WHERE r.registry_parent_id = ? GROUP BY i.image_name) as t2
ON t.tag_image_name = t2.image_name`, parentID,
)
if latestVersion {
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(*packageTypes) > 0 {
q = q.Where(sq.Eq{"r.registry_package_type": packageTypes})
}
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)
if len(*registryIDs) > 0 {
q = q.Where(sq.Eq{"r.registry_name": registryIDs})
}
if search != "" {
@ -409,39 +410,35 @@ func (t tagDao) GetAllArtifactsByParentID(
func (t tagDao) CountAllArtifactsByParentID(
ctx context.Context, parentID int64,
packageTypes *[]string, search string, labels []string,
registryIDs *[]string, search string, latestVersion bool,
) (int64, error) {
// nolint:goconst
q := databaseg.Builder.Select("COUNT(*)").
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"). // nolint:goconst
Where("r.registry_parent_id = ?", parentID).
Join(
"images ar ON ar.image_registry_id = t.tag_registry_id" +
" AND ar.image_name = t.tag_image_name",
).
Where("a.rank = 1 ")
)
if len(*packageTypes) > 0 {
q = q.Where(sq.Eq{"r.registry_package_type": packageTypes})
if latestVersion {
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 != "" {
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()
if err != nil {
return -1, errors.Wrap(err, "Failed to convert query to sql")
@ -461,9 +458,9 @@ func (t tagDao) GetTagDetail(
name string,
) (*types.TagDetail, error) {
q := databaseg.Builder.Select(
"tag_id as id, tag_name as name ,"+
" tag_image_name as image_name, tag_created_at as created_at, "+
" tag_updated_at as updated_at, manifest_total_size as size",
`tag_id as id, tag_name as name,
tag_image_name as image_name, tag_created_at as created_at,
tag_updated_at as updated_at, manifest_total_size as size`,
).
From("tags").
Join("manifests ON manifest_id = tag_manifest_id").
@ -494,13 +491,13 @@ func (t tagDao) GetLatestTagMetadata(
imageName string,
) (*types.ArtifactMetadata, error) {
q := databaseg.Builder.Select(
"r.registry_name as repo_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_updated_at as modified_at, ar.image_labels as labels",
`r.registry_name as repo_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_updated_at as modified_at, ar.image_labels as labels`,
).
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(
"images ar ON ar.image_registry_id = t.tag_registry_id "+
"AND ar.image_name = t.tag_image_name",
@ -618,18 +615,18 @@ func (t tagDao) GetAllArtifactsByRepo(
labels []string,
) (*[]types.ArtifactMetadata, error) {
q := databaseg.Builder.Select(
"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,"+
" t.tag_updated_at as modified_at, ar.image_labels as labels, "+
" COALESCE(t2.download_count, 0) as download_count ",
`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,
t.tag_updated_at as modified_at, ar.image_labels as labels,
COALESCE(t2.download_count, 0) as download_count `,
).
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 = ? AND r.registry_name = ? ) AS a"+
" ON t.tag_id = a.id", parentID, repoKey,
`(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 = ? AND r.registry_name = ? ) AS a
ON t.tag_id = a.id`, parentID, repoKey, // nolint:goconst
).
Join("registries r ON t.tag_registry_id = r.registry_id").
Join(
@ -637,15 +634,15 @@ func (t tagDao) GetAllArtifactsByRepo(
" AND ar.image_name = t.tag_image_name",
).
LeftJoin(
"( 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"+
" FROM artifacts a "+
" JOIN download_stats d ON d.download_stat_artifact_id = a.artifact_id GROUP BY"+
" a.artifact_image_id ) as t1 "+
" JOIN images i ON i.image_id = t1.artifact_image_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"+
" ON t.tag_image_name = t2.image_name", parentID, repoKey,
`( 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
FROM artifacts a
JOIN download_stats d ON d.download_stat_artifact_id = a.artifact_id GROUP BY
a.artifact_image_id ) as t1
JOIN images i ON i.image_id = t1.artifact_image_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
ON t.tag_image_name = t2.image_name`, parentID, repoKey,
).
Where("a.rank = 1 ")
@ -676,6 +673,7 @@ func (t tagDao) GetAllArtifactsByRepo(
return t.mapToArtifactMetadataList(ctx, dst)
}
// nolint:goconst
func (t tagDao) CountAllArtifactsByRepo(
ctx context.Context, parentID int64, repoKey string,
search string, labels []string,
@ -683,10 +681,10 @@ func (t tagDao) CountAllArtifactsByRepo(
q := databaseg.Builder.Select("COUNT(*)").
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 = ? AND r.registry_name = ? ) AS a ON t.tag_id = a.id", parentID, repoKey,
`(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 = ? 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(
@ -726,10 +724,10 @@ func (t tagDao) GetAllTagsByRepoAndImage(
search string,
) (*[]types.TagMetadata, error) {
q := databaseg.Builder.Select(
"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, "+
" m.manifest_schema_version, m.manifest_non_conformant, m.manifest_payload, "+
" mt.mt_media_type ",
`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,
m.manifest_schema_version, m.manifest_non_conformant, m.manifest_payload,
mt.mt_media_type `,
).
From("tags t").
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),
CreatedAt: time.UnixMilli(dst.CreatedAt),
ModifiedAt: time.UnixMilli(dst.ModifiedAt),
Version: dst.Version,
}, nil
}

View File

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

View File

@ -31,7 +31,7 @@ type Service interface {
Start(
ctx context.Context, sqlDB *sqlx.DB, spaceStore corestore.SpaceStore,
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)
BlobReschedule(ctx context.Context, b *registrytypes.GCBlobTask, d time.Duration) error

View File

@ -14,15 +14,11 @@
package types
import (
"time"
)
type GCBlobTask struct {
BlobID int64
ReviewAfter time.Time
ReviewAfter int64
ReviewCount int
CreatedAt time.Time
CreatedAt int64
Event string
}
@ -30,14 +26,8 @@ type GCBlobTask struct {
type GCManifestTask struct {
RegistryID int64
ManifestID int64
ReviewAfter time.Time
ReviewAfter int64
ReviewCount int
CreatedAt time.Time
CreatedAt int64
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
CreatedAt time.Time
ModifiedAt time.Time
Version string
}
type TagMetadata struct {

View File

@ -51,7 +51,7 @@
"@codemirror/view": "^6.9.6",
"@harnessio/design-system": "^2.1.1",
"@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",
"@tanstack/react-query": "4.20.4",
"@types/dompurify": "^3.0.2",

View File

@ -15,9 +15,9 @@
*/
import type React from 'react'
import type { Button } from '@harnessio/uicore'
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 NGBreadcrumbs from '@ar/__mocks__/components/NGBreadcrumbs'
import type DependencyView from '@ar/__mocks__/components/DependencyView'
@ -43,6 +43,16 @@ export interface Scope {
space?: string
}
export interface PipelineExecutionPathProps {
executionIdentifier: string
pipelineIdentifier: string
module: 'ci' | 'cd'
}
export interface ServiceDetailsPathProps {
serviceId: string
}
export interface PermissionsRequest {
resource: { resourceType: ResourceType; resourceIdentifier?: string }
permissions: PermissionIdentifier[]
@ -63,7 +73,7 @@ export interface ParentContextObj {
}
export interface Components {
RbacButton: typeof Button
RbacButton: typeof RbacButton
NGBreadcrumbs: typeof NGBreadcrumbs
RbacMenuItem: typeof RbacMenuItem
}
@ -96,6 +106,8 @@ export interface CustomUtils {
getCustomHeaders: () => Record<string, string>
getApiBaseUrl: (url: string) => string
getRouteDefinitions?: (routeParams: Record<string, string>) => ARRouteDefinitionsReturn
getRouteToPipelineExecutionView?: (params: Scope & PipelineExecutionPathProps) => string
getRouteToServiceDetailsView?: (params: Scope & ServiceDetailsPathProps) => string
}
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 { defaultTo, noop } from 'lodash-es'
import { Button, Container } from '@harnessio/uicore'
import { Container } from '@harnessio/uicore'
import type { MFEAppProps } from '@ar/MFEAppTypes'
@ -29,6 +29,7 @@ import {
useQueryParamsOptions,
useUpdateQueryParams
} from '@ar/__mocks__/hooks'
import RbacButton from '@ar/__mocks__/components/RbacButton'
import RbacMenuItem from '@ar/__mocks__/components/RbacMenuItem'
import NGBreadcrumbs from '@ar/__mocks__/components/NGBreadcrumbs'
import DependencyView from '@ar/__mocks__/components/DependencyView'
@ -76,7 +77,7 @@ const GitnessApp = (props: Partial<MFEAppProps>): JSX.Element => {
scope={defaultTo(scope, {})}
customScope={defaultTo(customScope, {})}
components={Object.assign(
{ RbacButton: Button, NGBreadcrumbs, RbacMenuItem, SecretFormInput, VulnerabilityView, DependencyView },
{ RbacButton, NGBreadcrumbs, RbacMenuItem, SecretFormInput, VulnerabilityView, DependencyView },
components
)}
NavComponent={NavComponent}

View File

@ -16,7 +16,7 @@
import type { IconName } from '@harnessio/icons'
import type { StringsMap } from '@ar/frameworks/strings'
import { RepositoryPackageType } from './types'
import { EnvironmentType, RepositoryPackageType } from './types'
export interface RepositoryTypeListItem {
label: keyof StringsMap
@ -80,3 +80,20 @@ export const RepositoryTypes: RepositoryTypeListItem[] = [
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 {
DELETE_SERVICE = 'core_service_delete',
EDIT_SERVICE = 'core_service_edit'
VIEW_ARTIFACT_REGISTRY = 'artifact_artregistry_view',
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 {
SERVICE = 'SERVICE'
ARTIFACT_REGISTRY = 'ARTIFACT_REGISTRY'
}

View File

@ -29,8 +29,8 @@ export type FormikFowardRef<T = unknown> =
| null
export enum EnvironmentType {
Production = 'Production',
PreProduction = 'PreProduction'
Prod = 'Production',
NonProd = 'PreProduction'
}
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>[]
small?: boolean
bold?: boolean
className?: string
}
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)
return (
<Container data-testid={id}>
<Container className={className} data-testid={id}>
<ButtonGroup className={css.btnGroup}>
{tabs.map(each => (
<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 {
const { label, name, placeholder, formik } = props
const { label, name, placeholder, formik, disabled } = props
const formValue = get(formik.values, name, []) as string[]
@ -53,6 +53,7 @@ function PatternInput<T>(props: PatternInputProps & { formik: FormikContextType<
onChange={selectedItems => {
formik.setFieldValue(name, selectedItems)
}}
readonly={disabled}
/>
</FormGroup>
)

View File

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

View File

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

View File

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

View File

@ -20,16 +20,35 @@
}
.nameCellContainer {
align-items: center;
display: flex;
justify-content: flex-start;
align-items: flex-start;
gap: var(--spacing-small);
}
.copyButton {
.copyUrlBtn {
& :global(.bp3-button-text) {
margin-left: var(--spacing-small) !important;
font-size: 12px;
padding-left: var(--spacing-xsmall);
}
& .copyUrlIcon {
--icon-padding: 0px !important;
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 */
// This is an auto-generated file
export declare const copyButton: string
export declare const copyUrlBtn: string
export declare const copyUrlIcon: string
export declare const deploymentsRow: string
export declare const nameCellContainer: string
export declare const nonProd: string
export declare const prod: string
export declare const toggleAccordion: string

View File

@ -15,13 +15,14 @@
*/
import React, { FC, PropsWithChildren, useState } from 'react'
import classNames from 'classnames'
import { defaultTo } from 'lodash-es'
import copy from 'clipboard-copy'
import { Link } from 'react-router-dom'
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 { Color } from '@harnessio/design-system'
import { Color, FontVariation } from '@harnessio/design-system'
import { killEvent } from '@ar/common/utils'
import { useStrings } from '@ar/frameworks/strings/String'
@ -31,6 +32,8 @@ import RepositoryLocationBadge from '@ar/components/Badge/RepositoryLocationBadg
import { DefaultIconProps } from './constants'
import { handleToggleExpandableRow } from './utils'
import CommandBlock from '../CommandBlock/CommandBlock'
import { NonProdTag, ProdTag } from '../Tag/Tags'
import css from './TableCells.module.scss'
@ -67,10 +70,11 @@ export const CopyUrlCell: FC<PropsWithChildren<CopyUrlCellProps>> = ({ value, ch
}
return (
<Button
className={css.copyButton}
className={classNames(css.copyButton, css.copyUrlBtn)}
intent="primary"
minimal
icon="link"
variation={ButtonVariation.LINK}
iconProps={{ size: 12, className: css.copyUrlIcon }}
onClick={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 {
value: RepositoryConfigType
}
@ -158,26 +202,58 @@ const ToggleAccordionCell = (props: ToggleAccordionCellProps): JSX.Element => {
interface LinkCellProps {
label: string
subLabel?: string
prefix?: React.ReactElement
postfix?: React.ReactElement
linkTo: string
}
const LinkCell = (props: LinkCellProps): JSX.Element => {
const { prefix, postfix, label, linkTo } = props
const { prefix, postfix, label, linkTo, subLabel } = props
return (
<Layout.Horizontal className={css.nameCellContainer} spacing="small">
<Layout.Horizontal
className={css.nameCellContainer}
flex={{ justifyContent: 'flex-start', alignItems: 'flex-start' }}>
{prefix}
<Layout.Vertical>
<Link to={linkTo}>
<Text color={Color.PRIMARY_7} lineClamp={1}>
{label}
</Text>
</Link>
{subLabel && <Text lineClamp={1}>{subLabel}</Text>}
</Layout.Vertical>
{postfix}
</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 {
UrlCell,
SizeCell,
@ -185,6 +261,9 @@ export default {
LinkCell,
TextCell,
CopyUrlCell,
CopyTextCell,
DeploymentsCell,
PullCommandCell,
LastModifiedCell,
ToggleAccordionCell,
RepositoryLocationBadgeCell

View File

@ -31,3 +31,19 @@
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 */
// This is an auto-generated file
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

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}
permission={{
resource: {
resourceType: ResourceType.SERVICE,
resourceType: ResourceType.ARTIFACT_REGISTRY,
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}
permission={{
resource: {
resourceType: ResourceType.SERVICE,
resourceType: ResourceType.ARTIFACT_REGISTRY,
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}
permission={{
resource: {
resourceType: ResourceType.SERVICE,
resourceType: ResourceType.ARTIFACT_REGISTRY,
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 ArtifactTags from '@ar/components/PageTitle/ArtifactTags'
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 RepositoryIcon from '@ar/frameworks/RepositoryStep/RepositoryIcon'
@ -109,6 +110,13 @@ function ArtifactDetailsHeaderContent(props: ArtifactDetailsHeaderContentProps):
onChange={handleUpdateArtifactLabels}
labels={defaultTo(labels, [])}
placeholder={getString('artifactDetails.artifactLabelInputPlaceholder')}
permission={{
permission: PermissionIdentifier.EDIT_ARTIFACT_REGISTRY,
resource: {
resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: repositoryIdentifier
}
}}
/>
</Layout.Vertical>
</Layout.Horizontal>

View File

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

View File

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

View File

@ -14,47 +14,38 @@
* limitations under the License.
*/
import React, { useCallback, useMemo, useRef } from 'react'
import React, { useMemo } from 'react'
import classNames from 'classnames'
import { flushSync } from 'react-dom'
import { defaultTo } from 'lodash-es'
import { Expander } from '@blueprintjs/core'
import { HarnessDocTooltip, Page, Button, ButtonVariation } from '@harnessio/uicore'
import {
ExpandingSearchInput,
HarnessDocTooltip,
Page,
type ExpandingSearchInputHandle,
Button,
ButtonVariation
} from '@harnessio/uicore'
import { useGetAllArtifactsQuery } from '@harnessio/react-har-service-client'
GetAllHarnessArtifactsQueryQueryParams,
useGetAllHarnessArtifactsQuery
} from '@harnessio/react-har-service-client'
import { useStrings } from '@ar/frameworks/strings'
import { DEFAULT_PAGE_INDEX, PreferenceScope } from '@ar/constants'
import { ButtonTab, ButtonTabs } from '@ar/components/ButtonTabs/ButtonTabs'
import { useGetSpaceRef, useParentComponents, useParentHooks } from '@ar/hooks'
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 ArtifactListTable from './components/ArtifactListTable/ArtifactListTable'
import RepositorySelector from './components/RepositorySelector/RepositorySelector'
import ArtifactSearchInput from './components/ArtifactSearchInput/ArtifactSearchInput'
import { useArtifactListQueryParamOptions, type ArtifactListPageQueryParams } from './utils'
import css from './ArtifactListPage.module.scss'
interface ArtifactListPageProps {
withHeader?: boolean
parentRepoKey?: string
pageBodyClassName?: string
}
function ArtifactListPage({ withHeader = true, parentRepoKey, pageBodyClassName }: ArtifactListPageProps): JSX.Element {
function ArtifactListPage(): JSX.Element {
const { getString } = useStrings()
const { NGBreadcrumbs } = useParentComponents()
const { useQueryParams, useUpdateQueryParams, usePreferenceStore } = useParentHooks()
const searchRef = useRef({} as ExpandingSearchInputHandle)
const { updateQueryParams } = useUpdateQueryParams<Partial<ArtifactListPageQueryParams>>()
const queryParams = useQueryParams<ArtifactListPageQueryParams>(useArtifactListQueryParamOptions())
const { searchTerm, isDeployedArtifacts, packageTypes, repositoryKey, page, size, labels } = queryParams
const shouldRenderRepositorySelectFilter = !parentRepoKey
const shouldRenderPackageTypeSelectFilter = !parentRepoKey
const { searchTerm, isDeployedArtifacts, repositoryKey, page, size, latestVersion, packageTypes, labels } =
queryParams
const spaceRef = useGetSpaceRef('')
const { preference: sortingPreference, setPreference: setSortingPreference } = usePreferenceStore<string | undefined>(
@ -73,50 +64,39 @@ function ArtifactListPage({ withHeader = true, parentRepoKey, pageBodyClassName
refetch,
isLoading: loading,
error
} = useGetAllArtifactsQuery({
} = useGetAllHarnessArtifactsQuery({
space_ref: spaceRef,
queryParams: {
page,
size,
label: labels,
package_type: packageTypes,
search_term: searchTerm,
sort_field: sortField,
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: {
arrayFormat: 'repeat'
}
})
const handleClearAllFilters = (): void => {
flushSync(searchRef.current.clear)
updateQueryParams({
page: 0,
searchTerm: '',
packageTypes: [],
isDeployedArtifacts: false
isDeployedArtifacts: false,
latestVersion: 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 hasFilter = !!searchTerm || isDeployedArtifacts || latestVersion
const responseData = data?.content?.data
return (
<>
{withHeader && (
<Page.Header
title={
<div className="ng-tooltip-native">
@ -126,55 +106,66 @@ function ArtifactListPage({ withHeader = true, parentRepoKey, pageBodyClassName
}
breadcrumbs={<NGBreadcrumbs links={[]} />}
/>
)}
<Page.SubHeader className={css.subHeader}>
<div className={css.subHeaderItems}>
{shouldRenderRepositorySelectFilter && (
<ArtifactSearchInput
searchTerm={searchTerm || ''}
onChange={text => {
updateQueryParams({ searchTerm: text || undefined, page: DEFAULT_PAGE_INDEX })
}}
placeholder={getString('search')}
/>
<RepositorySelector
value={repositoryKey}
onChange={val => {
updateQueryParams({ repositoryKey: val, page: DEFAULT_PAGE_INDEX })
}}
/>
)}
{shouldRenderPackageTypeSelectFilter && (
<PackageTypeSelector
value={packageTypes}
onChange={val => {
updateQueryParams({ packageTypes: val, page: DEFAULT_PAGE_INDEX })
}}
/>
)}
<LabelsSelector
value={labels}
onChange={val => {
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 />
<ExpandingSearchInput
alwaysExpanded
width={200}
placeholder={getString('search')}
onChange={text => {
updateQueryParams({ searchTerm: text || undefined, page: DEFAULT_PAGE_INDEX })
}}
defaultValue={searchTerm}
ref={searchRef}
<ButtonTabs
className={css.filterTabContainer}
small
bold
selectedTabId={
latestVersion ? ArtifactListVersionFilter.LATEST_VERSION : ArtifactListVersionFilter.ALL_VERSION
}
onChange={newTab => {
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>
</Page.SubHeader>
<Page.Body
className={classNames(css.pageBody, pageBodyClassName)}
className={classNames(css.pageBody)}
loading={loading}
error={error?.message}
retryOnError={() => refetch()}
@ -199,7 +190,6 @@ function ArtifactListPage({ withHeader = true, parentRepoKey, pageBodyClassName
setSortingPreference(JSON.stringify(sortArray))
updateQueryParams({ sort: sortArray })
}}
onClickLabel={handleClickLabel}
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--header'] {
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 {
align-items: center;
}
.cellBtn {
--padding: 0 !important;
}
.optionsMenu {
min-width: unset;
}

View File

@ -16,6 +16,8 @@
/* eslint-disable */
// This is an auto-generated file
export declare const cellBtn: string
export declare const nameCellContainer: string
export declare const optionsMenu: string
export declare const table: 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 { useParentHooks } from '@ar/hooks'
import {
ArtifactDeploymentsCell,
ArtifactDownloadsCell,
ArtifactListPullCommandCell,
ArtifactListVulnerabilitiesCell,
ArtifactNameCell,
LatestArtifactCell,
RepositoryNameCell
LatestArtifactCell
} from './ArtifactListTableCell'
import css from './ArtifactListTable.module.scss'
@ -40,11 +42,10 @@ export interface ArtifactListTableProps extends ArtifactListColumnActions {
setSortBy: (sortBy: string[]) => void
sortBy: string[]
minimal?: boolean
onClickLabel: (val: string) => void
}
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 { getString } = useStrings()
@ -72,17 +73,16 @@ export default function ArtifactListTable(props: ArtifactListTableProps): JSX.El
}
return [
{
Header: getString('artifactList.table.columns.name'),
Header: getString('artifactList.table.columns.artifactName'),
accessor: 'name',
Cell: ArtifactNameCell,
serverSortProps: getServerSortProps('name'),
onClickLabel
serverSortProps: getServerSortProps('name')
},
{
Header: getString('artifactList.table.columns.repository'),
accessor: 'registryIdentifier',
Cell: RepositoryNameCell,
serverSortProps: getServerSortProps('registryIdentifier')
Header: getString('artifactList.table.columns.pullCommand'),
accessor: 'pullCommand',
Cell: ArtifactListPullCommandCell,
disableSortBy: true
},
{
Header: getString('artifactList.table.columns.downloads'),
@ -91,13 +91,25 @@ export default function ArtifactListTable(props: ArtifactListTableProps): JSX.El
serverSortProps: getServerSortProps('downloadsCount')
},
{
Header: getString('artifactList.table.columns.latestVersion'),
accessor: 'latestVersion',
Header: getString('artifactList.table.columns.environments'),
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,
serverSortProps: getServerSortProps('latestVersion')
serverSortProps: getServerSortProps('lastUpdated')
}
].filter(Boolean) as unknown as Column<ArtifactMetadata>[]
}, [currentOrder, currentSort, getString, onClickLabel])
}, [currentOrder, currentSort, getString])
return (
<TableV2<ArtifactMetadata>

View File

@ -14,22 +14,25 @@
* limitations under the License.
*/
import React from 'react'
import React, { useState } from 'react'
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 { Layout, Text } from '@harnessio/uicore'
import { Color } from '@harnessio/design-system'
import type { ArtifactMetadata } from '@harnessio/react-har-service-client'
import type { ArtifactMetadata, StoDigestMetadata } 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 { useParentComponents, useRoutes } from '@ar/hooks'
import TableCells from '@ar/components/TableCells/TableCells'
import type { RepositoryPackageType } from '@ar/common/types'
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'
import { useStrings } from '@ar/frameworks/strings'
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> & {
column: ColumnInstance<D>
@ -51,15 +54,19 @@ export const ArtifactNameCell: Renderer<{
const { original } = row
const { onClickLabel } = column
const routes = useRoutes()
const value = original.name
const { name: value, version } = original
return (
<Layout.Vertical>
<TableCells.LinkCell
prefix={<RepositoryIcon packageType={original.packageType as RepositoryPackageType} iconProps={{ size: 24 }} />}
linkTo={routes.toARArtifactDetails({
linkTo={routes.toARVersionDetailsTab({
repositoryIdentifier: original.registryIdentifier,
artifactIdentifier: value
artifactIdentifier: value,
versionIdentifier: defaultTo(version, ''),
versionTab: VersionDetailsTab.OVERVIEW
})}
label={value}
subLabel={version}
postfix={
<LabelsPopover
popoverProps={{
@ -68,45 +75,16 @@ export const ArtifactNameCell: Renderer<{
labels={defaultTo(original.labels, [])}
tagProps={{
interactive: true,
onClick: e => onClickLabel(e.currentTarget.ariaValueText as string)
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')}
</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}
/>
</Layout.Vertical>
)
}
@ -114,23 +92,107 @@ export const ArtifactDownloadsCell: CellType = ({ value }) => {
return <TableCells.CountCell value={value} icon="download-box" iconProps={{ size: 12 }} />
}
export const LatestArtifactCell: CellType = ({ row }) => {
const { getString } = useStrings()
export const ArtifactDeploymentsCell: CellType = ({ row }) => {
const { original } = row
const { latestVersion, lastModified } = original || {}
if (!latestVersion) {
const { deploymentMetadata } = original
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 (
<Text color={Color.GREY_900} font={{ size: 'small' }}>
{getString('na')}
</Text>
<RbacMenuItem
text={getString('artifactList.table.actions.VulnerabilityStatus.digestMenuItemText', {
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 (
<Layout.Vertical spacing="small">
<Text color={Color.PRIMARY_7} font={{ size: 'small' }}>
{latestVersion}
<Button
className={css.cellBtn}
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>
<TableCells.LastModifiedCell value={defaultTo(lastModified, 0)} />
</Layout.Vertical>
</Button>
)
}
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
table:
noArtifactsTitle: There are no artifact available.
allRepositories: All Registries
allRepositories: Registries
latestVersions: Latest Versions
allVersions: All Versions
columns:
name: Name
artifactName: Artifact Name
repository: Registry
tags: Labels
downloads: Downloads (In the last week)
downloads: Downloads
latestVersion: Latest Version
pullCommand: Pull Command
environments: Environments
sto: Security Vulnerabilities
lastUpdated: Uploaded At
actions:
editRepository: Edit 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 { 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 { RepositoryPackageType } from '@ar/common/types'
import type { UseQueryParamsOptions } from '@ar/__mocks__/hooks'
type GetArtifactListQueryParams = {
accountIdentifier: string
orgIdentifier: string
projectIdentifier: string
export type ArtifactListPageQueryParams = {
page: number
size: number
sort: string[]
searchTerm?: string
isDeployedArtifacts: boolean
packageTypes: RepositoryPackageType[]
repositoryKey?: string
labels: string[]
latestVersion: boolean
isDeployedArtifacts: boolean
searchTerm?: string
repositoryKey?: string
}
export type ArtifactListPageQueryParams = Omit<
GetArtifactListQueryParams,
'accountIdentifier' | 'orgIdentifier' | 'projectIdentifier'
>
export const useArtifactListQueryParamOptions = (): UseQueryParamsOptions<ArtifactListPageQueryParams> => {
const { useQueryParamsOptions } = useParentHooks()
const _options = useQueryParamsOptions(
@ -48,10 +41,11 @@ export const useArtifactListQueryParamOptions = (): UseQueryParamsOptions<Artifa
size: DEFAULT_PAGE_SIZE,
sort: DEFAULT_PIPELINE_LIST_TABLE_SORT,
isDeployedArtifacts: false,
latestVersion: false,
packageTypes: [],
labels: []
},
{ ignoreEmptyString: false }
{ ignoreEmptyString: true }
)
const options = useMemo(() => ({ ..._options, strictNullHandling: true }), [_options])

View File

@ -34,7 +34,7 @@
div[class*='TableV2--cells'],
div[class*='TableV2--header'] {
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'] {

View File

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

View File

@ -15,13 +15,18 @@
*/
import React from 'react'
import { Link } from 'react-router-dom'
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 { useStrings } from '@ar/frameworks/strings'
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 TableCells from '@ar/components/TableCells/TableCells'
import type { ArtifactDetailsPathParams } from '@ar/routes/types'
import { VersionDetailsTab } from '@ar/pages/version-details/components/VersionDetailsTabs/constants'
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 }} />
}
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 = () => {
return <></>
}

View File

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

View File

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

View File

@ -53,10 +53,10 @@ export default function DeleteRepositoryMenuItem({ data }: RepositoryActionsProp
onClick={handleDeleteService}
permission={{
resource: {
resourceType: ResourceType.SERVICE,
resourceType: ResourceType.ARTIFACT_REGISTRY,
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}
permission={{
resource: {
resourceType: ResourceType.SERVICE,
resourceType: ResourceType.ARTIFACT_REGISTRY,
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}
permission={{
resource: {
resourceType: ResourceType.SERVICE,
resourceType: ResourceType.ARTIFACT_REGISTRY,
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}
permission={{
resource: {
resourceType: ResourceType.SERVICE,
resourceType: ResourceType.ARTIFACT_REGISTRY,
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}
permission={{
resource: {
resourceType: ResourceType.SERVICE,
resourceType: ResourceType.ARTIFACT_REGISTRY,
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 {
const { readonly, factory = repositoryFactory } = props
const { data } = useContext(RepositoryProviderContext)
const { data, setIsUpdating } = useContext(RepositoryProviderContext)
const { showSuccess, showError, clear } = useToaster()
const { getString } = useStrings()
const spaceRef = useGetSpaceRef()
@ -54,6 +54,7 @@ function RepositoryConfigurationForm(props: RepositoryConfigurationFormProps, fo
const handleModifyRepository = async (values: VirtualRegistryRequest): Promise<void> => {
try {
setIsUpdating(true)
const response = await modifyRepository({
registry_ref: spaceRef,
body: values as unknown as RegistryRequestRequestBody
@ -65,6 +66,8 @@ function RepositoryConfigurationForm(props: RepositoryConfigurationFormProps, fo
}
} catch (e: any) {
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 formattedValuesForCleanupPolicy = getFormattedFormDataForCleanupPolicy(formattedValues)
const response = await createRepository({
queryParams: {
space_ref: decodeRef(parentRef)
},
body: {
...formattedValuesForCleanupPolicy,
parentRef: decodeRef(parentRef)

View File

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

View File

@ -40,7 +40,8 @@ export function useCreateRepositoryModal(props: useCreateRepositoryModalProps) {
stepRef.current?.submitForm()
}
const [showModal, hideModal] = useModalHook(() => (
const [showModal, hideModal] = useModalHook(
() => (
<ModalDialog
isOpen={true}
enforceFocus={false}
@ -70,13 +71,16 @@ export function useCreateRepositoryModal(props: useCreateRepositoryModalProps) {
text={getString('repositoryDetails.repositoryForm.create')}
data-id="service-save"
onClick={handleSubmitForm}
disabled={showOverlay}
/>
<Button variation={ButtonVariation.TERTIARY} text={getString('cancel')} onClick={hideModal} />
</Layout.Horizontal>
}>
<RepositoryCreateForm onSuccess={onSuccess} setShowOverlay={setShowOverlay} ref={stepRef} />
</ModalDialog>
))
),
[showOverlay]
)
return [showModal, hideModal]
}

View File

@ -43,7 +43,7 @@ function RepositoryListPage(): JSX.Element {
const spaceRef = useGetSpaceRef()
const queryParamOptions = useArtifactRepositoriesQueryParamOptions()
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>(
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,17 +15,47 @@
*/
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 {
DockerRepositoryURLInputSource,
UpstreamProxyAuthenticationMode,
type UpstreamRegistryRequest
} 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) => {
if (draft.config.authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD) {
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) {
set(draft, 'config.url', '')

View File

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

View File

@ -33,10 +33,10 @@ export default function RestoreUpstreamProxy({ data }: UpstreamProxyActionProps)
onClick={noop}
permission={{
resource: {
resourceType: ResourceType.SERVICE,
resourceType: ResourceType.ARTIFACT_REGISTRY,
resourceIdentifier: data.identifier
},
permission: PermissionIdentifier.DELETE_SERVICE
permission: PermissionIdentifier.EDIT_ARTIFACT_REGISTRY
}}
/>
)

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