Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Boxiang Sun
gitlab-ce
Commits
55582b43
Commit
55582b43
authored
Sep 03, 2018
by
🌴🌴 Filipa Lacerda - OOO back on September 17th 🌴🌴
Committed by
Phil Hughes
Sep 03, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Adds Vuex store for the job log page
parent
e550b1ab
Changes
10
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
1224 additions
and
7 deletions
+1224
-7
app/assets/javascripts/jobs/store/actions.js
app/assets/javascripts/jobs/store/actions.js
+175
-0
app/assets/javascripts/jobs/store/index.js
app/assets/javascripts/jobs/store/index.js
+13
-0
app/assets/javascripts/jobs/store/mutation_types.js
app/assets/javascripts/jobs/store/mutation_types.js
+29
-0
app/assets/javascripts/jobs/store/mutations.js
app/assets/javascripts/jobs/store/mutations.js
+94
-0
app/assets/javascripts/jobs/store/state.js
app/assets/javascripts/jobs/store/state.js
+40
-0
app/assets/javascripts/lib/utils/common_utils.js
app/assets/javascripts/lib/utils/common_utils.js
+4
-1
locale/gitlab.pot
locale/gitlab.pot
+12
-0
spec/javascripts/jobs/store/actions_spec.js
spec/javascripts/jobs/store/actions_spec.js
+625
-0
spec/javascripts/jobs/store/mutations_spec.js
spec/javascripts/jobs/store/mutations_spec.js
+228
-0
spec/javascripts/lib/utils/common_utils_spec.js
spec/javascripts/lib/utils/common_utils_spec.js
+4
-6
No files found.
app/assets/javascripts/jobs/store/actions.js
0 → 100644
View file @
55582b43
import
Visibility
from
'
visibilityjs
'
;
import
*
as
types
from
'
./mutation_types
'
;
import
axios
from
'
../../lib/utils/axios_utils
'
;
import
Poll
from
'
../../lib/utils/poll
'
;
import
{
setCiStatusFavicon
}
from
'
../../lib/utils/common_utils
'
;
import
flash
from
'
../../flash
'
;
import
{
__
}
from
'
../../locale
'
;
export
const
setJobEndpoint
=
({
commit
},
endpoint
)
=>
commit
(
types
.
SET_JOB_ENDPOINT
,
endpoint
);
export
const
setTraceEndpoint
=
({
commit
},
endpoint
)
=>
commit
(
types
.
SET_TRACE_ENDPOINT
,
endpoint
);
export
const
setStagesEndpoint
=
({
commit
},
endpoint
)
=>
commit
(
types
.
SET_STAGES_ENDPOINT
,
endpoint
);
export
const
setJobsEndpoint
=
({
commit
},
endpoint
)
=>
commit
(
types
.
SET_JOBS_ENDPOINT
,
endpoint
);
let
eTagPoll
;
export
const
clearEtagPoll
=
()
=>
{
eTagPoll
=
null
;
};
export
const
stopPolling
=
()
=>
{
if
(
eTagPoll
)
eTagPoll
.
stop
();
};
export
const
restartPolling
=
()
=>
{
if
(
eTagPoll
)
eTagPoll
.
restart
();
};
export
const
requestJob
=
({
commit
})
=>
commit
(
types
.
REQUEST_JOB
);
export
const
fetchJob
=
({
state
,
dispatch
})
=>
{
dispatch
(
'
requestJob
'
);
eTagPoll
=
new
Poll
({
resource
:
{
getJob
(
endpoint
)
{
return
axios
.
get
(
endpoint
);
},
},
data
:
state
.
jobEndpoint
,
method
:
'
getJob
'
,
successCallback
:
({
data
})
=>
dispatch
(
'
receiveJobSuccess
'
,
data
),
errorCallback
:
()
=>
dispatch
(
'
receiveJobError
'
),
});
if
(
!
Visibility
.
hidden
())
{
eTagPoll
.
makeRequest
();
}
else
{
axios
.
get
(
state
.
jobEndpoint
)
.
then
(({
data
})
=>
dispatch
(
'
receiveJobSuccess
'
,
data
))
.
catch
(()
=>
dispatch
(
'
receiveJobError
'
));
}
Visibility
.
change
(()
=>
{
if
(
!
Visibility
.
hidden
())
{
dispatch
(
'
restartPolling
'
);
}
else
{
dispatch
(
'
stopPolling
'
);
}
});
};
export
const
receiveJobSuccess
=
({
commit
},
data
)
=>
commit
(
types
.
RECEIVE_JOB_SUCCESS
,
data
);
export
const
receiveJobError
=
({
commit
})
=>
{
commit
(
types
.
RECEIVE_JOB_ERROR
);
flash
(
__
(
'
An error occurred while fetching the job.
'
));
};
/**
* Job's Trace
*/
export
const
scrollTop
=
({
commit
})
=>
{
commit
(
types
.
SCROLL_TO_TOP
);
window
.
scrollTo
({
top
:
0
});
};
export
const
scrollBottom
=
({
commit
})
=>
{
commit
(
types
.
SCROLL_TO_BOTTOM
);
window
.
scrollTo
({
top
:
document
.
height
});
};
export
const
requestTrace
=
({
commit
})
=>
commit
(
types
.
REQUEST_TRACE
);
let
traceTimeout
;
export
const
fetchTrace
=
({
dispatch
,
state
})
=>
{
dispatch
(
'
requestTrace
'
);
axios
.
get
(
`
${
state
.
traceEndpoint
}
/trace.json`
,
{
params
:
{
state
:
state
.
traceState
},
})
.
then
(({
data
})
=>
{
if
(
!
state
.
fetchingStatusFavicon
)
{
dispatch
(
'
fetchFavicon
'
);
}
dispatch
(
'
receiveTraceSuccess
'
,
data
);
if
(
!
data
.
complete
)
{
traceTimeout
=
setTimeout
(()
=>
{
dispatch
(
'
fetchTrace
'
);
},
4000
);
}
else
{
dispatch
(
'
stopPollingTrace
'
);
}
})
.
catch
(()
=>
dispatch
(
'
receiveTraceError
'
));
};
export
const
stopPollingTrace
=
({
commit
})
=>
{
commit
(
types
.
STOP_POLLING_TRACE
);
clearTimeout
(
traceTimeout
);
};
export
const
receiveTraceSuccess
=
({
commit
},
log
)
=>
commit
(
types
.
RECEIVE_TRACE_SUCCESS
,
log
);
export
const
receiveTraceError
=
({
commit
})
=>
{
commit
(
types
.
RECEIVE_TRACE_ERROR
);
clearTimeout
(
traceTimeout
);
flash
(
__
(
'
An error occurred while fetching the job log.
'
));
};
export
const
fetchFavicon
=
({
state
,
dispatch
})
=>
{
dispatch
(
'
requestStatusFavicon
'
);
setCiStatusFavicon
(
`
${
state
.
pagePath
}
/status.json`
)
.
then
(()
=>
dispatch
(
'
receiveStatusFaviconSuccess
'
))
.
catch
(()
=>
dispatch
(
'
requestStatusFaviconError
'
));
};
export
const
requestStatusFavicon
=
({
commit
})
=>
commit
(
types
.
REQUEST_STATUS_FAVICON
);
export
const
receiveStatusFaviconSuccess
=
({
commit
})
=>
commit
(
types
.
RECEIVE_STATUS_FAVICON_SUCCESS
);
export
const
requestStatusFaviconError
=
({
commit
})
=>
commit
(
types
.
RECEIVE_STATUS_FAVICON_ERROR
);
/**
* Stages dropdown on sidebar
*/
export
const
requestStages
=
({
commit
})
=>
commit
(
types
.
REQUEST_STAGES
);
export
const
fetchStages
=
({
state
,
dispatch
})
=>
{
dispatch
(
'
requestStages
'
);
axios
.
get
(
state
.
stagesEndpoint
)
.
then
(({
data
})
=>
dispatch
(
'
receiveStagesSuccess
'
,
data
))
.
catch
(()
=>
dispatch
(
'
receiveStagesError
'
));
};
export
const
receiveStagesSuccess
=
({
commit
},
data
)
=>
commit
(
types
.
RECEIVE_STAGES_SUCCESS
,
data
);
export
const
receiveStagesError
=
({
commit
})
=>
{
commit
(
types
.
RECEIVE_STAGES_ERROR
);
flash
(
__
(
'
An error occurred while fetching stages.
'
));
};
/**
* Jobs list on sidebar - depend on stages dropdown
*/
export
const
requestJobsForStage
=
({
commit
})
=>
commit
(
types
.
REQUEST_JOBS_FOR_STAGE
);
export
const
setSelectedStage
=
({
commit
},
stage
)
=>
commit
(
types
.
SET_SELECTED_STAGE
,
stage
);
// On stage click, set selected stage + fetch job
export
const
fetchJobsForStage
=
({
state
,
dispatch
},
stage
)
=>
{
dispatch
(
'
setSelectedStage
'
,
stage
);
dispatch
(
'
requestJobsForStage
'
);
axios
.
get
(
state
.
stageJobsEndpoint
)
.
then
(({
data
})
=>
dispatch
(
'
receiveJobsForStageSuccess
'
,
data
))
.
catch
(()
=>
dispatch
(
'
receiveJobsForStageError
'
));
};
export
const
receiveJobsForStageSuccess
=
({
commit
},
data
)
=>
commit
(
types
.
RECEIVE_JOBS_FOR_STAGE_SUCCESS
,
data
);
export
const
receiveJobsForStageError
=
({
commit
})
=>
{
commit
(
types
.
RECEIVE_JOBS_FOR_STAGE_ERROR
);
flash
(
__
(
'
An error occurred while fetching the jobs.
'
));
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
app/assets/javascripts/jobs/store/index.js
0 → 100644
View file @
55582b43
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
state
from
'
./state
'
;
import
*
as
actions
from
'
./actions
'
;
import
mutations
from
'
./mutations
'
;
Vue
.
use
(
Vuex
);
export
default
()
=>
new
Vuex
.
Store
({
actions
,
mutations
,
state
:
state
(),
});
app/assets/javascripts/jobs/store/mutation_types.js
0 → 100644
View file @
55582b43
export
const
SET_JOB_ENDPOINT
=
'
SET_JOB_ENDPOINT
'
;
export
const
SET_TRACE_ENDPOINT
=
'
SET_TRACE_ENDPOINT
'
;
export
const
SET_STAGES_ENDPOINT
=
'
SET_STAGES_ENDPOINT
'
;
export
const
SET_JOBS_ENDPOINT
=
'
SET_JOBS_ENDPOINT
'
;
export
const
SCROLL_TO_TOP
=
'
SCROLL_TO_TOP
'
;
export
const
SCROLL_TO_BOTTOM
=
'
SCROLL_TO_BOTTOM
'
;
export
const
REQUEST_JOB
=
'
REQUEST_JOB
'
;
export
const
RECEIVE_JOB_SUCCESS
=
'
RECEIVE_JOB_SUCCESS
'
;
export
const
RECEIVE_JOB_ERROR
=
'
RECEIVE_JOB_ERROR
'
;
export
const
REQUEST_TRACE
=
'
REQUEST_TRACE
'
;
export
const
STOP_POLLING_TRACE
=
'
STOP_POLLING_TRACE
'
;
export
const
RECEIVE_TRACE_SUCCESS
=
'
RECEIVE_TRACE_SUCCESS
'
;
export
const
RECEIVE_TRACE_ERROR
=
'
RECEIVE_TRACE_ERROR
'
;
export
const
REQUEST_STATUS_FAVICON
=
'
REQUEST_STATUS_FAVICON
'
;
export
const
RECEIVE_STATUS_FAVICON_SUCCESS
=
'
RECEIVE_STATUS_FAVICON_SUCCESS
'
;
export
const
RECEIVE_STATUS_FAVICON_ERROR
=
'
RECEIVE_STATUS_FAVICON_ERROR
'
;
export
const
REQUEST_STAGES
=
'
REQUEST_STAGES
'
;
export
const
RECEIVE_STAGES_SUCCESS
=
'
RECEIVE_STAGES_SUCCESS
'
;
export
const
RECEIVE_STAGES_ERROR
=
'
RECEIVE_STAGES_ERROR
'
;
export
const
SET_SELECTED_STAGE
=
'
SET_SELECTED_STAGE
'
;
export
const
REQUEST_JOBS_FOR_STAGE
=
'
REQUEST_JOBS_FOR_STAGE
'
;
export
const
RECEIVE_JOBS_FOR_STAGE_SUCCESS
=
'
RECEIVE_JOBS_FOR_STAGE_SUCCESS
'
;
export
const
RECEIVE_JOBS_FOR_STAGE_ERROR
=
'
RECEIVE_JOBS_FOR_STAGE_ERROR
'
;
app/assets/javascripts/jobs/store/mutations.js
0 → 100644
View file @
55582b43
/* eslint-disable no-param-reassign */
import
*
as
types
from
'
./mutation_types
'
;
export
default
{
[
types
.
REQUEST_STATUS_FAVICON
](
state
)
{
state
.
fetchingStatusFavicon
=
true
;
},
[
types
.
RECEIVE_STATUS_FAVICON_SUCCESS
](
state
)
{
state
.
fetchingStatusFavicon
=
false
;
},
[
types
.
RECEIVE_STATUS_FAVICON_ERROR
](
state
)
{
state
.
fetchingStatusFavicon
=
false
;
},
[
types
.
RECEIVE_TRACE_SUCCESS
](
state
,
log
)
{
if
(
log
.
state
)
{
state
.
traceState
=
log
.
state
;
}
if
(
log
.
append
)
{
state
.
trace
+=
log
.
html
;
state
.
traceSize
+=
log
.
size
;
}
else
{
state
.
trace
=
log
.
html
;
state
.
traceSize
=
log
.
size
;
}
if
(
state
.
traceSize
<
log
.
total
)
{
state
.
isTraceSizeVisible
=
true
;
}
else
{
state
.
isTraceSizeVisible
=
false
;
}
state
.
isTraceComplete
=
log
.
complete
;
state
.
hasTraceError
=
false
;
},
[
types
.
STOP_POLLING_TRACE
](
state
)
{
state
.
isTraceComplete
=
true
;
},
// todo_fl: check this.
[
types
.
RECEIVE_TRACE_ERROR
](
state
)
{
state
.
isLoadingTrace
=
false
;
state
.
isTraceComplete
=
true
;
state
.
hasTraceError
=
true
;
},
[
types
.
REQUEST_JOB
](
state
)
{
state
.
isLoading
=
true
;
},
[
types
.
RECEIVE_JOB_SUCCESS
](
state
,
job
)
{
state
.
isLoading
=
false
;
state
.
hasError
=
false
;
state
.
job
=
job
;
},
[
types
.
RECEIVE_JOB_ERROR
](
state
)
{
state
.
isLoading
=
false
;
state
.
hasError
=
true
;
state
.
job
=
{};
},
[
types
.
SCROLL_TO_TOP
](
state
)
{
state
.
isTraceScrolledToBottom
=
false
;
state
.
hasBeenScrolled
=
true
;
},
[
types
.
SCROLL_TO_BOTTOM
](
state
)
{
state
.
isTraceScrolledToBottom
=
true
;
state
.
hasBeenScrolled
=
true
;
},
[
types
.
REQUEST_STAGES
](
state
)
{
state
.
isLoadingStages
=
true
;
},
[
types
.
RECEIVE_STAGES_SUCCESS
](
state
,
stages
)
{
state
.
isLoadingStages
=
false
;
state
.
stages
=
stages
;
},
[
types
.
RECEIVE_STAGES_ERROR
](
state
)
{
state
.
isLoadingStages
=
false
;
state
.
stages
=
[];
},
[
types
.
REQUEST_JOBS_FOR_STAGE
](
state
)
{
state
.
isLoadingJobs
=
true
;
},
[
types
.
RECEIVE_JOBS_FOR_STAGE_SUCCESS
](
state
,
jobs
)
{
state
.
isLoadingJobs
=
false
;
state
.
jobs
=
jobs
;
},
[
types
.
RECEIVE_JOBS_FOR_STAGE_ERROR
](
state
)
{
state
.
isLoadingJobs
=
false
;
state
.
jobs
=
[];
},
};
app/assets/javascripts/jobs/store/state.js
0 → 100644
View file @
55582b43
export
default
()
=>
({
jobEndpoint
:
null
,
traceEndpoint
:
null
,
// dropdown options
stagesEndpoint
:
null
,
// list of jobs on sidebard
stageJobsEndpoint
:
null
,
// job log
isLoading
:
false
,
hasError
:
false
,
job
:
{},
// trace
isLoadingTrace
:
false
,
hasTraceError
:
false
,
trace
:
''
,
isTraceScrolledToBottom
:
false
,
hasBeenScrolled
:
false
,
isTraceComplete
:
false
,
traceSize
:
0
,
// todo_fl: needs to be converted into human readable format in components
isTraceSizeVisible
:
false
,
fetchingStatusFavicon
:
false
,
// used as a query parameter
traceState
:
null
,
// used to check if we need to redirect the user - todo_fl: check if actually needed
traceStatus
:
null
,
// sidebar dropdown
isLoadingStages
:
false
,
isLoadingJobs
:
false
,
selectedStage
:
null
,
stages
:
[],
jobs
:
[],
});
app/assets/javascripts/lib/utils/common_utils.js
View file @
55582b43
...
@@ -491,7 +491,10 @@ export const setCiStatusFavicon = pageUrl =>
...
@@ -491,7 +491,10 @@ export const setCiStatusFavicon = pageUrl =>
}
}
return
resetFavicon
();
return
resetFavicon
();
})
})
.
catch
(
resetFavicon
);
.
catch
((
error
)
=>
{
resetFavicon
();
throw
error
;
});
export
const
spriteIcon
=
(
icon
,
className
=
''
)
=>
{
export
const
spriteIcon
=
(
icon
,
className
=
''
)
=>
{
const
classAttribute
=
className
.
length
>
0
?
`class="
${
className
}
"`
:
''
;
const
classAttribute
=
className
.
length
>
0
?
`class="
${
className
}
"`
:
''
;
...
...
locale/gitlab.pot
View file @
55582b43
...
@@ -505,6 +505,18 @@ msgstr ""
...
@@ -505,6 +505,18 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgid "An error occurred while fetching sidebar data"
msgstr ""
msgstr ""
msgid "An error occurred while fetching stages."
msgstr ""
msgid "An error occurred while fetching the job log."
msgstr ""
msgid "An error occurred while fetching the job."
msgstr ""
msgid "An error occurred while fetching the jobs."
msgstr ""
msgid "An error occurred while fetching the pipeline."
msgid "An error occurred while fetching the pipeline."
msgstr ""
msgstr ""
...
...
spec/javascripts/jobs/store/actions_spec.js
0 → 100644
View file @
55582b43
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
setJobEndpoint
,
setTraceEndpoint
,
setStagesEndpoint
,
setJobsEndpoint
,
clearEtagPoll
,
stopPolling
,
requestJob
,
fetchJob
,
receiveJobSuccess
,
receiveJobError
,
scrollTop
,
scrollBottom
,
requestTrace
,
fetchTrace
,
stopPollingTrace
,
receiveTraceSuccess
,
receiveTraceError
,
fetchFavicon
,
requestStatusFavicon
,
receiveStatusFaviconSuccess
,
requestStatusFaviconError
,
requestStages
,
fetchStages
,
receiveStagesSuccess
,
receiveStagesError
,
requestJobsForStage
,
setSelectedStage
,
fetchJobsForStage
,
receiveJobsForStageSuccess
,
receiveJobsForStageError
,
}
from
'
~/jobs/store/actions
'
;
import
state
from
'
~/jobs/store/state
'
;
import
*
as
types
from
'
~/jobs/store/mutation_types
'
;
import
testAction
from
'
spec/helpers/vuex_action_helper
'
;
import
{
TEST_HOST
}
from
'
spec/test_constants
'
;
describe
(
'
Job State actions
'
,
()
=>
{
let
mockedState
;
beforeEach
(()
=>
{
mockedState
=
state
();
});
describe
(
'
setJobEndpoint
'
,
()
=>
{
it
(
'
should commit SET_JOB_ENDPOINT mutation
'
,
done
=>
{
testAction
(
setJobEndpoint
,
'
job/872324.json
'
,
mockedState
,
[{
type
:
types
.
SET_JOB_ENDPOINT
,
payload
:
'
job/872324.json
'
}],
[],
done
,
);
});
});
describe
(
'
setTraceEndpoint
'
,
()
=>
{
it
(
'
should commit SET_TRACE_ENDPOINT mutation
'
,
done
=>
{
testAction
(
setTraceEndpoint
,
'
job/872324/trace.json
'
,
mockedState
,
[{
type
:
types
.
SET_TRACE_ENDPOINT
,
payload
:
'
job/872324/trace.json
'
}],
[],
done
,
);
});
});
describe
(
'
setStagesEndpoint
'
,
()
=>
{
it
(
'
should commit SET_STAGES_ENDPOINT mutation
'
,
done
=>
{
testAction
(
setStagesEndpoint
,
'
job/872324/stages.json
'
,
mockedState
,
[{
type
:
types
.
SET_STAGES_ENDPOINT
,
payload
:
'
job/872324/stages.json
'
}],
[],
done
,
);
});
});
describe
(
'
setJobsEndpoint
'
,
()
=>
{
it
(
'
should commit SET_JOBS_ENDPOINT mutation
'
,
done
=>
{
testAction
(
setJobsEndpoint
,
'
job/872324/stages/build.json
'
,
mockedState
,
[{
type
:
types
.
SET_JOBS_ENDPOINT
,
payload
:
'
job/872324/stages/build.json
'
}],
[],
done
,
);
});
});
describe
(
'
requestJob
'
,
()
=>
{
it
(
'
should commit REQUEST_JOB mutation
'
,
done
=>
{
testAction
(
requestJob
,
null
,
mockedState
,
[{
type
:
types
.
REQUEST_JOB
}],
[],
done
);
});
});
describe
(
'
fetchJob
'
,
()
=>
{
let
mock
;
beforeEach
(()
=>
{
mockedState
.
jobEndpoint
=
`
${
TEST_HOST
}
/endpoint.json`
;
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
mock
.
restore
();
stopPolling
();
clearEtagPoll
();
});
describe
(
'
success
'
,
()
=>
{
it
(
'
dispatches requestJob and receiveJobSuccess
'
,
done
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint.json`
).
replyOnce
(
200
,
{
id
:
121212
,
name
:
'
karma
'
});
testAction
(
fetchJob
,
null
,
mockedState
,
[],
[
{
type
:
'
requestJob
'
,
},
{
payload
:
{
id
:
121212
,
name
:
'
karma
'
},
type
:
'
receiveJobSuccess
'
,
},
],
done
,
);
});
});
describe
(
'
error
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint.json`
).
reply
(
500
);
});
it
(
'
dispatches requestJob and receiveJobError
'
,
done
=>
{
testAction
(
fetchJob
,
null
,
mockedState
,
[],
[
{
type
:
'
requestJob
'
,
},
{
type
:
'
receiveJobError
'
,
},
],
done
,
);
});
});
});
describe
(
'
receiveJobSuccess
'
,
()
=>
{
it
(
'
should commit RECEIVE_JOB_SUCCESS mutation
'
,
done
=>
{
testAction
(
receiveJobSuccess
,
{
id
:
121232132
},
mockedState
,
[{
type
:
types
.
RECEIVE_JOB_SUCCESS
,
payload
:
{
id
:
121232132
}
}],
[],
done
,
);
});
});
describe
(
'
receiveJobError
'
,
()
=>
{
it
(
'
should commit RECEIVE_JOB_ERROR mutation
'
,
done
=>
{
testAction
(
receiveJobError
,
null
,
mockedState
,
[{
type
:
types
.
RECEIVE_JOB_ERROR
}],
[],
done
);
});
});
describe
(
'
scrollTop
'
,
()
=>
{
it
(
'
should commit SCROLL_TO_TOP mutation
'
,
done
=>
{
testAction
(
scrollTop
,
null
,
mockedState
,
[{
type
:
types
.
SCROLL_TO_TOP
}],
[],
done
);
});
});
describe
(
'
scrollBottom
'
,
()
=>
{
it
(
'
should commit SCROLL_TO_BOTTOM mutation
'
,
done
=>
{
testAction
(
scrollBottom
,
null
,
mockedState
,
[{
type
:
types
.
SCROLL_TO_BOTTOM
}],
[],
done
);
});
});
describe
(
'
requestTrace
'
,
()
=>
{
it
(
'
should commit REQUEST_TRACE mutation
'
,
done
=>
{
testAction
(
requestTrace
,
null
,
mockedState
,
[{
type
:
types
.
REQUEST_TRACE
}],
[],
done
);
});
});
describe
(
'
fetchTrace
'
,
()
=>
{
let
mock
;
beforeEach
(()
=>
{
mockedState
.
traceEndpoint
=
`
${
TEST_HOST
}
/endpoint`
;
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
mock
.
restore
();
stopPolling
();
clearEtagPoll
();
});
describe
(
'
success
'
,
()
=>
{
it
(
'
dispatches requestTrace, fetchFavicon, receiveTraceSuccess and stopPollingTrace when job is complete
'
,
done
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint/trace.json`
).
replyOnce
(
200
,
{
html
:
'
I, [2018-08-17T22:57:45.707325 #1841] INFO -- :
'
,
complete
:
true
,
});
testAction
(
fetchTrace
,
null
,
mockedState
,
[],
[
{
type
:
'
requestTrace
'
,
},
{
type
:
'
fetchFavicon
'
,
},
{
payload
:
{
html
:
'
I, [2018-08-17T22:57:45.707325 #1841] INFO -- :
'
,
complete
:
true
,
},
type
:
'
receiveTraceSuccess
'
,
},
{
type
:
'
stopPollingTrace
'
,
},
],
done
,
);
});
});
describe
(
'
error
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint/trace.json`
).
reply
(
500
);
});
it
(
'
dispatches requestTrace and receiveTraceError
'
,
done
=>
{
testAction
(
fetchTrace
,
null
,
mockedState
,
[],
[
{
type
:
'
requestTrace
'
,
},
{
type
:
'
receiveTraceError
'
,
},
],
done
,
);
});
});
});
describe
(
'
stopPollingTrace
'
,
()
=>
{
it
(
'
should commit STOP_POLLING_TRACE mutation
'
,
done
=>
{
testAction
(
stopPollingTrace
,
null
,
mockedState
,
[{
type
:
types
.
STOP_POLLING_TRACE
}],
[],
done
,
);
});
});
describe
(
'
receiveTraceSuccess
'
,
()
=>
{
it
(
'
should commit RECEIVE_TRACE_SUCCESS mutation
'
,
done
=>
{
testAction
(
receiveTraceSuccess
,
'
hello world
'
,
mockedState
,
[{
type
:
types
.
RECEIVE_TRACE_SUCCESS
,
payload
:
'
hello world
'
}],
[],
done
,
);
});
});
describe
(
'
receiveTraceError
'
,
()
=>
{
it
(
'
should commit RECEIVE_TRACE_ERROR mutation
'
,
done
=>
{
testAction
(
receiveTraceError
,
null
,
mockedState
,
[{
type
:
types
.
RECEIVE_TRACE_ERROR
}],
[],
done
,
);
});
});
describe
(
'
fetchFavicon
'
,
()
=>
{
let
mock
;
beforeEach
(()
=>
{
mockedState
.
pagePath
=
`
${
TEST_HOST
}
/endpoint`
;
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
mock
.
restore
();
});
describe
(
'
success
'
,
()
=>
{
it
(
'
dispatches requestStatusFavicon and receiveStatusFaviconSuccess
'
,
done
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint/status.json`
).
replyOnce
(
200
);
testAction
(
fetchFavicon
,
null
,
mockedState
,
[],
[
{
type
:
'
requestStatusFavicon
'
,
},
{
type
:
'
receiveStatusFaviconSuccess
'
,
},
],
done
,
);
});
});
describe
(
'
error
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint/status.json`
).
replyOnce
(
500
);
});
it
(
'
dispatches requestStatusFavicon and requestStatusFaviconError
'
,
done
=>
{
testAction
(
fetchFavicon
,
null
,
mockedState
,
[],
[
{
type
:
'
requestStatusFavicon
'
,
},
{
type
:
'
requestStatusFaviconError
'
,
},
],
done
,
);
});
});
});
describe
(
'
requestStatusFavicon
'
,
()
=>
{
it
(
'
should commit REQUEST_STATUS_FAVICON mutation
'
,
done
=>
{
testAction
(
requestStatusFavicon
,
null
,
mockedState
,
[{
type
:
types
.
REQUEST_STATUS_FAVICON
}],
[],
done
,
);
});
});
describe
(
'
receiveStatusFaviconSuccess
'
,
()
=>
{
it
(
'
should commit RECEIVE_STATUS_FAVICON_SUCCESS mutation
'
,
done
=>
{
testAction
(
receiveStatusFaviconSuccess
,
null
,
mockedState
,
[{
type
:
types
.
RECEIVE_STATUS_FAVICON_SUCCESS
}],
[],
done
,
);
});
});
describe
(
'
requestStatusFaviconError
'
,
()
=>
{
it
(
'
should commit RECEIVE_STATUS_FAVICON_ERROR mutation
'
,
done
=>
{
testAction
(
requestStatusFaviconError
,
null
,
mockedState
,
[{
type
:
types
.
RECEIVE_STATUS_FAVICON_ERROR
}],
[],
done
,
);
});
});
describe
(
'
requestStages
'
,
()
=>
{
it
(
'
should commit REQUEST_STAGES mutation
'
,
done
=>
{
testAction
(
requestStages
,
null
,
mockedState
,
[{
type
:
types
.
REQUEST_STAGES
}],
[],
done
);
});
});
describe
(
'
fetchStages
'
,
()
=>
{
let
mock
;
beforeEach
(()
=>
{
mockedState
.
stagesEndpoint
=
`
${
TEST_HOST
}
/endpoint.json`
;
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
mock
.
restore
();
});
describe
(
'
success
'
,
()
=>
{
it
(
'
dispatches requestStages and receiveStagesSuccess
'
,
done
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint.json`
).
replyOnce
(
200
,
[{
id
:
121212
,
name
:
'
build
'
}]);
testAction
(
fetchStages
,
null
,
mockedState
,
[],
[
{
type
:
'
requestStages
'
,
},
{
payload
:
[{
id
:
121212
,
name
:
'
build
'
}],
type
:
'
receiveStagesSuccess
'
,
},
],
done
,
);
});
});
describe
(
'
error
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint.json`
).
reply
(
500
);
});
it
(
'
dispatches requestStages and receiveStagesError
'
,
done
=>
{
testAction
(
fetchStages
,
null
,
mockedState
,
[],
[
{
type
:
'
requestStages
'
,
},
{
type
:
'
receiveStagesError
'
,
},
],
done
,
);
});
});
});
describe
(
'
receiveStagesSuccess
'
,
()
=>
{
it
(
'
should commit RECEIVE_STAGES_SUCCESS mutation
'
,
done
=>
{
testAction
(
receiveStagesSuccess
,
{},
mockedState
,
[{
type
:
types
.
RECEIVE_STAGES_SUCCESS
,
payload
:
{}
}],
[],
done
,
);
});
});
describe
(
'
receiveStagesError
'
,
()
=>
{
it
(
'
should commit RECEIVE_STAGES_ERROR mutation
'
,
done
=>
{
testAction
(
receiveStagesError
,
null
,
mockedState
,
[{
type
:
types
.
RECEIVE_STAGES_ERROR
}],
[],
done
,
);
});
});
describe
(
'
requestJobsForStage
'
,
()
=>
{
it
(
'
should commit REQUEST_JOBS_FOR_STAGE mutation
'
,
done
=>
{
testAction
(
requestJobsForStage
,
null
,
mockedState
,
[{
type
:
types
.
REQUEST_JOBS_FOR_STAGE
}],
[],
done
,
);
});
});
describe
(
'
setSelectedStage
'
,
()
=>
{
it
(
'
should commit SET_SELECTED_STAGE mutation
'
,
done
=>
{
testAction
(
setSelectedStage
,
{
name
:
'
build
'
},
mockedState
,
[{
type
:
types
.
SET_SELECTED_STAGE
,
payload
:
{
name
:
'
build
'
}
}],
[],
done
,
);
});
});
describe
(
'
fetchJobsForStage
'
,
()
=>
{
let
mock
;
beforeEach
(()
=>
{
mockedState
.
stageJobsEndpoint
=
`
${
TEST_HOST
}
/endpoint.json`
;
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
mock
.
restore
();
});
describe
(
'
success
'
,
()
=>
{
it
(
'
dispatches setSelectedStage, requestJobsForStage and receiveJobsForStageSuccess
'
,
done
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint.json`
).
replyOnce
(
200
,
[{
id
:
121212
,
name
:
'
build
'
}]);
testAction
(
fetchJobsForStage
,
null
,
mockedState
,
[],
[
{
type
:
'
setSelectedStage
'
,
payload
:
null
,
},
{
type
:
'
requestJobsForStage
'
,
},
{
payload
:
[{
id
:
121212
,
name
:
'
build
'
}],
type
:
'
receiveJobsForStageSuccess
'
,
},
],
done
,
);
});
});
describe
(
'
error
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
`
${
TEST_HOST
}
/endpoint.json`
).
reply
(
500
);
});
it
(
'
dispatches setSelectedStage, requestJobsForStage and receiveJobsForStageError
'
,
done
=>
{
testAction
(
fetchJobsForStage
,
null
,
mockedState
,
[],
[
{
payload
:
null
,
type
:
'
setSelectedStage
'
,
},
{
type
:
'
requestJobsForStage
'
,
},
{
type
:
'
receiveJobsForStageError
'
,
},
],
done
,
);
});
});
});
describe
(
'
receiveJobsForStageSuccess
'
,
()
=>
{
it
(
'
should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation
'
,
done
=>
{
testAction
(
receiveJobsForStageSuccess
,
[{
id
:
121212
,
name
:
'
karma
'
}],
mockedState
,
[{
type
:
types
.
RECEIVE_JOBS_FOR_STAGE_SUCCESS
,
payload
:
[{
id
:
121212
,
name
:
'
karma
'
}]
}],
[],
done
,
);
});
});
describe
(
'
receiveJobsForStageError
'
,
()
=>
{
it
(
'
should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation
'
,
done
=>
{
testAction
(
receiveJobsForStageError
,
null
,
mockedState
,
[{
type
:
types
.
RECEIVE_JOBS_FOR_STAGE_ERROR
}],
[],
done
,
);
});
});
});
spec/javascripts/jobs/store/mutations_spec.js
0 → 100644
View file @
55582b43
import
state
from
'
~/jobs/store/state
'
;
import
mutations
from
'
~/jobs/store/mutations
'
;
import
*
as
types
from
'
~/jobs/store/mutation_types
'
;
describe
(
'
Jobs Store Mutations
'
,
()
=>
{
let
stateCopy
;
const
html
=
'
I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png<br>I
'
;
beforeEach
(()
=>
{
stateCopy
=
state
();
});
describe
(
'
REQUEST_STATUS_FAVICON
'
,
()
=>
{
it
(
'
should set fetchingStatusFavicon to true
'
,
()
=>
{
mutations
[
types
.
REQUEST_STATUS_FAVICON
](
stateCopy
);
expect
(
stateCopy
.
fetchingStatusFavicon
).
toEqual
(
true
);
});
});
describe
(
'
RECEIVE_STATUS_FAVICON_SUCCESS
'
,
()
=>
{
it
(
'
should set fetchingStatusFavicon to false
'
,
()
=>
{
mutations
[
types
.
RECEIVE_STATUS_FAVICON_SUCCESS
](
stateCopy
);
expect
(
stateCopy
.
fetchingStatusFavicon
).
toEqual
(
false
);
});
});
describe
(
'
RECEIVE_STATUS_FAVICON_ERROR
'
,
()
=>
{
it
(
'
should set fetchingStatusFavicon to false
'
,
()
=>
{
mutations
[
types
.
RECEIVE_STATUS_FAVICON_ERROR
](
stateCopy
);
expect
(
stateCopy
.
fetchingStatusFavicon
).
toEqual
(
false
);
});
});
describe
(
'
RECEIVE_TRACE_SUCCESS
'
,
()
=>
{
describe
(
'
when trace has state
'
,
()
=>
{
it
(
'
sets traceState
'
,
()
=>
{
const
stateLog
=
'
eyJvZmZzZXQiOjczNDQ1MSwibl9vcGVuX3RhZ3MiOjAsImZnX2NvbG9yIjpudWxsLCJiZ19jb2xvciI6bnVsbCwic3R5bGVfbWFzayI6MH0=
'
;
mutations
[
types
.
RECEIVE_TRACE_SUCCESS
](
stateCopy
,
{
state
:
stateLog
,
});
expect
(
stateCopy
.
traceState
).
toEqual
(
stateLog
);
});
});
describe
(
'
when traceSize is smaller than the total size
'
,
()
=>
{
it
(
'
sets isTraceSizeVisible to true
'
,
()
=>
{
mutations
[
types
.
RECEIVE_TRACE_SUCCESS
](
stateCopy
,
{
total
:
51184600
,
size
:
1231
});
expect
(
stateCopy
.
isTraceSizeVisible
).
toEqual
(
true
);
});
});
describe
(
'
when traceSize is bigger than the total size
'
,
()
=>
{
it
(
'
sets isTraceSizeVisible to false
'
,
()
=>
{
const
copy
=
Object
.
assign
({},
stateCopy
,
{
traceSize
:
5118460
,
size
:
2321312
});
mutations
[
types
.
RECEIVE_TRACE_SUCCESS
](
copy
,
{
total
:
511846
});
expect
(
copy
.
isTraceSizeVisible
).
toEqual
(
false
);
});
});
it
(
'
sets trace, trace size and isTraceComplete
'
,
()
=>
{
mutations
[
types
.
RECEIVE_TRACE_SUCCESS
](
stateCopy
,
{
append
:
true
,
html
,
size
:
511846
,
complete
:
true
,
});
expect
(
stateCopy
.
trace
).
toEqual
(
html
);
expect
(
stateCopy
.
traceSize
).
toEqual
(
511846
);
expect
(
stateCopy
.
isTraceComplete
).
toEqual
(
true
);
});
});
describe
(
'
STOP_POLLING_TRACE
'
,
()
=>
{
it
(
'
sets isTraceComplete to true
'
,
()
=>
{
mutations
[
types
.
STOP_POLLING_TRACE
](
stateCopy
);
expect
(
stateCopy
.
isTraceComplete
).
toEqual
(
true
);
});
});
describe
(
'
RECEIVE_TRACE_ERROR
'
,
()
=>
{
it
(
'
resets trace state and sets error to true
'
,
()
=>
{
mutations
[
types
.
RECEIVE_TRACE_ERROR
](
stateCopy
);
expect
(
stateCopy
.
isLoadingTrace
).
toEqual
(
false
);
expect
(
stateCopy
.
isTraceComplete
).
toEqual
(
true
);
expect
(
stateCopy
.
hasTraceError
).
toEqual
(
true
);
});
});
describe
(
'
REQUEST_JOB
'
,
()
=>
{
it
(
'
sets isLoading to true
'
,
()
=>
{
mutations
[
types
.
REQUEST_JOB
](
stateCopy
);
expect
(
stateCopy
.
isLoading
).
toEqual
(
true
);
});
});
describe
(
'
RECEIVE_JOB_SUCCESS
'
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
RECEIVE_JOB_SUCCESS
](
stateCopy
,
{
id
:
1312321
});
});
it
(
'
sets is loading to false
'
,
()
=>
{
expect
(
stateCopy
.
isLoading
).
toEqual
(
false
);
});
it
(
'
sets hasError to false
'
,
()
=>
{
expect
(
stateCopy
.
hasError
).
toEqual
(
false
);
});
it
(
'
sets job data
'
,
()
=>
{
expect
(
stateCopy
.
job
).
toEqual
({
id
:
1312321
});
});
});
describe
(
'
RECEIVE_JOB_ERROR
'
,
()
=>
{
it
(
'
resets job data
'
,
()
=>
{
mutations
[
types
.
RECEIVE_JOB_ERROR
](
stateCopy
);
expect
(
stateCopy
.
isLoading
).
toEqual
(
false
);
expect
(
stateCopy
.
hasError
).
toEqual
(
true
);
expect
(
stateCopy
.
job
).
toEqual
({});
});
});
describe
(
'
SCROLL_TO_TOP
'
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
SCROLL_TO_TOP
](
stateCopy
);
});
it
(
'
sets isTraceScrolledToBottom to false
'
,
()
=>
{
expect
(
stateCopy
.
isTraceScrolledToBottom
).
toEqual
(
false
);
});
it
(
'
sets hasBeenScrolled to true
'
,
()
=>
{
expect
(
stateCopy
.
hasBeenScrolled
).
toEqual
(
true
);
});
});
describe
(
'
SCROLL_TO_BOTTOM
'
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
SCROLL_TO_BOTTOM
](
stateCopy
);
});
it
(
'
sets isTraceScrolledToBottom to true
'
,
()
=>
{
expect
(
stateCopy
.
isTraceScrolledToBottom
).
toEqual
(
true
);
});
it
(
'
sets hasBeenScrolled to true
'
,
()
=>
{
expect
(
stateCopy
.
hasBeenScrolled
).
toEqual
(
true
);
});
});
describe
(
'
REQUEST_STAGES
'
,
()
=>
{
it
(
'
sets isLoadingStages to true
'
,
()
=>
{
mutations
[
types
.
REQUEST_STAGES
](
stateCopy
);
expect
(
stateCopy
.
isLoadingStages
).
toEqual
(
true
);
});
});
describe
(
'
RECEIVE_STAGES_SUCCESS
'
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
RECEIVE_STAGES_SUCCESS
](
stateCopy
,
[{
name
:
'
build
'
}]);
});
it
(
'
sets isLoadingStages to false
'
,
()
=>
{
expect
(
stateCopy
.
isLoadingStages
).
toEqual
(
false
);
});
it
(
'
sets stages
'
,
()
=>
{
expect
(
stateCopy
.
stages
).
toEqual
([{
name
:
'
build
'
}]);
});
});
describe
(
'
RECEIVE_STAGES_ERROR
'
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
RECEIVE_STAGES_ERROR
](
stateCopy
);
});
it
(
'
sets isLoadingStages to false
'
,
()
=>
{
expect
(
stateCopy
.
isLoadingStages
).
toEqual
(
false
);
});
it
(
'
resets stages
'
,
()
=>
{
expect
(
stateCopy
.
stages
).
toEqual
([]);
});
});
describe
(
'
REQUEST_JOBS_FOR_STAGE
'
,
()
=>
{
it
(
'
sets isLoadingStages to true
'
,
()
=>
{
mutations
[
types
.
REQUEST_JOBS_FOR_STAGE
](
stateCopy
);
expect
(
stateCopy
.
isLoadingJobs
).
toEqual
(
true
);
});
});
describe
(
'
RECEIVE_JOBS_FOR_STAGE_SUCCESS
'
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
RECEIVE_JOBS_FOR_STAGE_SUCCESS
](
stateCopy
,
[{
name
:
'
karma
'
}]);
});
it
(
'
sets isLoadingJobs to false
'
,
()
=>
{
expect
(
stateCopy
.
isLoadingJobs
).
toEqual
(
false
);
});
it
(
'
sets jobs
'
,
()
=>
{
expect
(
stateCopy
.
jobs
).
toEqual
([{
name
:
'
karma
'
}]);
});
});
describe
(
'
RECEIVE_JOBS_FOR_STAGE_ERROR
'
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
RECEIVE_JOBS_FOR_STAGE_ERROR
](
stateCopy
);
});
it
(
'
sets isLoadingJobs to false
'
,
()
=>
{
expect
(
stateCopy
.
isLoadingJobs
).
toEqual
(
false
);
});
it
(
'
resets jobs
'
,
()
=>
{
expect
(
stateCopy
.
jobs
).
toEqual
([]);
});
});
});
spec/javascripts/lib/utils/common_utils_spec.js
View file @
55582b43
...
@@ -403,6 +403,7 @@ describe('common_utils', () => {
...
@@ -403,6 +403,7 @@ describe('common_utils', () => {
afterEach
(()
=>
{
afterEach
(()
=>
{
document
.
body
.
removeChild
(
document
.
getElementById
(
'
favicon
'
));
document
.
body
.
removeChild
(
document
.
getElementById
(
'
favicon
'
));
});
});
it
(
'
should set page favicon to provided favicon
'
,
()
=>
{
it
(
'
should set page favicon to provided favicon
'
,
()
=>
{
const
faviconPath
=
'
//custom_favicon
'
;
const
faviconPath
=
'
//custom_favicon
'
;
commonUtils
.
setFavicon
(
faviconPath
);
commonUtils
.
setFavicon
(
faviconPath
);
...
@@ -479,17 +480,14 @@ describe('common_utils', () => {
...
@@ -479,17 +480,14 @@ describe('common_utils', () => {
});
});
it
(
'
should reset favicon in case of error
'
,
(
done
)
=>
{
it
(
'
should reset favicon in case of error
'
,
(
done
)
=>
{
mock
.
onGet
(
BUILD_URL
).
networkError
(
);
mock
.
onGet
(
BUILD_URL
).
replyOnce
(
500
);
commonUtils
.
setCiStatusFavicon
(
BUILD_URL
)
commonUtils
.
setCiStatusFavicon
(
BUILD_URL
)
.
then
(()
=>
{
.
catch
(()
=>
{
const
favicon
=
document
.
getElementById
(
'
favicon
'
);
const
favicon
=
document
.
getElementById
(
'
favicon
'
);
expect
(
favicon
.
getAttribute
(
'
href
'
)).
toEqual
(
faviconDataUrl
);
expect
(
favicon
.
getAttribute
(
'
href
'
)).
toEqual
(
faviconDataUrl
);
done
();
done
();
})
});
// Error is already caught in catch() block of setCiStatusFavicon,
// It won't throw another error for us to catch
.
catch
(
done
.
fail
);
});
});
it
(
'
should set page favicon to CI status favicon based on provided status
'
,
(
done
)
=>
{
it
(
'
should set page favicon to CI status favicon based on provided status
'
,
(
done
)
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment