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
1
Merge Requests
1
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
nexedi
gitlab-ce
Commits
75fa9fd2
Commit
75fa9fd2
authored
Dec 04, 2020
by
Kushal Pandya
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '262848-display-schedule' into 'master'
Display schedule See merge request gitlab-org/gitlab!48560
parents
f3eb4750
4ce0c767
Changes
22
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
917 additions
and
23 deletions
+917
-23
app/assets/stylesheets/page_bundles/oncall_schedules.scss
app/assets/stylesheets/page_bundles/oncall_schedules.scss
+112
-0
ee/app/assets/javascripts/oncall_schedules/components/add_schedule_modal.vue
...cripts/oncall_schedules/components/add_schedule_modal.vue
+2
-1
ee/app/assets/javascripts/oncall_schedules/components/oncall_schedule.vue
...vascripts/oncall_schedules/components/oncall_schedule.vue
+61
-0
ee/app/assets/javascripts/oncall_schedules/components/oncall_schedules_wrapper.vue
.../oncall_schedules/components/oncall_schedules_wrapper.vue
+38
-3
ee/app/assets/javascripts/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue
...ts/schedule/components/preset_weeks/weeks_header_item.vue
+74
-0
ee/app/assets/javascripts/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_sub_item.vue
...chedule/components/preset_weeks/weeks_header_sub_item.vue
+74
-0
ee/app/assets/javascripts/oncall_schedules/components/schedule/components/schedule_timeline_section.vue
...ponents/schedule/components/schedule_timeline_section.vue
+32
-0
ee/app/assets/javascripts/oncall_schedules/components/schedule/constants.js
...scripts/oncall_schedules/components/schedule/constants.js
+15
-0
ee/app/assets/javascripts/oncall_schedules/components/schedule/mixins/common_mixin.js
...call_schedules/components/schedule/mixins/common_mixin.js
+36
-0
ee/app/assets/javascripts/oncall_schedules/components/schedule/utils.js
...javascripts/oncall_schedules/components/schedule/utils.js
+34
-0
ee/app/assets/javascripts/oncall_schedules/graphql/get_oncall_schedules.query.graphql
...call_schedules/graphql/get_oncall_schedules.query.graphql
+12
-0
ee/app/assets/javascripts/oncall_schedules/utils.js
ee/app/assets/javascripts/oncall_schedules/utils.js
+18
-0
ee/spec/frontend/oncall_schedule/__snapshots__/add_schedule_modal_spec.js.snap
...ll_schedule/__snapshots__/add_schedule_modal_spec.js.snap
+1
-1
ee/spec/frontend/oncall_schedule/add_schedule_modal_spec.js
ee/spec/frontend/oncall_schedule/add_schedule_modal_spec.js
+1
-1
ee/spec/frontend/oncall_schedule/oncall_schedule_spec.js
ee/spec/frontend/oncall_schedule/oncall_schedule_spec.js
+73
-0
ee/spec/frontend/oncall_schedule/oncall_schedule_wrapper_spec.js
.../frontend/oncall_schedule/oncall_schedule_wrapper_spec.js
+46
-17
ee/spec/frontend/oncall_schedule/schedule/components/preset_weeks/weeks_header_item_spec.js
...chedule/components/preset_weeks/weeks_header_item_spec.js
+98
-0
ee/spec/frontend/oncall_schedule/schedule/components/preset_weeks/weeks_header_sub_item_spec.js
...ule/components/preset_weeks/weeks_header_sub_item_spec.js
+89
-0
ee/spec/frontend/oncall_schedule/schedule/components/schedule_timeline_section_spec.js
...ule/schedule/components/schedule_timeline_section_spec.js
+46
-0
ee/spec/frontend/oncall_schedule/schedule/components/utils_spec.js
...rontend/oncall_schedule/schedule/components/utils_spec.js
+36
-0
ee/spec/frontend/oncall_schedule/utils_spec.js
ee/spec/frontend/oncall_schedule/utils_spec.js
+10
-0
locale/gitlab.pot
locale/gitlab.pot
+9
-0
No files found.
app/assets/stylesheets/page_bundles/oncall_schedules.scss
View file @
75fa9fd2
...
@@ -28,3 +28,115 @@
...
@@ -28,3 +28,115 @@
}
}
}
}
}
}
//// Copied from roadmaps.scss - adapted for on-call schedules
$header-item-height
:
60px
;
$details-cell-width
:
px-to-rem
(
150px
);
$timeline-cell-height
:
32px
;
$timeline-cell-width
:
180px
;
$border-style
:
1px
solid
var
(
--
gray-100
,
$gray-100
);
$gradient-dark-gray
:
rgba
(
0
,
0
,
0
,
0
.15
);
$gradient-gray
:
rgba
(
255
,
255
,
255
,
0
.001
);
$scroll-top-gradient
:
linear-gradient
(
to
bottom
,
$gradient-dark-gray
0%
,
$gradient-gray
100%
);
$scroll-bottom-gradient
:
linear-gradient
(
to
bottom
,
$gradient-gray
0%
,
$gradient-dark-gray
100%
);
$column-right-gradient
:
linear-gradient
(
to
right
,
$gradient-dark-gray
0%
,
$gradient-gray
100%
);
$epic-details-cell-width
:
150px
;
.schedule-shell
{
@include
gl-relative
;
@include
gl-h-full
;
@include
gl-w-full
;
@include
gl-overflow-x-auto
;
@include
gl-border-gray-100
;
@include
gl-border-1
;
@include
gl-border-solid
;
@include
gl-rounded-base
;
}
.timeline-section
{
@include
gl-sticky
;
position
:
-
webkit-sticky
;
@include
gl-top-0
;
z-index
:
20
;
.timeline-header-blank
,
.timeline-header-item
{
@include
float-left
;
height
:
$header-item-height
;
border-bottom
:
$border-style
;
background-color
:
var
(
--
white
,
$white
);
}
.timeline-header-blank
{
@include
gl-sticky
;
position
:
-
webkit-sticky
;
@include
gl-top-0
;
@include
gl-left-0
;
width
:
$details-cell-width
;
z-index
:
2
;
&
:
:
after
{
height
:
$header-item-height
;
@include
gl-content-empty
;
@include
gl-absolute
;
@include
gl-top-0
;
right
:
-
$grid-size
;
width
:
$grid-size
;
@include
gl-pointer-events-none
;
background
:
$column-right-gradient
;
}
}
.timeline-header-item
{
// container size minus left panel width divided by 2 week timeframes
width
:
calc
((
100%
-
#{
$epic-details-cell-width
}
)
/
2
);
&
:last-of-type
.item-label
{
@include
gl-border-r-0
;
}
.item-label
,
.item-sublabel
.sublabel-value
{
color
:
var
(
--
gray-400
,
$gray-400
);
@include
gl-font-weight-normal
;
&
.label-dark
{
@include
gl-text-gray-900
;
}
&
.label-bold
{
@include
gl-font-weight-bold
;
}
}
.item-label
{
padding
:
$gl-padding-8
$gl-padding
;
border-right
:
$border-style
;
border-bottom
:
$border-style
;
}
.item-sublabel
{
@include
gl-relative
;
@include
gl-display-flex
;
.sublabel-value
{
@include
gl-flex-grow-1
;
@include
gl-flex-basis-0
;
text-align
:
center
;
font-size
:
$code-font-size
;
line-height
:
1
.5
;
padding
:
2px
0
;
}
}
.current-day-indicator-header
{
@include
gl-bottom-0
;
height
:
$gl-vert-padding
;
width
:
$gl-vert-padding
;
background-color
:
var
(
--
red-500
,
$red-500
);
border-radius
:
50%
;
transform
:
translateX
(
-3px
);
}
}
}
ee/app/assets/javascripts/oncall_schedules/components/add_schedule_modal.vue
View file @
75fa9fd2
...
@@ -12,6 +12,7 @@ import {
...
@@ -12,6 +12,7 @@ import {
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
createOncallScheduleMutation
from
'
../graphql/create_oncall_schedule.mutation.graphql
'
;
import
createOncallScheduleMutation
from
'
../graphql/create_oncall_schedule.mutation.graphql
'
;
import
{
getFormattedTimezone
}
from
'
../utils
'
;
export
const
i18n
=
{
export
const
i18n
=
{
selectTimezone
:
s__
(
'
OnCallSchedules|Select timezone
'
),
selectTimezone
:
s__
(
'
OnCallSchedules|Select timezone
'
),
...
@@ -145,7 +146,7 @@ export default {
...
@@ -145,7 +146,7 @@ export default {
this
.
form
.
timezone
=
tz
;
this
.
form
.
timezone
=
tz
;
},
},
getFormattedTimezone
(
tz
)
{
getFormattedTimezone
(
tz
)
{
return
__
(
`(UTC
${
tz
.
formatted_offset
}
)
${
tz
.
abbr
}
${
tz
.
name
}
`
);
return
getFormattedTimezone
(
tz
);
},
},
isTimezoneSelected
(
tz
)
{
isTimezoneSelected
(
tz
)
{
return
isEqual
(
tz
,
this
.
form
.
timezone
);
return
isEqual
(
tz
,
this
.
form
.
timezone
);
...
...
ee/app/assets/javascripts/oncall_schedules/components/oncall_schedule.vue
0 → 100644
View file @
75fa9fd2
<
script
>
import
{
GlSprintf
,
GlCard
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
ScheduleTimelineSection
from
'
./schedule/components/schedule_timeline_section.vue
'
;
import
{
getTimeframeForWeeksView
}
from
'
./schedule/utils
'
;
import
{
PRESET_TYPES
}
from
'
./schedule/constants
'
;
import
{
getFormattedTimezone
}
from
'
../utils
'
;
export
const
i18n
=
{
title
:
s__
(
'
OnCallSchedules|On-call schedule
'
),
scheduleForTz
:
s__
(
'
OnCallSchedules|On-call schedule for the %{tzShort}
'
),
};
export
default
{
i18n
,
presetType
:
PRESET_TYPES
.
WEEKS
,
inject
:
[
'
timezones
'
],
components
:
{
GlSprintf
,
GlCard
,
ScheduleTimelineSection
,
},
props
:
{
schedule
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
tzLong
()
{
const
selectedTz
=
this
.
timezones
.
find
(
tz
=>
tz
.
identifier
===
this
.
schedule
.
timezone
);
return
getFormattedTimezone
(
selectedTz
);
},
timeframe
()
{
return
getTimeframeForWeeksView
();
},
},
};
</
script
>
<
template
>
<div>
<h2>
{{
$options
.
i18n
.
title
}}
</h2>
<gl-card>
<template
#header
>
<h3
class=
"gl-font-weight-bold gl-font-lg gl-m-0"
>
{{
schedule
.
name
}}
</h3>
</
template
>
<p
class=
"gl-text-gray-500 gl-mb-5"
>
<gl-sprintf
:message=
"$options.i18n.scheduleForTz"
>
<
template
#tzShort
>
{{
schedule
.
timezone
}}
</
template
>
</gl-sprintf>
| {{ tzLong }}
</p>
<div
class=
"schedule-shell"
>
<schedule-timeline-section
:preset-type=
"$options.presetType"
:timeframe=
"timeframe"
/>
</div>
</gl-card>
</div>
</template>
ee/app/assets/javascripts/oncall_schedules/components/oncall_schedules_wrapper.vue
View file @
75fa9fd2
<
script
>
<
script
>
import
{
GlEmptyState
,
GlButton
,
GlModalDirective
}
from
'
@gitlab/ui
'
;
import
{
GlEmptyState
,
GlButton
,
GlLoadingIcon
,
GlModalDirective
}
from
'
@gitlab/ui
'
;
import
*
as
Sentry
from
'
~/sentry/wrapper
'
;
import
AddScheduleModal
from
'
./add_schedule_modal.vue
'
;
import
AddScheduleModal
from
'
./add_schedule_modal.vue
'
;
import
AddRotationModal
from
'
./rotations/add_rotation_modal.vue
'
;
import
AddRotationModal
from
'
./rotations/add_rotation_modal.vue
'
;
import
OncallSchedule
from
'
./oncall_schedule.vue
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
s__
}
from
'
~/locale
'
;
import
getOncallSchedules
from
'
../graphql/get_oncall_schedules.query.graphql
'
;
import
{
fetchPolicies
}
from
'
~/lib/graphql
'
;
const
addScheduleModalId
=
'
addScheduleModal
'
;
const
addScheduleModalId
=
'
addScheduleModal
'
;
...
@@ -17,23 +21,54 @@ export const i18n = {
...
@@ -17,23 +21,54 @@ export const i18n = {
export
default
{
export
default
{
i18n
,
i18n
,
addScheduleModalId
,
addScheduleModalId
,
inject
:
[
'
emptyOncallSchedulesSvgPath
'
],
inject
:
[
'
emptyOncallSchedulesSvgPath
'
,
'
projectPath
'
],
components
:
{
components
:
{
GlEmptyState
,
GlEmptyState
,
GlButton
,
GlButton
,
GlLoadingIcon
,
AddScheduleModal
,
AddScheduleModal
,
AddRotationModal
,
AddRotationModal
,
OncallSchedule
,
},
},
directives
:
{
directives
:
{
GlModal
:
GlModalDirective
,
GlModal
:
GlModalDirective
,
},
},
methods
:
{},
data
()
{
return
{
schedule
:
{},
};
},
apollo
:
{
schedule
:
{
fetchPolicy
:
fetchPolicies
.
CACHE_AND_NETWORK
,
query
:
getOncallSchedules
,
variables
()
{
return
{
projectPath
:
this
.
projectPath
,
};
},
update
(
data
)
{
return
data
?.
project
?.
incidentManagementOncallSchedules
?.
nodes
?.[
0
]
??
null
;
},
error
(
error
)
{
Sentry
.
captureException
(
error
);
},
},
},
computed
:
{
isLoading
()
{
return
this
.
$apollo
.
queries
.
schedule
.
loading
;
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div>
<div>
<gl-loading-icon
v-if=
"isLoading"
size=
"lg"
class=
"gl-mt-3"
/>
<oncall-schedule
v-else-if=
"schedule"
:schedule=
"schedule"
/>
<gl-empty-state
<gl-empty-state
v-else
:title=
"$options.i18n.emptyState.title"
:title=
"$options.i18n.emptyState.title"
:description=
"$options.i18n.emptyState.description"
:description=
"$options.i18n.emptyState.description"
:svg-path=
"emptyOncallSchedulesSvgPath"
:svg-path=
"emptyOncallSchedulesSvgPath"
...
...
ee/app/assets/javascripts/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue
0 → 100644
View file @
75fa9fd2
<
script
>
import
{
monthInWords
}
from
'
~/lib/utils/datetime_utility
'
;
import
WeeksHeaderSubItem
from
'
./weeks_header_sub_item.vue
'
;
export
default
{
components
:
{
WeeksHeaderSubItem
,
},
props
:
{
timeframeIndex
:
{
type
:
Number
,
required
:
true
,
},
timeframeItem
:
{
type
:
Date
,
required
:
true
,
},
timeframe
:
{
type
:
Array
,
required
:
true
,
},
},
data
()
{
const
currentDate
=
new
Date
();
currentDate
.
setHours
(
0
,
0
,
0
,
0
);
return
{
currentDate
,
};
},
computed
:
{
lastDayOfCurrentWeek
()
{
const
lastDayOfCurrentWeek
=
new
Date
(
this
.
timeframeItem
.
getTime
());
lastDayOfCurrentWeek
.
setDate
(
lastDayOfCurrentWeek
.
getDate
()
+
7
);
return
lastDayOfCurrentWeek
;
},
timelineHeaderLabel
()
{
const
timeframeItemMonth
=
this
.
timeframeItem
.
getMonth
();
const
timeframeItemDate
=
this
.
timeframeItem
.
getDate
();
if
(
this
.
timeframeIndex
===
0
||
(
timeframeItemMonth
===
0
&&
timeframeItemDate
<=
7
))
{
return
`
${
this
.
timeframeItem
.
getFullYear
()}
${
monthInWords
(
this
.
timeframeItem
,
true
,
)}
${
timeframeItemDate
}
`
;
}
return
`
${
monthInWords
(
this
.
timeframeItem
,
true
)}
${
timeframeItemDate
}
`
;
},
timelineHeaderClass
()
{
const
currentDateTime
=
this
.
currentDate
.
getTime
();
const
lastDayOfCurrentWeekTime
=
this
.
lastDayOfCurrentWeek
.
getTime
();
if
(
currentDateTime
>=
this
.
timeframeItem
.
getTime
()
&&
currentDateTime
<=
lastDayOfCurrentWeekTime
)
{
return
'
label-dark label-bold
'
;
}
return
''
;
},
},
};
</
script
>
<
template
>
<span
class=
"timeline-header-item"
>
<div
:class=
"timelineHeaderClass"
class=
"item-label"
>
{{
timelineHeaderLabel
}}
</div>
<weeks-header-sub-item
:timeframe-item=
"timeframeItem"
:current-date=
"currentDate"
/>
</span>
</
template
>
ee/app/assets/javascripts/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_sub_item.vue
0 → 100644
View file @
75fa9fd2
<
script
>
import
{
PRESET_TYPES
}
from
'
../../constants
'
;
import
CommonMixin
from
'
../../mixins/common_mixin
'
;
export
default
{
mixins
:
[
CommonMixin
],
props
:
{
currentDate
:
{
type
:
Date
,
required
:
true
,
},
timeframeItem
:
{
type
:
Date
,
required
:
true
,
},
},
data
()
{
return
{
presetType
:
PRESET_TYPES
.
WEEKS
,
indicatorStyle
:
{},
};
},
computed
:
{
headerSubItems
()
{
const
timeframeItem
=
new
Date
(
this
.
timeframeItem
.
getTime
());
const
headerSubItems
=
new
Array
(
7
)
.
fill
()
.
map
(
(
val
,
i
)
=>
new
Date
(
timeframeItem
.
getFullYear
(),
timeframeItem
.
getMonth
(),
timeframeItem
.
getDate
()
+
i
,
),
);
return
headerSubItems
;
},
},
mounted
()
{
this
.
$nextTick
(()
=>
{
this
.
indicatorStyle
=
this
.
getIndicatorStyles
();
});
},
methods
:
{
getSubItemValueClass
(
subItem
)
{
// Show dark color text only for current & upcoming dates
if
(
subItem
.
getTime
()
===
this
.
currentDate
.
getTime
())
{
return
'
label-dark label-bold
'
;
}
else
if
(
subItem
>
this
.
currentDate
)
{
return
'
label-dark
'
;
}
return
''
;
},
},
};
</
script
>
<
template
>
<div
class=
"item-sublabel"
>
<span
v-for=
"(subItem, index) in headerSubItems"
:key=
"index"
:class=
"getSubItemValueClass(subItem)"
class=
"sublabel-value"
>
{{
subItem
.
getDate
()
}}
</span
>
<span
v-if=
"hasToday"
:style=
"indicatorStyle"
class=
"current-day-indicator-header preset-weeks gl-absolute"
></span>
</div>
</
template
>
ee/app/assets/javascripts/oncall_schedules/components/schedule/components/schedule_timeline_section.vue
0 → 100644
View file @
75fa9fd2
<
script
>
import
WeeksHeaderItem
from
'
./preset_weeks/weeks_header_item.vue
'
;
export
default
{
components
:
{
WeeksHeaderItem
,
},
props
:
{
presetType
:
{
type
:
String
,
required
:
true
,
},
timeframe
:
{
type
:
Array
,
required
:
true
,
},
},
};
</
script
>
<
template
>
<div
class=
"timeline-section clearfix"
>
<span
class=
"timeline-header-blank"
></span>
<weeks-header-item
v-for=
"(timeframeItem, index) in timeframe"
:key=
"index"
:timeframe-index=
"index"
:timeframe-item=
"timeframeItem"
:timeframe=
"timeframe"
/>
</div>
</
template
>
ee/app/assets/javascripts/oncall_schedules/components/schedule/constants.js
0 → 100644
View file @
75fa9fd2
export
const
DAYS_IN_WEEK
=
7
;
export
const
PRESET_TYPES
=
{
WEEKS
:
'
WEEKS
'
,
};
export
const
PRESET_DEFAULTS
=
{
WEEKS
:
{
TIMEFRAME_LENGTH
:
2
,
},
};
export
const
PAST_DATE
=
new
Date
(
new
Date
().
getFullYear
()
-
100
,
0
,
1
);
export
const
FUTURE_DATE
=
new
Date
(
new
Date
().
getFullYear
()
+
100
,
0
,
1
);
ee/app/assets/javascripts/oncall_schedules/components/schedule/mixins/common_mixin.js
0 → 100644
View file @
75fa9fd2
import
{
DAYS_IN_WEEK
}
from
'
../constants
'
;
export
default
{
computed
:
{
hasToday
()
{
const
timeframeItem
=
new
Date
(
this
.
timeframeItem
.
getTime
());
const
headerSubItems
=
new
Array
(
7
)
.
fill
()
.
map
(
(
val
,
i
)
=>
new
Date
(
timeframeItem
.
getFullYear
(),
timeframeItem
.
getMonth
(),
timeframeItem
.
getDate
()
+
i
,
),
);
return
(
this
.
currentDate
.
getTime
()
>=
headerSubItems
[
0
].
getTime
()
&&
this
.
currentDate
.
getTime
()
<=
headerSubItems
[
headerSubItems
.
length
-
1
].
getTime
()
);
},
},
methods
:
{
getIndicatorStyles
()
{
// as we start schedule scale from the current date the indicator will always be on the first date. So we find
// the percentage of space one day cell takes and divide it by 2 cause the tick is in the middle of the cell.
// It might be updated to more precise position - time of the day
const
left
=
100
/
DAYS_IN_WEEK
/
2
;
return
{
left
:
`
${
left
}
%`
,
};
},
},
};
ee/app/assets/javascripts/oncall_schedules/components/schedule/utils.js
0 → 100644
View file @
75fa9fd2
import
{
newDate
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
PRESET_DEFAULTS
,
DAYS_IN_WEEK
}
from
'
./constants
'
;
/**
* This method returns array of Dates representing 2-weeks timeframe based on provided initialDate
*
* For eg; If initialDate is 31th Dec 2017
* we show 2 weeks starting from the current date
* So returned array from this method will be;
* [
* 31 Dec 2017, 7 Jan 2018
* ]
*
* @param {Date} initialDate
*/
export
const
getTimeframeForWeeksView
=
(
initialDate
=
new
Date
())
=>
{
const
timeframe
=
[];
const
startDate
=
newDate
(
initialDate
);
startDate
.
setHours
(
0
,
0
,
0
,
0
);
const
rangeLength
=
PRESET_DEFAULTS
.
WEEKS
.
TIMEFRAME_LENGTH
;
// Iterate for the length of this preset
for
(
let
i
=
0
;
i
<
rangeLength
;
i
+=
1
)
{
// Push date to timeframe only when day is
// the first day of the next week (if initial date is Tuesday next date will be also Tuesday but of the next week)
timeframe
.
push
(
newDate
(
startDate
));
// Move date to the next in a week
startDate
.
setDate
(
startDate
.
getDate
()
+
DAYS_IN_WEEK
);
}
return
timeframe
;
};
ee/app/assets/javascripts/oncall_schedules/graphql/get_oncall_schedules.query.graphql
0 → 100644
View file @
75fa9fd2
query
getOncallSchedules
(
$projectPath
:
ID
!)
{
project
(
fullPath
:
$projectPath
)
{
incidentManagementOncallSchedules
{
nodes
{
iid
name
description
timezone
}
}
}
}
ee/app/assets/javascripts/oncall_schedules/utils.js
0 → 100644
View file @
75fa9fd2
import
{
sprintf
,
__
}
from
'
~/locale
'
;
/**
* Returns formatted timezone string, e.g. (UTC-09:00) AKST Alaska
*
* @param {Object} tz
* @param {String} tz.name
* @param {String} tz.formatted_offset
* @param {String} tz.abbr
*
* @returns {String}
*/
export
const
getFormattedTimezone
=
tz
=>
{
return
sprintf
(
__
(
'
(UTC%{offset}) %{timezone}
'
),
{
offset
:
tz
.
formatted_offset
,
timezone
:
`
${
tz
.
abbr
}
${
tz
.
name
}
`
,
});
};
ee/spec/frontend/oncall_schedule/__snapshots__/add_schedule_modal_spec.js.snap
View file @
75fa9fd2
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Add
ScheduleM
odal renders modal layout 1`] = `
exports[`Add
schedule m
odal renders modal layout 1`] = `
<gl-modal-stub
<gl-modal-stub
actioncancel="[object Object]"
actioncancel="[object Object]"
actionprimary="[object Object]"
actionprimary="[object Object]"
...
...
ee/spec/frontend/oncall_schedule/add_schedule_modal_spec.js
View file @
75fa9fd2
...
@@ -4,7 +4,7 @@ import waitForPromises from 'helpers/wait_for_promises';
...
@@ -4,7 +4,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import
AddScheduleModal
,
{
i18n
}
from
'
ee/oncall_schedules/components/add_schedule_modal.vue
'
;
import
AddScheduleModal
,
{
i18n
}
from
'
ee/oncall_schedules/components/add_schedule_modal.vue
'
;
import
mockTimezones
from
'
./mocks/mockTimezones.json
'
;
import
mockTimezones
from
'
./mocks/mockTimezones.json
'
;
describe
(
'
Add
ScheduleM
odal
'
,
()
=>
{
describe
(
'
Add
schedule m
odal
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
const
projectPath
=
'
group/project
'
;
const
projectPath
=
'
group/project
'
;
const
mutate
=
jest
.
fn
();
const
mutate
=
jest
.
fn
();
...
...
ee/spec/frontend/oncall_schedule/oncall_schedule_spec.js
0 → 100644
View file @
75fa9fd2
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlCard
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
OnCallSchedule
,
{
i18n
}
from
'
ee/oncall_schedules/components/oncall_schedule.vue
'
;
import
ScheduleTimelineSection
from
'
ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue
'
;
import
*
as
utils
from
'
ee/oncall_schedules/components/schedule/utils
'
;
import
*
as
commonUtils
from
'
ee/oncall_schedules/utils
'
;
import
{
PRESET_TYPES
}
from
'
ee/oncall_schedules/components/schedule/constants
'
;
import
mockTimezones
from
'
./mocks/mockTimezones.json
'
;
describe
(
'
On-call schedule
'
,
()
=>
{
let
wrapper
;
const
lastTz
=
mockTimezones
[
mockTimezones
.
length
-
1
];
const
mockSchedule
=
{
description
:
'
monitor description
'
,
iid
:
'
3
'
,
name
:
'
monitor schedule
'
,
timezone
:
lastTz
.
identifier
,
};
const
mockWeeksTimeFrame
=
[
'
31 Dec 2020
'
,
'
7 Jan 2021
'
,
'
14 Jan 2021
'
];
const
formattedTimezone
=
'
(UTC-09:00) AKST Alaska
'
;
function
mountComponent
({
schedule
}
=
{})
{
wrapper
=
shallowMount
(
OnCallSchedule
,
{
propsData
:
{
schedule
,
},
provide
:
{
timezones
:
mockTimezones
,
},
stubs
:
{
GlCard
,
GlSprintf
,
},
});
}
beforeEach
(()
=>
{
jest
.
spyOn
(
utils
,
'
getTimeframeForWeeksView
'
).
mockReturnValue
(
mockWeeksTimeFrame
);
jest
.
spyOn
(
commonUtils
,
'
getFormattedTimezone
'
).
mockReturnValue
(
formattedTimezone
);
mountComponent
({
schedule
:
mockSchedule
});
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
const
findCardHeader
=
()
=>
wrapper
.
find
(
'
.gl-card-header
'
);
const
findCardDescription
=
()
=>
wrapper
.
find
(
'
.gl-card-body
'
);
const
findScheduleTimeline
=
()
=>
findCardDescription
().
find
(
ScheduleTimelineSection
);
it
(
'
shows schedule title
'
,
()
=>
{
expect
(
findCardHeader
().
text
()).
toBe
(
mockSchedule
.
name
);
});
it
(
'
shows timezone info
'
,
()
=>
{
const
shortTz
=
i18n
.
scheduleForTz
.
replace
(
'
%{tzShort}
'
,
lastTz
.
identifier
);
const
longTz
=
formattedTimezone
;
const
description
=
findCardDescription
().
text
();
expect
(
description
).
toContain
(
shortTz
);
expect
(
description
).
toContain
(
longTz
);
});
it
(
'
renders ScheduleShell
'
,
()
=>
{
const
timeline
=
findScheduleTimeline
();
expect
(
timeline
.
exists
()).
toBe
(
true
);
expect
(
timeline
.
props
()).
toEqual
({
presetType
:
PRESET_TYPES
.
WEEKS
,
timeframe
:
mockWeeksTimeFrame
,
});
});
});
ee/spec/frontend/oncall_schedule/oncall_schedule_wrapper_spec.js
View file @
75fa9fd2
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlEmptyState
}
from
'
@gitlab/ui
'
;
import
{
GlEmptyState
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
OnCallScheduleWrapper
,
{
import
OnCallScheduleWrapper
,
{
i18n
,
i18n
,
}
from
'
ee/oncall_schedules/components/oncall_schedules_wrapper.vue
'
;
}
from
'
ee/oncall_schedules/components/oncall_schedules_wrapper.vue
'
;
import
OnCallSchedule
from
'
ee/oncall_schedules/components/oncall_schedule.vue
'
;
describe
(
'
AlertManagementEmptyState
'
,
()
=>
{
describe
(
'
On-call schedule wrapper
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
const
emptyOncallSchedulesSvgPath
=
'
illustration/path.svg
'
;
const
emptyOncallSchedulesSvgPath
=
'
illustration/path.svg
'
;
const
projectPath
=
'
group/project
'
;
function
mountComponent
({
loading
,
schedule
}
=
{})
{
const
$apollo
=
{
queries
:
{
schedule
:
{
loading
,
},
},
};
function
mountComponent
()
{
wrapper
=
shallowMount
(
OnCallScheduleWrapper
,
{
wrapper
=
shallowMount
(
OnCallScheduleWrapper
,
{
data
()
{
return
{
schedule
,
};
},
provide
:
{
provide
:
{
emptyOncallSchedulesSvgPath
,
emptyOncallSchedulesSvgPath
,
projectPath
,
},
},
mocks
:
{
$apollo
},
});
});
}
}
beforeEach
(()
=>
{
mountComponent
();
});
afterEach
(()
=>
{
afterEach
(()
=>
{
if
(
wrapper
)
{
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
wrapper
=
null
;
}
});
});
const
findLoader
=
()
=>
wrapper
.
find
(
GlLoadingIcon
);
const
findEmptyState
=
()
=>
wrapper
.
find
(
GlEmptyState
);
const
findEmptyState
=
()
=>
wrapper
.
find
(
GlEmptyState
);
const
findSchedule
=
()
=>
wrapper
.
find
(
OnCallSchedule
);
describe
(
'
Empty state
'
,
()
=>
{
it
(
'
shows a loader while data is requested
'
,
()
=>
{
it
(
'
shows empty state and passed correct attributes to it
'
,
()
=>
{
mountComponent
({
loading
:
true
});
expect
(
findEmptyState
().
exists
()).
toBe
(
true
);
expect
(
findLoader
().
exists
()).
toBe
(
true
);
expect
(
findEmptyState
().
attributes
(
'
title
'
)).
toBe
(
i18n
.
emptyState
.
title
);
});
expect
(
findEmptyState
().
attributes
(
'
description
'
)).
toBe
(
i18n
.
emptyState
.
description
);
expect
(
findEmptyState
().
attributes
(
'
svgpath
'
)).
toBe
(
emptyOncallSchedulesSvgPath
);
it
(
'
shows empty state and passed correct attributes to it when not loading and no schedule
'
,
()
=>
{
mountComponent
({
loading
:
false
,
schedule
:
null
});
const
emptyState
=
findEmptyState
();
expect
(
emptyState
.
exists
()).
toBe
(
true
);
expect
(
emptyState
.
attributes
()).
toEqual
({
title
:
i18n
.
emptyState
.
title
,
svgpath
:
emptyOncallSchedulesSvgPath
,
description
:
i18n
.
emptyState
.
description
,
});
});
});
});
it
(
'
renders On-call schedule when data received
'
,
()
=>
{
mountComponent
({
loading
:
false
,
schedule
:
{
name
:
'
monitor rotation
'
}
});
const
schedule
=
findSchedule
();
expect
(
findLoader
().
exists
()).
toBe
(
false
);
expect
(
findEmptyState
().
exists
()).
toBe
(
false
);
expect
(
schedule
.
exists
()).
toBe
(
true
);
});
});
});
ee/spec/frontend/oncall_schedule/schedule/components/preset_weeks/weeks_header_item_spec.js
0 → 100644
View file @
75fa9fd2
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
WeeksHeaderItemComponent
from
'
ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue
'
;
import
{
getTimeframeForWeeksView
}
from
'
ee/oncall_schedules/components/schedule/utils
'
;
describe
(
'
WeeksHeaderItemComponent
'
,
()
=>
{
let
wrapper
;
const
mockTimeframeIndex
=
0
;
const
mockTimeframeInitialDate
=
new
Date
(
2018
,
0
,
1
);
const
mockTimeframeWeeks
=
getTimeframeForWeeksView
(
mockTimeframeInitialDate
);
function
mountComponent
({
timeframeIndex
=
mockTimeframeIndex
,
timeframeItem
=
mockTimeframeWeeks
[
mockTimeframeIndex
],
timeframe
=
mockTimeframeWeeks
,
})
{
wrapper
=
shallowMount
(
WeeksHeaderItemComponent
,
{
propsData
:
{
timeframeIndex
,
timeframeItem
,
timeframe
,
},
});
}
beforeEach
(()
=>
{
mountComponent
({});
});
afterEach
(()
=>
{
if
(
wrapper
)
{
wrapper
.
destroy
();
wrapper
=
null
;
}
});
describe
(
'
data
'
,
()
=>
{
it
(
'
returns default data props
'
,
()
=>
{
const
currentDate
=
new
Date
();
expect
(
wrapper
.
vm
.
currentDate
.
getDate
()).
toBe
(
currentDate
.
getDate
());
});
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
lastDayOfCurrentWeek
'
,
()
=>
{
it
(
'
returns date object representing last day of the week as set in `timeframeItem`
'
,
()
=>
{
expect
(
wrapper
.
vm
.
lastDayOfCurrentWeek
.
getDate
()).
toBe
(
mockTimeframeWeeks
[
mockTimeframeIndex
].
getDate
()
+
7
,
);
});
});
describe
(
'
timelineHeaderLabel
'
,
()
=>
{
it
(
'
returns string containing Year, Month and Date for the first timeframe item in the entire timeframe
'
,
()
=>
{
expect
(
wrapper
.
vm
.
timelineHeaderLabel
).
toBe
(
'
2018 Jan 1
'
);
});
it
(
'
returns string containing Year, Month and Date for timeframe item when it is first week of the year
'
,
()
=>
{
mountComponent
({
timeframeIndex
:
3
,
timeframeItem
:
new
Date
(
2019
,
0
,
6
),
});
expect
(
wrapper
.
vm
.
timelineHeaderLabel
).
toBe
(
'
2019 Jan 6
'
);
});
it
(
'
returns string containing only Month and Date timeframe item when it is somewhere in the middle of timeframe
'
,
()
=>
{
mountComponent
({
timeframeIndex
:
mockTimeframeIndex
+
1
,
timeframeItem
:
mockTimeframeWeeks
[
mockTimeframeIndex
+
1
],
});
expect
(
wrapper
.
vm
.
timelineHeaderLabel
).
toBe
(
'
Jan 8
'
);
});
});
describe
(
'
timelineHeaderClass
'
,
()
=>
{
it
(
'
returns empty string when timeframeItem week is less than current week
'
,
()
=>
{
expect
(
wrapper
.
vm
.
timelineHeaderClass
).
toBe
(
''
);
});
it
(
'
returns string containing `label-dark label-bold` when current week is same as timeframeItem week
'
,
()
=>
{
wrapper
.
setData
({
currentDate
:
mockTimeframeWeeks
[
mockTimeframeIndex
]
});
expect
(
wrapper
.
vm
.
timelineHeaderClass
).
toBe
(
'
label-dark label-bold
'
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element with class `timeline-header-item`
'
,
()
=>
{
expect
(
wrapper
.
classes
()).
toContain
(
'
timeline-header-item
'
);
});
it
(
'
renders item label element class `item-label` and value as `timelineHeaderLabel`
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.item-label
'
).
text
()).
toBe
(
'
2018 Jan 1
'
);
});
});
});
ee/spec/frontend/oncall_schedule/schedule/components/preset_weeks/weeks_header_sub_item_spec.js
0 → 100644
View file @
75fa9fd2
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
WeeksHeaderSubItemComponent
from
'
ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_sub_item.vue
'
;
import
{
getTimeframeForWeeksView
}
from
'
ee/oncall_schedules/components/schedule/utils
'
;
import
{
PRESET_TYPES
}
from
'
ee/oncall_schedules/components/schedule/constants
'
;
describe
(
'
MonthsHeaderSubItemComponent
'
,
()
=>
{
let
wrapper
;
const
mockTimeframeInitialDate
=
new
Date
(
2018
,
0
,
1
);
const
mockTimeframeWeeks
=
getTimeframeForWeeksView
(
mockTimeframeInitialDate
);
function
mountComponent
({
currentDate
=
mockTimeframeWeeks
[
0
],
timeframeItem
=
mockTimeframeWeeks
[
0
],
})
{
wrapper
=
shallowMount
(
WeeksHeaderSubItemComponent
,
{
propsData
:
{
currentDate
,
timeframeItem
,
},
});
}
beforeEach
(()
=>
{
mountComponent
({});
});
afterEach
(()
=>
{
if
(
wrapper
)
{
wrapper
.
destroy
();
wrapper
=
null
;
}
});
describe
(
'
data
'
,
()
=>
{
it
(
'
initializes `presetType` and `indicatorStyles` data props
'
,
()
=>
{
expect
(
wrapper
.
vm
.
presetType
).
toBe
(
PRESET_TYPES
.
WEEKS
);
expect
(
wrapper
.
vm
.
indicatorStyle
).
toBeDefined
();
});
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
headerSubItems
'
,
()
=>
{
it
(
'
returns `headerSubItems` array of dates containing days of week from timeframeItem
'
,
()
=>
{
expect
(
wrapper
.
vm
.
headerSubItems
).
toBeInstanceOf
(
Array
);
expect
(
wrapper
.
vm
.
headerSubItems
).
toHaveLength
(
7
);
wrapper
.
vm
.
headerSubItems
.
forEach
(
subItem
=>
{
expect
(
subItem
).
toBeInstanceOf
(
Date
);
});
});
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
getSubItemValueClass
'
,
()
=>
{
it
(
'
returns string containing `label-dark` when provided subItem is greater than current week day
'
,
()
=>
{
mountComponent
({
currentDate
:
new
Date
(
2018
,
0
,
1
),
// Jan 1, 2018
});
const
subItem
=
new
Date
(
2018
,
0
,
25
);
// Jan 25, 2018
expect
(
wrapper
.
vm
.
getSubItemValueClass
(
subItem
)).
toBe
(
'
label-dark
'
);
});
it
(
'
returns string containing `label-dark label-bold` when provided subItem is same as current week day
'
,
()
=>
{
const
currentDate
=
new
Date
(
2018
,
0
,
25
);
mountComponent
({
currentDate
,
});
const
subItem
=
currentDate
;
expect
(
wrapper
.
vm
.
getSubItemValueClass
(
subItem
)).
toBe
(
'
label-dark label-bold
'
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element with class `item-sublabel`
'
,
()
=>
{
expect
(
wrapper
.
classes
()).
toContain
(
'
item-sublabel
'
);
});
it
(
'
renders sub item element with class `sublabel-value`
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.sublabel-value
'
).
exists
()).
toBe
(
true
);
});
it
(
'
renders element with class `current-day-indicator-header` when hasToday is true
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.current-day-indicator-header.preset-weeks
'
).
exists
()).
toBe
(
true
);
});
});
});
ee/spec/frontend/oncall_schedule/schedule/components/schedule_timeline_section_spec.js
0 → 100644
View file @
75fa9fd2
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
ScheduleTimelineSection
from
'
ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue
'
;
import
WeeksHeaderItem
from
'
ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue
'
;
import
{
getTimeframeForWeeksView
}
from
'
ee/oncall_schedules/components/schedule/utils
'
;
import
{
PRESET_TYPES
}
from
'
ee/oncall_schedules/components/schedule/constants
'
;
describe
(
'
RoadmapTimelineSectionComponent
'
,
()
=>
{
let
wrapper
;
const
mockTimeframeInitialDate
=
new
Date
(
2018
,
0
,
1
);
const
mockTimeframeWeeks
=
getTimeframeForWeeksView
(
mockTimeframeInitialDate
);
function
mountComponent
({
presetType
=
PRESET_TYPES
.
WEEKS
,
timeframe
=
mockTimeframeWeeks
,
}
=
{})
{
wrapper
=
shallowMount
(
ScheduleTimelineSection
,
{
propsData
:
{
presetType
,
timeframe
,
},
});
}
beforeEach
(()
=>
{
mountComponent
({});
});
afterEach
(()
=>
{
if
(
wrapper
)
{
wrapper
.
destroy
();
wrapper
=
null
;
}
});
it
(
'
renders component container element with class `timeline-section`
'
,
()
=>
{
expect
(
wrapper
.
classes
()).
toContain
(
'
timeline-section
'
);
});
it
(
'
renders empty header cell element with class `timeline-header-blank`
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.timeline-header-blank
'
).
exists
()).
toBe
(
true
);
});
it
(
'
renders weeks header items based on timeframe data
'
,
()
=>
{
expect
(
wrapper
.
findAll
(
WeeksHeaderItem
).
length
).
toBe
(
mockTimeframeWeeks
.
length
);
});
});
ee/spec/frontend/oncall_schedule/schedule/components/utils_spec.js
0 → 100644
View file @
75fa9fd2
import
{
getTimeframeForWeeksView
}
from
'
ee/oncall_schedules/components/schedule/utils
'
;
describe
(
'
getTimeframeForWeeksView
'
,
()
=>
{
const
mockTimeframeInitialDate
=
new
Date
(
2018
,
0
,
1
);
const
timeframe
=
getTimeframeForWeeksView
(
mockTimeframeInitialDate
);
it
(
'
returns timeframe with total of 2 weeks
'
,
()
=>
{
expect
(
timeframe
).
toHaveLength
(
2
);
});
it
(
'
first timeframe item refers to the start date
'
,
()
=>
{
const
timeframeItem
=
timeframe
[
0
];
const
expectedMonth
=
{
year
:
2018
,
month
:
0
,
date
:
1
,
};
expect
(
timeframeItem
.
getFullYear
()).
toBe
(
expectedMonth
.
year
);
expect
(
timeframeItem
.
getMonth
()).
toBe
(
expectedMonth
.
month
);
expect
(
timeframeItem
.
getDate
()).
toBe
(
expectedMonth
.
date
);
});
it
(
'
second timeframe item refers to first date of the next week week
'
,
()
=>
{
const
timeframeItem
=
timeframe
[
timeframe
.
length
-
1
];
const
expectedMonth
=
{
year
:
2018
,
month
:
0
,
date
:
8
,
};
expect
(
timeframeItem
.
getFullYear
()).
toBe
(
expectedMonth
.
year
);
expect
(
timeframeItem
.
getMonth
()).
toBe
(
expectedMonth
.
month
);
expect
(
timeframeItem
.
getDate
()).
toBe
(
expectedMonth
.
date
);
});
});
ee/spec/frontend/oncall_schedule/utils_spec.js
0 → 100644
View file @
75fa9fd2
import
{
getFormattedTimezone
}
from
'
ee/oncall_schedules/utils
'
;
import
mockTimezones
from
'
./mocks/mockTimezones.json
'
;
describe
(
'
getFormattedTimezone
'
,
()
=>
{
it
(
'
formats the timezone
'
,
()
=>
{
const
tz
=
mockTimezones
[
0
];
const
expectedValue
=
`(UTC
${
tz
.
formatted_offset
}
)
${
tz
.
abbr
}
${
tz
.
name
}
`
;
expect
(
getFormattedTimezone
(
tz
)).
toBe
(
expectedValue
);
});
});
locale/gitlab.pot
View file @
75fa9fd2
...
@@ -941,6 +941,9 @@ msgstr ""
...
@@ -941,6 +941,9 @@ msgstr ""
msgid "(No changes)"
msgid "(No changes)"
msgstr ""
msgstr ""
msgid "(UTC%{offset}) %{timezone}"
msgstr ""
msgid "(check progress)"
msgid "(check progress)"
msgstr ""
msgstr ""
...
@@ -19112,6 +19115,12 @@ msgstr ""
...
@@ -19112,6 +19115,12 @@ msgstr ""
msgid "OnCallSchedules|Failed to add schedule"
msgid "OnCallSchedules|Failed to add schedule"
msgstr ""
msgstr ""
msgid "OnCallSchedules|On-call schedule"
msgstr ""
msgid "OnCallSchedules|On-call schedule for the %{tzShort}"
msgstr ""
msgid "OnCallSchedules|Rotation length"
msgid "OnCallSchedules|Rotation length"
msgstr ""
msgstr ""
...
...
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