Merge pull request #6830 from concourse/issue/6617
Re ordering instanced pipelines
This commit is contained in:
commit
5c311157aa
|
@ -60,6 +60,7 @@ var DefaultRoles = map[string]string{
|
|||
atc.GetPipeline: ViewerRole,
|
||||
atc.DeletePipeline: MemberRole,
|
||||
atc.OrderPipelines: MemberRole,
|
||||
atc.OrderPipelinesWithinGroup: MemberRole,
|
||||
atc.PausePipeline: OperatorRole,
|
||||
atc.ArchivePipeline: OwnerRole,
|
||||
atc.UnpausePipeline: OperatorRole,
|
||||
|
|
|
@ -139,21 +139,22 @@ func NewHandler(
|
|||
|
||||
atc.ClearTaskCache: pipelineHandlerFactory.HandlerFor(jobServer.ClearTaskCache),
|
||||
|
||||
atc.ListAllPipelines: http.HandlerFunc(pipelineServer.ListAllPipelines),
|
||||
atc.ListPipelines: http.HandlerFunc(pipelineServer.ListPipelines),
|
||||
atc.GetPipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.GetPipeline),
|
||||
atc.DeletePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.DeletePipeline),
|
||||
atc.OrderPipelines: http.HandlerFunc(pipelineServer.OrderPipelines),
|
||||
atc.PausePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.PausePipeline),
|
||||
atc.ArchivePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.ArchivePipeline),
|
||||
atc.UnpausePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.UnpausePipeline),
|
||||
atc.ExposePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.ExposePipeline),
|
||||
atc.HidePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.HidePipeline),
|
||||
atc.GetVersionsDB: pipelineHandlerFactory.HandlerFor(pipelineServer.GetVersionsDB),
|
||||
atc.RenamePipeline: teamHandlerFactory.HandlerFor(pipelineServer.RenamePipeline),
|
||||
atc.ListPipelineBuilds: pipelineHandlerFactory.HandlerFor(pipelineServer.ListPipelineBuilds),
|
||||
atc.CreatePipelineBuild: pipelineHandlerFactory.HandlerFor(pipelineServer.CreateBuild),
|
||||
atc.PipelineBadge: pipelineHandlerFactory.HandlerFor(pipelineServer.PipelineBadge),
|
||||
atc.ListAllPipelines: http.HandlerFunc(pipelineServer.ListAllPipelines),
|
||||
atc.ListPipelines: http.HandlerFunc(pipelineServer.ListPipelines),
|
||||
atc.GetPipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.GetPipeline),
|
||||
atc.DeletePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.DeletePipeline),
|
||||
atc.OrderPipelines: teamHandlerFactory.HandlerFor(pipelineServer.OrderPipelines),
|
||||
atc.OrderPipelinesWithinGroup: teamHandlerFactory.HandlerFor(pipelineServer.OrderPipelinesWithinGroup),
|
||||
atc.PausePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.PausePipeline),
|
||||
atc.ArchivePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.ArchivePipeline),
|
||||
atc.UnpausePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.UnpausePipeline),
|
||||
atc.ExposePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.ExposePipeline),
|
||||
atc.HidePipeline: pipelineHandlerFactory.HandlerFor(pipelineServer.HidePipeline),
|
||||
atc.GetVersionsDB: pipelineHandlerFactory.HandlerFor(pipelineServer.GetVersionsDB),
|
||||
atc.RenamePipeline: teamHandlerFactory.HandlerFor(pipelineServer.RenamePipeline),
|
||||
atc.ListPipelineBuilds: pipelineHandlerFactory.HandlerFor(pipelineServer.ListPipelineBuilds),
|
||||
atc.CreatePipelineBuild: pipelineHandlerFactory.HandlerFor(pipelineServer.CreateBuild),
|
||||
atc.PipelineBadge: pipelineHandlerFactory.HandlerFor(pipelineServer.PipelineBadge),
|
||||
|
||||
atc.ListAllResources: http.HandlerFunc(resourceServer.ListAllResources),
|
||||
atc.ListResources: pipelineHandlerFactory.HandlerFor(resourceServer.ListResources),
|
||||
|
|
|
@ -1346,6 +1346,106 @@ var _ = Describe("Pipelines API", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("PUT /api/v1/teams/:team_name/pipelines/:pipeline_name/ordering", func() {
|
||||
var response *http.Response
|
||||
var instanceVars []atc.InstanceVars
|
||||
|
||||
BeforeEach(func() {
|
||||
instanceVars = []atc.InstanceVars{
|
||||
{"branch": "test"},
|
||||
{},
|
||||
{"branch": "test-2"},
|
||||
}
|
||||
})
|
||||
|
||||
JustBeforeEach(func() {
|
||||
requestPayload, err := json.Marshal(instanceVars)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
request, err := http.NewRequest("PUT", server.URL+"/api/v1/teams/a-team/pipelines/a-pipeline/ordering", bytes.NewBuffer(requestPayload))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
response, err = client.Do(request)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("when authenticated", func() {
|
||||
BeforeEach(func() {
|
||||
fakeAccess.IsAuthenticatedReturns(true)
|
||||
})
|
||||
|
||||
Context("when requester belongs to the team", func() {
|
||||
BeforeEach(func() {
|
||||
fakeAccess.IsAuthorizedReturns(true)
|
||||
dbTeamFactory.FindTeamReturns(fakeTeam, true, nil)
|
||||
})
|
||||
|
||||
It("constructs team with provided team name", func() {
|
||||
Expect(dbTeamFactory.FindTeamCallCount()).To(Equal(1))
|
||||
Expect(dbTeamFactory.FindTeamArgsForCall(0)).To(Equal("a-team"))
|
||||
})
|
||||
|
||||
Context("when ordering the pipelines succeeds", func() {
|
||||
BeforeEach(func() {
|
||||
fakeTeam.OrderPipelinesWithinGroupReturns(nil)
|
||||
})
|
||||
|
||||
It("orders the pipelines", func() {
|
||||
Expect(fakeTeam.OrderPipelinesWithinGroupCallCount()).To(Equal(1))
|
||||
groupName, actualInstanceVars := fakeTeam.OrderPipelinesWithinGroupArgsForCall(0)
|
||||
Expect(groupName).To(Equal("a-pipeline"))
|
||||
Expect(actualInstanceVars).To(Equal(instanceVars))
|
||||
})
|
||||
|
||||
It("returns 200", func() {
|
||||
Expect(response.StatusCode).To(Equal(http.StatusOK))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when a pipeline does not exist", func() {
|
||||
BeforeEach(func() {
|
||||
fakeTeam.OrderPipelinesWithinGroupReturns(db.ErrPipelineNotFound{Name: "a-pipeline"})
|
||||
})
|
||||
|
||||
It("returns 400", func() {
|
||||
Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
|
||||
Expect(ioutil.ReadAll(response.Body)).To(ContainSubstring("pipeline 'a-pipeline' not found"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when ordering the pipelines fails", func() {
|
||||
BeforeEach(func() {
|
||||
fakeTeam.OrderPipelinesWithinGroupReturns(errors.New("welp"))
|
||||
})
|
||||
|
||||
It("returns 500", func() {
|
||||
Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when requester does not belong to the team", func() {
|
||||
BeforeEach(func() {
|
||||
fakeAccess.IsAuthorizedReturns(false)
|
||||
})
|
||||
|
||||
It("returns 403", func() {
|
||||
Expect(response.StatusCode).To(Equal(http.StatusForbidden))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when not authenticated", func() {
|
||||
BeforeEach(func() {
|
||||
fakeAccess.IsAuthenticatedReturns(false)
|
||||
})
|
||||
|
||||
It("returns 401 Unauthorized", func() {
|
||||
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GET /api/v1/teams/:team_name/pipelines/:pipeline_name/versions-db", func() {
|
||||
var response *http.Response
|
||||
|
||||
|
|
|
@ -10,44 +10,32 @@ import (
|
|||
"github.com/concourse/concourse/atc/db"
|
||||
)
|
||||
|
||||
func (s *Server) OrderPipelines(w http.ResponseWriter, r *http.Request) {
|
||||
logger := s.logger.Session("order-pipelines")
|
||||
func (s *Server) OrderPipelines(team db.Team) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger := s.logger.Session("order-pipelines")
|
||||
|
||||
var pipelinesNames []string
|
||||
if err := json.NewDecoder(r.Body).Decode(&pipelinesNames); err != nil {
|
||||
logger.Error("invalid-json", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
teamName := r.FormValue(":team_name")
|
||||
team, found, err := s.teamFactory.FindTeam(teamName)
|
||||
if err != nil {
|
||||
logger.Error("failed-to-get-team", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !found {
|
||||
logger.Info("team-not-found")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
err = team.OrderPipelines(pipelinesNames)
|
||||
if err != nil {
|
||||
logger.Error("failed-to-order-pipelines", err, lager.Data{
|
||||
"pipeline_names": pipelinesNames,
|
||||
})
|
||||
var errNotFound db.ErrPipelineNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
var pipelinesNames []string
|
||||
if err := json.NewDecoder(r.Body).Decode(&pipelinesNames); err != nil {
|
||||
logger.Error("invalid-json", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintln(w, err.Error())
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := team.OrderPipelines(pipelinesNames)
|
||||
if err != nil {
|
||||
logger.Error("failed-to-order-pipelines", err, lager.Data{
|
||||
"pipeline_names": pipelinesNames,
|
||||
})
|
||||
var errNotFound db.ErrPipelineNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintln(w, err.Error())
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package pipelineserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.cloudfoundry.org/lager"
|
||||
"github.com/concourse/concourse/atc"
|
||||
"github.com/concourse/concourse/atc/db"
|
||||
)
|
||||
|
||||
func (s *Server) OrderPipelinesWithinGroup(team db.Team) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger := s.logger.Session("order-pipelines-within-group")
|
||||
|
||||
var instanceVars []atc.InstanceVars
|
||||
if err := json.NewDecoder(r.Body).Decode(&instanceVars); err != nil {
|
||||
logger.Error("invalid-json", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
groupName := r.FormValue(":pipeline_name")
|
||||
|
||||
err := team.OrderPipelinesWithinGroup(groupName, instanceVars)
|
||||
if err != nil {
|
||||
logger.Error("failed-to-order-pipelines", err, lager.Data{
|
||||
"team_name": team.Name(),
|
||||
"pipeline_name": groupName,
|
||||
"instance_vars": instanceVars,
|
||||
})
|
||||
var errNotFound db.ErrPipelineNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintln(w, err.Error())
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
}
|
|
@ -95,6 +95,7 @@ func (a *auditor) ValidateAction(action string) bool {
|
|||
atc.GetPipeline,
|
||||
atc.DeletePipeline,
|
||||
atc.OrderPipelines,
|
||||
atc.OrderPipelinesWithinGroup,
|
||||
atc.PausePipeline,
|
||||
atc.ArchivePipeline,
|
||||
atc.UnpausePipeline,
|
||||
|
|
|
@ -274,6 +274,18 @@ type FakeTeam struct {
|
|||
orderPipelinesReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
OrderPipelinesWithinGroupStub func(string, []atc.InstanceVars) error
|
||||
orderPipelinesWithinGroupMutex sync.RWMutex
|
||||
orderPipelinesWithinGroupArgsForCall []struct {
|
||||
arg1 string
|
||||
arg2 []atc.InstanceVars
|
||||
}
|
||||
orderPipelinesWithinGroupReturns struct {
|
||||
result1 error
|
||||
}
|
||||
orderPipelinesWithinGroupReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
PipelineStub func(atc.PipelineRef) (db.Pipeline, bool, error)
|
||||
pipelineMutex sync.RWMutex
|
||||
pipelineArgsForCall []struct {
|
||||
|
@ -1652,6 +1664,73 @@ func (fake *FakeTeam) OrderPipelinesReturnsOnCall(i int, result1 error) {
|
|||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderPipelinesWithinGroup(arg1 string, arg2 []atc.InstanceVars) error {
|
||||
var arg2Copy []atc.InstanceVars
|
||||
if arg2 != nil {
|
||||
arg2Copy = make([]atc.InstanceVars, len(arg2))
|
||||
copy(arg2Copy, arg2)
|
||||
}
|
||||
fake.orderPipelinesWithinGroupMutex.Lock()
|
||||
ret, specificReturn := fake.orderPipelinesWithinGroupReturnsOnCall[len(fake.orderPipelinesWithinGroupArgsForCall)]
|
||||
fake.orderPipelinesWithinGroupArgsForCall = append(fake.orderPipelinesWithinGroupArgsForCall, struct {
|
||||
arg1 string
|
||||
arg2 []atc.InstanceVars
|
||||
}{arg1, arg2Copy})
|
||||
stub := fake.OrderPipelinesWithinGroupStub
|
||||
fakeReturns := fake.orderPipelinesWithinGroupReturns
|
||||
fake.recordInvocation("OrderPipelinesWithinGroup", []interface{}{arg1, arg2Copy})
|
||||
fake.orderPipelinesWithinGroupMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderPipelinesWithinGroupCallCount() int {
|
||||
fake.orderPipelinesWithinGroupMutex.RLock()
|
||||
defer fake.orderPipelinesWithinGroupMutex.RUnlock()
|
||||
return len(fake.orderPipelinesWithinGroupArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderPipelinesWithinGroupCalls(stub func(string, []atc.InstanceVars) error) {
|
||||
fake.orderPipelinesWithinGroupMutex.Lock()
|
||||
defer fake.orderPipelinesWithinGroupMutex.Unlock()
|
||||
fake.OrderPipelinesWithinGroupStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderPipelinesWithinGroupArgsForCall(i int) (string, []atc.InstanceVars) {
|
||||
fake.orderPipelinesWithinGroupMutex.RLock()
|
||||
defer fake.orderPipelinesWithinGroupMutex.RUnlock()
|
||||
argsForCall := fake.orderPipelinesWithinGroupArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderPipelinesWithinGroupReturns(result1 error) {
|
||||
fake.orderPipelinesWithinGroupMutex.Lock()
|
||||
defer fake.orderPipelinesWithinGroupMutex.Unlock()
|
||||
fake.OrderPipelinesWithinGroupStub = nil
|
||||
fake.orderPipelinesWithinGroupReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderPipelinesWithinGroupReturnsOnCall(i int, result1 error) {
|
||||
fake.orderPipelinesWithinGroupMutex.Lock()
|
||||
defer fake.orderPipelinesWithinGroupMutex.Unlock()
|
||||
fake.OrderPipelinesWithinGroupStub = nil
|
||||
if fake.orderPipelinesWithinGroupReturnsOnCall == nil {
|
||||
fake.orderPipelinesWithinGroupReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.orderPipelinesWithinGroupReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) Pipeline(arg1 atc.PipelineRef) (db.Pipeline, bool, error) {
|
||||
fake.pipelineMutex.Lock()
|
||||
ret, specificReturn := fake.pipelineReturnsOnCall[len(fake.pipelineArgsForCall)]
|
||||
|
@ -2319,6 +2398,8 @@ func (fake *FakeTeam) Invocations() map[string][][]interface{} {
|
|||
defer fake.nameMutex.RUnlock()
|
||||
fake.orderPipelinesMutex.RLock()
|
||||
defer fake.orderPipelinesMutex.RUnlock()
|
||||
fake.orderPipelinesWithinGroupMutex.RLock()
|
||||
defer fake.orderPipelinesWithinGroupMutex.RUnlock()
|
||||
fake.pipelineMutex.RLock()
|
||||
defer fake.pipelineMutex.RUnlock()
|
||||
fake.pipelinesMutex.RLock()
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package migration_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Add secondary ordering column", func() {
|
||||
const preMigrationVersion = 1616768782
|
||||
const postMigrationVersion = 1618347807
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
)
|
||||
|
||||
Context("Up", func() {
|
||||
It("migrates populates secondary ordering", func() {
|
||||
db = postgresRunner.OpenDBAtVersion(preMigrationVersion)
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO teams(id, name) VALUES
|
||||
(1, 'team1'),
|
||||
(2, 'team2')
|
||||
`)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO pipelines(id, team_id, name, instance_vars) VALUES
|
||||
(1, 1, 'group1', '{"version": 1}'::jsonb),
|
||||
(2, 1, 'group1', '{"version": 2}'::jsonb),
|
||||
(3, 1, 'group2', '{"version": 1}'::jsonb),
|
||||
(4, 1, 'group1', NULL),
|
||||
(5, 1, 'pipeline', NULL),
|
||||
(6, 2, 'group1', '{"version": 3}'::jsonb)
|
||||
`)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
db.Close()
|
||||
|
||||
db = postgresRunner.OpenDBAtVersion(postMigrationVersion)
|
||||
|
||||
rows, err := db.Query(`SELECT id, secondary_ordering FROM pipelines ORDER BY id ASC`)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
type pipelineOrdering struct {
|
||||
pipelineID int
|
||||
secondaryOrdering int
|
||||
}
|
||||
|
||||
ordering := []pipelineOrdering{}
|
||||
for rows.Next() {
|
||||
var o pipelineOrdering
|
||||
|
||||
err := rows.Scan(&o.pipelineID, &o.secondaryOrdering)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
ordering = append(ordering, o)
|
||||
}
|
||||
|
||||
_ = db.Close()
|
||||
|
||||
Expect(ordering).To(Equal([]pipelineOrdering{
|
||||
{1, 1},
|
||||
{2, 2},
|
||||
{3, 1},
|
||||
{4, 3},
|
||||
{5, 1},
|
||||
{6, 1},
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE pipelines DROP COLUMN secondary_ordering;
|
|
@ -0,0 +1,12 @@
|
|||
ALTER TABLE pipelines ADD COLUMN secondary_ordering integer;
|
||||
|
||||
WITH s AS (
|
||||
SELECT id, row_number() OVER (PARTITION BY team_id, name ORDER BY id) as secondary_ordering
|
||||
FROM pipelines
|
||||
)
|
||||
UPDATE pipelines p
|
||||
SET secondary_ordering = s.secondary_ordering
|
||||
FROM s
|
||||
WHERE p.id = s.id;
|
||||
|
||||
ALTER TABLE pipelines ALTER COLUMN secondary_ordering SET NOT NULL;
|
|
@ -34,7 +34,7 @@ func (f *pipelineFactory) VisiblePipelines(teamNames []string) ([]Pipeline, erro
|
|||
|
||||
rows, err := pipelinesQuery.
|
||||
Where(sq.Eq{"t.name": teamNames}).
|
||||
OrderBy("t.name ASC", "p.ordering ASC", "p.id ASC").
|
||||
OrderBy("t.name ASC", "p.ordering ASC", "p.secondary_ordering ASC").
|
||||
RunWith(tx).
|
||||
Query()
|
||||
if err != nil {
|
||||
|
@ -71,7 +71,7 @@ func (f *pipelineFactory) VisiblePipelines(teamNames []string) ([]Pipeline, erro
|
|||
|
||||
func (f *pipelineFactory) AllPipelines() ([]Pipeline, error) {
|
||||
rows, err := pipelinesQuery.
|
||||
OrderBy("t.name ASC", "p.ordering ASC", "p.id ASC").
|
||||
OrderBy("t.name ASC", "p.ordering ASC", "p.secondary_ordering ASC").
|
||||
RunWith(f.conn).
|
||||
Query()
|
||||
if err != nil {
|
||||
|
|
|
@ -20,13 +20,14 @@ var _ = Describe("Pipeline Factory", func() {
|
|||
pipeline2 db.Pipeline
|
||||
pipeline3 db.Pipeline
|
||||
pipeline4 db.Pipeline
|
||||
team db.Team
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
err := defaultPipeline.Destroy()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
team, err := teamFactory.CreateTeam(atc.Team{Name: "some-team"})
|
||||
team, err = teamFactory.CreateTeam(atc.Team{Name: "some-team"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
pipeline1, _, err = team.SavePipeline(atc.PipelineRef{Name: "fake-pipeline"}, atc.Config{
|
||||
|
@ -66,36 +67,61 @@ var _ = Describe("Pipeline Factory", func() {
|
|||
It("returns all pipelines visible for the given teams", func() {
|
||||
pipelines, err := pipelineFactory.VisiblePipelines([]string{"some-team"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(pipelines)).To(Equal(3))
|
||||
Expect(pipelines[0].Name()).To(Equal(pipeline1.Name()))
|
||||
Expect(pipelines[1].Name()).To(Equal(pipeline4.Name()))
|
||||
Expect(pipelines[2].Name()).To(Equal(pipeline3.Name()))
|
||||
Expect(pipelineRefs(pipelines)).To(Equal([]atc.PipelineRef{
|
||||
pipelineRef(pipeline1),
|
||||
pipelineRef(pipeline4),
|
||||
pipelineRef(pipeline3),
|
||||
}))
|
||||
})
|
||||
|
||||
It("returns all pipelines visible when empty team name provided", func() {
|
||||
pipelines, err := pipelineFactory.VisiblePipelines([]string{""})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(pipelines)).To(Equal(1))
|
||||
Expect(pipelines[0].Name()).To(Equal(pipeline3.Name()))
|
||||
Expect(pipelineRefs(pipelines)).To(Equal([]atc.PipelineRef{
|
||||
pipelineRef(pipeline3),
|
||||
}))
|
||||
})
|
||||
|
||||
It("returns all pipelines visible when empty teams provided", func() {
|
||||
pipelines, err := pipelineFactory.VisiblePipelines([]string{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(pipelines)).To(Equal(1))
|
||||
Expect(pipelines[0].Name()).To(Equal(pipeline3.Name()))
|
||||
Expect(pipelineRefs(pipelines)).To(Equal([]atc.PipelineRef{
|
||||
pipelineRef(pipeline3),
|
||||
}))
|
||||
})
|
||||
|
||||
It("returns all pipelines visible when nil teams provided", func() {
|
||||
pipelines, err := pipelineFactory.VisiblePipelines(nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(pipelines)).To(Equal(1))
|
||||
Expect(pipelines[0].Name()).To(Equal(pipeline3.Name()))
|
||||
Expect(pipelineRefs(pipelines)).To(Equal([]atc.PipelineRef{
|
||||
pipelineRef(pipeline3),
|
||||
}))
|
||||
})
|
||||
|
||||
Describe("When instance pipeline ordered is change", func() {
|
||||
BeforeEach(func() {
|
||||
err := team.OrderPipelinesWithinGroup("fake-pipeline", []atc.InstanceVars{
|
||||
{"branch": "master"},
|
||||
{},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("Should keep the right order", func() {
|
||||
pipelines, err := pipelineFactory.VisiblePipelines([]string{"some-team"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pipelineRefs(pipelines)).To(Equal([]atc.PipelineRef{
|
||||
pipelineRef(pipeline4),
|
||||
pipelineRef(pipeline1),
|
||||
pipelineRef(pipeline3),
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("AllPipelines", func() {
|
||||
var (
|
||||
team db.Team
|
||||
pipeline1 db.Pipeline
|
||||
pipeline2 db.Pipeline
|
||||
pipeline3 db.Pipeline
|
||||
|
@ -106,7 +132,7 @@ var _ = Describe("Pipeline Factory", func() {
|
|||
err := defaultPipeline.Destroy()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
team, err := teamFactory.CreateTeam(atc.Team{Name: "some-team"})
|
||||
team, err = teamFactory.CreateTeam(atc.Team{Name: "some-team"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
pipeline2, _, err = team.SavePipeline(atc.PipelineRef{Name: "fake-pipeline-two"}, atc.Config{
|
||||
|
@ -145,14 +171,48 @@ var _ = Describe("Pipeline Factory", func() {
|
|||
|
||||
})
|
||||
|
||||
It("returns all pipelines ordered by team id -> ordering -> pipeline id", func() {
|
||||
It("returns all pipelines ordered by team id -> ordering -> secondary_ordering", func() {
|
||||
pipelines, err := pipelineFactory.AllPipelines()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(pipelines)).To(Equal(4))
|
||||
Expect(pipelines[0].Name()).To(Equal(pipeline1.Name()))
|
||||
Expect(pipelines[1].Name()).To(Equal(pipeline2.Name()))
|
||||
Expect(pipelines[2].Name()).To(Equal(pipeline4.Name()))
|
||||
Expect(pipelines[3].Name()).To(Equal(pipeline3.Name()))
|
||||
Expect(pipelineRefs(pipelines)).To(Equal([]atc.PipelineRef{
|
||||
pipelineRef(pipeline1),
|
||||
pipelineRef(pipeline2),
|
||||
pipelineRef(pipeline4),
|
||||
pipelineRef(pipeline3),
|
||||
}))
|
||||
})
|
||||
|
||||
Describe("When instance pipeline ordered is change", func() {
|
||||
BeforeEach(func() {
|
||||
err := team.OrderPipelinesWithinGroup("fake-pipeline-two", []atc.InstanceVars{
|
||||
{"branch": "master"},
|
||||
{},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("Should keep the right order", func() {
|
||||
pipelines, err := pipelineFactory.AllPipelines()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pipelineRefs(pipelines)).To(Equal([]atc.PipelineRef{
|
||||
pipelineRef(pipeline1),
|
||||
pipelineRef(pipeline4),
|
||||
pipelineRef(pipeline2),
|
||||
pipelineRef(pipeline3),
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func pipelineRef(pipeline db.Pipeline) atc.PipelineRef {
|
||||
return atc.PipelineRef{Name: pipeline.Name(), InstanceVars: pipeline.InstanceVars()}
|
||||
}
|
||||
|
||||
func pipelineRefs(pipelines []db.Pipeline) []atc.PipelineRef {
|
||||
refs := make([]atc.PipelineRef, len(pipelines))
|
||||
for i, p := range pipelines {
|
||||
refs[i] = pipelineRef(p)
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
|
|
@ -21,12 +21,10 @@ import (
|
|||
|
||||
var ErrConfigComparisonFailed = errors.New("comparison with existing config failed during save")
|
||||
|
||||
type ErrPipelineNotFound struct {
|
||||
Name string
|
||||
}
|
||||
type ErrPipelineNotFound atc.PipelineRef
|
||||
|
||||
func (e ErrPipelineNotFound) Error() string {
|
||||
return fmt.Sprintf("pipeline '%s' not found", e.Name)
|
||||
return fmt.Sprintf("pipeline '%s' not found", atc.PipelineRef(e))
|
||||
}
|
||||
|
||||
//counterfeiter:generate . Team
|
||||
|
@ -52,6 +50,7 @@ type Team interface {
|
|||
Pipelines() ([]Pipeline, error)
|
||||
PublicPipelines() ([]Pipeline, error)
|
||||
OrderPipelines([]string) error
|
||||
OrderPipelinesWithinGroup(string, []atc.InstanceVars) error
|
||||
|
||||
CreateOneOffBuild() (Build, error)
|
||||
CreateStartedBuild(plan atc.Plan) (Build, error)
|
||||
|
@ -430,7 +429,8 @@ func savePipeline(
|
|||
"instance_vars": instanceVars,
|
||||
}
|
||||
var ordering sql.NullInt64
|
||||
err := psql.Select("max(ordering)").
|
||||
var secondaryOrdering sql.NullInt64
|
||||
err := psql.Select("max(ordering), max(secondary_ordering)").
|
||||
From("pipelines").
|
||||
Where(sq.Eq{
|
||||
"team_id": teamID,
|
||||
|
@ -438,14 +438,16 @@ func savePipeline(
|
|||
}).
|
||||
RunWith(tx).
|
||||
QueryRow().
|
||||
Scan(&ordering)
|
||||
Scan(&ordering, &secondaryOrdering)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if ordering.Valid {
|
||||
values["ordering"] = ordering.Int64
|
||||
values["secondary_ordering"] = secondaryOrdering.Int64 + 1
|
||||
} else {
|
||||
values["ordering"] = sq.Expr("currval('pipelines_id_seq')")
|
||||
values["secondary_ordering"] = 1
|
||||
}
|
||||
err = psql.Insert("pipelines").
|
||||
SetMap(values).
|
||||
|
@ -660,7 +662,7 @@ func (t *team) Pipelines() ([]Pipeline, error) {
|
|||
Where(sq.Eq{
|
||||
"team_id": t.id,
|
||||
}).
|
||||
OrderBy("p.ordering", "p.id").
|
||||
OrderBy("p.ordering", "p.secondary_ordering").
|
||||
RunWith(t.conn).
|
||||
Query()
|
||||
if err != nil {
|
||||
|
@ -681,7 +683,7 @@ func (t *team) PublicPipelines() ([]Pipeline, error) {
|
|||
"team_id": t.id,
|
||||
"public": true,
|
||||
}).
|
||||
OrderBy("t.name ASC", "ordering ASC").
|
||||
OrderBy("p.ordering ASC", "p.secondary_ordering ASC").
|
||||
RunWith(t.conn).
|
||||
Query()
|
||||
if err != nil {
|
||||
|
@ -731,6 +733,54 @@ func (t *team) OrderPipelines(names []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (t *team) OrderPipelinesWithinGroup(groupName string, instanceVars []atc.InstanceVars) error {
|
||||
|
||||
tx, err := t.conn.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer Rollback(tx)
|
||||
|
||||
for i, vars := range instanceVars {
|
||||
filter := sq.Eq{
|
||||
"team_id": t.id,
|
||||
"name": groupName,
|
||||
}
|
||||
|
||||
if len(vars) == 0 {
|
||||
filter["instance_vars"] = nil
|
||||
} else {
|
||||
varsJson, err := json.Marshal(vars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filter["instance_vars"] = varsJson
|
||||
}
|
||||
|
||||
pipelineUpdate, err := psql.Update("pipelines").
|
||||
Set("secondary_ordering", i+1).
|
||||
Where(filter).
|
||||
RunWith(tx).
|
||||
Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updatedPipelines, err := pipelineUpdate.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if updatedPipelines == 0 {
|
||||
return ErrPipelineNotFound{Name: groupName, InstanceVars: vars}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// XXX: This is only begin used by tests, replace all tests to CreateBuild on a job
|
||||
func (t *team) CreateOneOffBuild() (Build, error) {
|
||||
tx, err := t.conn.Begin()
|
||||
|
|
|
@ -1107,7 +1107,7 @@ var _ = Describe("Team", func() {
|
|||
Context("when pipeline does not exist", func() {
|
||||
It("returns not found error", func() {
|
||||
err := otherTeam.OrderPipelines([]string{"pipeline1", "invalid-pipeline"})
|
||||
Expect(err).To(MatchError(db.ErrPipelineNotFound{"invalid-pipeline"}))
|
||||
Expect(err).To(MatchError(db.ErrPipelineNotFound{Name: "invalid-pipeline"}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -53,20 +53,21 @@ const (
|
|||
|
||||
GetCC = "GetCC"
|
||||
|
||||
ListAllPipelines = "ListAllPipelines"
|
||||
ListPipelines = "ListPipelines"
|
||||
GetPipeline = "GetPipeline"
|
||||
DeletePipeline = "DeletePipeline"
|
||||
OrderPipelines = "OrderPipelines"
|
||||
PausePipeline = "PausePipeline"
|
||||
ArchivePipeline = "ArchivePipeline"
|
||||
UnpausePipeline = "UnpausePipeline"
|
||||
ExposePipeline = "ExposePipeline"
|
||||
HidePipeline = "HidePipeline"
|
||||
RenamePipeline = "RenamePipeline"
|
||||
ListPipelineBuilds = "ListPipelineBuilds"
|
||||
CreatePipelineBuild = "CreatePipelineBuild"
|
||||
PipelineBadge = "PipelineBadge"
|
||||
ListAllPipelines = "ListAllPipelines"
|
||||
ListPipelines = "ListPipelines"
|
||||
GetPipeline = "GetPipeline"
|
||||
DeletePipeline = "DeletePipeline"
|
||||
OrderPipelines = "OrderPipelines"
|
||||
OrderPipelinesWithinGroup = "OrderPipelinesWithinGroup"
|
||||
PausePipeline = "PausePipeline"
|
||||
ArchivePipeline = "ArchivePipeline"
|
||||
UnpausePipeline = "UnpausePipeline"
|
||||
ExposePipeline = "ExposePipeline"
|
||||
HidePipeline = "HidePipeline"
|
||||
RenamePipeline = "RenamePipeline"
|
||||
ListPipelineBuilds = "ListPipelineBuilds"
|
||||
CreatePipelineBuild = "CreatePipelineBuild"
|
||||
PipelineBadge = "PipelineBadge"
|
||||
|
||||
RegisterWorker = "RegisterWorker"
|
||||
LandWorker = "LandWorker"
|
||||
|
@ -153,6 +154,7 @@ var Routes = rata.Routes([]rata.Route{
|
|||
{Path: "/api/v1/teams/:team_name/pipelines/:pipeline_name", Method: "GET", Name: GetPipeline},
|
||||
{Path: "/api/v1/teams/:team_name/pipelines/:pipeline_name", Method: "DELETE", Name: DeletePipeline},
|
||||
{Path: "/api/v1/teams/:team_name/pipelines/ordering", Method: "PUT", Name: OrderPipelines},
|
||||
{Path: "/api/v1/teams/:team_name/pipelines/:pipeline_name/ordering", Method: "PUT", Name: OrderPipelinesWithinGroup},
|
||||
{Path: "/api/v1/teams/:team_name/pipelines/:pipeline_name/pause", Method: "PUT", Name: PausePipeline},
|
||||
{Path: "/api/v1/teams/:team_name/pipelines/:pipeline_name/archive", Method: "PUT", Name: ArchivePipeline},
|
||||
{Path: "/api/v1/teams/:team_name/pipelines/:pipeline_name/unpause", Method: "PUT", Name: UnpausePipeline},
|
||||
|
|
|
@ -141,6 +141,7 @@ func (wrappa *APIAuthWrappa) Wrap(handlers rata.Handlers) rata.Handlers {
|
|||
atc.GetVersionsDB,
|
||||
atc.ListJobInputs,
|
||||
atc.OrderPipelines,
|
||||
atc.OrderPipelinesWithinGroup,
|
||||
atc.PauseJob,
|
||||
atc.PausePipeline,
|
||||
atc.RenamePipeline,
|
||||
|
|
|
@ -109,6 +109,7 @@ func (rw *RejectArchivedWrappa) Wrap(handlers rata.Handlers) rata.Handlers {
|
|||
atc.GetVersionsDB,
|
||||
atc.ListJobInputs,
|
||||
atc.OrderPipelines,
|
||||
atc.OrderPipelinesWithinGroup,
|
||||
atc.PauseJob,
|
||||
atc.ArchivePipeline,
|
||||
atc.RenamePipeline,
|
||||
|
|
|
@ -45,19 +45,20 @@ type FlyCommand struct {
|
|||
UnpauseJob UnpauseJobCommand `command:"unpause-job" alias:"uj" description:"Unpause a job"`
|
||||
ScheduleJob ScheduleJobCommand `command:"schedule-job" alias:"sj" description:"Request the scheduler to run for a job. Introduced as a recovery command for the v6.0 scheduler."`
|
||||
|
||||
Pipelines PipelinesCommand `command:"pipelines" alias:"ps" description:"List the configured pipelines"`
|
||||
DestroyPipeline DestroyPipelineCommand `command:"destroy-pipeline" alias:"dp" description:"Destroy a pipeline"`
|
||||
GetPipeline GetPipelineCommand `command:"get-pipeline" alias:"gp" description:"Get a pipeline's current configuration"`
|
||||
SetPipeline SetPipelineCommand `command:"set-pipeline" alias:"sp" description:"Create or update a pipeline's configuration"`
|
||||
PausePipeline PausePipelineCommand `command:"pause-pipeline" alias:"pp" description:"Pause a pipeline"`
|
||||
ArchivePipeline ArchivePipelineCommand `command:"archive-pipeline" alias:"ap" description:"Archive a pipeline"`
|
||||
UnpausePipeline UnpausePipelineCommand `command:"unpause-pipeline" alias:"up" description:"Un-pause a pipeline"`
|
||||
ExposePipeline ExposePipelineCommand `command:"expose-pipeline" alias:"ep" description:"Make a pipeline publicly viewable"`
|
||||
HidePipeline HidePipelineCommand `command:"hide-pipeline" alias:"hp" description:"Hide a pipeline from the public"`
|
||||
RenamePipeline RenamePipelineCommand `command:"rename-pipeline" alias:"rp" description:"Rename a pipeline"`
|
||||
ValidatePipeline ValidatePipelineCommand `command:"validate-pipeline" alias:"vp" description:"Validate a pipeline config"`
|
||||
FormatPipeline FormatPipelineCommand `command:"format-pipeline" alias:"fp" description:"Format a pipeline config"`
|
||||
OrderPipelines OrderPipelinesCommand `command:"order-pipelines" alias:"op" description:"Orders pipelines"`
|
||||
Pipelines PipelinesCommand `command:"pipelines" alias:"ps" description:"List the configured pipelines"`
|
||||
DestroyPipeline DestroyPipelineCommand `command:"destroy-pipeline" alias:"dp" description:"Destroy a pipeline"`
|
||||
GetPipeline GetPipelineCommand `command:"get-pipeline" alias:"gp" description:"Get a pipeline's current configuration"`
|
||||
SetPipeline SetPipelineCommand `command:"set-pipeline" alias:"sp" description:"Create or update a pipeline's configuration"`
|
||||
PausePipeline PausePipelineCommand `command:"pause-pipeline" alias:"pp" description:"Pause a pipeline"`
|
||||
ArchivePipeline ArchivePipelineCommand `command:"archive-pipeline" alias:"ap" description:"Archive a pipeline"`
|
||||
UnpausePipeline UnpausePipelineCommand `command:"unpause-pipeline" alias:"up" description:"Un-pause a pipeline"`
|
||||
ExposePipeline ExposePipelineCommand `command:"expose-pipeline" alias:"ep" description:"Make a pipeline publicly viewable"`
|
||||
HidePipeline HidePipelineCommand `command:"hide-pipeline" alias:"hp" description:"Hide a pipeline from the public"`
|
||||
RenamePipeline RenamePipelineCommand `command:"rename-pipeline" alias:"rp" description:"Rename a pipeline"`
|
||||
ValidatePipeline ValidatePipelineCommand `command:"validate-pipeline" alias:"vp" description:"Validate a pipeline config"`
|
||||
FormatPipeline FormatPipelineCommand `command:"format-pipeline" alias:"fp" description:"Format a pipeline config"`
|
||||
OrderPipelines OrderPipelinesCommand `command:"order-pipelines" alias:"op" description:"Orders pipelines"`
|
||||
OrderPipelinesWithinGroup OrderInstancedPipelinesCommand `command:"order-instanced-pipelines" alias:"oip" description:"Orders instanced pipelines within an instance group"`
|
||||
|
||||
Resources ResourcesCommand `command:"resources" alias:"rs" description:"List the resources in the pipeline"`
|
||||
ResourceVersions ResourceVersionsCommand `command:"resource-versions" alias:"rvs" description:"List the versions of a resource"`
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package flaghelpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/concourse/concourse/atc"
|
||||
"github.com/concourse/concourse/vars"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type InstanceVarsFlag struct {
|
||||
InstanceVars atc.InstanceVars
|
||||
}
|
||||
|
||||
func (flag *InstanceVarsFlag) UnmarshalFlag(value string) error {
|
||||
var err error
|
||||
flag.InstanceVars, err = unmarshalInstanceVars(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalInstanceVars(s string) (atc.InstanceVars, error) {
|
||||
var kvPairs vars.KVPairs
|
||||
for {
|
||||
colonIndex, ok := findUnquoted(s, `"`, nextOccurrenceOf(':'))
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
rawKey := s[:colonIndex]
|
||||
var kvPair vars.KVPair
|
||||
var err error
|
||||
kvPair.Ref, err = vars.ParseReference(rawKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s = s[colonIndex+1:]
|
||||
rawValue := []byte(s)
|
||||
commaIndex, hasComma := findUnquoted(s, `"'`, nextOccurrenceOfOutsideOfYAML(','))
|
||||
if hasComma {
|
||||
rawValue = rawValue[:commaIndex]
|
||||
s = s[commaIndex+1:]
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(rawValue, &kvPair.Value, useNumber); err != nil {
|
||||
return nil, fmt.Errorf("invalid value for key '%s': %w", rawKey, err)
|
||||
}
|
||||
kvPairs = append(kvPairs, kvPair)
|
||||
|
||||
if !hasComma {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(kvPairs) == 0 {
|
||||
return nil, errors.New("instance vars should be formatted as <key1:value1>(,<key2:value2>)")
|
||||
}
|
||||
|
||||
return atc.InstanceVars(kvPairs.Expand()), nil
|
||||
}
|
||||
|
||||
func findUnquoted(s string, quoteset string, stop func(c rune) bool) (int, bool) {
|
||||
var quoteChar rune
|
||||
for i, c := range s {
|
||||
if quoteChar == 0 {
|
||||
if stop(c) {
|
||||
return i, true
|
||||
}
|
||||
if strings.ContainsRune(quoteset, c) {
|
||||
quoteChar = c
|
||||
}
|
||||
} else if c == quoteChar {
|
||||
quoteChar = 0
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func nextOccurrenceOf(r rune) func(rune) bool {
|
||||
return func(c rune) bool {
|
||||
return c == r
|
||||
}
|
||||
}
|
||||
|
||||
func nextOccurrenceOfOutsideOfYAML(r rune) func(rune) bool {
|
||||
braceCount := 0
|
||||
bracketCount := 0
|
||||
return func(c rune) bool {
|
||||
switch c {
|
||||
case r:
|
||||
if braceCount == 0 && bracketCount == 0 {
|
||||
return true
|
||||
}
|
||||
case '{':
|
||||
braceCount++
|
||||
case '}':
|
||||
braceCount--
|
||||
case '[':
|
||||
bracketCount++
|
||||
case ']':
|
||||
bracketCount--
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package flaghelpers_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/concourse/concourse/atc"
|
||||
"github.com/concourse/concourse/fly/commands/internal/flaghelpers"
|
||||
)
|
||||
|
||||
var _ = Describe("InstanceVarsFlag", func() {
|
||||
Describe("UnmarshalFlag", func() {
|
||||
|
||||
var instanceVarsFlag *flaghelpers.InstanceVarsFlag
|
||||
|
||||
BeforeEach(func() {
|
||||
instanceVarsFlag = &flaghelpers.InstanceVarsFlag{}
|
||||
})
|
||||
|
||||
for _, tt := range []struct {
|
||||
desc string
|
||||
flag string
|
||||
instanceVars atc.InstanceVars
|
||||
err string
|
||||
}{
|
||||
{
|
||||
desc: "instance var",
|
||||
flag: "branch:master",
|
||||
instanceVars: atc.InstanceVars{"branch": "master"},
|
||||
},
|
||||
{
|
||||
desc: "multiple instance vars",
|
||||
flag: `branch:master,list:[1, "2"],other:{foo:bar: 123}`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"branch": "master",
|
||||
"list": []interface{}{json.Number("1"), "2"},
|
||||
"other": map[string]interface{}{"foo:bar": json.Number("123")},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "quoted yaml brackets/braces with \"",
|
||||
flag: `field1:{a: "{", b: "["},field2:hello`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field1": map[string]interface{}{
|
||||
"a": "{",
|
||||
"b": "[",
|
||||
},
|
||||
"field2": "hello",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "quoted yaml brackets/braces with '",
|
||||
flag: `field1:{a: '{', b: '['},field2:hello`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field1": map[string]interface{}{
|
||||
"a": "{",
|
||||
"b": "[",
|
||||
},
|
||||
"field2": "hello",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "empty values",
|
||||
flag: `field1:,field2:"",field3:null`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field1": nil,
|
||||
"field2": "",
|
||||
"field3": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "yaml list",
|
||||
flag: `field:[{a: '{', b: '['}, 1]`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field": []interface{}{
|
||||
map[string]interface{}{
|
||||
"a": "{",
|
||||
"b": "[",
|
||||
},
|
||||
json.Number("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "indexing by numerical field still uses map",
|
||||
flag: `field.0:0,field.1:1`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field": map[string]interface{}{
|
||||
"0": json.Number("0"),
|
||||
"1": json.Number("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "whitespace trimmed from path/values",
|
||||
flag: `branch: master, other: 123`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"branch": "master",
|
||||
"other": json.Number("123"),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "quoted fields can contain special characters",
|
||||
flag: `"some.field:here":abc`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"some.field:here": "abc",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "special characters in quoted yaml",
|
||||
flag: `field1:'foo,bar',field2:"value1:value2"`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field1": "foo,bar",
|
||||
"field2": "value1:value2",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "supports dot notation",
|
||||
flag: `"my.field".subkey1."subkey:2":"my-value","my.field".other:'other-value'`,
|
||||
instanceVars: atc.InstanceVars{
|
||||
"my.field": map[string]interface{}{
|
||||
"subkey1": map[string]interface{}{
|
||||
"subkey:2": "my-value",
|
||||
},
|
||||
"other": "other-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "errors if invalid ref is passed",
|
||||
flag: `"my.field".:bad`,
|
||||
err: `invalid var '"my.field".': empty field`,
|
||||
},
|
||||
{
|
||||
desc: "errors if invalid YAML is passed as the value",
|
||||
flag: `hello:{bad: yaml`,
|
||||
err: `invalid value for key 'hello': error converting YAML to JSON: yaml: line 1: did not find expected ',' or '}'`,
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
It(tt.desc, func() {
|
||||
err := instanceVarsFlag.UnmarshalFlag(tt.flag)
|
||||
if tt.err == "" {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(instanceVarsFlag.InstanceVars).To(Equal(tt.instanceVars))
|
||||
} else {
|
||||
Expect(err).To(MatchError(tt.err))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
|
@ -39,7 +39,7 @@ func (flag *JobFlag) UnmarshalFlag(value string) error {
|
|||
var err error
|
||||
flag.PipelineRef.InstanceVars, err = unmarshalInstanceVars(vs[1])
|
||||
if err != nil {
|
||||
return errors.New(err.Error() + "/<job>")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ var _ = Describe("JobFlag", func() {
|
|||
{
|
||||
desc: "malformed instance var",
|
||||
flag: "some-pipeline/branch=master/some-job",
|
||||
err: "argument format should be <pipeline>/<key:value>/<job>",
|
||||
err: "instance vars should be formatted as <key1:value1>(,<key2:value2>)",
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
|
|
|
@ -2,12 +2,9 @@ package flaghelpers
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/concourse/concourse/vars"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/concourse/concourse/atc"
|
||||
"github.com/concourse/concourse/fly/rc"
|
||||
|
@ -62,90 +59,6 @@ func (flag *PipelineFlag) UnmarshalFlag(value string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func unmarshalInstanceVars(s string) (atc.InstanceVars, error) {
|
||||
var kvPairs vars.KVPairs
|
||||
for {
|
||||
colonIndex, ok := findUnquoted(s, `"`, nextOccurrenceOf(':'))
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
rawKey := s[:colonIndex]
|
||||
var kvPair vars.KVPair
|
||||
var err error
|
||||
kvPair.Ref, err = vars.ParseReference(rawKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s = s[colonIndex+1:]
|
||||
rawValue := []byte(s)
|
||||
commaIndex, hasComma := findUnquoted(s, `"'`, nextOccurrenceOfOutsideOfYAML(','))
|
||||
if hasComma {
|
||||
rawValue = rawValue[:commaIndex]
|
||||
s = s[commaIndex+1:]
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(rawValue, &kvPair.Value, useNumber); err != nil {
|
||||
return nil, fmt.Errorf("invalid value for key '%s': %w", rawKey, err)
|
||||
}
|
||||
kvPairs = append(kvPairs, kvPair)
|
||||
|
||||
if !hasComma {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(kvPairs) == 0 {
|
||||
return nil, errors.New("argument format should be <pipeline>/<key:value>")
|
||||
}
|
||||
|
||||
return atc.InstanceVars(kvPairs.Expand()), nil
|
||||
}
|
||||
|
||||
func findUnquoted(s string, quoteset string, stop func(c rune) bool) (int, bool) {
|
||||
var quoteChar rune
|
||||
for i, c := range s {
|
||||
if quoteChar == 0 {
|
||||
if stop(c) {
|
||||
return i, true
|
||||
}
|
||||
if strings.ContainsRune(quoteset, c) {
|
||||
quoteChar = c
|
||||
}
|
||||
} else if c == quoteChar {
|
||||
quoteChar = 0
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func nextOccurrenceOf(r rune) func(rune) bool {
|
||||
return func(c rune) bool {
|
||||
return c == r
|
||||
}
|
||||
}
|
||||
|
||||
func nextOccurrenceOfOutsideOfYAML(r rune) func(rune) bool {
|
||||
braceCount := 0
|
||||
bracketCount := 0
|
||||
return func(c rune) bool {
|
||||
switch c {
|
||||
case r:
|
||||
if braceCount == 0 && bracketCount == 0 {
|
||||
return true
|
||||
}
|
||||
case '{':
|
||||
braceCount++
|
||||
case '}':
|
||||
braceCount--
|
||||
case '[':
|
||||
bracketCount++
|
||||
case ']':
|
||||
bracketCount--
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (flag *PipelineFlag) Complete(match string) []flags.Completion {
|
||||
fly := parseFlags()
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package flaghelpers_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
|
@ -24,7 +22,6 @@ var _ = Describe("PipelineFlag", func() {
|
|||
flag string
|
||||
name string
|
||||
instanceVars atc.InstanceVars
|
||||
err string
|
||||
}{
|
||||
{
|
||||
desc: "name",
|
||||
|
@ -37,135 +34,13 @@ var _ = Describe("PipelineFlag", func() {
|
|||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{"branch": "master"},
|
||||
},
|
||||
{
|
||||
desc: "multiple instance vars",
|
||||
flag: `some-pipeline/branch:master,list:[1, "2"],other:{foo:bar: 123}`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"branch": "master",
|
||||
"list": []interface{}{json.Number("1"), "2"},
|
||||
"other": map[string]interface{}{"foo:bar": json.Number("123")},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "quoted yaml brackets/braces with \"",
|
||||
flag: `some-pipeline/field1:{a: "{", b: "["},field2:hello`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field1": map[string]interface{}{
|
||||
"a": "{",
|
||||
"b": "[",
|
||||
},
|
||||
"field2": "hello",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "quoted yaml brackets/braces with '",
|
||||
flag: `some-pipeline/field1:{a: '{', b: '['},field2:hello`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field1": map[string]interface{}{
|
||||
"a": "{",
|
||||
"b": "[",
|
||||
},
|
||||
"field2": "hello",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "empty values",
|
||||
flag: `some-pipeline/field1:,field2:"",field3:null`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field1": nil,
|
||||
"field2": "",
|
||||
"field3": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "yaml list",
|
||||
flag: `some-pipeline/field:[{a: '{', b: '['}, 1]`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field": []interface{}{
|
||||
map[string]interface{}{
|
||||
"a": "{",
|
||||
"b": "[",
|
||||
},
|
||||
json.Number("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "indexing by numerical field still uses map",
|
||||
flag: `some-pipeline/field.0:0,field.1:1`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field": map[string]interface{}{
|
||||
"0": json.Number("0"),
|
||||
"1": json.Number("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "whitespace trimmed from path/values",
|
||||
flag: `some-pipeline/branch: master, other: 123`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"branch": "master",
|
||||
"other": json.Number("123"),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "quoted fields can contain special characters",
|
||||
flag: `some-pipeline/"some.field:here":abc`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"some.field:here": "abc",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "special characters in quoted yaml",
|
||||
flag: `some-pipeline/field1:'foo,bar',field2:"value1:value2"`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"field1": "foo,bar",
|
||||
"field2": "value1:value2",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "supports dot notation",
|
||||
flag: `some-pipeline/"my.field".subkey1."subkey:2":"my-value","my.field".other:'other-value'`,
|
||||
name: "some-pipeline",
|
||||
instanceVars: atc.InstanceVars{
|
||||
"my.field": map[string]interface{}{
|
||||
"subkey1": map[string]interface{}{
|
||||
"subkey:2": "my-value",
|
||||
},
|
||||
"other": "other-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "errors if invalid ref is passed",
|
||||
flag: `some-pipeline/"my.field".:bad`,
|
||||
err: `invalid var '"my.field".': empty field`,
|
||||
},
|
||||
{
|
||||
desc: "errors if invalid YAML is passed as the value",
|
||||
flag: `some-pipeline/hello:{bad: yaml`,
|
||||
err: `invalid value for key 'hello': error converting YAML to JSON: yaml: line 1: did not find expected ',' or '}'`,
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
It(tt.desc, func() {
|
||||
err := pipelineFlag.UnmarshalFlag(tt.flag)
|
||||
if tt.err == "" {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pipelineFlag.Name).To(Equal(tt.name))
|
||||
Expect(pipelineFlag.InstanceVars).To(Equal(tt.instanceVars))
|
||||
} else {
|
||||
Expect(err).To(MatchError(tt.err))
|
||||
}
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pipelineFlag.Name).To(Equal(tt.name))
|
||||
Expect(pipelineFlag.InstanceVars).To(Equal(tt.instanceVars))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -36,7 +36,7 @@ func (flag *ResourceFlag) UnmarshalFlag(value string) error {
|
|||
var err error
|
||||
flag.PipelineRef.InstanceVars, err = unmarshalInstanceVars(vs[1])
|
||||
if err != nil {
|
||||
return errors.New(err.Error() + "/<resource>")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ var _ = Describe("ResourceFlag", func() {
|
|||
{
|
||||
desc: "malformed instance var",
|
||||
flag: "some-pipeline/branch=master/some-resource",
|
||||
err: "argument format should be <pipeline>/<key:value>/<resource>",
|
||||
err: "instance vars should be formatted as <key1:value1>(,<key2:value2>)",
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/concourse/concourse/atc"
|
||||
"github.com/concourse/concourse/go-concourse/concourse"
|
||||
|
||||
"github.com/concourse/concourse/fly/commands/internal/displayhelpers"
|
||||
"github.com/concourse/concourse/fly/commands/internal/flaghelpers"
|
||||
"github.com/concourse/concourse/fly/rc"
|
||||
)
|
||||
|
||||
type OrderInstancedPipelinesCommand struct {
|
||||
Group string `short:"g" long:"group" required:"true" description:"Name of the instance group"`
|
||||
InstanceVars []flaghelpers.InstanceVarsFlag `short:"p" long:"pipeline" required:"true" description:"Instance vars identifying pipeline (can be specified multiple times to provide relative ordering)"`
|
||||
Team string `long:"team" description:"Name of the team to which the pipelines belong, if different from the target default"`
|
||||
}
|
||||
|
||||
func (command *OrderInstancedPipelinesCommand) Execute(args []string) error {
|
||||
target, err := rc.LoadTarget(Fly.Target, Fly.Verbose)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = target.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var team concourse.Team
|
||||
if command.Team != "" {
|
||||
team, err = target.FindTeam(command.Team)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
team = target.Team()
|
||||
}
|
||||
|
||||
var instanceVars []atc.InstanceVars
|
||||
|
||||
for _, instanceVar := range command.InstanceVars {
|
||||
instanceVars = append(instanceVars, instanceVar.InstanceVars)
|
||||
}
|
||||
|
||||
err = team.OrderingPipelinesWithinGroup(command.Group, instanceVars)
|
||||
if err != nil {
|
||||
displayhelpers.FailWithErrorf("failed to order instanced pipelines", err)
|
||||
}
|
||||
|
||||
fmt.Printf("ordered instanced pipelines \n")
|
||||
for _, iv := range instanceVars {
|
||||
fmt.Printf(" - %s \n", iv)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -3,19 +3,19 @@ package commands
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/concourse/concourse/go-concourse/concourse"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/concourse/concourse/fly/commands/internal/displayhelpers"
|
||||
"github.com/concourse/concourse/fly/rc"
|
||||
"github.com/concourse/concourse/go-concourse/concourse"
|
||||
)
|
||||
|
||||
var ErrMissingPipelineName = errors.New("Need to specify at least one pipeline name")
|
||||
|
||||
type OrderPipelinesCommand struct {
|
||||
Alphabetical bool `short:"a" long:"alphabetical" description:"Order all pipelines alphabetically"`
|
||||
Pipelines []string `short:"p" long:"pipeline" description:"Name of pipelines to order"`
|
||||
Pipelines []string `short:"p" long:"pipeline" description:"Name of pipeline (can be specified multiple times to provide relative ordering)"`
|
||||
Team string `long:"team" description:"Name of the team to which the pipelines belong, if different from the target default"`
|
||||
}
|
||||
|
||||
|
|
|
@ -1054,7 +1054,7 @@ run:
|
|||
sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Eventually(sess.Err).Should(gbytes.Say("argument format should be <pipeline>/<key:value>/<job>"))
|
||||
Eventually(sess.Err).Should(gbytes.Say("instance vars should be formatted as <key1:value1>\\(,<key2:value2>\\)"))
|
||||
|
||||
<-sess.Exited
|
||||
Expect(sess.ExitCode()).To(Equal(1))
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
package integration_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/concourse/concourse/atc"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
"github.com/onsi/gomega/ghttp"
|
||||
"github.com/tedsuo/rata"
|
||||
)
|
||||
|
||||
var _ = Describe("Fly CLI", func() {
|
||||
Describe("order-instanced-pipelines", func() {
|
||||
Context("when pipelines are specified", func() {
|
||||
var (
|
||||
path string
|
||||
err error
|
||||
)
|
||||
BeforeEach(func() {
|
||||
path, err = atc.Routes.CreatePathForRoute(atc.OrderPipelinesWithinGroup, rata.Params{"team_name": "main", "pipeline_name": "awesome-pipeline"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("when the pipeline exists", func() {
|
||||
var instanceVars []atc.InstanceVars
|
||||
|
||||
BeforeEach(func() {
|
||||
instanceVars = []atc.InstanceVars{
|
||||
{"branch": "main"},
|
||||
{"branch": "test"},
|
||||
}
|
||||
})
|
||||
|
||||
JustBeforeEach(func() {
|
||||
atcServer.AppendHandlers(
|
||||
ghttp.CombineHandlers(
|
||||
ghttp.VerifyJSONRepresenting(instanceVars),
|
||||
ghttp.VerifyRequest("PUT", path),
|
||||
ghttp.RespondWith(http.StatusOK, nil),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
It("orders the instance pipelines", func() {
|
||||
Expect(func() {
|
||||
flyCmd := exec.Command(flyPath, "-t", targetName, "order-instanced-pipelines", "-g", "awesome-pipeline", "-p", "branch:main", "-p", "branch:test")
|
||||
|
||||
sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
<-sess.Exited
|
||||
Expect(sess.ExitCode()).To(Equal(0))
|
||||
Eventually(sess).Should(gbytes.Say(`ordered instanced pipelines`))
|
||||
Eventually(sess).Should(gbytes.Say(` - branch:main`))
|
||||
Eventually(sess).Should(gbytes.Say(` - branch:test`))
|
||||
|
||||
}).To(Change(func() int {
|
||||
return len(atcServer.ReceivedRequests())
|
||||
}).By(2))
|
||||
})
|
||||
|
||||
It("orders the instance pipeline with alias", func() {
|
||||
Expect(func() {
|
||||
flyCmd := exec.Command(flyPath, "-t", targetName, "oip", "-g", "awesome-pipeline", "-p", "branch:main", "-p", "branch:test")
|
||||
|
||||
sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
<-sess.Exited
|
||||
Expect(sess.ExitCode()).To(Equal(0))
|
||||
Eventually(sess).Should(gbytes.Say(`ordered instanced pipelines`))
|
||||
Eventually(sess).Should(gbytes.Say(` - branch:main`))
|
||||
Eventually(sess).Should(gbytes.Say(` - branch:test`))
|
||||
|
||||
}).To(Change(func() int {
|
||||
return len(atcServer.ReceivedRequests())
|
||||
}).By(2))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when ordering fails", func() {
|
||||
BeforeEach(func() {
|
||||
atcServer.AppendHandlers(
|
||||
ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("PUT", path),
|
||||
ghttp.RespondWith(http.StatusBadRequest, "pipeline 'awesome-pipeline/branch:main' not found"),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
It("prints error message", func() {
|
||||
Expect(func() {
|
||||
flyCmd := exec.Command(flyPath, "-t", targetName, "order-instanced-pipelines", "-g", "awesome-pipeline", "-p", "branch:main")
|
||||
|
||||
sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
<-sess.Exited
|
||||
Expect(sess.ExitCode()).To(Equal(1))
|
||||
Eventually(sess.Err).Should(gbytes.Say(`failed to order instanced pipelines`))
|
||||
Consistently(sess.Err).ShouldNot(gbytes.Say(`Unexpected Response`))
|
||||
}).To(Change(func() int {
|
||||
return len(atcServer.ReceivedRequests())
|
||||
}).By(2))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the pipeline name is not specified", func() {
|
||||
It("errors", func() {
|
||||
Expect(func() {
|
||||
flyCmd := exec.Command(flyPath, "-t", targetName, "order-instanced-pipelines")
|
||||
|
||||
sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
<-sess.Exited
|
||||
Expect(sess.ExitCode()).To(Equal(1))
|
||||
Expect(sess.Err).Should(gbytes.Say("error: the required flags `-g, --group' and `-p, --pipeline' were not specified"))
|
||||
}).To(Change(func() int {
|
||||
return len(atcServer.ReceivedRequests())
|
||||
}).By(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -131,7 +131,7 @@ var _ = Describe("Fly CLI", func() {
|
|||
atcServer.AppendHandlers(
|
||||
ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("PUT", path),
|
||||
ghttp.RespondWith(http.StatusInternalServerError, nil),
|
||||
ghttp.RespondWith(http.StatusBadRequest, "pipeline 'awsome-pipeline' not found"),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
@ -147,6 +147,8 @@ var _ = Describe("Fly CLI", func() {
|
|||
Expect(sess.ExitCode()).To(Equal(1))
|
||||
Eventually(sess.Err).Should(gbytes.Say(`failed to order pipelines`))
|
||||
|
||||
Consistently(sess.Err).ShouldNot(gbytes.Say(`Unexpected Response`))
|
||||
|
||||
}).To(Change(func() int {
|
||||
return len(atcServer.ReceivedRequests())
|
||||
}).By(2))
|
||||
|
|
|
@ -505,6 +505,18 @@ type FakeTeam struct {
|
|||
orderingPipelinesReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
OrderingPipelinesWithinGroupStub func(string, []atc.InstanceVars) error
|
||||
orderingPipelinesWithinGroupMutex sync.RWMutex
|
||||
orderingPipelinesWithinGroupArgsForCall []struct {
|
||||
arg1 string
|
||||
arg2 []atc.InstanceVars
|
||||
}
|
||||
orderingPipelinesWithinGroupReturns struct {
|
||||
result1 error
|
||||
}
|
||||
orderingPipelinesWithinGroupReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
PauseJobStub func(atc.PipelineRef, string) (bool, error)
|
||||
pauseJobMutex sync.RWMutex
|
||||
pauseJobArgsForCall []struct {
|
||||
|
@ -3031,6 +3043,73 @@ func (fake *FakeTeam) OrderingPipelinesReturnsOnCall(i int, result1 error) {
|
|||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderingPipelinesWithinGroup(arg1 string, arg2 []atc.InstanceVars) error {
|
||||
var arg2Copy []atc.InstanceVars
|
||||
if arg2 != nil {
|
||||
arg2Copy = make([]atc.InstanceVars, len(arg2))
|
||||
copy(arg2Copy, arg2)
|
||||
}
|
||||
fake.orderingPipelinesWithinGroupMutex.Lock()
|
||||
ret, specificReturn := fake.orderingPipelinesWithinGroupReturnsOnCall[len(fake.orderingPipelinesWithinGroupArgsForCall)]
|
||||
fake.orderingPipelinesWithinGroupArgsForCall = append(fake.orderingPipelinesWithinGroupArgsForCall, struct {
|
||||
arg1 string
|
||||
arg2 []atc.InstanceVars
|
||||
}{arg1, arg2Copy})
|
||||
stub := fake.OrderingPipelinesWithinGroupStub
|
||||
fakeReturns := fake.orderingPipelinesWithinGroupReturns
|
||||
fake.recordInvocation("OrderingPipelinesWithinGroup", []interface{}{arg1, arg2Copy})
|
||||
fake.orderingPipelinesWithinGroupMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderingPipelinesWithinGroupCallCount() int {
|
||||
fake.orderingPipelinesWithinGroupMutex.RLock()
|
||||
defer fake.orderingPipelinesWithinGroupMutex.RUnlock()
|
||||
return len(fake.orderingPipelinesWithinGroupArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderingPipelinesWithinGroupCalls(stub func(string, []atc.InstanceVars) error) {
|
||||
fake.orderingPipelinesWithinGroupMutex.Lock()
|
||||
defer fake.orderingPipelinesWithinGroupMutex.Unlock()
|
||||
fake.OrderingPipelinesWithinGroupStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderingPipelinesWithinGroupArgsForCall(i int) (string, []atc.InstanceVars) {
|
||||
fake.orderingPipelinesWithinGroupMutex.RLock()
|
||||
defer fake.orderingPipelinesWithinGroupMutex.RUnlock()
|
||||
argsForCall := fake.orderingPipelinesWithinGroupArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderingPipelinesWithinGroupReturns(result1 error) {
|
||||
fake.orderingPipelinesWithinGroupMutex.Lock()
|
||||
defer fake.orderingPipelinesWithinGroupMutex.Unlock()
|
||||
fake.OrderingPipelinesWithinGroupStub = nil
|
||||
fake.orderingPipelinesWithinGroupReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) OrderingPipelinesWithinGroupReturnsOnCall(i int, result1 error) {
|
||||
fake.orderingPipelinesWithinGroupMutex.Lock()
|
||||
defer fake.orderingPipelinesWithinGroupMutex.Unlock()
|
||||
fake.OrderingPipelinesWithinGroupStub = nil
|
||||
if fake.orderingPipelinesWithinGroupReturnsOnCall == nil {
|
||||
fake.orderingPipelinesWithinGroupReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.orderingPipelinesWithinGroupReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeTeam) PauseJob(arg1 atc.PipelineRef, arg2 string) (bool, error) {
|
||||
fake.pauseJobMutex.Lock()
|
||||
ret, specificReturn := fake.pauseJobReturnsOnCall[len(fake.pauseJobArgsForCall)]
|
||||
|
@ -4242,6 +4321,8 @@ func (fake *FakeTeam) Invocations() map[string][][]interface{} {
|
|||
defer fake.nameMutex.RUnlock()
|
||||
fake.orderingPipelinesMutex.RLock()
|
||||
defer fake.orderingPipelinesMutex.RUnlock()
|
||||
fake.orderingPipelinesWithinGroupMutex.RLock()
|
||||
defer fake.orderingPipelinesWithinGroupMutex.RUnlock()
|
||||
fake.pauseJobMutex.RLock()
|
||||
defer fake.pauseJobMutex.RUnlock()
|
||||
fake.pausePipelineMutex.RLock()
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/concourse/concourse/atc"
|
||||
|
@ -47,14 +48,80 @@ func (team *team) OrderingPipelines(pipelineNames []string) error {
|
|||
return fmt.Errorf("Unable to marshal pipeline names: %s", err)
|
||||
}
|
||||
|
||||
return team.connection.Send(internal.Request{
|
||||
resp, err := team.httpAgent.Send(internal.Request{
|
||||
RequestName: atc.OrderPipelines,
|
||||
Params: params,
|
||||
Body: buffer,
|
||||
Header: http.Header{
|
||||
"Content-Type": {"application/json"},
|
||||
},
|
||||
}, &internal.Response{})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusForbidden:
|
||||
return fmt.Errorf("you do not have a role on team '%s'", team.Name())
|
||||
case http.StatusBadRequest:
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf(string(body))
|
||||
default:
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return internal.UnexpectedResponseError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Status: resp.Status,
|
||||
Body: string(body),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (team *team) OrderingPipelinesWithinGroup(groupName string, instanceVars []atc.InstanceVars) error {
|
||||
params := rata.Params{
|
||||
"team_name": team.Name(),
|
||||
"pipeline_name": groupName,
|
||||
}
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
err := json.NewEncoder(buffer).Encode(instanceVars)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to marshal pipeline instance vars: %s", err)
|
||||
}
|
||||
|
||||
resp, err := team.httpAgent.Send(internal.Request{
|
||||
RequestName: atc.OrderPipelinesWithinGroup,
|
||||
Params: params,
|
||||
Body: buffer,
|
||||
Header: http.Header{
|
||||
"Content-Type": {"application/json"},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusForbidden:
|
||||
return fmt.Errorf("you do not have a role on team '%s'", team.Name())
|
||||
case http.StatusBadRequest:
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf(string(body))
|
||||
default:
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return internal.UnexpectedResponseError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Status: resp.Status,
|
||||
Body: string(body),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (team *team) ListPipelines() ([]atc.Pipeline, error) {
|
||||
|
|
|
@ -257,6 +257,48 @@ var _ = Describe("ATC Handler Pipelines", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("OrderingInstancePipelines", func() {
|
||||
instanceVars := []atc.InstanceVars{
|
||||
{"branch": "main"},
|
||||
{"branch": "test"},
|
||||
}
|
||||
|
||||
Context("when the API call succeeds", func() {
|
||||
BeforeEach(func() {
|
||||
expectedURL := "/api/v1/teams/some-team/pipelines/my-pipeline/ordering"
|
||||
|
||||
atcServer.AppendHandlers(
|
||||
ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("PUT", expectedURL),
|
||||
ghttp.VerifyJSONRepresenting(instanceVars),
|
||||
ghttp.RespondWith(http.StatusOK, ""),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
It("return no error", func() {
|
||||
err := team.OrderingPipelinesWithinGroup("my-pipeline", instanceVars)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the API call errors", func() {
|
||||
BeforeEach(func() {
|
||||
expectedURL := "/api/v1/teams/some-team/pipelines/my-pipeline/ordering"
|
||||
atcServer.AppendHandlers(
|
||||
ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("PUT", expectedURL),
|
||||
ghttp.RespondWithJSONEncoded(http.StatusNotFound, ""),
|
||||
),
|
||||
)
|
||||
})
|
||||
It("returns error", func() {
|
||||
err := team.OrderingPipelinesWithinGroup("my-pipeline", instanceVars)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Pipeline", func() {
|
||||
var expectedPipeline atc.Pipeline
|
||||
expectedURL := "/api/v1/teams/some-team/pipelines/mypipeline"
|
||||
|
|
|
@ -70,6 +70,7 @@ type Team interface {
|
|||
CreateBuild(plan atc.Plan) (atc.Build, error)
|
||||
Builds(page Page) ([]atc.Build, Pagination, error)
|
||||
OrderingPipelines(pipelineNames []string) error
|
||||
OrderingPipelinesWithinGroup(groupName string, instanceVars []atc.InstanceVars) error
|
||||
|
||||
CreateArtifact(io.Reader, string, []string) (atc.WorkerArtifact, error)
|
||||
GetArtifact(int) (io.ReadCloser, error)
|
||||
|
|
|
@ -30,8 +30,8 @@ var _ = Describe("Reference", func() {
|
|||
},
|
||||
{
|
||||
desc: "segments contain special chars",
|
||||
ref: vars.Reference{Path: "hello.world", Fields: []string{"a", "foo:bar", "other field"}},
|
||||
result: `"hello.world".a."foo:bar"."other field"`,
|
||||
ref: vars.Reference{Path: "hello.world", Fields: []string{"a", "foo:bar", "other field", "another/field"}},
|
||||
result: `"hello.world".a."foo:bar"."other field"."another/field"`,
|
||||
},
|
||||
{
|
||||
desc: "var source",
|
||||
|
|
|
@ -105,7 +105,7 @@ func (r Reference) String() string {
|
|||
}
|
||||
|
||||
func refSegmentString(seg string) string {
|
||||
if strings.ContainsAny(seg, ",.: ") {
|
||||
if strings.ContainsAny(seg, ",.:/ ") {
|
||||
return fmt.Sprintf("%q", seg)
|
||||
}
|
||||
return seg
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
module Api.Endpoints exposing
|
||||
( BuildEndpoint(..)
|
||||
, Endpoint(..)
|
||||
, InstanceGroupEndpoint(..)
|
||||
, JobEndpoint(..)
|
||||
, PipelineEndpoint(..)
|
||||
, ResourceEndpoint(..)
|
||||
|
@ -30,6 +31,7 @@ type Endpoint
|
|||
| Cli
|
||||
| UserInfo
|
||||
| Logout
|
||||
| InstanceGroup Concourse.InstanceGroupIdentifier InstanceGroupEndpoint
|
||||
|
||||
|
||||
type PipelineEndpoint
|
||||
|
@ -79,6 +81,10 @@ type TeamEndpoint
|
|||
| OrderTeamPipelines
|
||||
|
||||
|
||||
type InstanceGroupEndpoint
|
||||
= OrderInstanceGroupPipelines
|
||||
|
||||
|
||||
base : RouteBuilder
|
||||
base =
|
||||
( [ "api", "v1" ], [] )
|
||||
|
@ -175,6 +181,12 @@ builder endpoint =
|
|||
Logout ->
|
||||
baseSky |> appendPath [ "logout" ]
|
||||
|
||||
InstanceGroup { teamName, name } subEndpoint ->
|
||||
base
|
||||
|> appendPath [ "teams", teamName ]
|
||||
|> appendPath [ "pipelines", name ]
|
||||
|> append (instanceGroupEndpoint subEndpoint)
|
||||
|
||||
|
||||
pipelineEndpoint : PipelineEndpoint -> RouteBuilder
|
||||
pipelineEndpoint endpoint =
|
||||
|
@ -297,3 +309,12 @@ teamEndpoint endpoint =
|
|||
[ "pipelines", "ordering" ]
|
||||
, []
|
||||
)
|
||||
|
||||
|
||||
instanceGroupEndpoint : InstanceGroupEndpoint -> RouteBuilder
|
||||
instanceGroupEndpoint endpoint =
|
||||
( case endpoint of
|
||||
OrderInstanceGroupPipelines ->
|
||||
[ "ordering" ]
|
||||
, []
|
||||
)
|
||||
|
|
|
@ -22,7 +22,15 @@ import Dashboard.Footer as Footer
|
|||
import Dashboard.Grid as Grid
|
||||
import Dashboard.Grid.Constants as GridConstants
|
||||
import Dashboard.Group as Group
|
||||
import Dashboard.Group.Models exposing (Card(..), Pipeline, cardName)
|
||||
import Dashboard.Group.Models
|
||||
exposing
|
||||
( Card(..)
|
||||
, Pipeline
|
||||
, cardName
|
||||
, cardTeamName
|
||||
, groupCardsWithinTeam
|
||||
, ungroupCards
|
||||
)
|
||||
import Dashboard.Models as Models
|
||||
exposing
|
||||
( DragState(..)
|
||||
|
@ -675,24 +683,42 @@ update session msg =
|
|||
updateBody : Session -> Message -> ET Model
|
||||
updateBody session msg ( model, effects ) =
|
||||
case msg of
|
||||
DragStart teamName cardId ->
|
||||
( { model | dragState = Models.Dragging teamName cardId }, effects )
|
||||
DragStart card ->
|
||||
( { model | dragState = Models.Dragging card }, effects )
|
||||
|
||||
DragOver target ->
|
||||
( { model | dropState = Models.Dropping target }, effects )
|
||||
|
||||
DragEnd ->
|
||||
case ( model.dragState, model.dropState ) of
|
||||
( Dragging teamName identifier, Dropping target ) ->
|
||||
( Dragging card, Dropping target ) ->
|
||||
let
|
||||
teamName =
|
||||
cardTeamName card
|
||||
|
||||
viewingInstanceGroups =
|
||||
case card of
|
||||
InstancedPipelineCard _ ->
|
||||
True
|
||||
|
||||
_ ->
|
||||
False
|
||||
|
||||
cardGroupFunction =
|
||||
if viewingInstanceGroups then
|
||||
List.map InstancedPipelineCard
|
||||
|
||||
else
|
||||
groupCardsWithinTeam
|
||||
|
||||
teamCards =
|
||||
model.pipelines
|
||||
|> Maybe.andThen (Dict.get teamName)
|
||||
|> Maybe.withDefault []
|
||||
|> groupCardsWithinTeam
|
||||
|> cardGroupFunction
|
||||
|> (\cards ->
|
||||
cards
|
||||
|> (case Drag.dragCardIndices identifier target cards of
|
||||
|> (case Drag.dragCardIndices card target cards of
|
||||
Just ( from, to ) ->
|
||||
Drag.drag from to
|
||||
|
||||
|
@ -701,26 +727,29 @@ updateBody session msg ( model, effects ) =
|
|||
)
|
||||
)
|
||||
|
||||
teamPipelines =
|
||||
ungroupCards teamCards
|
||||
|
||||
pipelines =
|
||||
model.pipelines
|
||||
|> Maybe.withDefault Dict.empty
|
||||
|> Dict.update teamName
|
||||
(always <|
|
||||
Just <|
|
||||
List.concatMap
|
||||
(\card ->
|
||||
case card of
|
||||
PipelineCard p ->
|
||||
[ p ]
|
||||
|> Dict.update teamName (always <| Just teamPipelines)
|
||||
|
||||
InstancedPipelineCard p ->
|
||||
[ p ]
|
||||
request =
|
||||
if viewingInstanceGroups then
|
||||
let
|
||||
instanceGroupName =
|
||||
cardName card
|
||||
in
|
||||
teamPipelines
|
||||
|> List.filter (.name >> (==) instanceGroupName)
|
||||
|> List.map .instanceVars
|
||||
|> SendOrderPipelinesWithinGroupRequest { teamName = teamName, name = instanceGroupName }
|
||||
|
||||
InstanceGroupCard p ps ->
|
||||
p :: ps
|
||||
)
|
||||
teamCards
|
||||
)
|
||||
else
|
||||
teamCards
|
||||
|> List.map cardName
|
||||
|> SendOrderPipelinesRequest teamName
|
||||
in
|
||||
( { model
|
||||
| pipelines = Just pipelines
|
||||
|
@ -728,9 +757,7 @@ updateBody session msg ( model, effects ) =
|
|||
, dropState = DroppingWhileApiRequestInFlight teamName
|
||||
}
|
||||
, effects
|
||||
++ [ teamCards
|
||||
|> List.map cardName
|
||||
|> SendOrderPipelinesRequest teamName
|
||||
++ [ request
|
||||
, pipelines
|
||||
|> Dict.values
|
||||
|> List.concat
|
||||
|
@ -1298,28 +1325,15 @@ regularCardsView session params =
|
|||
filteredPipelinesByTeam
|
||||
|> List.map
|
||||
(\( team, teamPipelines ) ->
|
||||
( team
|
||||
, groupCardsWithinTeam teamPipelines
|
||||
)
|
||||
{ header = team
|
||||
, cards = groupCardsWithinTeam teamPipelines
|
||||
, teamName = team
|
||||
}
|
||||
)
|
||||
in
|
||||
cardsView session params teamCards
|
||||
|
||||
|
||||
groupCardsWithinTeam : List Pipeline -> List Card
|
||||
groupCardsWithinTeam =
|
||||
Concourse.groupPipelinesWithinTeam
|
||||
>> List.map
|
||||
(\g ->
|
||||
case g of
|
||||
Concourse.RegularPipeline p ->
|
||||
PipelineCard p
|
||||
|
||||
Concourse.InstanceGroup p ps ->
|
||||
InstanceGroupCard p ps
|
||||
)
|
||||
|
||||
|
||||
instanceGroupCardsView : Session -> Model -> List (Html Message)
|
||||
instanceGroupCardsView session model =
|
||||
let
|
||||
|
@ -1329,7 +1343,7 @@ instanceGroupCardsView session model =
|
|||
|> Dict.toList
|
||||
|> List.sortWith (Ordering.byFieldWith (Group.ordering session) Tuple.first)
|
||||
|
||||
instanceGroups : List ( String, List Card )
|
||||
instanceGroups : List (Group.Section Card)
|
||||
instanceGroups =
|
||||
filteredPipelines
|
||||
|> List.concatMap
|
||||
|
@ -1337,16 +1351,17 @@ instanceGroupCardsView session model =
|
|||
List.Extra.gatherEqualsBy .name teamPipelines
|
||||
|> List.map
|
||||
(\( p, ps ) ->
|
||||
( team ++ " / " ++ p.name
|
||||
, p :: ps |> List.map InstancedPipelineCard
|
||||
)
|
||||
{ header = team ++ " / " ++ p.name
|
||||
, cards = p :: ps |> List.map InstancedPipelineCard
|
||||
, teamName = team
|
||||
}
|
||||
)
|
||||
)
|
||||
in
|
||||
cardsView session model instanceGroups
|
||||
|
||||
|
||||
cardsView : Session -> Model -> List ( String, List Card ) -> List (Html Message)
|
||||
cardsView : Session -> Model -> List (Group.Section Card) -> List (Html Message)
|
||||
cardsView session params teamCards =
|
||||
let
|
||||
jobs =
|
||||
|
@ -1364,7 +1379,7 @@ cardsView session params teamCards =
|
|||
let
|
||||
favoritedCards =
|
||||
teamCards
|
||||
|> List.concatMap Tuple.second
|
||||
|> List.concatMap .cards
|
||||
|> List.concatMap
|
||||
(\c ->
|
||||
case c of
|
||||
|
@ -1461,7 +1476,7 @@ cardsView session params teamCards =
|
|||
|
||||
else
|
||||
List.foldl
|
||||
(\( teamName, cards ) ( htmlList, totalOffset ) ->
|
||||
(\{ header, teamName, cards } ( htmlList, totalOffset ) ->
|
||||
let
|
||||
startingOffset =
|
||||
totalOffset
|
||||
|
@ -1495,8 +1510,10 @@ cardsView session params teamCards =
|
|||
, query = params.query
|
||||
, viewingInstanceGroups = viewingInstanceGroups
|
||||
}
|
||||
teamName
|
||||
layout.cards
|
||||
{ header = header
|
||||
, teamName = teamName
|
||||
, cards = layout.cards
|
||||
}
|
||||
|> (\html ->
|
||||
( html :: htmlList
|
||||
, startingOffset + layout.height
|
||||
|
|
|
@ -15,14 +15,14 @@ insertAt idx x xs =
|
|||
x :: xs
|
||||
|
||||
|
||||
dragCardIndices : String -> DropTarget -> List Card -> Maybe ( Int, Int )
|
||||
dragCardIndices cardId target cards =
|
||||
dragCardIndices : Card -> DropTarget -> List Card -> Maybe ( Int, Int )
|
||||
dragCardIndices card target cards =
|
||||
let
|
||||
cardIndex id =
|
||||
cards |> List.Extra.findIndex (cardIdentifier >> (==) id)
|
||||
cardIndex c =
|
||||
List.Extra.findIndex (cardIdentifier >> (==) (cardIdentifier c)) cards
|
||||
|
||||
fromIndex =
|
||||
cardIndex cardId
|
||||
cardIndex card
|
||||
|
||||
toIndex =
|
||||
(case target of
|
||||
|
|
|
@ -21,7 +21,6 @@ import Dashboard.Grid.Layout as Layout
|
|||
import Dashboard.Group.Models as Models
|
||||
exposing
|
||||
( Card(..)
|
||||
, cardIdentifier
|
||||
, cardName
|
||||
, cardTeamName
|
||||
)
|
||||
|
@ -87,9 +86,9 @@ computeLayout params teamName cards =
|
|||
let
|
||||
dragIndices =
|
||||
case ( params.dragState, params.dropState ) of
|
||||
( Dragging team cardId, Dropping target ) ->
|
||||
if teamName == team then
|
||||
Drag.dragCardIndices cardId target cards
|
||||
( Dragging card, Dropping target ) ->
|
||||
if teamName == cardTeamName card then
|
||||
Drag.dragCardIndices card target cards
|
||||
|
||||
else
|
||||
Nothing
|
||||
|
@ -149,7 +148,7 @@ computeLayout params teamName cards =
|
|||
dropAreaBounds bounds
|
||||
|
||||
curDropArea =
|
||||
{ bounds = curBounds, target = Before <| cardIdentifier origCard }
|
||||
{ bounds = curBounds, target = Before origCard }
|
||||
in
|
||||
( curDropArea :: dropAreas, Just curCard )
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
module Dashboard.Group exposing
|
||||
( PipelineIndex
|
||||
, Section
|
||||
, hdView
|
||||
, ordering
|
||||
, pipelineNotSetView
|
||||
|
@ -11,7 +12,7 @@ import Application.Models exposing (Session)
|
|||
import Concourse
|
||||
import Dashboard.Grid as Grid
|
||||
import Dashboard.Grid.Constants as GridConstants
|
||||
import Dashboard.Group.Models exposing (Card(..), Pipeline, cardIdentifier)
|
||||
import Dashboard.Group.Models exposing (Card(..), Pipeline, cardIdentifier, cardTeamName)
|
||||
import Dashboard.Group.Tag as Tag
|
||||
import Dashboard.InstanceGroup as InstanceGroup
|
||||
import Dashboard.Models exposing (DragState(..), DropState(..))
|
||||
|
@ -45,6 +46,13 @@ type alias PipelineIndex =
|
|||
Int
|
||||
|
||||
|
||||
type alias Section card =
|
||||
{ teamName : String
|
||||
, header : String
|
||||
, cards : List card
|
||||
}
|
||||
|
||||
|
||||
view :
|
||||
Session
|
||||
->
|
||||
|
@ -61,10 +69,9 @@ view :
|
|||
, query : String
|
||||
, viewingInstanceGroups : Bool
|
||||
}
|
||||
-> Concourse.TeamName
|
||||
-> List Grid.Card
|
||||
-> Section Grid.Card
|
||||
-> Html Message
|
||||
view session params teamName cards =
|
||||
view session params { header, teamName, cards } =
|
||||
let
|
||||
cardViews =
|
||||
if List.isEmpty cards then
|
||||
|
@ -117,9 +124,14 @@ view session params teamName cards =
|
|||
(\{ bounds, target } ->
|
||||
pipelineDropAreaView params.dragState teamName bounds target
|
||||
)
|
||||
|
||||
-- we use the header as the ID so that instance groups have unique
|
||||
-- IDs despite being in the same team
|
||||
groupId =
|
||||
header
|
||||
in
|
||||
Html.div
|
||||
[ id <| Effects.toHtmlID <| DashboardGroup teamName
|
||||
[ id <| Effects.toHtmlID <| DashboardGroup groupId
|
||||
, class "dashboard-team-group"
|
||||
, attribute "data-team-name" teamName
|
||||
]
|
||||
|
@ -133,7 +145,7 @@ view session params teamName cards =
|
|||
[ class "dashboard-team-name"
|
||||
, style "font-weight" Views.Styles.fontWeightBold
|
||||
]
|
||||
[ Html.text teamName ]
|
||||
[ Html.text header ]
|
||||
:: (Maybe.Extra.toList <|
|
||||
Maybe.map (Tag.view False) (tag session teamName)
|
||||
)
|
||||
|
@ -256,14 +268,14 @@ hdView :
|
|||
, query : String
|
||||
}
|
||||
-> { a | userState : UserState, pipelineRunningKeyframes : String }
|
||||
-> ( Concourse.TeamName, List Card )
|
||||
-> Section Card
|
||||
-> List (Html Message)
|
||||
hdView { pipelinesWithResourceErrors, pipelineJobs, jobs, dashboardView, query } session ( teamName, cards ) =
|
||||
hdView { pipelinesWithResourceErrors, pipelineJobs, jobs, dashboardView, query } session { teamName, cards, header } =
|
||||
let
|
||||
header =
|
||||
headerElement =
|
||||
Html.div
|
||||
[ class "dashboard-team-name" ]
|
||||
[ Html.text teamName ]
|
||||
[ Html.text header ]
|
||||
:: (Maybe.Extra.toList <| Maybe.map (Tag.view True) (tag session teamName))
|
||||
|
||||
teamPipelines =
|
||||
|
@ -320,14 +332,14 @@ hdView { pipelinesWithResourceErrors, pipelineJobs, jobs, dashboardView, query }
|
|||
in
|
||||
case teamPipelines of
|
||||
[] ->
|
||||
header
|
||||
headerElement
|
||||
|
||||
p :: ps ->
|
||||
-- Wrap the team name and the first pipeline together so
|
||||
-- the team name is not the last element in a column
|
||||
Html.div
|
||||
(class "dashboard-team-name-wrapper" :: Styles.teamNameHd)
|
||||
(header ++ [ p ])
|
||||
(headerElement ++ [ p ])
|
||||
:: ps
|
||||
|
||||
|
||||
|
@ -367,6 +379,14 @@ pipelineCardView :
|
|||
-> String
|
||||
-> Html Message
|
||||
pipelineCardView session params section { bounds, headerHeight, pipeline, inInstanceGroup } teamName =
|
||||
let
|
||||
card =
|
||||
if inInstanceGroup then
|
||||
InstancedPipelineCard pipeline
|
||||
|
||||
else
|
||||
PipelineCard pipeline
|
||||
in
|
||||
Html.div
|
||||
([ class "card-wrapper"
|
||||
, style "position" "absolute"
|
||||
|
@ -400,27 +420,32 @@ pipelineCardView session params section { bounds, headerHeight, pipeline, inInst
|
|||
, style "height" "100%"
|
||||
, attribute "data-pipeline-name" pipeline.name
|
||||
]
|
||||
++ (if section == AllPipelinesSection && not pipeline.stale && not params.viewingInstanceGroups then
|
||||
++ (if section == AllPipelinesSection && not pipeline.stale then
|
||||
[ attribute
|
||||
"ondragstart"
|
||||
"event.dataTransfer.setData('text/plain', '');"
|
||||
, draggable "true"
|
||||
, on "dragstart"
|
||||
(Json.Decode.succeed (DragStart pipeline.teamName <| cardIdentifier <| PipelineCard pipeline))
|
||||
(Json.Decode.succeed (DragStart <| card))
|
||||
, on "dragend" (Json.Decode.succeed DragEnd)
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
)
|
||||
++ (if params.dragState == Dragging pipeline.teamName (cardIdentifier <| PipelineCard pipeline) then
|
||||
[ style "width" "0"
|
||||
, style "margin" "0 12.5px"
|
||||
, style "overflow" "hidden"
|
||||
]
|
||||
++ (case params.dragState of
|
||||
Dragging currCard ->
|
||||
if cardIdentifier currCard == cardIdentifier card then
|
||||
[ style "width" "0"
|
||||
, style "margin" "0 12.5px"
|
||||
, style "overflow" "hidden"
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
else
|
||||
[]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
)
|
||||
++ (if params.dropState == DroppingWhileApiRequestInFlight teamName then
|
||||
[ style "opacity" "0.45", style "pointer-events" "none" ]
|
||||
|
@ -507,21 +532,26 @@ instanceGroupCardView session params section { bounds, headerHeight } p ps =
|
|||
"event.dataTransfer.setData('text/plain', '');"
|
||||
, draggable "true"
|
||||
, on "dragstart"
|
||||
(Json.Decode.succeed (DragStart p.teamName <| cardIdentifier <| InstanceGroupCard p ps))
|
||||
(Json.Decode.succeed (DragStart <| InstanceGroupCard p ps))
|
||||
, on "dragend" (Json.Decode.succeed DragEnd)
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
)
|
||||
++ (if params.dragState == Dragging p.teamName (cardIdentifier <| InstanceGroupCard p ps) then
|
||||
[ style "width" "0"
|
||||
, style "margin" "0 12.5px"
|
||||
, style "overflow" "hidden"
|
||||
]
|
||||
++ (case params.dragState of
|
||||
Dragging card ->
|
||||
if cardIdentifier card == cardIdentifier (InstanceGroupCard p ps) then
|
||||
[ style "width" "0"
|
||||
, style "margin" "0 12.5px"
|
||||
, style "overflow" "hidden"
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
else
|
||||
[]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
)
|
||||
++ (if params.dropState == DroppingWhileApiRequestInFlight p.teamName then
|
||||
[ style "opacity" "0.45", style "pointer-events" "none" ]
|
||||
|
@ -556,8 +586,8 @@ pipelineDropAreaView dragState name { x, y, width, height } target =
|
|||
let
|
||||
active =
|
||||
case dragState of
|
||||
Dragging team _ ->
|
||||
team == name
|
||||
Dragging card ->
|
||||
cardTeamName card == name
|
||||
|
||||
_ ->
|
||||
False
|
||||
|
|
|
@ -4,6 +4,8 @@ module Dashboard.Group.Models exposing
|
|||
, cardIdentifier
|
||||
, cardName
|
||||
, cardTeamName
|
||||
, groupCardsWithinTeam
|
||||
, ungroupCards
|
||||
)
|
||||
|
||||
import Concourse
|
||||
|
@ -15,6 +17,36 @@ type Card
|
|||
| InstanceGroupCard Pipeline (List Pipeline)
|
||||
|
||||
|
||||
groupCardsWithinTeam : List Pipeline -> List Card
|
||||
groupCardsWithinTeam =
|
||||
Concourse.groupPipelinesWithinTeam
|
||||
>> List.map
|
||||
(\g ->
|
||||
case g of
|
||||
Concourse.RegularPipeline p ->
|
||||
PipelineCard p
|
||||
|
||||
Concourse.InstanceGroup p ps ->
|
||||
InstanceGroupCard p ps
|
||||
)
|
||||
|
||||
|
||||
ungroupCards : List Card -> List Pipeline
|
||||
ungroupCards =
|
||||
List.concatMap
|
||||
(\c ->
|
||||
case c of
|
||||
PipelineCard p ->
|
||||
[ p ]
|
||||
|
||||
InstancedPipelineCard p ->
|
||||
[ p ]
|
||||
|
||||
InstanceGroupCard p ps ->
|
||||
p :: ps
|
||||
)
|
||||
|
||||
|
||||
cardIdentifier : Card -> String
|
||||
cardIdentifier c =
|
||||
case c of
|
||||
|
|
|
@ -8,7 +8,7 @@ module Dashboard.Models exposing
|
|||
)
|
||||
|
||||
import Concourse
|
||||
import Dashboard.Group.Models
|
||||
import Dashboard.Group.Models as GroupModels
|
||||
import Dict exposing (Dict)
|
||||
import FetchResult exposing (FetchResult)
|
||||
import Login.Login as Login
|
||||
|
@ -54,7 +54,7 @@ type FetchError
|
|||
|
||||
type DragState
|
||||
= NotDragging
|
||||
| Dragging Concourse.TeamName String
|
||||
| Dragging GroupModels.Card
|
||||
|
||||
|
||||
type DropState
|
||||
|
@ -68,7 +68,7 @@ type alias FooterModel r =
|
|||
| hideFooter : Bool
|
||||
, hideFooterCounter : Int
|
||||
, showHelp : Bool
|
||||
, pipelines : Maybe (Dict String (List Dashboard.Group.Models.Pipeline))
|
||||
, pipelines : Maybe (Dict String (List GroupModels.Pipeline))
|
||||
, dropdown : Dropdown
|
||||
, highDensity : Bool
|
||||
, dashboardView : Routes.DashboardView
|
||||
|
|
|
@ -148,7 +148,7 @@ pipelineView session { now, pipeline, hovered, resourceError, existingJobs, laye
|
|||
in
|
||||
Html.div
|
||||
(Styles.pipelineCard
|
||||
++ (if section == AllPipelinesSection && not pipeline.stale && not viewingInstanceGroups then
|
||||
++ (if section == AllPipelinesSection && not pipeline.stale then
|
||||
[ style "cursor" "move" ]
|
||||
|
||||
else
|
||||
|
|
|
@ -29,7 +29,7 @@ type Callback
|
|||
| PipelineFetched (Fetched Concourse.Pipeline)
|
||||
| PipelinesFetched (Fetched (List Concourse.Pipeline))
|
||||
| PipelineToggled Concourse.PipelineIdentifier (Fetched ())
|
||||
| PipelinesOrdered String (Fetched ())
|
||||
| PipelinesOrdered Concourse.TeamName (Fetched ())
|
||||
| UserFetched (Fetched Concourse.User)
|
||||
| ResourcesFetched (Fetched (List Concourse.Resource))
|
||||
| BuildResourcesFetched (Fetched ( Int, Concourse.BuildResources ))
|
||||
|
|
|
@ -160,7 +160,8 @@ type Effect
|
|||
| SetPinComment Concourse.ResourceIdentifier String
|
||||
| SendTokenToFly String Int
|
||||
| SendTogglePipelineRequest Concourse.PipelineIdentifier Bool
|
||||
| SendOrderPipelinesRequest String (List String)
|
||||
| SendOrderPipelinesRequest Concourse.TeamName (List Concourse.PipelineName)
|
||||
| SendOrderPipelinesWithinGroupRequest Concourse.InstanceGroupIdentifier (List Concourse.InstanceVars)
|
||||
| SendLogOutRequest
|
||||
| GetScreenSize
|
||||
| PinTeamNames StickyHeaderConfig
|
||||
|
@ -473,6 +474,15 @@ runEffect effect key csrfToken =
|
|||
|> Api.request
|
||||
|> Task.attempt (PipelinesOrdered teamName)
|
||||
|
||||
SendOrderPipelinesWithinGroupRequest id instanceVars ->
|
||||
Api.put
|
||||
(Endpoints.OrderInstanceGroupPipelines |> Endpoints.InstanceGroup id)
|
||||
csrfToken
|
||||
|> Api.withJsonBody
|
||||
(Json.Encode.list Concourse.encodeInstanceVars instanceVars)
|
||||
|> Api.request
|
||||
|> Task.attempt (PipelinesOrdered id.teamName)
|
||||
|
||||
SendLogOutRequest ->
|
||||
Api.get Endpoints.Logout
|
||||
|> Api.request
|
||||
|
|
|
@ -11,6 +11,7 @@ module Message.Message exposing
|
|||
import Concourse
|
||||
import Concourse.Cli as Cli
|
||||
import Concourse.Pagination exposing (Page)
|
||||
import Dashboard.Group.Models
|
||||
import Routes exposing (StepID)
|
||||
import StrictEvents
|
||||
|
||||
|
@ -24,7 +25,7 @@ type Message
|
|||
| ToggleGroup Concourse.PipelineGroup
|
||||
| SetGroups (List String)
|
||||
-- Dashboard
|
||||
| DragStart String String
|
||||
| DragStart Dashboard.Group.Models.Card
|
||||
| DragOver DropTarget
|
||||
| DragEnd
|
||||
-- Resource
|
||||
|
@ -130,5 +131,5 @@ type alias VersionId =
|
|||
|
||||
|
||||
type DropTarget
|
||||
= Before String
|
||||
= Before Dashboard.Group.Models.Card
|
||||
| End
|
||||
|
|
|
@ -3,6 +3,7 @@ module DashboardCacheTests exposing (all)
|
|||
import Application.Application as Application
|
||||
import Common
|
||||
import Concourse.BuildStatus exposing (BuildStatus(..))
|
||||
import Dashboard.Group.Models exposing (Card(..))
|
||||
import DashboardTests exposing (whenOnDashboard)
|
||||
import Data
|
||||
import Message.Callback exposing (Callback(..))
|
||||
|
@ -240,7 +241,7 @@ all =
|
|||
)
|
||||
|> Tuple.first
|
||||
|> Application.update
|
||||
(TopLevelMessage.Update <| Message.DragStart "team" "1")
|
||||
(TopLevelMessage.Update <| Message.DragStart <| PipelineCard (Data.dashboardPipeline "team" 1 |> Data.withName "pipeline"))
|
||||
|> Tuple.first
|
||||
|> Application.update
|
||||
(TopLevelMessage.Update <| Message.DragOver End)
|
||||
|
|
|
@ -370,19 +370,6 @@ all =
|
|||
Effects.toHtmlID <|
|
||||
Msgs.PipelineCardInstanceVar Msgs.AllPipelinesSection 1 "a" "foo"
|
||||
]
|
||||
, test "is not draggable" <|
|
||||
\_ ->
|
||||
whenOnDashboardViewingInstanceGroup { dashboardView = ViewNonArchivedPipelines }
|
||||
|> gotPipelines
|
||||
[ pipelineInstanceWithVars 1
|
||||
[ ( "a", JsonString "foo" ) ]
|
||||
]
|
||||
|> Common.queryView
|
||||
|> findCard
|
||||
|> Expect.all
|
||||
[ Query.hasNot [ attribute <| Attr.attribute "draggable" "true" ]
|
||||
, Query.hasNot [ style "cursor" "move" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ import Concourse
|
|||
import Concourse.BuildStatus exposing (BuildStatus(..))
|
||||
import Concourse.Cli as Cli
|
||||
import Concourse.PipelineStatus exposing (PipelineStatus(..))
|
||||
import Dashboard.Group.Models exposing (Card(..))
|
||||
import Data
|
||||
import Dict
|
||||
import Expect exposing (Expectation)
|
||||
|
@ -2000,7 +2001,11 @@ all =
|
|||
(Callback.AllJobsFetched <| Ok [])
|
||||
|> Tuple.first
|
||||
|> Application.update
|
||||
(ApplicationMsgs.Update <| Msgs.DragStart "team" "1")
|
||||
(ApplicationMsgs.Update <|
|
||||
Msgs.DragStart <|
|
||||
PipelineCard <|
|
||||
Data.dashboardPipeline "team" 1
|
||||
)
|
||||
|> Tuple.first
|
||||
|> Application.handleDelivery
|
||||
(ClockTicked FiveSeconds <|
|
||||
|
|
|
@ -158,7 +158,7 @@ resource pinnedVersion =
|
|||
}
|
||||
|
||||
|
||||
pipeline : String -> Int -> Concourse.Pipeline
|
||||
pipeline : Concourse.TeamName -> Int -> Concourse.Pipeline
|
||||
pipeline team id =
|
||||
{ id = id
|
||||
, name = "pipeline-" ++ String.fromInt id
|
||||
|
@ -172,13 +172,13 @@ pipeline team id =
|
|||
}
|
||||
|
||||
|
||||
dashboardPipeline : Int -> Bool -> Dashboard.Group.Models.Pipeline
|
||||
dashboardPipeline id public =
|
||||
dashboardPipeline : Concourse.TeamName -> Int -> Dashboard.Group.Models.Pipeline
|
||||
dashboardPipeline team id =
|
||||
{ id = id
|
||||
, name = pipelineName
|
||||
, name = "pipeline-" ++ String.fromInt id
|
||||
, instanceVars = Dict.empty
|
||||
, teamName = teamName
|
||||
, public = public
|
||||
, teamName = team
|
||||
, public = True
|
||||
, isToggleLoading = False
|
||||
, isVisibilityLoading = False
|
||||
, paused = False
|
||||
|
|
|
@ -3,6 +3,7 @@ module DragAndDropTests exposing (all)
|
|||
import Application.Application as Application
|
||||
import Common exposing (given, then_, when)
|
||||
import Concourse exposing (JsonValue(..))
|
||||
import Dashboard.Group.Models exposing (Card(..))
|
||||
import DashboardTests exposing (whenOnDashboard)
|
||||
import Data
|
||||
import Dict exposing (Dict)
|
||||
|
@ -11,9 +12,9 @@ import Http
|
|||
import Json.Encode as Encode
|
||||
import Message.Callback as Callback
|
||||
import Message.Effects as Effects
|
||||
import Message.Message as Message exposing (DropTarget(..))
|
||||
import Message.Message as Message exposing (DropTarget(..), Message(..))
|
||||
import Message.Subscription exposing (Delivery(..), Interval(..))
|
||||
import Message.TopLevelMessage as TopLevelMessage exposing (TopLevelMessage)
|
||||
import Message.TopLevelMessage as TopLevelMessage exposing (TopLevelMessage(..))
|
||||
import Test exposing (Test, describe, test)
|
||||
import Test.Html.Event as Event
|
||||
import Test.Html.Query as Query
|
||||
|
@ -28,18 +29,18 @@ all =
|
|||
[ test "pipeline card has dragstart listener" <|
|
||||
given iVisitedTheDashboard
|
||||
>> given myBrowserFetchedOnePipeline
|
||||
>> when iAmLookingAtTheFirstPipelineCard
|
||||
>> then_ (itListensForDragStartWithId "1")
|
||||
>> when iAmLookingAtTheFirstCard
|
||||
>> then_ (itListensForDragStartWithCard firstPipelineCard)
|
||||
, test "instance group card has drag start listener with id independent of the visible instances" <|
|
||||
given iVisitedTheDashboard
|
||||
>> given myBrowserFetchedPipelinesWithInstanceVars
|
||||
>> when iAmLookingAtTheInstanceGroupCard
|
||||
>> then_ (itListensForDragStartWithId "team/other-pipeline")
|
||||
>> then_ (itListensForDragStartWithCard instanceGroupCard)
|
||||
, test "pipeline card disappears when dragging starts" <|
|
||||
given iVisitedTheDashboard
|
||||
>> given myBrowserFetchedOnePipeline
|
||||
>> given iAmDraggingTheFirstPipelineCard
|
||||
>> when iAmLookingAtTheFirstPipelineCard
|
||||
>> when iAmLookingAtTheFirstCard
|
||||
>> then_ itIsInvisible
|
||||
, test "pipeline cards wrappers transition their transform when dragging" <|
|
||||
given iVisitedTheDashboard
|
||||
|
@ -61,14 +62,14 @@ all =
|
|||
given iVisitedTheDashboard
|
||||
>> given myBrowserFetchedOnePipeline
|
||||
>> given iAmDraggingTheFirstPipelineCard
|
||||
>> when iAmLookingAtTheFirstPipelineCard
|
||||
>> when iAmLookingAtTheFirstCard
|
||||
>> then_ itListensForDragEnd
|
||||
, test "pipeline card becomes visible when it is dropped" <|
|
||||
given iVisitedTheDashboard
|
||||
>> given myBrowserFetchedOnePipeline
|
||||
>> given iAmDraggingTheFirstPipelineCard
|
||||
>> given iDropThePipelineCard
|
||||
>> when iAmLookingAtTheFirstPipelineCard
|
||||
>> when iAmLookingAtTheFirstCard
|
||||
>> then_ itIsVisible
|
||||
, test "dropping first pipeline card on final drop area rearranges cards" <|
|
||||
given iVisitedTheDashboard
|
||||
|
@ -76,7 +77,7 @@ all =
|
|||
>> given iAmDraggingTheFirstPipelineCard
|
||||
>> given iAmDraggingOverTheThirdDropArea
|
||||
>> given iDropThePipelineCard
|
||||
>> when iAmLookingAtTheFirstPipelineCard
|
||||
>> when iAmLookingAtTheFirstCard
|
||||
>> then_ itIsTheOtherPipelineCard
|
||||
, test "dropping first pipeline card on final drop area makes API call" <|
|
||||
given iVisitedTheDashboard
|
||||
|
@ -113,6 +114,20 @@ all =
|
|||
>> given iAmDraggingOverTheFirstDropArea
|
||||
>> when iDropTheCard
|
||||
>> then_ myBrowserMakesTheOrderPipelinesAPICall
|
||||
, test "instanced pipelines can be reordered" <|
|
||||
given iVisitedTheDashboard
|
||||
>> given myBrowserFetchedPipelinesWithInstanceVars
|
||||
>> given iAmViewingTheInstanceGroup
|
||||
>> given iAmDraggingTheFirstInstancedPipelineCard
|
||||
>> given iAmDraggingOverTheThirdDropArea
|
||||
>> when iDropTheCard
|
||||
>> then_ myBrowserMakesTheOrderPipelinesWithinGroupAPICall
|
||||
, test "instanced pipeline card has dragstart listener" <|
|
||||
given iVisitedTheDashboard
|
||||
>> given myBrowserFetchedPipelinesWithInstanceVars
|
||||
>> given iAmViewingTheInstanceGroup
|
||||
>> when iAmLookingAtTheFirstCard
|
||||
>> then_ (itListensForDragStartWithCard firstInstancedPipelineCard)
|
||||
, test "dashboard does not auto-refresh during dragging" <|
|
||||
given iVisitedTheDashboard
|
||||
>> given myBrowserFetchedPipelinesFromMultipleTeams
|
||||
|
@ -209,6 +224,10 @@ myBrowserFetchedOnePipeline =
|
|||
)
|
||||
|
||||
|
||||
firstPipelineCard =
|
||||
PipelineCard <| (Data.dashboardPipeline "team" 1 |> Data.withName "pipeline")
|
||||
|
||||
|
||||
myBrowserFetchedTwoPipelines =
|
||||
Application.handleCallback
|
||||
(Callback.AllPipelinesFetched <|
|
||||
|
@ -228,10 +247,42 @@ myBrowserFetchedPipelinesWithInstanceVars =
|
|||
, Data.pipeline "team" 3
|
||||
|> Data.withName "other-pipeline"
|
||||
|> Data.withInstanceVars (Dict.fromList [ ( "hello", JsonString "world" ) ])
|
||||
, Data.pipeline "team" 4
|
||||
|> Data.withName "other-pipeline"
|
||||
|> Data.withInstanceVars (Dict.fromList [ ( "brach", JsonString "world-1" ) ])
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
firstInstancedPipelineCard =
|
||||
InstancedPipelineCard <|
|
||||
(Data.dashboardPipeline "team" 3
|
||||
|> Data.withName "other-pipeline"
|
||||
|> Data.withInstanceVars (Dict.fromList [ ( "hello", JsonString "world" ) ])
|
||||
)
|
||||
|
||||
|
||||
instanceGroupCard =
|
||||
-- pipeline 2 is not included because it's archived and we aren't viewing archived pipelines
|
||||
InstanceGroupCard
|
||||
(Data.dashboardPipeline "team" 3
|
||||
|> Data.withName "other-pipeline"
|
||||
|> Data.withInstanceVars (Dict.fromList [ ( "hello", JsonString "world" ) ])
|
||||
)
|
||||
[ Data.dashboardPipeline "team" 4
|
||||
|> Data.withName "other-pipeline"
|
||||
|> Data.withInstanceVars (Dict.fromList [ ( "brach", JsonString "world-1" ) ])
|
||||
]
|
||||
|
||||
|
||||
iAmViewingTheInstanceGroup =
|
||||
Tuple.first
|
||||
>> Application.update
|
||||
(Update <|
|
||||
FilterMsg "group:\"other-pipeline\""
|
||||
)
|
||||
|
||||
|
||||
myBrowserFetchedPipelinesFromMultipleTeams =
|
||||
Application.handleCallback
|
||||
(Callback.AllPipelinesFetched <|
|
||||
|
@ -243,7 +294,7 @@ myBrowserFetchedPipelinesFromMultipleTeams =
|
|||
)
|
||||
|
||||
|
||||
iAmLookingAtTheFirstPipelineCard =
|
||||
iAmLookingAtTheFirstCard =
|
||||
Tuple.first
|
||||
>> Common.queryView
|
||||
>> Query.findAll [ class "card" ]
|
||||
|
@ -277,23 +328,29 @@ iAmLookingAtAllPipelineCardsOfThatTeam =
|
|||
>> Query.findAll [ class "card" ]
|
||||
|
||||
|
||||
itListensForDragStartWithId : String -> Query.Single TopLevelMessage -> Expectation
|
||||
itListensForDragStartWithId id =
|
||||
itListensForDragStartWithCard : Card -> Query.Single TopLevelMessage -> Expectation
|
||||
itListensForDragStartWithCard card =
|
||||
Event.simulate (Event.custom "dragstart" (Encode.object []))
|
||||
>> Event.expect
|
||||
(TopLevelMessage.Update <| Message.DragStart "team" id)
|
||||
(TopLevelMessage.Update <| Message.DragStart card)
|
||||
|
||||
|
||||
iAmDraggingTheFirstPipelineCard =
|
||||
Tuple.first
|
||||
>> Application.update
|
||||
(TopLevelMessage.Update <| Message.DragStart "team" "1")
|
||||
(TopLevelMessage.Update <| Message.DragStart firstPipelineCard)
|
||||
|
||||
|
||||
iAmDraggingTheFirstInstancedPipelineCard =
|
||||
Tuple.first
|
||||
>> Application.update
|
||||
(TopLevelMessage.Update <| Message.DragStart firstInstancedPipelineCard)
|
||||
|
||||
|
||||
iAmDraggingTheInstanceGroupCard =
|
||||
Tuple.first
|
||||
>> Application.update
|
||||
(TopLevelMessage.Update <| Message.DragStart "team" "team/other-pipeline")
|
||||
(TopLevelMessage.Update <| Message.DragStart instanceGroupCard)
|
||||
|
||||
|
||||
itIsInvisible =
|
||||
|
@ -354,7 +411,7 @@ itListensForDragOverPreventingDefault =
|
|||
iAmDraggingOverTheFirstDropArea =
|
||||
Tuple.first
|
||||
>> Application.update
|
||||
(TopLevelMessage.Update <| Message.DragOver <| Before "1")
|
||||
(TopLevelMessage.Update <| Message.DragOver <| Before firstPipelineCard)
|
||||
|
||||
|
||||
iAmDraggingOverTheThirdDropArea =
|
||||
|
@ -426,6 +483,17 @@ myBrowserMakesTheOrderPipelinesAPICall =
|
|||
)
|
||||
|
||||
|
||||
myBrowserMakesTheOrderPipelinesWithinGroupAPICall =
|
||||
Tuple.second
|
||||
>> Common.contains
|
||||
(Effects.SendOrderPipelinesWithinGroupRequest { teamName = "team", name = "other-pipeline" }
|
||||
[ Dict.empty
|
||||
, Dict.fromList [ ( "brach", JsonString "world-1" ) ]
|
||||
, Dict.fromList [ ( "hello", JsonString "world" ) ]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
myBrowserMakesTheFetchPipelinesAPICall =
|
||||
Tuple.second
|
||||
>> Common.contains
|
||||
|
|
Loading…
Reference in New Issue