Commit d041e4f6 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'prettify-all-the-ee-things' into 'master'

Prettify all the things (EE)

See merge request gitlab-org/gitlab-ee!8114
parents fd633eed 83827b7d
<script>
import Flash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import GitlabSlackService from '../services/gitlab_slack_service';
export default {
props: {
projects: {
type: Array,
required: false,
default: () => [],
},
isSignedIn: {
type: Boolean,
required: true,
},
gitlabForSlackGifPath: {
type: String,
required: true,
},
signInPath: {
type: String,
required: true,
},
slackLinkPath: {
type: String,
required: true,
},
gitlabLogoPath: {
type: String,
required: true,
},
slackLogoPath: {
type: String,
required: true,
},
docsPath: {
type: String,
required: true,
},
import Flash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import GitlabSlackService from '../services/gitlab_slack_service';
export default {
props: {
projects: {
type: Array,
required: false,
default: () => [],
},
isSignedIn: {
type: Boolean,
required: true,
},
gitlabForSlackGifPath: {
type: String,
required: true,
},
signInPath: {
type: String,
required: true,
},
data() {
return {
popupOpen: false,
selectedProjectId: this.projects && this.projects.length ? this.projects[0].id : 0,
};
slackLinkPath: {
type: String,
required: true,
},
computed: {
doubleHeadedArrowSvg() {
return gl.utils.spriteIcon('double-headed-arrow');
},
gitlabLogoPath: {
type: String,
required: true,
},
slackLogoPath: {
type: String,
required: true,
},
docsPath: {
type: String,
required: true,
},
},
data() {
return {
popupOpen: false,
selectedProjectId: this.projects && this.projects.length ? this.projects[0].id : 0,
};
},
computed: {
doubleHeadedArrowSvg() {
return gl.utils.spriteIcon('double-headed-arrow');
},
arrowRightSvg() {
return gl.utils.spriteIcon('arrow-right');
},
arrowRightSvg() {
return gl.utils.spriteIcon('arrow-right');
},
hasProjects() {
return this.projects.length > 0;
},
hasProjects() {
return this.projects.length > 0;
},
},
methods: {
togglePopup() {
this.popupOpen = !this.popupOpen;
},
methods: {
togglePopup() {
this.popupOpen = !this.popupOpen;
},
addToSlack() {
GitlabSlackService.addToSlack(this.slackLinkPath, this.selectedProjectId)
.then(response => redirectTo(response.data.add_to_slack_link))
.catch(() => Flash('Unable to build Slack link.'));
},
addToSlack() {
GitlabSlackService.addToSlack(this.slackLinkPath, this.selectedProjectId)
.then(response => redirectTo(response.data.add_to_slack_link))
.catch(() => Flash('Unable to build Slack link.'));
},
};
},
};
</script>
<template>
......
......@@ -2,48 +2,60 @@ import $ from 'jquery';
import _ from 'underscore';
export default () => {
$('.approver-list').on('click', '.unsaved-approvers.approver .btn-remove', function approverListClickCallback(ev) {
const removeElement = $(this).closest('li');
const approverId = parseInt(removeElement.attr('id').replace('user_', ''), 10);
const approverIds = $('input#merge_request_approver_ids');
const skipUsers = approverIds.data('skipUsers') || [];
const approverIndex = skipUsers.indexOf(approverId);
$('.approver-list').on(
'click',
'.unsaved-approvers.approver .btn-remove',
function approverListClickCallback(ev) {
const removeElement = $(this).closest('li');
const approverId = parseInt(removeElement.attr('id').replace('user_', ''), 10);
const approverIds = $('input#merge_request_approver_ids');
const skipUsers = approverIds.data('skipUsers') || [];
const approverIndex = skipUsers.indexOf(approverId);
removeElement.remove();
removeElement.remove();
if (approverIndex > -1) {
approverIds.data('skipUsers', skipUsers.splice(approverIndex, 1));
}
if (approverIndex > -1) {
approverIds.data('skipUsers', skipUsers.splice(approverIndex, 1));
}
ev.preventDefault();
});
ev.preventDefault();
},
);
$('.approver-list').on('click', '.unsaved-approvers.approver-group .btn-remove', function approverListRemoveClickCallback(ev) {
const removeElement = $(this).closest('li');
const approverGroupId = parseInt(removeElement.attr('id').replace('group_', ''), 10);
const approverGroupIds = $('input#merge_request_approver_group_ids');
const skipGroups = approverGroupIds.data('skipGroups') || [];
const approverGroupIndex = skipGroups.indexOf(approverGroupId);
$('.approver-list').on(
'click',
'.unsaved-approvers.approver-group .btn-remove',
function approverListRemoveClickCallback(ev) {
const removeElement = $(this).closest('li');
const approverGroupId = parseInt(removeElement.attr('id').replace('group_', ''), 10);
const approverGroupIds = $('input#merge_request_approver_group_ids');
const skipGroups = approverGroupIds.data('skipGroups') || [];
const approverGroupIndex = skipGroups.indexOf(approverGroupId);
removeElement.remove();
removeElement.remove();
if (approverGroupIndex > -1) {
approverGroupIds.data('skipGroups', skipGroups.splice(approverGroupIndex, 1));
}
if (approverGroupIndex > -1) {
approverGroupIds.data('skipGroups', skipGroups.splice(approverGroupIndex, 1));
}
ev.preventDefault();
});
ev.preventDefault();
},
);
$('form.merge-request-form').on('submit', function mergeRequestFormSubmitCallback() {
if ($('input#merge_request_approver_ids').length) {
let approverIds = $.map($('li.unsaved-approvers.approver').not('.approver-template'), li => li.id.replace('user_', ''));
let approverIds = $.map($('li.unsaved-approvers.approver').not('.approver-template'), li =>
li.id.replace('user_', ''),
);
const approversInput = $(this).find('input#merge_request_approver_ids');
approverIds = approverIds.concat(approversInput.val().split(','));
approversInput.val(_.compact(approverIds).join(','));
}
if ($('input#merge_request_approver_group_ids').length) {
let approverGroupIds = $.map($('li.unsaved-approvers.approver-group'), li => li.id.replace('group_', ''));
let approverGroupIds = $.map($('li.unsaved-approvers.approver-group'), li =>
li.id.replace('group_', ''),
);
const approverGroupsInput = $(this).find('input#merge_request_approver_group_ids');
approverGroupIds = approverGroupIds.concat(approverGroupsInput.val().split(','));
approverGroupsInput.val(_.compact(approverGroupIds).join(','));
......@@ -58,9 +70,11 @@ export default () => {
return false;
}
const approverItemHTML = $('.unsaved-approvers.approver-template').clone()
const approverItemHTML = $('.unsaved-approvers.approver-template')
.clone()
.removeClass('hide approver-template')[0]
.outerHTML.replace(/\{approver_name\}/g, username).replace(/\{user_id\}/g, userId);
.outerHTML.replace(/\{approver_name\}/g, username)
.replace(/\{user_id\}/g, userId);
$('.no-approvers').remove();
$('.approver-list').append(approverItemHTML);
......
import BoardsListSelector from './boards_list_selector/index';
export default function () {
export default function() {
const $addListEl = document.querySelector('#js-add-list');
return new BoardsListSelector({
propsData: {
......
......@@ -20,8 +20,8 @@ export default Board.extend({
}
const { issuesSize, totalWeight } = this.list;
return sprintf(__(
`${n__('%d issue', '%d issues', issuesSize)} with %{totalWeight} total weight`),
return sprintf(
__(`${n__('%d issue', '%d issues', issuesSize)} with %{totalWeight} total weight`),
{
totalWeight,
},
......
......@@ -43,15 +43,17 @@ export default Vue.extend({
})
.catch(() => {
this.loading = false;
Flash(sprintf(__('Something went wrong while fetching %{listType} list'), {
listType: this.listType,
}));
Flash(
sprintf(__('Something went wrong while fetching %{listType} list'), {
listType: this.listType,
}),
);
});
},
filterItems(term, items) {
const query = term.toLowerCase();
return items.filter((item) => {
return items.filter(item => {
const name = item.name ? item.name.toLowerCase() : item.title.toLowerCase();
const foundName = name.indexOf(query) > -1;
......
......@@ -31,7 +31,7 @@ export default {
if (!this.query) return this.items;
const query = this.query.toLowerCase();
return this.items.filter((item) => {
return this.items.filter(item => {
const name = item.name ? item.name.toLowerCase() : item.title.toLowerCase();
if (this.listType === 'milestones') {
......
......@@ -140,8 +140,7 @@ export default Vue.extend({
handleDropdownTabClick(e) {
const $addListEl = $('#js-add-list');
$addListEl.data('preventClose', true);
if (e.target.dataset.action === 'tab-assignees' &&
!this.hasAssigneesListMounted) {
if (e.target.dataset.action === 'tab-assignees' && !this.hasAssigneesListMounted) {
this.assigneeList = AssigneeList();
this.hasAssigneesListMounted = true;
}
......
import BoardsListSelector from './boards_list_selector';
export default function () {
export default function() {
const $addListEl = document.querySelector('#js-add-list');
return new BoardsListSelector({
propsData: {
......
<script>
import MilestoneSelect from '~/milestone_select';
import MilestoneSelect from '~/milestone_select';
const ANY_MILESTONE = 'Any Milestone';
const NO_MILESTONE = 'No Milestone';
const ANY_MILESTONE = 'Any Milestone';
const NO_MILESTONE = 'No Milestone';
export default {
props: {
board: {
type: Object,
required: true,
},
milestonePath: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
export default {
props: {
board: {
type: Object,
required: true,
},
milestonePath: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
milestoneTitle() {
if (this.noMilestone) return NO_MILESTONE;
return this.board.milestone ? this.board.milestone.title : ANY_MILESTONE;
},
noMilestone() {
return this.milestoneId === 0;
},
milestoneId() {
return this.board.milestone_id;
},
milestoneTitleClass() {
return this.milestoneTitle === ANY_MILESTONE ? 'text-secondary' : 'bold';
},
selected() {
if (this.noMilestone) return NO_MILESTONE;
return this.board.milestone ? this.board.milestone.name : '';
},
computed: {
milestoneTitle() {
if (this.noMilestone) return NO_MILESTONE;
return this.board.milestone ? this.board.milestone.title : ANY_MILESTONE;
},
noMilestone() {
return this.milestoneId === 0;
},
milestoneId() {
return this.board.milestone_id;
},
milestoneTitleClass() {
return this.milestoneTitle === ANY_MILESTONE ? 'text-secondary' : 'bold';
},
mounted() {
this.milestoneDropdown = new MilestoneSelect(null, this.$refs.dropdownButton, {
handleClick: this.selectMilestone,
});
selected() {
if (this.noMilestone) return NO_MILESTONE;
return this.board.milestone ? this.board.milestone.name : '';
},
methods: {
selectMilestone(milestone) {
let { id } = milestone;
// swap the IDs of 'Any' and 'No' milestone to what backend requires
if (milestone.title === ANY_MILESTONE) {
id = -1;
} else if (milestone.title === NO_MILESTONE) {
id = 0;
}
this.board.milestone_id = id;
this.board.milestone = {
...milestone,
id,
};
},
},
mounted() {
this.milestoneDropdown = new MilestoneSelect(null, this.$refs.dropdownButton, {
handleClick: this.selectMilestone,
});
},
methods: {
selectMilestone(milestone) {
let { id } = milestone;
// swap the IDs of 'Any' and 'No' milestone to what backend requires
if (milestone.title === ANY_MILESTONE) {
id = -1;
} else if (milestone.title === NO_MILESTONE) {
id = 0;
}
this.board.milestone_id = id;
this.board.milestone = {
...milestone,
id,
};
},
};
},
};
</script>
<template>
......
<script>
/* eslint-disable vue/require-default-prop */
/* eslint-disable vue/require-default-prop */
import WeightSelect from 'ee/weight_select';
import WeightSelect from 'ee/weight_select';
const ANY_WEIGHT = 'Any Weight';
const NO_WEIGHT = 'No Weight';
const ANY_WEIGHT = 'Any Weight';
const NO_WEIGHT = 'No Weight';
export default {
props: {
board: {
type: Object,
required: true,
},
value: {
type: [Number, String],
required: false,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
weights: {
type: Array,
required: true,
},
export default {
props: {
board: {
type: Object,
required: true,
},
data() {
return {
fieldName: 'weight',
};
value: {
type: [Number, String],
required: false,
},
computed: {
valueClass() {
if (this.valueText === ANY_WEIGHT) {
return 'text-secondary';
}
return 'bold';
},
valueText() {
if (this.value > 0) return this.value;
if (this.value === 0) return NO_WEIGHT;
return ANY_WEIGHT;
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
mounted() {
this.weightDropdown = new WeightSelect(this.$refs.dropdownButton, {
handleClick: this.selectWeight,
selected: this.value,
fieldName: this.fieldName,
});
weights: {
type: Array,
required: true,
},
methods: {
selectWeight(weight) {
this.board.weight = this.weightInt(weight);
},
weightInt(weight) {
if (weight > 0) {
return weight;
}
if (weight === NO_WEIGHT) {
return 0;
}
return -1;
},
},
data() {
return {
fieldName: 'weight',
};
},
computed: {
valueClass() {
if (this.valueText === ANY_WEIGHT) {
return 'text-secondary';
}
return 'bold';
},
};
valueText() {
if (this.value > 0) return this.value;
if (this.value === 0) return NO_WEIGHT;
return ANY_WEIGHT;
},
},
mounted() {
this.weightDropdown = new WeightSelect(this.$refs.dropdownButton, {
handleClick: this.selectWeight,
selected: this.value,
fieldName: this.fieldName,
});
},
methods: {
selectWeight(weight) {
this.board.weight = this.weightInt(weight);
},
weightInt(weight) {
if (weight > 0) {
return weight;
}
if (weight === NO_WEIGHT) {
return 0;
}
return -1;
},
},
};
</script>
<template>
......
......@@ -19,7 +19,8 @@ const d3 = {
axisLeft,
line,
transition,
easeLinear };
easeLinear,
};
const margin = { top: 5, right: 65, bottom: 30, left: 50 };
// const parseDate = d3.timeFormat('%Y-%m-%d');
const bisectDate = d3.bisector(d => d.date).left;
......@@ -28,7 +29,9 @@ const tooltipDistance = 15;
export default class BurndownChart {
constructor({ container, startDate, dueDate }) {
this.canvas = d3.select(container).append('svg')
this.canvas = d3
.select(container)
.append('svg')
.attr('height', '100%')
.attr('width', '100%');
......@@ -56,21 +59,35 @@ export default class BurndownChart {
this.chartLegendIdealKey = this.chartLegendGroup.append('g');
this.chartLegendIdealKey.append('line').attr('class', 'ideal line');
this.chartLegendIdealKey.append('text').text('Guideline');
this.chartLegendIdealKeyBBox = this.chartLegendIdealKey.select('text').node().getBBox();
this.chartLegendIdealKeyBBox = this.chartLegendIdealKey
.select('text')
.node()
.getBBox();
this.chartLegendActualKey = this.chartLegendGroup.append('g');
this.chartLegendActualKey.append('line').attr('class', 'actual line');
this.chartLegendActualKey.append('text').text('Progress');
this.chartLegendActualKeyBBox = this.chartLegendActualKey.select('text').node().getBBox();
this.chartLegendActualKeyBBox = this.chartLegendActualKey
.select('text')
.node()
.getBBox();
// create tooltips
this.chartFocus = this.chartGroup.append('g').attr('class', 'focus').style('display', 'none');
this.chartFocus = this.chartGroup
.append('g')
.attr('class', 'focus')
.style('display', 'none');
this.chartFocus.append('circle').attr('r', 4);
this.tooltipGroup = this.chartFocus.append('g').attr('class', 'chart-tooltip');
this.tooltipGroup.append('rect').attr('rx', 3).attr('ry', 3);
this.tooltipGroup
.append('rect')
.attr('rx', 3)
.attr('ry', 3);
this.tooltipGroup.append('text');
this.chartOverlay = this.chartGroup.append('rect').attr('class', 'overlay')
this.chartOverlay = this.chartGroup
.append('rect')
.attr('class', 'overlay')
.on('mouseover', () => this.chartFocus.style('display', null))
.on('mouseout', () => this.chartFocus.style('display', 'none'))
.on('mousemove', () => this.handleMousemove());
......@@ -91,28 +108,33 @@ export default class BurndownChart {
this.yMax = 1;
// create scales
this.xScale = d3.scaleTime()
this.xScale = d3
.scaleTime()
.range([0, this.chartWidth])
.domain([this.startDate, this.xMax]);
this.yScale = d3.scaleLinear()
this.yScale = d3
.scaleLinear()
.range([this.chartHeight, 0])
.domain([0, this.yMax]);
// create axes
this.xAxis = d3.axisBottom()
this.xAxis = d3
.axisBottom()
.scale(this.xScale)
.tickFormat(d3.timeFormat('%b %-d'))
.tickPadding(6)
.tickSize(4, 0);
this.yAxis = d3.axisLeft()
this.yAxis = d3
.axisLeft()
.scale(this.yScale)
.tickPadding(6)
.tickSize(4, 0);
// create lines
this.line = d3.line()
this.line = d3
.line()
.x(d => this.xScale(new Date(d.date)))
.y(d => this.yScale(d.value));
......@@ -122,10 +144,12 @@ export default class BurndownChart {
// set data and force re-render
setData(data, { label = 'Remaining', animate } = {}) {
this.data = data.map(datum => ({
date: new Date(datum[0]),
value: parseInt(datum[1], 10),
})).sort((a, b) => (a.date - b.date));
this.data = data
.map(datum => ({
date: new Date(datum[0]),
value: parseInt(datum[1], 10),
}))
.sort((a, b) => a.date - b.date);
// adjust axis domain to correspond with data
this.xMax = Math.max(d3.max(this.data, d => d.date) || 0, this.dueDate);
......@@ -138,7 +162,10 @@ export default class BurndownChart {
// (this must be done here to prevent layout thrashing)
if (this.label !== label) {
this.label = label;
this.yAxisLabelBBox = this.yAxisLabelText.text(label).node().getBBox();
this.yAxisLabelBBox = this.yAxisLabelText
.text(label)
.node()
.getBBox();
}
// set ideal line data
......@@ -206,15 +233,18 @@ export default class BurndownChart {
// replace x-axis line with one which continues into the right margin
this.xAxisGroup.select('.domain').remove();
this.xAxisGroup.select('.domain-line').attr('x1', 0).attr('x2', this.chartWidth + margin.right);
this.xAxisGroup
.select('.domain-line')
.attr('x1', 0)
.attr('x2', this.chartWidth + margin.right);
// update y-axis label
const axisLabelOffset = (this.yAxisLabelBBox.height / 2) - margin.left;
const axisLabelOffset = this.yAxisLabelBBox.height / 2 - margin.left;
const axisLabelPadding = (this.chartHeight - this.yAxisLabelBBox.width - 10) / 2;
this.yAxisLabelText
.attr('y', 0 - margin.left)
.attr('x', 0 - (this.chartHeight / 2))
.attr('x', 0 - this.chartHeight / 2)
.attr('dy', '1em')
.style('text-anchor', 'middle')
.attr('transform', 'rotate(-90)');
......@@ -240,18 +270,21 @@ export default class BurndownChart {
const idealKeyOffset = legendPadding;
const actualKeyOffset = legendPadding + keyHeight + legendSpacing;
const legendWidth = (legendPadding * 2) + 24 + keyWidth;
const legendHeight = (legendPadding * 2) + (keyHeight * 2) + legendSpacing;
const legendOffset = (this.chartWidth + margin.right) - legendWidth - 1;
const legendWidth = legendPadding * 2 + 24 + keyWidth;
const legendHeight = legendPadding * 2 + keyHeight * 2 + legendSpacing;
const legendOffset = this.chartWidth + margin.right - legendWidth - 1;
this.chartLegendGroup.select('rect')
this.chartLegendGroup
.select('rect')
.attr('width', legendWidth)
.attr('height', legendHeight);
this.chartLegendGroup.selectAll('text')
this.chartLegendGroup
.selectAll('text')
.attr('x', 24)
.attr('dy', '1em');
this.chartLegendGroup.selectAll('line')
this.chartLegendGroup
.selectAll('line')
.attr('y1', keyHeight / 2)
.attr('y2', keyHeight / 2)
.attr('x1', 0)
......@@ -298,15 +331,19 @@ export default class BurndownChart {
const x = this.xScale(datum.date);
const y = this.yScale(datum.value);
const textSize = this.tooltipGroup.select('text').text(tooltip).node().getBBox();
const width = textSize.width + (tooltipPadding.x * 2);
const height = textSize.height + (tooltipPadding.y * 2);
const textSize = this.tooltipGroup
.select('text')
.text(tooltip)
.node()
.getBBox();
const width = textSize.width + tooltipPadding.x * 2;
const height = textSize.height + tooltipPadding.y * 2;
// calculate bounraries
const xMin = 0 - x - margin.left;
const yMin = 0 - y - margin.top;
const xMax = (this.chartWidth + margin.right) - x - width;
const yMax = (this.chartHeight + margin.bottom) - y - height;
const xMax = this.chartWidth + margin.right - x - width;
const yMax = this.chartHeight + margin.bottom - y - height;
// try to fit tooltip above point
let xOffset = 0 - Math.floor(width / 2);
......@@ -331,12 +368,14 @@ export default class BurndownChart {
this.chartFocus.attr('transform', `translate(${x}, ${y})`);
this.tooltipGroup.attr('transform', `translate(${xOffset}, ${yOffset})`);
this.tooltipGroup.select('text')
this.tooltipGroup
.select('text')
.attr('dy', '1em')
.attr('x', tooltipPadding.x)
.attr('y', tooltipPadding.y);
this.tooltipGroup.select('rect')
this.tooltipGroup
.select('rect')
.attr('width', width)
.attr('height', height);
}
......@@ -357,15 +396,18 @@ export default class BurndownChart {
static animateLinePath(path, duration = 1000, cb) {
const lineLength = path.node().getTotalLength();
const linearTransition = d3.transition().duration(duration).ease(d3.easeLinear);
const linearTransition = d3
.transition()
.duration(duration)
.ease(d3.easeLinear);
path
.attr('stroke-dasharray', `${lineLength} ${lineLength}`)
.attr('stroke-dashoffset', lineLength)
.transition(linearTransition)
.attr('stroke-dashoffset', 0)
.on('end', () => {
path.attr('stroke-dasharray', null);
if (cb) cb();
});
.attr('stroke-dashoffset', 0)
.on('end', () => {
path.attr('stroke-dasharray', null);
if (cb) cb();
});
}
}
......@@ -31,7 +31,10 @@ export default () => {
const show = $this.data('show');
if (currentView !== show) {
currentView = show;
$this.addClass('active').siblings().removeClass('active');
$this
.addClass('active')
.siblings()
.removeClass('active');
switch (show) {
case 'count':
chart.setData(openIssuesCount, { label: 'Open issues', animate: true });
......
<script>
/**
* Renders a deploy board.
*
* A deploy board is composed by:
* - Information area with percentage of completion.
* - Instances with status.
* - Button Actions.
* [Mockup](https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*/
import _ from 'underscore';
import { n__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg';
import instanceComponent from './deploy_board_instance_component.vue';
/**
* Renders a deploy board.
*
* A deploy board is composed by:
* - Information area with percentage of completion.
* - Instances with status.
* - Button Actions.
* [Mockup](https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*/
import _ from 'underscore';
import { n__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg';
import instanceComponent from './deploy_board_instance_component.vue';
export default {
components: {
instanceComponent,
export default {
components: {
instanceComponent,
},
directives: {
tooltip,
},
props: {
deployBoardData: {
type: Object,
required: true,
},
directives: {
tooltip,
isLoading: {
type: Boolean,
required: true,
},
props: {
deployBoardData: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
isEmpty: {
type: Boolean,
required: true,
},
logsPath: {
type: String,
required: false,
default: '',
},
isEmpty: {
type: Boolean,
required: true,
},
computed: {
canRenderDeployBoard() {
return !this.isLoading && !this.isEmpty && !_.isEmpty(this.deployBoardData);
},
canRenderEmptyState() {
return !this.isLoading && this.isEmpty;
},
instanceCount() {
const { instances } = this.deployBoardData;
logsPath: {
type: String,
required: false,
default: '',
},
},
computed: {
canRenderDeployBoard() {
return !this.isLoading && !this.isEmpty && !_.isEmpty(this.deployBoardData);
},
canRenderEmptyState() {
return !this.isLoading && this.isEmpty;
},
instanceCount() {
const { instances } = this.deployBoardData;
return Array.isArray(instances) ? instances.length : 0;
},
instanceIsCompletedCount() {
const completionPercentage = this.deployBoardData.completion / 100;
const completionCount = Math.floor(completionPercentage * this.instanceCount);
return Array.isArray(instances) ? instances.length : 0;
},
instanceIsCompletedCount() {
const completionPercentage = this.deployBoardData.completion / 100;
const completionCount = Math.floor(completionPercentage * this.instanceCount);
return Number.isNaN(completionCount) ? 0 : completionCount;
},
instanceIsCompletedText() {
const title = n__('instance completed', 'instances completed', this.instanceIsCompletedCount);
return Number.isNaN(completionCount) ? 0 : completionCount;
},
instanceIsCompletedText() {
const title = n__('instance completed', 'instances completed', this.instanceIsCompletedCount);
return `${this.instanceIsCompletedCount} ${title}`;
},
instanceTitle() {
return n__('Instance', 'Instances', this.instanceCount);
},
projectName() {
return '<projectname>';
},
deployBoardSvg() {
return deployBoardSvg;
},
return `${this.instanceIsCompletedCount} ${title}`;
},
instanceTitle() {
return n__('Instance', 'Instances', this.instanceCount);
},
projectName() {
return '<projectname>';
},
deployBoardSvg() {
return deployBoardSvg;
},
};
},
};
</script>
<template>
<div class="js-deploy-board deploy-board">
......
<script>
/**
* An instance in deploy board is represented by a square in this mockup:
* https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png
*
* Each instance has a state and a tooltip.
* The state needs to be represented in different colors,
* see more information about this in
* https://gitlab.com/gitlab-org/gitlab-ee/uploads/5fff049fd88336d9ee0c6ef77b1ba7e3/monitoring__deployboard--key.png
*
* An instance can represent a normal deploy or a canary deploy. In the latter we need to provide
* this information in the tooltip and the colors.
* Mockup is https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1551#note_26595150
*/
import tooltip from '~/vue_shared/directives/tooltip';
/**
* An instance in deploy board is represented by a square in this mockup:
* https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png
*
* Each instance has a state and a tooltip.
* The state needs to be represented in different colors,
* see more information about this in
* https://gitlab.com/gitlab-org/gitlab-ee/uploads/5fff049fd88336d9ee0c6ef77b1ba7e3/monitoring__deployboard--key.png
*
* An instance can represent a normal deploy or a canary deploy. In the latter we need to provide
* this information in the tooltip and the colors.
* Mockup is https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1551#note_26595150
*/
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
export default {
directives: {
tooltip,
},
props: {
/**
* Represents the status of the pod. Each state is represented with a different
* color.
* It should be one of the following:
* finished || deploying || failed || ready || preparing || waiting
*/
status: {
type: String,
required: true,
default: 'finished',
},
props: {
/**
* Represents the status of the pod. Each state is represented with a different
* color.
* It should be one of the following:
* finished || deploying || failed || ready || preparing || waiting
*/
status: {
type: String,
required: true,
default: 'finished',
},
tooltipText: {
type: String,
required: false,
default: '',
},
tooltipText: {
type: String,
required: false,
default: '',
},
stable: {
type: Boolean,
required: false,
default: true,
},
stable: {
type: Boolean,
required: false,
default: true,
},
podName: {
type: String,
required: false,
default: '',
},
podName: {
type: String,
required: false,
default: '',
},
logsPath: {
type: String,
required: true,
},
logsPath: {
type: String,
required: true,
},
},
computed: {
cssClass() {
let cssClassName = `deploy-board-instance-${this.status}`;
computed: {
cssClass() {
let cssClassName = `deploy-board-instance-${this.status}`;
if (!this.stable) {
cssClassName = `${cssClassName} deploy-board-instance-canary`;
}
if (!this.stable) {
cssClassName = `${cssClassName} deploy-board-instance-canary`;
}
return cssClassName;
},
return cssClassName;
},
computedLogPath() {
return `${this.logsPath}?pod_name=${this.podName}`;
},
computedLogPath() {
return `${this.logsPath}?pod_name=${this.podName}`;
},
};
},
};
</script>
<template>
<a
......
<script>
import $ from 'jquery';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { __, s__ } from '~/locale';
import eventHub from '../../event_hub';
import { stateEvent } from '../../constants';
import $ from 'jquery';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { __, s__ } from '~/locale';
import eventHub from '../../event_hub';
import { stateEvent } from '../../constants';
export default {
name: 'EpicHeader',
directives: {
tooltip,
export default {
name: 'EpicHeader',
directives: {
tooltip,
},
components: {
Icon,
LoadingButton,
userAvatarLink,
timeagoTooltip,
},
props: {
author: {
type: Object,
required: true,
validator: value => value.url && value.username && value.name,
},
components: {
Icon,
LoadingButton,
userAvatarLink,
timeagoTooltip,
created: {
type: String,
required: true,
},
props: {
author: {
type: Object,
required: true,
validator: value => value.url && value.username && value.name,
},
created: {
type: String,
required: true,
},
open: {
type: Boolean,
required: true,
},
canUpdate: {
required: true,
type: Boolean,
},
open: {
type: Boolean,
required: true,
},
data() {
return {
deleteLoading: false,
statusUpdating: false,
isEpicOpen: this.open,
};
canUpdate: {
required: true,
type: Boolean,
},
computed: {
statusIcon() {
return this.isEpicOpen ? 'issue-open-m' : 'mobile-issue-close';
},
statusText() {
return this.isEpicOpen ? __('Open') : __('Closed');
},
actionButtonClass() {
return `btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button ${this.isEpicOpen ? 'btn-close' : 'btn-open'}`;
},
actionButtonText() {
return this.isEpicOpen ? __('Close epic') : __('Reopen epic');
},
},
data() {
return {
deleteLoading: false,
statusUpdating: false,
isEpicOpen: this.open,
};
},
computed: {
statusIcon() {
return this.isEpicOpen ? 'issue-open-m' : 'mobile-issue-close';
},
mounted() {
$(document).on('issuable_vue_app:change', (e, isClosed) => {
this.isEpicOpen = e.detail ? !e.detail.isClosed : !isClosed;
this.statusUpdating = false;
});
statusText() {
return this.isEpicOpen ? __('Open') : __('Closed');
},
methods: {
deleteEpic() {
if (window.confirm(s__('Epic will be removed! Are you sure?'))) { // eslint-disable-line no-alert
this.deleteLoading = true;
this.$emit('deleteEpic');
}
},
toggleSidebar() {
eventHub.$emit('toggleSidebar');
},
toggleStatus() {
this.statusUpdating = true;
this.$emit('toggleEpicStatus', this.isEpicOpen ? stateEvent.close : stateEvent.reopen);
},
actionButtonClass() {
return `btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button ${
this.isEpicOpen ? 'btn-close' : 'btn-open'
}`;
},
};
actionButtonText() {
return this.isEpicOpen ? __('Close epic') : __('Reopen epic');
},
},
mounted() {
$(document).on('issuable_vue_app:change', (e, isClosed) => {
this.isEpicOpen = e.detail ? !e.detail.isClosed : !isClosed;
this.statusUpdating = false;
});
},
methods: {
deleteEpic() {
// eslint-disable-next-line no-alert
if (window.confirm(s__('Epic will be removed! Are you sure?'))) {
this.deleteLoading = true;
this.$emit('deleteEpic');
}
},
toggleSidebar() {
eventHub.$emit('toggleSidebar');
},
toggleStatus() {
this.statusUpdating = true;
this.$emit('toggleEpicStatus', this.isEpicOpen ? stateEvent.close : stateEvent.reopen);
},
},
};
</script>
<template>
......
......@@ -7,7 +7,9 @@ export default class SidebarContext {
constructor() {
const $issuableSidebar = $('.js-issuable-update');
Mousetrap.bind('l', () => SidebarContext.openSidebarDropdown($issuableSidebar.find('.js-labels-block')));
Mousetrap.bind('l', () =>
SidebarContext.openSidebarDropdown($issuableSidebar.find('.js-labels-block')),
);
$issuableSidebar
.off('click', '.js-sidebar-dropdown-toggle')
......
<script>
import Flash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import loadingButton from '~/vue_shared/components/loading_button.vue';
import NewEpicService from '../services/new_epic_service';
import Flash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import loadingButton from '~/vue_shared/components/loading_button.vue';
import NewEpicService from '../services/new_epic_service';
export default {
name: 'NewEpic',
components: {
loadingButton,
export default {
name: 'NewEpic',
components: {
loadingButton,
},
props: {
endpoint: {
type: String,
required: true,
},
props: {
endpoint: {
type: String,
required: true,
},
alignRight: {
type: Boolean,
required: false,
default: false,
},
alignRight: {
type: Boolean,
required: false,
default: false,
},
data() {
return {
service: new NewEpicService(this.endpoint),
creating: false,
title: '',
};
},
data() {
return {
service: new NewEpicService(this.endpoint),
creating: false,
title: '',
};
},
computed: {
buttonLabel() {
return this.creating ? s__('Creating epic') : s__('Create epic');
},
computed: {
buttonLabel() {
return this.creating ? s__('Creating epic') : s__('Create epic');
},
isCreatingDisabled() {
return this.title.length === 0;
},
isCreatingDisabled() {
return this.title.length === 0;
},
methods: {
createEpic() {
this.creating = true;
this.service.createEpic(this.title)
.then(({ data }) => {
visitUrl(data.web_url);
})
.catch(() => {
this.creating = false;
Flash(s__('Error creating epic'));
});
},
focusInput() {
// Wait for dropdown to appear because of transition CSS
setTimeout(() => {
this.$refs.title.focus();
}, 25);
},
},
methods: {
createEpic() {
this.creating = true;
this.service
.createEpic(this.title)
.then(({ data }) => {
visitUrl(data.web_url);
})
.catch(() => {
this.creating = false;
Flash(s__('Error creating epic'));
});
},
};
focusInput() {
// Wait for dropdown to appear because of transition CSS
setTimeout(() => {
this.$refs.title.focus();
}, 25);
},
},
};
</script>
<template>
......
......@@ -7,14 +7,16 @@ export default () => {
if (el) {
const props = el.dataset;
new Vue({ // eslint-disable-line no-new
// eslint-disable-next-line no-new
new Vue({
el,
components: {
'new-epic-app': NewEpicApp,
},
render: createElement => createElement('new-epic-app', {
props,
}),
render: createElement =>
createElement('new-epic-app', {
props,
}),
});
}
};
......@@ -119,7 +119,9 @@ export default {
},
popoverOptions() {
return this.getPopoverConfig({
title: s__('Epics|These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely.'),
title: s__(
'Epics|These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely.',
),
content: `
<a
href="${gon.gitlab_url}/help/user/group/epics/index.md#start-date-and-due-date"
......@@ -304,4 +306,3 @@ export default {
</div>
</div>
</template>
<script>
import Participants from '~/sidebar/components/participants/participants.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
export default {
components: {
Participants,
export default {
components: {
Participants,
},
props: {
participants: {
type: Array,
required: true,
},
props: {
participants: {
type: Array,
required: true,
},
},
methods: {
onToggleSidebar() {
this.$emit('toggleCollapse');
},
methods: {
onToggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
},
};
</script>
<template>
......
<script>
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
export default {
components: {
Subscriptions,
export default {
components: {
Subscriptions,
},
props: {
loading: {
type: Boolean,
required: true,
},
props: {
loading: {
type: Boolean,
required: true,
},
subscribed: {
type: Boolean,
required: true,
},
subscribed: {
type: Boolean,
required: true,
},
methods: {
onToggleSubscription() {
this.$emit('toggleSubscription');
},
onToggleSidebar() {
this.$emit('toggleCollapse');
},
},
methods: {
onToggleSubscription() {
this.$emit('toggleSubscription');
},
};
onToggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
......
<script>
import { __, s__ } from '~/locale';
import eventHub from '../event_hub';
import { NODE_ACTIONS } from '../constants';
import Icon from '~/vue_shared/components/icon.vue';
import { __, s__ } from '~/locale';
import eventHub from '../event_hub';
import { NODE_ACTIONS } from '../constants';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
export default {
components: {
Icon,
},
props: {
node: {
type: Object,
required: true,
},
props: {
node: {
type: Object,
required: true,
},
nodeActionsAllowed: {
type: Boolean,
required: true,
},
nodeEditAllowed: {
type: Boolean,
required: true,
},
nodeMissingOauth: {
type: Boolean,
required: true,
},
nodeActionsAllowed: {
type: Boolean,
required: true,
},
computed: {
isToggleAllowed() {
return !this.node.primary && this.nodeEditAllowed;
},
nodeToggleLabel() {
return this.node.enabled ? __('Disable') : __('Enable');
},
isSecondaryNode() {
return !this.node.primary;
},
nodeEditAllowed: {
type: Boolean,
required: true,
},
methods: {
onToggleNode() {
eventHub.$emit('showNodeActionModal', {
actionType: NODE_ACTIONS.TOGGLE,
node: this.node,
modalMessage: s__('GeoNodes|Disabling a node stops the sync process. Are you sure?'),
modalActionLabel: this.nodeToggleLabel,
});
},
onRemoveNode() {
eventHub.$emit('showNodeActionModal', {
actionType: NODE_ACTIONS.REMOVE,
node: this.node,
modalKind: 'danger',
modalMessage: s__('GeoNodes|Removing a node stops the sync process. Are you sure?'),
modalActionLabel: __('Remove'),
});
},
onRepairNode() {
eventHub.$emit('repairNode', this.node);
},
nodeMissingOauth: {
type: Boolean,
required: true,
},
};
},
computed: {
isToggleAllowed() {
return !this.node.primary && this.nodeEditAllowed;
},
nodeToggleLabel() {
return this.node.enabled ? __('Disable') : __('Enable');
},
isSecondaryNode() {
return !this.node.primary;
},
},
methods: {
onToggleNode() {
eventHub.$emit('showNodeActionModal', {
actionType: NODE_ACTIONS.TOGGLE,
node: this.node,
modalMessage: s__('GeoNodes|Disabling a node stops the sync process. Are you sure?'),
modalActionLabel: this.nodeToggleLabel,
});
},
onRemoveNode() {
eventHub.$emit('showNodeActionModal', {
actionType: NODE_ACTIONS.REMOVE,
node: this.node,
modalKind: 'danger',
modalMessage: s__('GeoNodes|Removing a node stops the sync process. Are you sure?'),
modalActionLabel: __('Remove'),
});
},
onRepairNode() {
eventHub.$emit('repairNode', this.node);
},
},
};
</script>
<template>
......
<script>
import { s__ } from '~/locale';
import popover from '~/vue_shared/directives/popover';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue';
import { s__ } from '~/locale';
import popover from '~/vue_shared/directives/popover';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue';
import { VALUE_TYPE, CUSTOM_TYPE } from '../constants';
import { VALUE_TYPE, CUSTOM_TYPE } from '../constants';
import GeoNodeSyncSettings from './geo_node_sync_settings.vue';
import GeoNodeEventStatus from './geo_node_event_status.vue';
import GeoNodeSyncSettings from './geo_node_sync_settings.vue';
import GeoNodeEventStatus from './geo_node_event_status.vue';
export default {
components: {
Icon,
StackedProgressBar,
GeoNodeSyncSettings,
GeoNodeEventStatus,
},
directives: {
popover,
tooltip,
},
props: {
itemTitle: {
type: String,
required: true,
},
cssClass: {
type: String,
required: false,
default: '',
},
itemValue: {
type: [Object, String, Number],
required: true,
},
itemValueStale: {
type: Boolean,
required: false,
default: false,
},
itemValueStaleTooltip: {
type: String,
required: false,
default: '',
},
successLabel: {
type: String,
required: false,
default: s__('GeoNodes|Synced'),
},
failureLabel: {
type: String,
required: false,
default: s__('GeoNodes|Failed'),
},
neutralLabel: {
type: String,
required: false,
default: s__('GeoNodes|Out of sync'),
},
itemValueType: {
type: String,
required: true,
},
customType: {
type: String,
required: false,
default: '',
},
eventTypeLogStatus: {
type: Boolean,
required: false,
default: false,
},
helpInfo: {
type: [Boolean, Object],
required: false,
default: false,
},
},
computed: {
hasHelpInfo() {
return typeof this.helpInfo === 'object';
},
isValueTypePlain() {
return this.itemValueType === VALUE_TYPE.PLAIN;
},
isValueTypeGraph() {
return this.itemValueType === VALUE_TYPE.GRAPH;
},
isValueTypeCustom() {
return this.itemValueType === VALUE_TYPE.CUSTOM;
},
isCustomTypeSync() {
return this.customType === CUSTOM_TYPE.SYNC;
},
popoverConfig() {
return {
html: true,
trigger: 'click',
placement: 'top',
template: `
export default {
components: {
Icon,
StackedProgressBar,
GeoNodeSyncSettings,
GeoNodeEventStatus,
},
directives: {
popover,
tooltip,
},
props: {
itemTitle: {
type: String,
required: true,
},
cssClass: {
type: String,
required: false,
default: '',
},
itemValue: {
type: [Object, String, Number],
required: true,
},
itemValueStale: {
type: Boolean,
required: false,
default: false,
},
itemValueStaleTooltip: {
type: String,
required: false,
default: '',
},
successLabel: {
type: String,
required: false,
default: s__('GeoNodes|Synced'),
},
failureLabel: {
type: String,
required: false,
default: s__('GeoNodes|Failed'),
},
neutralLabel: {
type: String,
required: false,
default: s__('GeoNodes|Out of sync'),
},
itemValueType: {
type: String,
required: true,
},
customType: {
type: String,
required: false,
default: '',
},
eventTypeLogStatus: {
type: Boolean,
required: false,
default: false,
},
helpInfo: {
type: [Boolean, Object],
required: false,
default: false,
},
},
computed: {
hasHelpInfo() {
return typeof this.helpInfo === 'object';
},
isValueTypePlain() {
return this.itemValueType === VALUE_TYPE.PLAIN;
},
isValueTypeGraph() {
return this.itemValueType === VALUE_TYPE.GRAPH;
},
isValueTypeCustom() {
return this.itemValueType === VALUE_TYPE.CUSTOM;
},
isCustomTypeSync() {
return this.customType === CUSTOM_TYPE.SYNC;
},
popoverConfig() {
return {
html: true,
trigger: 'click',
placement: 'top',
template: `
<div class="popover geo-node-detail-popover" role="tooltip">
<div class="arrow"></div>
<p class="popover-header"></p>
<div class="popover-body"></div>
</div>
`,
title: this.helpInfo.title,
content: `
title: this.helpInfo.title,
content: `
<a href="${this.helpInfo.url}">
${this.helpInfo.urlText}
</a>
`,
};
},
};
},
};
},
};
</script>
<template>
......
<script>
/* eslint-disable vue/no-side-effects-in-computed-properties */
import { s__ } from '~/locale';
/* eslint-disable vue/no-side-effects-in-computed-properties */
import { s__ } from '~/locale';
import NodeDetailsSectionMain from './node_detail_sections/node_details_section_main.vue';
import NodeDetailsSectionSync from './node_detail_sections/node_details_section_sync.vue';
import NodeDetailsSectionVerification from './node_detail_sections/node_details_section_verification.vue';
import NodeDetailsSectionOther from './node_detail_sections/node_details_section_other.vue';
import NodeDetailsSectionMain from './node_detail_sections/node_details_section_main.vue';
import NodeDetailsSectionSync from './node_detail_sections/node_details_section_sync.vue';
import NodeDetailsSectionVerification from './node_detail_sections/node_details_section_verification.vue';
import NodeDetailsSectionOther from './node_detail_sections/node_details_section_other.vue';
export default {
components: {
NodeDetailsSectionMain,
NodeDetailsSectionSync,
NodeDetailsSectionVerification,
NodeDetailsSectionOther,
export default {
components: {
NodeDetailsSectionMain,
NodeDetailsSectionSync,
NodeDetailsSectionVerification,
NodeDetailsSectionOther,
},
props: {
node: {
type: Object,
required: true,
},
props: {
node: {
type: Object,
required: true,
},
nodeDetails: {
type: Object,
required: true,
},
nodeActionsAllowed: {
type: Boolean,
required: true,
},
nodeEditAllowed: {
type: Boolean,
required: true,
},
nodeDetails: {
type: Object,
required: true,
},
data() {
return {
showAdvanceItems: false,
errorMessage: '',
};
nodeActionsAllowed: {
type: Boolean,
required: true,
},
computed: {
hasError() {
if (!this.nodeDetails.healthy) {
this.errorMessage = this.nodeDetails.health;
}
return !this.nodeDetails.healthy;
},
hasVersionMismatch() {
if (this.nodeDetails.version !== this.nodeDetails.primaryVersion ||
this.nodeDetails.revision !== this.nodeDetails.primaryRevision) {
this.errorMessage = s__('GeoNodes|GitLab version does not match the primary node version');
return true;
}
return false;
},
nodeEditAllowed: {
type: Boolean,
required: true,
},
};
},
data() {
return {
showAdvanceItems: false,
errorMessage: '',
};
},
computed: {
hasError() {
if (!this.nodeDetails.healthy) {
this.errorMessage = this.nodeDetails.health;
}
return !this.nodeDetails.healthy;
},
hasVersionMismatch() {
if (
this.nodeDetails.version !== this.nodeDetails.primaryVersion ||
this.nodeDetails.revision !== this.nodeDetails.primaryRevision
) {
this.errorMessage = s__('GeoNodes|GitLab version does not match the primary node version');
return true;
}
return false;
},
},
};
</script>
<template>
......
<script>
import { formatDate } from '~/lib/utils/datetime_utility';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import tooltip from '~/vue_shared/directives/tooltip';
import { formatDate } from '~/lib/utils/datetime_utility';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
mixins: [timeAgoMixin],
props: {
eventId: {
type: Number,
required: true,
},
mixins: [
timeAgoMixin,
],
props: {
eventId: {
type: Number,
required: true,
},
eventTimeStamp: {
type: Number,
required: true,
default: 0,
},
eventTypeLogStatus: {
type: Boolean,
required: false,
default: false,
},
eventTimeStamp: {
type: Number,
required: true,
default: 0,
},
computed: {
timeStamp() {
return new Date(this.eventTimeStamp * 1000);
},
timeStampString() {
return formatDate(this.timeStamp);
},
eventString() {
return this.eventId;
},
eventTypeLogStatus: {
type: Boolean,
required: false,
default: false,
},
};
},
computed: {
timeStamp() {
return new Date(this.eventTimeStamp * 1000);
},
timeStampString() {
return formatDate(this.timeStamp);
},
eventString() {
return this.eventId;
},
},
};
</script>
<template>
......
<script>
import { s__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { s__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
icon,
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
node: {
type: Object,
required: true,
},
directives: {
tooltip,
nodeDetails: {
type: Object,
required: true,
},
props: {
node: {
type: Object,
required: true,
},
nodeDetails: {
type: Object,
required: true,
},
nodeDetailsLoading: {
type: Boolean,
required: true,
},
nodeDetailsFailed: {
type: Boolean,
required: true,
},
nodeDetailsLoading: {
type: Boolean,
required: true,
},
computed: {
isNodeHTTP() {
return this.node.url.startsWith('http://');
},
showNodeStatusIcon() {
if (this.nodeDetailsLoading) {
return false;
}
nodeDetailsFailed: {
type: Boolean,
required: true,
},
},
computed: {
isNodeHTTP() {
return this.node.url.startsWith('http://');
},
showNodeStatusIcon() {
if (this.nodeDetailsLoading) {
return false;
}
return this.isNodeHTTP || this.nodeDetailsFailed;
},
nodeStatusIconClass() {
const iconClasses = 'prepend-left-10 node-status-icon';
if (this.nodeDetailsFailed) {
return `${iconClasses} status-icon-failure`;
}
return `${iconClasses} status-icon-warning`;
},
nodeStatusIconName() {
if (this.nodeDetailsFailed) {
return 'status_failed_borderless';
}
return 'warning';
},
nodeStatusIconTooltip() {
if (this.nodeDetailsFailed) {
return '';
}
return s__('GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS.');
},
return this.isNodeHTTP || this.nodeDetailsFailed;
},
nodeStatusIconClass() {
const iconClasses = 'prepend-left-10 node-status-icon';
if (this.nodeDetailsFailed) {
return `${iconClasses} status-icon-failure`;
}
return `${iconClasses} status-icon-warning`;
},
nodeStatusIconName() {
if (this.nodeDetailsFailed) {
return 'status_failed_borderless';
}
return 'warning';
},
nodeStatusIconTooltip() {
if (this.nodeDetailsFailed) {
return '';
}
return s__(
'GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS.',
);
},
};
},
};
</script>
<template>
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import { HEALTH_STATUS_ICON } from '../constants';
import icon from '~/vue_shared/components/icon.vue';
import { HEALTH_STATUS_ICON } from '../constants';
export default {
components: {
icon,
export default {
components: {
icon,
},
props: {
status: {
type: String,
required: true,
},
props: {
status: {
type: String,
required: true,
},
},
computed: {
healthCssClass() {
return `geo-node-${this.status.toLowerCase()}`;
},
computed: {
healthCssClass() {
return `geo-node-${this.status.toLowerCase()}`;
},
statusIconName() {
return HEALTH_STATUS_ICON[this.status.toLowerCase()];
},
statusIconName() {
return HEALTH_STATUS_ICON[this.status.toLowerCase()];
},
};
},
};
</script>
<template>
......
<script>
import { s__ } from '~/locale';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import { s__ } from '~/locale';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import { TIME_DIFF } from '../constants';
import { TIME_DIFF } from '../constants';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
components: {
icon,
},
props: {
syncStatusUnavailable: {
type: Boolean,
required: false,
default: false,
},
components: {
icon,
selectiveSyncType: {
type: String,
required: false,
default: null,
},
props: {
syncStatusUnavailable: {
type: Boolean,
required: false,
default: false,
},
selectiveSyncType: {
type: String,
required: false,
default: null,
},
lastEvent: {
type: Object,
required: true,
},
cursorLastEvent: {
type: Object,
required: true,
},
lastEvent: {
type: Object,
required: true,
},
cursorLastEvent: {
type: Object,
required: true,
},
},
computed: {
syncType() {
if (this.selectiveSyncType === null || this.selectiveSyncType === '') {
return s__('GeoNodes|Full');
}
computed: {
syncType() {
if (this.selectiveSyncType === null || this.selectiveSyncType === '') {
return s__('GeoNodes|Full');
}
return `${s__('GeoNodes|Selective')} (${this.selectiveSyncType})`;
},
eventTimestampEmpty() {
return this.lastEvent.timeStamp === 0 || this.cursorLastEvent.timeStamp === 0;
},
syncLagInSeconds() {
return this.lagInSeconds(this.lastEvent.timeStamp, this.cursorLastEvent.timeStamp);
},
syncStatusIcon() {
return this.statusIcon(this.syncLagInSeconds);
},
syncStatusEventInfo() {
return this.statusEventInfo(
this.lastEvent.id,
this.cursorLastEvent.id,
this.syncLagInSeconds,
);
},
syncStatusTooltip() {
return this.statusTooltip(this.syncLagInSeconds);
},
return `${s__('GeoNodes|Selective')} (${this.selectiveSyncType})`;
},
eventTimestampEmpty() {
return this.lastEvent.timeStamp === 0 || this.cursorLastEvent.timeStamp === 0;
},
syncLagInSeconds() {
return this.lagInSeconds(this.lastEvent.timeStamp, this.cursorLastEvent.timeStamp);
},
syncStatusIcon() {
return this.statusIcon(this.syncLagInSeconds);
},
syncStatusEventInfo() {
return this.statusEventInfo(
this.lastEvent.id,
this.cursorLastEvent.id,
this.syncLagInSeconds,
);
},
methods: {
lagInSeconds(lastEventTimeStamp, cursorLastEventTimeStamp) {
let eventDateTime;
let cursorDateTime;
syncStatusTooltip() {
return this.statusTooltip(this.syncLagInSeconds);
},
},
methods: {
lagInSeconds(lastEventTimeStamp, cursorLastEventTimeStamp) {
let eventDateTime;
let cursorDateTime;
if (lastEventTimeStamp && lastEventTimeStamp > 0) {
eventDateTime = new Date(lastEventTimeStamp * 1000);
}
if (lastEventTimeStamp && lastEventTimeStamp > 0) {
eventDateTime = new Date(lastEventTimeStamp * 1000);
}
if (cursorLastEventTimeStamp && cursorLastEventTimeStamp > 0) {
cursorDateTime = new Date(cursorLastEventTimeStamp * 1000);
}
if (cursorLastEventTimeStamp && cursorLastEventTimeStamp > 0) {
cursorDateTime = new Date(cursorLastEventTimeStamp * 1000);
}
return (cursorDateTime - eventDateTime) / 1000;
},
statusIcon(syncLag) {
if (syncLag <= TIME_DIFF.FIVE_MINS) {
return 'retry';
} else if (syncLag > TIME_DIFF.FIVE_MINS &&
syncLag <= TIME_DIFF.HOUR) {
return 'warning';
}
return 'status_failed';
},
statusEventInfo(lastEventId, cursorLastEventId, lagInSeconds) {
const timeAgoStr = timeIntervalInWords(lagInSeconds);
const pendingEvents = lastEventId - cursorLastEventId;
return `${timeAgoStr} (${pendingEvents} events)`;
},
statusTooltip(lagInSeconds) {
if (this.eventTimestampEmpty ||
lagInSeconds <= TIME_DIFF.FIVE_MINS) {
return '';
} else if (lagInSeconds > TIME_DIFF.FIVE_MINS &&
lagInSeconds <= TIME_DIFF.HOUR) {
return s__('GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage.');
}
return s__('GeoNodeSyncStatus|Node is failing or broken.');
},
return (cursorDateTime - eventDateTime) / 1000;
},
statusIcon(syncLag) {
if (syncLag <= TIME_DIFF.FIVE_MINS) {
return 'retry';
} else if (syncLag > TIME_DIFF.FIVE_MINS && syncLag <= TIME_DIFF.HOUR) {
return 'warning';
}
return 'status_failed';
},
statusEventInfo(lastEventId, cursorLastEventId, lagInSeconds) {
const timeAgoStr = timeIntervalInWords(lagInSeconds);
const pendingEvents = lastEventId - cursorLastEventId;
return `${timeAgoStr} (${pendingEvents} events)`;
},
statusTooltip(lagInSeconds) {
if (this.eventTimestampEmpty || lagInSeconds <= TIME_DIFF.FIVE_MINS) {
return '';
} else if (lagInSeconds > TIME_DIFF.FIVE_MINS && lagInSeconds <= TIME_DIFF.HOUR) {
return s__(
'GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage.',
);
}
return s__('GeoNodeSyncStatus|Node is failing or broken.');
},
};
},
};
</script>
<template>
......
<script>
import { __ } from '~/locale';
import { __ } from '~/locale';
import GeoNodeHealthStatus from '../geo_node_health_status.vue';
import GeoNodeActions from '../geo_node_actions.vue';
import GeoNodeHealthStatus from '../geo_node_health_status.vue';
import GeoNodeActions from '../geo_node_actions.vue';
export default {
components: {
GeoNodeHealthStatus,
GeoNodeActions,
export default {
components: {
GeoNodeHealthStatus,
GeoNodeActions,
},
props: {
node: {
type: Object,
required: true,
},
props: {
node: {
type: Object,
required: true,
},
nodeDetails: {
type: Object,
required: true,
},
nodeActionsAllowed: {
type: Boolean,
required: true,
},
nodeEditAllowed: {
type: Boolean,
required: true,
},
versionMismatch: {
type: Boolean,
required: true,
},
nodeDetails: {
type: Object,
required: true,
},
computed: {
nodeVersion() {
if (this.nodeDetails.version == null &&
this.nodeDetails.revision == null) {
return __('Unknown');
}
return `${this.nodeDetails.version} (${this.nodeDetails.revision})`;
},
nodeHealthStatus() {
return this.nodeDetails.healthy ? this.nodeDetails.health : this.nodeDetails.healthStatus;
},
nodeActionsAllowed: {
type: Boolean,
required: true,
},
};
nodeEditAllowed: {
type: Boolean,
required: true,
},
versionMismatch: {
type: Boolean,
required: true,
},
},
computed: {
nodeVersion() {
if (this.nodeDetails.version == null && this.nodeDetails.revision == null) {
return __('Unknown');
}
return `${this.nodeDetails.version} (${this.nodeDetails.revision})`;
},
nodeHealthStatus() {
return this.nodeDetails.healthy ? this.nodeDetails.health : this.nodeDetails.healthStatus;
},
},
};
</script>
<template>
......
<script>
import { s__, __ } from '~/locale';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { s__, __ } from '~/locale';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { VALUE_TYPE } from '../../constants';
import { VALUE_TYPE } from '../../constants';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
export default {
valueType: VALUE_TYPE,
components: {
SectionRevealButton,
GeoNodeDetailItem,
export default {
valueType: VALUE_TYPE,
components: {
SectionRevealButton,
GeoNodeDetailItem,
},
mixins: [DetailsSectionMixin],
props: {
nodeDetails: {
type: Object,
required: true,
},
mixins: [
DetailsSectionMixin,
],
props: {
nodeDetails: {
type: Object,
required: true,
},
nodeTypePrimary: {
type: Boolean,
required: true,
},
nodeTypePrimary: {
type: Boolean,
required: true,
},
data() {
return {
showSectionItems: false,
};
},
computed: {
nodeDetailItems() {
if (this.nodeTypePrimary) {
// Return primary node detail items
const primaryNodeDetailItems = [
{
itemTitle: s__('GeoNodes|Replication slots'),
itemValue: this.nodeDetails.replicationSlots,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Used slots'),
neutraLabel: s__('GeoNodes|Unused slots'),
},
];
if (this.nodeDetails.replicationSlots.totalCount) {
primaryNodeDetailItems.push(
{
itemTitle: s__('GeoNodes|Replication slot WAL'),
itemValue: numberToHumanSize(this.nodeDetails.replicationSlotWAL),
itemValueType: VALUE_TYPE.PLAIN,
cssClass: 'node-detail-value-bold',
},
);
}
return primaryNodeDetailItems;
}
// Return secondary node detail items
return [
},
data() {
return {
showSectionItems: false,
};
},
computed: {
nodeDetailItems() {
if (this.nodeTypePrimary) {
// Return primary node detail items
const primaryNodeDetailItems = [
{
itemTitle: s__('GeoNodes|Storage config'),
itemValue: this.storageShardsStatus,
itemValueType: VALUE_TYPE.PLAIN,
cssClass: this.storageShardsCssClass,
itemTitle: s__('GeoNodes|Replication slots'),
itemValue: this.nodeDetails.replicationSlots,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Used slots'),
neutraLabel: s__('GeoNodes|Unused slots'),
},
];
},
storageShardsStatus() {
if (this.nodeDetails.storageShardsMatch == null) {
return __('Unknown');
if (this.nodeDetails.replicationSlots.totalCount) {
primaryNodeDetailItems.push({
itemTitle: s__('GeoNodes|Replication slot WAL'),
itemValue: numberToHumanSize(this.nodeDetails.replicationSlotWAL),
itemValueType: VALUE_TYPE.PLAIN,
cssClass: 'node-detail-value-bold',
});
}
return this.nodeDetails.storageShardsMatch ? __('OK') : s__('GeoNodes|Does not match the primary storage configuration');
},
storageShardsCssClass() {
const cssClass = 'node-detail-value-bold';
return !this.nodeDetails.storageShardsMatch ? `${cssClass} node-detail-value-error` : cssClass;
},
return primaryNodeDetailItems;
}
// Return secondary node detail items
return [
{
itemTitle: s__('GeoNodes|Storage config'),
itemValue: this.storageShardsStatus,
itemValueType: VALUE_TYPE.PLAIN,
cssClass: this.storageShardsCssClass,
},
];
},
storageShardsStatus() {
if (this.nodeDetails.storageShardsMatch == null) {
return __('Unknown');
}
return this.nodeDetails.storageShardsMatch
? __('OK')
: s__('GeoNodes|Does not match the primary storage configuration');
},
storageShardsCssClass() {
const cssClass = 'node-detail-value-bold';
return !this.nodeDetails.storageShardsMatch
? `${cssClass} node-detail-value-error`
: cssClass;
},
methods: {
handleSectionToggle(toggleState) {
this.showSectionItems = toggleState;
},
},
methods: {
handleSectionToggle(toggleState) {
this.showSectionItems = toggleState;
},
};
},
};
</script>
<template>
......
<script>
import { s__ } from '~/locale';
import { s__ } from '~/locale';
import { VALUE_TYPE, HELP_INFO_URL } from '../../constants';
import { VALUE_TYPE, HELP_INFO_URL } from '../../constants';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
export default {
components: {
GeoNodeDetailItem,
SectionRevealButton,
export default {
components: {
GeoNodeDetailItem,
SectionRevealButton,
},
mixins: [DetailsSectionMixin],
props: {
nodeDetails: {
type: Object,
required: true,
},
mixins: [
DetailsSectionMixin,
],
props: {
nodeDetails: {
type: Object,
required: true,
},
nodeTypePrimary: {
type: Boolean,
required: true,
},
nodeTypePrimary: {
type: Boolean,
required: true,
},
data() {
return {
showSectionItems: false,
primaryNodeDetailItems: this.getPrimaryNodeDetailItems(),
secondaryNodeDetailItems: this.getSecondaryNodeDetailItems(),
};
},
data() {
return {
showSectionItems: false,
primaryNodeDetailItems: this.getPrimaryNodeDetailItems(),
secondaryNodeDetailItems: this.getSecondaryNodeDetailItems(),
};
},
computed: {
nodeDetailItems() {
return this.nodeTypePrimary
? this.getPrimaryNodeDetailItems()
: this.getSecondaryNodeDetailItems();
},
computed: {
nodeDetailItems() {
return this.nodeTypePrimary ?
this.getPrimaryNodeDetailItems() :
this.getSecondaryNodeDetailItems();
},
},
methods: {
getPrimaryNodeDetailItems() {
return [
{
itemTitle: s__('GeoNodes|Repository checksum progress'),
itemValue: this.nodeDetails.repositoriesChecksummed,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Checksummed'),
neutraLabel: s__('GeoNodes|Not checksummed'),
failureLabel: s__('GeoNodes|Failed'),
helpInfo: {
title: s__('GeoNodes|Repositories checksummed for verification with their counterparts on Secondary nodes'),
url: HELP_INFO_URL,
urlText: s__('GeoNodes|Learn more about Repository checksum progress'),
},
},
methods: {
getPrimaryNodeDetailItems() {
return [
{
itemTitle: s__('GeoNodes|Repository checksum progress'),
itemValue: this.nodeDetails.repositoriesChecksummed,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Checksummed'),
neutraLabel: s__('GeoNodes|Not checksummed'),
failureLabel: s__('GeoNodes|Failed'),
helpInfo: {
title: s__(
'GeoNodes|Repositories checksummed for verification with their counterparts on Secondary nodes',
),
url: HELP_INFO_URL,
urlText: s__('GeoNodes|Learn more about Repository checksum progress'),
},
{
itemTitle: s__('GeoNodes|Wiki checksum progress'),
itemValue: this.nodeDetails.wikisChecksummed,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Checksummed'),
neutraLabel: s__('GeoNodes|Not checksummed'),
failureLabel: s__('GeoNodes|Failed'),
helpInfo: {
title: s__('GeoNodes|Wikis checksummed for verification with their counterparts on Secondary nodes'),
url: HELP_INFO_URL,
urlText: s__('GeoNodes|Learn more about Wiki checksum progress'),
},
},
{
itemTitle: s__('GeoNodes|Wiki checksum progress'),
itemValue: this.nodeDetails.wikisChecksummed,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Checksummed'),
neutraLabel: s__('GeoNodes|Not checksummed'),
failureLabel: s__('GeoNodes|Failed'),
helpInfo: {
title: s__(
'GeoNodes|Wikis checksummed for verification with their counterparts on Secondary nodes',
),
url: HELP_INFO_URL,
urlText: s__('GeoNodes|Learn more about Wiki checksum progress'),
},
];
},
getSecondaryNodeDetailItems() {
return [
{
itemTitle: s__('GeoNodes|Repository verification progress'),
itemValue: this.nodeDetails.verifiedRepositories,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Verified'),
neutraLabel: s__('GeoNodes|Unverified'),
failureLabel: s__('GeoNodes|Failed'),
helpInfo: {
title: s__('GeoNodes|Repositories verified with their counterparts on the Primary node'),
url: HELP_INFO_URL,
urlText: s__('GeoNodes|Learn more about Repository verification'),
},
},
];
},
getSecondaryNodeDetailItems() {
return [
{
itemTitle: s__('GeoNodes|Repository verification progress'),
itemValue: this.nodeDetails.verifiedRepositories,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Verified'),
neutraLabel: s__('GeoNodes|Unverified'),
failureLabel: s__('GeoNodes|Failed'),
helpInfo: {
title: s__(
'GeoNodes|Repositories verified with their counterparts on the Primary node',
),
url: HELP_INFO_URL,
urlText: s__('GeoNodes|Learn more about Repository verification'),
},
{
itemTitle: s__('GeoNodes|Wiki verification progress'),
itemValue: this.nodeDetails.verifiedWikis,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Verified'),
neutraLabel: s__('GeoNodes|Unverified'),
failureLabel: s__('GeoNodes|Failed'),
helpInfo: {
title: s__('GeoNodes|Wikis verified with their counterparts on the Primary node'),
url: HELP_INFO_URL,
urlText: s__('GeoNodes|Learn more about Wiki verification'),
},
},
{
itemTitle: s__('GeoNodes|Wiki verification progress'),
itemValue: this.nodeDetails.verifiedWikis,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Verified'),
neutraLabel: s__('GeoNodes|Unverified'),
failureLabel: s__('GeoNodes|Failed'),
helpInfo: {
title: s__('GeoNodes|Wikis verified with their counterparts on the Primary node'),
url: HELP_INFO_URL,
urlText: s__('GeoNodes|Learn more about Wiki verification'),
},
];
},
handleSectionToggle(toggleState) {
this.showSectionItems = toggleState;
},
},
];
},
handleSectionToggle(toggleState) {
this.showSectionItems = toggleState;
},
};
},
};
</script>
<template>
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
export default {
components: {
icon,
},
props: {
buttonTitle: {
type: String,
required: true,
},
props: {
buttonTitle: {
type: String,
required: true,
},
},
data() {
return {
toggleState: false,
};
},
computed: {
toggleButtonIcon() {
return this.toggleState ? 'angle-up' : 'angle-down';
},
data() {
return {
toggleState: false,
};
},
methods: {
onClickButton() {
this.toggleState = !this.toggleState;
this.$emit('toggleButton', this.toggleState);
},
computed: {
toggleButtonIcon() {
return this.toggleState ? 'angle-up' : 'angle-down';
},
},
methods: {
onClickButton() {
this.toggleState = !this.toggleState;
this.$emit('toggleButton', this.toggleState);
},
},
};
},
};
</script>
<template>
......
......@@ -30,4 +30,5 @@ export const TIME_DIFF = {
export const STATUS_DELAY_THRESHOLD_MS = 60000;
export const HELP_INFO_URL = 'https://docs.gitlab.com/ee/administration/geo/disaster_recovery/background_verification.html#repository-verification';
export const HELP_INFO_URL =
'https://docs.gitlab.com/ee/administration/geo/disaster_recovery/background_verification.html#repository-verification';
......@@ -13,9 +13,7 @@ export default {
},
statusInfoStaleMessage() {
return sprintf(s__('GeoNodes|Data is out of date from %{timeago}'), {
timeago: this.timeFormated(
this.nodeDetails.statusCheckTimestamp,
),
timeago: this.timeFormated(this.nodeDetails.statusCheckTimestamp),
});
},
},
......
......@@ -8,9 +8,7 @@ export default class GeoNodesStore {
}
setNodes(nodes) {
this.state.nodes = nodes.map(
node => GeoNodesStore.formatNode(node),
);
this.state.nodes = nodes.map(node => GeoNodesStore.formatNode(node));
}
getNodes() {
......
......@@ -19,7 +19,7 @@ const setupAutoCompleteEpics = ($input, defaultCallbacks) => {
callbacks: {
...defaultCallbacks,
beforeSave(merges) {
return $.map(merges, (m) => {
return $.map(merges, m => {
if (m.title == null) {
return m;
}
......
......@@ -11,18 +11,16 @@ export default () => {
modal: true,
show: false,
})
.on('show.bs.modal', (e) => {
const {
cloneUrlPrimary,
cloneUrlSecondary,
} = $(e.currentTarget).data();
.on('show.bs.modal', e => {
const { cloneUrlPrimary, cloneUrlSecondary } = $(e.currentTarget).data();
$('#geo-info-1').val(
`git clone ${(cloneUrlSecondary || '<clone url for secondary repository>')}`,
`git clone ${cloneUrlSecondary || '<clone url for secondary repository>'}`,
);
$('#geo-info-2').val(
`git remote set-url --push origin ${(cloneUrlPrimary || '<clone url for primary repository>')}`,
`git remote set-url --push origin ${cloneUrlPrimary ||
'<clone url for primary repository>'}`,
);
});
};
......@@ -78,11 +78,15 @@ export default class KubernetesPodLogs extends LogOutputBehaviours {
this.$podDropdown
.find('.dropdown-menu-toggle')
.html(`<span class="dropdown-toggle-text">${this.podName}</span><i class="fa fa-chevron-down"></i>`);
.html(
`<span class="dropdown-toggle-text">${
this.podName
}</span><i class="fa fa-chevron-down"></i>`,
);
$podDropdownMenu.off('click');
$podDropdownMenu.empty();
pods.forEach((pod) => {
pods.forEach(pod => {
$podDropdownMenu.append(`
<button class='dropdown-item'>
${_.escape(pod)}
......
......@@ -16,7 +16,7 @@ export default function initLDAPGroupsSelect() {
id: function(group) {
return group.cn;
},
placeholder: "Search for a LDAP group",
placeholder: 'Search for a LDAP group',
minimumInputLength: 1,
query: function(query) {
var provider;
......@@ -24,7 +24,7 @@ export default function initLDAPGroupsSelect() {
return Api.ldap_groups(query.term, provider, function(groups) {
var data;
data = {
results: groups
results: groups,
};
return query.callback(data);
});
......@@ -32,18 +32,18 @@ export default function initLDAPGroupsSelect() {
initSelection: function(element, callback) {
var id;
id = $(element).val();
if (id !== "") {
if (id !== '') {
return callback({
cn: id
cn: id,
});
}
},
formatResult: ldapGroupResult,
formatSelection: groupFormatSelection,
dropdownCssClass: "ajax-groups-dropdown",
dropdownCssClass: 'ajax-groups-dropdown',
formatNoMatches: function(nomatch) {
return "Match not found; try refining your search query.";
}
return 'Match not found; try refining your search query.';
},
});
});
return $('#ldap_group_link_provider').on('change', function() {
......
......@@ -96,7 +96,11 @@ export default class MirrorPull {
// Make backOff polling to get data
backOff((next, stop) => {
axios
.get(`${projectMirrorSSHEndpoint}?ssh_url=${repositoryUrl}&compare_host_keys=${encodeURIComponent(currentKnownHosts)}`)
.get(
`${projectMirrorSSHEndpoint}?ssh_url=${repositoryUrl}&compare_host_keys=${encodeURIComponent(
currentKnownHosts,
)}`,
)
.then(({ data, status }) => {
if (status === 204) {
this.backOffRequestCounter += 1;
......
......@@ -97,14 +97,12 @@ export default {
this.isLoading = true;
return Promise.all(
this.alerts.map(alertPath =>
this.service
.readAlert(alertPath)
.then(alertData => {
this.$emit('setAlerts', this.customMetricId, {
...this.alertData,
[alertPath]: alertData,
});
}),
this.service.readAlert(alertPath).then(alertData => {
this.$emit('setAlerts', this.customMetricId, {
...this.alertData,
[alertPath]: alertData,
});
}),
),
)
.then(() => {
......
......@@ -26,17 +26,15 @@ export default {
const [xMin, xMax] = this.graphDrawData.xDom;
const [yMin, yMax] = this.graphDrawData.yDom;
const outOfRange = (this.operator === '>' && this.threshold > yMax) ||
const outOfRange =
(this.operator === '>' && this.threshold > yMax) ||
(this.operator === '<' && this.threshold < yMin);
if (outOfRange) {
return [];
}
return [
{ time: xMin, value: this.threshold },
{ time: xMax, value: this.threshold },
];
return [{ time: xMin, value: this.threshold }, { time: xMax, value: this.threshold }];
},
linePath() {
if (!this.graphDrawData.lineFunction) {
......
......@@ -4,60 +4,74 @@ import $ from 'jquery';
import Api from '~/api';
function AdminEmailSelect() {
$('.ajax-admin-email-select').each((function(_this) {
return function(i, select) {
var skip_ldap;
skip_ldap = $(select).hasClass('skip_ldap');
return $(select).select2({
placeholder: "Select group or project",
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
const groupsFetch = Api.groups(query.term, {});
const projectsFetch = Api.projects(query.term, {
order_by: 'id',
membership: false
});
return Promise.all([projectsFetch, groupsFetch]).then(function([projects, groups]) {
var all, data;
all = {
id: "all"
};
data = [all].concat(groups, projects);
return query.callback({
results: data
$('.ajax-admin-email-select').each(
(function(_this) {
return function(i, select) {
var skip_ldap;
skip_ldap = $(select).hasClass('skip_ldap');
return $(select).select2({
placeholder: 'Select group or project',
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
const groupsFetch = Api.groups(query.term, {});
const projectsFetch = Api.projects(query.term, {
order_by: 'id',
membership: false,
});
});
},
id: function(object) {
if (object.path_with_namespace) {
return "project-" + object.id;
} else if (object.path) {
return "group-" + object.id;
} else {
return "all";
}
},
formatResult(...args) {
return _this.formatResult(...args);
},
formatSelection(...args) {
return _this.formatSelection(...args);
},
dropdownCssClass: "ajax-admin-email-dropdown",
escapeMarkup: function(m) {
return m;
}
});
};
})(this));
return Promise.all([projectsFetch, groupsFetch]).then(function([projects, groups]) {
var all, data;
all = {
id: 'all',
};
data = [all].concat(groups, projects);
return query.callback({
results: data,
});
});
},
id: function(object) {
if (object.path_with_namespace) {
return 'project-' + object.id;
} else if (object.path) {
return 'group-' + object.id;
} else {
return 'all';
}
},
formatResult(...args) {
return _this.formatResult(...args);
},
formatSelection(...args) {
return _this.formatSelection(...args);
},
dropdownCssClass: 'ajax-admin-email-dropdown',
escapeMarkup: function(m) {
return m;
},
});
};
})(this),
);
}
AdminEmailSelect.prototype.formatResult = function(object) {
if (object.path_with_namespace) {
return "<div class='project-result'> <div class='project-name'>" + object.name + "</div> <div class='project-path'>" + object.path_with_namespace + "</div> </div>";
return (
"<div class='project-result'> <div class='project-name'>" +
object.name +
"</div> <div class='project-path'>" +
object.path_with_namespace +
'</div> </div>'
);
} else if (object.path) {
return "<div class='group-result'> <div class='group-name'>" + object.name + "</div> <div class='group-path'>" + object.path + "</div> </div>";
return (
"<div class='group-result'> <div class='group-name'>" +
object.name +
"</div> <div class='group-path'>" +
object.path +
'</div> </div>'
);
} else {
return "<div class='group-result'> <div class='group-name'>All</div> <div class='group-path'>All groups and projects</div> </div>";
}
......@@ -65,11 +79,11 @@ AdminEmailSelect.prototype.formatResult = function(object) {
AdminEmailSelect.prototype.formatSelection = function(object) {
if (object.path_with_namespace) {
return "Project: " + object.name;
return 'Project: ' + object.name;
} else if (object.path) {
return "Group: " + object.name;
return 'Group: ' + object.name;
} else {
return "All groups and projects";
return 'All groups and projects';
}
};
......
......@@ -24,11 +24,11 @@ export default function geoNodeForm() {
const $syncByNamespaces = $('.js-sync-by-namespace', $container);
const $syncByShards = $('.js-sync-by-shard', $container);
$primaryCheckbox.on('change', e =>
onPrimaryCheckboxChange(e, $namespaces));
$primaryCheckbox.on('change', e => onPrimaryCheckboxChange(e, $namespaces));
$selectiveSyncTypeSelect.on('change', e =>
onSelectiveSyncTypeChange(e, $syncByNamespaces, $syncByShards));
onSelectiveSyncTypeChange(e, $syncByNamespaces, $syncByShards),
);
$select2Dropdown.select2({
placeholder: s__('Geo|Select groups to replicate.'),
......
......@@ -25,7 +25,7 @@ document.addEventListener('DOMContentLoaded', () => {
merge_requests_created: [],
};
outputElIds.forEach((id) => {
outputElIds.forEach(id => {
data[id].data.forEach((d, index) => {
formattedData[id].push({
name: data.labels[index],
......
......@@ -107,10 +107,9 @@ export default class EEMirrorRepos extends MirrorRepos {
};
}
return super.deleteMirror(event, payload)
.then(() => {
if (isPullMirror) this.$mirrorDirectionSelect.removeAttr('disabled');
});
return super.deleteMirror(event, payload).then(() => {
if (isPullMirror) this.$mirrorDirectionSelect.removeAttr('disabled');
});
}
removeRow($target) {
......
......@@ -4,10 +4,7 @@ export default () => {
const dataEl = document.getElementById('js-file-lock');
if (dataEl) {
const {
toggle_path,
path,
} = JSON.parse(dataEl.innerHTML);
const { toggle_path, path } = JSON.parse(dataEl.innerHTML);
initPathLocks(toggle_path, path);
}
......
......@@ -4,13 +4,16 @@ import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
export default function initPathLocks(url, path) {
$('a.path-lock').on('click', (e) => {
$('a.path-lock').on('click', e => {
e.preventDefault();
axios.post(url, {
path,
}).then(() => {
window.location.reload();
}).catch(() => flash(__('An error occurred while initializing path locks')));
axios
.post(url, {
path,
})
.then(() => {
window.location.reload();
})
.catch(() => flash(__('An error occurred while initializing path locks')));
});
}
<script>
import ciStatus from '~/vue_shared/components/ci_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ciStatus from '~/vue_shared/components/ci_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
components: {
ciStatus,
},
props: {
pipelineId: {
type: Number,
required: true,
},
components: {
ciStatus,
pipelinePath: {
type: String,
required: true,
},
props: {
pipelineId: {
type: Number,
required: true,
},
pipelinePath: {
type: String,
required: true,
},
pipelineStatus: {
type: Object,
required: true,
},
projectName: {
type: String,
required: true,
},
pipelineStatus: {
type: Object,
required: true,
},
computed: {
tooltipText() {
return `${this.projectName} - ${this.pipelineStatus.label}`;
},
projectName: {
type: String,
required: true,
},
};
},
computed: {
tooltipText() {
return `${this.projectName} - ${this.pipelineStatus.label}`;
},
},
};
</script>
<template>
......
<script>
import linkedPipeline from './linked_pipeline.vue';
import linkedPipeline from './linked_pipeline.vue';
export default {
components: {
linkedPipeline,
export default {
components: {
linkedPipeline,
},
props: {
columnTitle: {
type: String,
required: true,
},
props: {
columnTitle: {
type: String,
required: true,
},
linkedPipelines: {
type: Array,
required: true,
},
graphPosition: {
type: String,
required: true,
},
linkedPipelines: {
type: Array,
required: true,
},
graphPosition: {
type: String,
required: true,
},
},
computed: {
columnClass() {
return `graph-position-${this.graphPosition}`;
},
computed: {
columnClass() {
return `graph-position-${this.graphPosition}`;
},
};
},
};
</script>
<template>
......
......@@ -46,8 +46,9 @@ const bindEvents = () => {
const $activeTabProjectName = $('.tab-pane.active #project_name');
const $activeTabProjectPath = $('.tab-pane.active #project_path');
$activeTabProjectName.focus();
$activeTabProjectName
.keyup(() => projectNew.onProjectNameChange($activeTabProjectName, $activeTabProjectPath));
$activeTabProjectName.keyup(() =>
projectNew.onProjectNameChange($activeTabProjectName, $activeTabProjectPath),
);
}
$useCustomTemplateBtn.on('change', chooseTemplate);
......@@ -60,7 +61,6 @@ const bindEvents = () => {
};
export default () => {
const $navElement = $('.nav-link[href="#custom-templates"]');
const $tabContent = $('.project-templates-buttons#custom-templates');
......
<script>
import Flash from '~/flash';
import serviceDeskSetting from './service_desk_setting.vue';
import ServiceDeskStore from '../stores/service_desk_store';
import ServiceDeskService from '../services/service_desk_service';
import eventHub from '../event_hub';
import Flash from '~/flash';
import serviceDeskSetting from './service_desk_setting.vue';
import ServiceDeskStore from '../stores/service_desk_store';
import ServiceDeskService from '../services/service_desk_service';
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskRoot',
export default {
name: 'ServiceDeskRoot',
components: {
serviceDeskSetting,
components: {
serviceDeskSetting,
},
props: {
initialIsEnabled: {
type: Boolean,
required: true,
},
props: {
initialIsEnabled: {
type: Boolean,
required: true,
},
endpoint: {
type: String,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
endpoint: {
type: String,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
},
data() {
const store = new ServiceDeskStore({
incomingEmail: this.incomingEmail,
});
data() {
const store = new ServiceDeskStore({
incomingEmail: this.incomingEmail,
});
return {
store,
state: store.state,
isEnabled: this.initialIsEnabled,
};
},
return {
store,
state: store.state,
isEnabled: this.initialIsEnabled,
};
},
created() {
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
created() {
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
this.service = new ServiceDeskService(this.endpoint);
this.service = new ServiceDeskService(this.endpoint);
if (this.isEnabled && !this.store.state.incomingEmail) {
this.fetchIncomingEmail();
}
},
if (this.isEnabled && !this.store.state.incomingEmail) {
this.fetchIncomingEmail();
}
},
beforeDestroy() {
eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
},
beforeDestroy() {
eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
},
methods: {
fetchIncomingEmail() {
if (this.flash) {
this.flash.innerHTML = '';
}
methods: {
fetchIncomingEmail() {
if (this.flash) {
this.flash.innerHTML = '';
}
this.service.fetchIncomingEmail()
.then(res => res.json())
.then((data) => {
const email = data.service_desk_address;
if (!email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
this.service
.fetchIncomingEmail()
.then(res => res.json())
.then(data => {
const email = data.service_desk_address;
if (!email) {
throw new Error("Response didn't include `service_desk_address`");
}
this.store.setIncomingEmail(email);
})
.catch(() => {
this.flash = new Flash('An error occurred while fetching the Service Desk address.', 'alert', this.$el);
});
},
this.store.setIncomingEmail(email);
})
.catch(() => {
this.flash = new Flash(
'An error occurred while fetching the Service Desk address.',
'alert',
this.$el,
);
});
},
onEnableToggled(isChecked) {
this.isEnabled = isChecked;
this.store.resetIncomingEmail();
if (this.flash) {
this.flash.destroy();
}
onEnableToggled(isChecked) {
this.isEnabled = isChecked;
this.store.resetIncomingEmail();
if (this.flash) {
this.flash.destroy();
}
this.service.toggleServiceDesk(isChecked)
.then(res => res.json())
.then((data) => {
const email = data.service_desk_address;
if (isChecked && !email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
this.service
.toggleServiceDesk(isChecked)
.then(res => res.json())
.then(data => {
const email = data.service_desk_address;
if (isChecked && !email) {
throw new Error("Response didn't include `service_desk_address`");
}
this.store.setIncomingEmail(email);
})
.catch(() => {
const verb = isChecked ? 'enabling' : 'disabling';
this.flash = new Flash(`An error occurred while ${verb} Service Desk.`, 'alert', this.$el);
});
},
this.store.setIncomingEmail(email);
})
.catch(() => {
const verb = isChecked ? 'enabling' : 'disabling';
this.flash = new Flash(
`An error occurred while ${verb} Service Desk.`,
'alert',
this.$el,
);
});
},
};
},
};
</script>
<template>
......
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import eventHub from '../event_hub';
import tooltip from '~/vue_shared/directives/tooltip';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskSetting',
directives: {
tooltip,
},
export default {
name: 'ServiceDeskSetting',
directives: {
tooltip,
},
components: {
ClipboardButton,
},
components: {
ClipboardButton,
},
props: {
isEnabled: {
type: Boolean,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
props: {
isEnabled: {
type: Boolean,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
methods: {
onCheckboxToggle(e) {
const isChecked = e.target.checked;
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
},
},
methods: {
onCheckboxToggle(e) {
const isChecked = e.target.checked;
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
},
};
},
};
</script>
<template>
......
......@@ -14,9 +14,7 @@ export default () => {
data() {
const { dataset } = serviceDeskRootElement;
return {
initialIsEnabled: convertPermissionToBoolean(
dataset.enabled,
),
initialIsEnabled: convertPermissionToBoolean(dataset.enabled),
endpoint: dataset.endpoint,
incomingEmail: dataset.incomingEmail,
};
......
class ServiceDeskStore {
constructor(initialState = {}) {
this.state = Object.assign({
incomingEmail: '',
}, initialState);
this.state = Object.assign(
{
incomingEmail: '',
},
initialState,
);
}
setIncomingEmail(value) {
......
import Stats from 'ee/stats';
const bindTrackEvents = (container) => {
const bindTrackEvents = container => {
Stats.bindTrackableContainer(container);
};
......
......@@ -11,18 +11,32 @@ export default class EEPrometheusMetrics extends PrometheusMetrics {
super(wrapperSelector);
this.$wrapperCustomMetrics = $(wrapperSelector);
this.$monitoredCustomMetricsPanel = this.$wrapperCustomMetrics.find('.js-panel-custom-monitored-metrics');
this.$monitoredCustomMetricsCount = this.$monitoredCustomMetricsPanel.find('.js-custom-monitored-count');
this.$monitoredCustomMetricsLoading = this.$monitoredCustomMetricsPanel.find('.js-loading-custom-metrics');
this.$monitoredCustomMetricsEmpty = this.$monitoredCustomMetricsPanel.find('.js-empty-custom-metrics');
this.$monitoredCustomMetricsList = this.$monitoredCustomMetricsPanel.find('.js-custom-metrics-list');
this.$monitoredCustomMetricsPanel = this.$wrapperCustomMetrics.find(
'.js-panel-custom-monitored-metrics',
);
this.$monitoredCustomMetricsCount = this.$monitoredCustomMetricsPanel.find(
'.js-custom-monitored-count',
);
this.$monitoredCustomMetricsLoading = this.$monitoredCustomMetricsPanel.find(
'.js-loading-custom-metrics',
);
this.$monitoredCustomMetricsEmpty = this.$monitoredCustomMetricsPanel.find(
'.js-empty-custom-metrics',
);
this.$monitoredCustomMetricsList = this.$monitoredCustomMetricsPanel.find(
'.js-custom-metrics-list',
);
this.$newCustomMetricButton = this.$monitoredCustomMetricsPanel.find('.js-new-metric-button');
this.$flashCustomMetricsContainer = this.$wrapperCustomMetrics.find('.flash-container');
this.customMetrics = [];
this.environmentsData = [];
this.activeCustomMetricsEndpoint = this.$monitoredCustomMetricsPanel.data('active-custom-metrics');
this.environmentsDataEndpoint = this.$monitoredCustomMetricsPanel.data('environments-data-endpoint');
this.activeCustomMetricsEndpoint = this.$monitoredCustomMetricsPanel.data(
'active-custom-metrics',
);
this.environmentsDataEndpoint = this.$monitoredCustomMetricsPanel.data(
'environments-data-endpoint',
);
}
showMonitoringCustomMetricsPanelState(stateName) {
......@@ -49,20 +63,25 @@ export default class EEPrometheusMetrics extends PrometheusMetrics {
}
populateCustomMetrics() {
const sortedMetrics = _(this.customMetrics).chain()
const sortedMetrics = _(this.customMetrics)
.chain()
.map(metric => ({ ...metric, group: capitalizeFirstCharacter(metric.group) }))
.sortBy('title')
.sortBy('group')
.value();
sortedMetrics.forEach((metric) => {
sortedMetrics.forEach(metric => {
this.$monitoredCustomMetricsList.append(EEPrometheusMetrics.customMetricTemplate(metric));
});
this.$monitoredCustomMetricsCount.text(this.customMetrics.length);
this.showMonitoringCustomMetricsPanelState(PANEL_STATE.LIST);
if (!this.environmentsData) {
this.showFlashMessage(s__('PrometheusService|These metrics will only be monitored after your first deployment to an environment'));
this.showFlashMessage(
s__(
'PrometheusService|These metrics will only be monitored after your first deployment to an environment',
),
);
}
}
......@@ -86,7 +105,7 @@ export default class EEPrometheusMetrics extends PrometheusMetrics {
this.populateCustomMetrics(customMetrics.data.metrics);
}
})
.catch((customMetricError) => {
.catch(customMetricError => {
this.showFlashMessage(customMetricError);
this.showMonitoringCustomMetricsPanelState(PANEL_STATE.EMPTY);
});
......
......@@ -17,4 +17,3 @@ export default class ProtectedEnvironmentEditList {
});
}
}
......@@ -43,8 +43,9 @@ export default {
return `Paste issue link${this.allowAutoComplete ? ' or <#issue id>' : ''}`;
},
isSubmitButtonDisabled() {
return (this.inputValue.length === 0 && this.pendingReferences.length === 0)
|| this.isSubmitting;
return (
(this.inputValue.length === 0 && this.pendingReferences.length === 0) || this.isSubmitting
);
},
allowAutoComplete() {
return Object.keys(this.autoCompleteSources).length > 0;
......
......@@ -113,10 +113,10 @@ export default {
if (issueToRemove) {
RelatedIssuesService.remove(issueToRemove.relation_path)
.then(res => res.json())
.then((data) => {
.then(data => {
this.store.setRelatedIssues(data.issues);
})
.catch((res) => {
.catch(res => {
if (res && res.status !== 404) {
Flash('An error occurred while removing issues.');
}
......@@ -136,9 +136,10 @@ export default {
if (this.state.pendingReferences.length > 0) {
this.isSubmitting = true;
this.service.addRelatedIssues(this.state.pendingReferences)
this.service
.addRelatedIssues(this.state.pendingReferences)
.then(res => res.json())
.then((data) => {
.then(data => {
// We could potentially lose some pending issues in the interim here
this.store.setPendingReferences([]);
this.store.setRelatedIssues(data.issues);
......@@ -147,9 +148,9 @@ export default {
// Close the form on submission
this.isFormVisible = false;
})
.catch((res) => {
.catch(res => {
this.isSubmitting = false;
let errorMessage = 'We can\'t find an issue that matches what you are looking for.';
let errorMessage = "We can't find an issue that matches what you are looking for.";
if (res.data && res.data.message) {
errorMessage = res.data.message;
}
......@@ -164,9 +165,10 @@ export default {
},
fetchRelatedIssues() {
this.isFetching = true;
this.service.fetchRelatedIssues()
this.service
.fetchRelatedIssues()
.then(res => res.json())
.then((issues) => {
.then(issues => {
this.store.setRelatedIssues(issues);
this.isFetching = false;
})
......@@ -185,27 +187,26 @@ export default {
move_before_id: beforeId,
move_after_id: afterId,
})
.then(res => res.json())
.then((res) => {
if (!res.message) {
this.store.updateIssueOrder(oldIndex, newIndex);
}
})
.catch(() => {
Flash('An error occurred while reordering issues.');
});
.then(res => res.json())
.then(res => {
if (!res.message) {
this.store.updateIssueOrder(oldIndex, newIndex);
}
})
.catch(() => {
Flash('An error occurred while reordering issues.');
});
}
},
onInput(newValue, caretPos) {
const rawReferences = newValue
.split(/\s/);
const rawReferences = newValue.split(/\s/);
let touchedReference;
let iteratingPos = 0;
const untouchedRawReferences = rawReferences
.filter((reference) => {
.filter(reference => {
let isTouched = false;
if (caretPos >= iteratingPos && caretPos <= (iteratingPos + reference.length)) {
if (caretPos >= iteratingPos && caretPos <= iteratingPos + reference.length) {
touchedReference = reference;
isTouched = true;
}
......@@ -216,22 +217,16 @@ export default {
})
.filter(reference => reference.trim().length > 0);
this.store.setPendingReferences(
this.state.pendingReferences.concat(untouchedRawReferences),
);
this.store.setPendingReferences(this.state.pendingReferences.concat(untouchedRawReferences));
this.inputValue = `${touchedReference}`;
},
onBlur(newValue) {
this.processAllReferences(newValue);
},
processAllReferences(value = '') {
const rawReferences = value
.split(/\s+/)
.filter(reference => reference.trim().length > 0);
const rawReferences = value.split(/\s+/).filter(reference => reference.trim().length > 0);
this.store.setPendingReferences(
this.state.pendingReferences.concat(rawReferences),
);
this.store.setPendingReferences(this.state.pendingReferences.concat(rawReferences));
this.inputValue = '';
},
},
......
......@@ -11,15 +11,16 @@ export default function initRelatedIssues() {
components: {
relatedIssuesRoot: RelatedIssuesRoot,
},
render: createElement => createElement('related-issues-root', {
props: {
endpoint: relatedIssuesRootElement.dataset.endpoint,
canAdmin: convertPermissionToBoolean(
relatedIssuesRootElement.dataset.canAddRelatedIssues,
),
helpPath: relatedIssuesRootElement.dataset.helpPath,
},
}),
render: createElement =>
createElement('related-issues-root', {
props: {
endpoint: relatedIssuesRootElement.dataset.endpoint,
canAdmin: convertPermissionToBoolean(
relatedIssuesRootElement.dataset.canAddRelatedIssues,
),
helpPath: relatedIssuesRootElement.dataset.helpPath,
},
}),
});
}
}
......@@ -13,9 +13,12 @@ class RelatedIssuesService {
}
addRelatedIssues(newIssueReferences) {
return this.relatedIssuesResource.save({}, {
issue_references: newIssueReferences,
});
return this.relatedIssuesResource.save(
{},
{
issue_references: newIssueReferences,
},
);
}
static saveOrder({ endpoint, move_before_id, move_after_id }) {
......
......@@ -28,10 +28,10 @@ class RelatedIssuesStore {
}
removePendingRelatedIssue(indexToRemove) {
this.state.pendingReferences =
this.state.pendingReferences.filter((reference, index) => index !== indexToRemove);
this.state.pendingReferences = this.state.pendingReferences.filter(
(reference, index) => index !== indexToRemove,
);
}
}
export default RelatedIssuesStore;
<script>
import _ from 'underscore';
import Flash from '~/flash';
import { s__ } from '~/locale';
import _ from 'underscore';
import Flash from '~/flash';
import { s__ } from '~/locale';
import epicsListEmpty from './epics_list_empty.vue';
import roadmapShell from './roadmap_shell.vue';
import epicsListEmpty from './epics_list_empty.vue';
import roadmapShell from './roadmap_shell.vue';
export default {
components: {
epicsListEmpty,
roadmapShell,
export default {
components: {
epicsListEmpty,
roadmapShell,
},
props: {
store: {
type: Object,
required: true,
},
props: {
store: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
presetType: {
type: String,
required: true,
},
hasFiltersApplied: {
type: Boolean,
required: true,
},
newEpicEndpoint: {
type: String,
required: true,
},
emptyStateIllustrationPath: {
type: String,
required: true,
},
service: {
type: Object,
required: true,
},
data() {
return {
isLoading: true,
isEpicsListEmpty: false,
hasError: false,
handleResizeThrottled: {},
};
presetType: {
type: String,
required: true,
},
computed: {
epics() {
return this.store.getEpics();
},
timeframe() {
return this.store.getTimeframe();
},
timeframeStart() {
return this.timeframe[0];
},
timeframeEnd() {
const last = this.timeframe.length - 1;
return this.timeframe[last];
},
currentGroupId() {
return this.store.getCurrentGroupId();
},
showRoadmap() {
return !this.hasError && !this.isLoading && !this.isEpicsListEmpty;
},
hasFiltersApplied: {
type: Boolean,
required: true,
},
mounted() {
this.fetchEpics();
this.handleResizeThrottled = _.throttle(this.handleResize, 600);
window.addEventListener('resize', this.handleResizeThrottled, false);
newEpicEndpoint: {
type: String,
required: true,
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResizeThrottled, false);
emptyStateIllustrationPath: {
type: String,
required: true,
},
methods: {
fetchEpics() {
this.hasError = false;
this.service.getEpics()
.then(res => res.data)
.then((epics) => {
this.isLoading = false;
if (epics.length) {
this.store.setEpics(epics);
} else {
this.isEpicsListEmpty = true;
}
})
.catch(() => {
this.isLoading = false;
this.hasError = true;
Flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
});
},
/**
* Roadmap view works with absolute sizing and positioning
* of following child components of RoadmapShell;
*
* - RoadmapTimelineSection
* - TimelineTodayIndicator
* - EpicItemTimeline
*
* And hence when window is resized, any size attributes passed
* down to child components are no longer valid, so best approach
* to refresh entire app is to re-render it on resize, hence
* we toggle `isLoading` variable which is bound to `RoadmapShell`.
*/
handleResize() {
this.isLoading = true;
// We need to debounce the toggle to make sure loading animation
// shows up while app is being rerendered.
_.debounce(() => {
},
data() {
return {
isLoading: true,
isEpicsListEmpty: false,
hasError: false,
handleResizeThrottled: {},
};
},
computed: {
epics() {
return this.store.getEpics();
},
timeframe() {
return this.store.getTimeframe();
},
timeframeStart() {
return this.timeframe[0];
},
timeframeEnd() {
const last = this.timeframe.length - 1;
return this.timeframe[last];
},
currentGroupId() {
return this.store.getCurrentGroupId();
},
showRoadmap() {
return !this.hasError && !this.isLoading && !this.isEpicsListEmpty;
},
},
mounted() {
this.fetchEpics();
this.handleResizeThrottled = _.throttle(this.handleResize, 600);
window.addEventListener('resize', this.handleResizeThrottled, false);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResizeThrottled, false);
},
methods: {
fetchEpics() {
this.hasError = false;
this.service
.getEpics()
.then(res => res.data)
.then(epics => {
this.isLoading = false;
}, 200)();
},
if (epics.length) {
this.store.setEpics(epics);
} else {
this.isEpicsListEmpty = true;
}
})
.catch(() => {
this.isLoading = false;
this.hasError = true;
Flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
});
},
/**
* Roadmap view works with absolute sizing and positioning
* of following child components of RoadmapShell;
*
* - RoadmapTimelineSection
* - TimelineTodayIndicator
* - EpicItemTimeline
*
* And hence when window is resized, any size attributes passed
* down to child components are no longer valid, so best approach
* to refresh entire app is to re-render it on resize, hence
* we toggle `isLoading` variable which is bound to `RoadmapShell`.
*/
handleResize() {
this.isLoading = true;
// We need to debounce the toggle to make sure loading animation
// shows up while app is being rerendered.
_.debounce(() => {
this.isLoading = false;
}, 200)();
},
};
},
};
</script>
<template>
......
<script>
import epicItemDetails from './epic_item_details.vue';
import epicItemTimeline from './epic_item_timeline.vue';
import epicItemDetails from './epic_item_details.vue';
import epicItemTimeline from './epic_item_timeline.vue';
export default {
components: {
epicItemDetails,
epicItemTimeline,
export default {
components: {
epicItemDetails,
epicItemTimeline,
},
props: {
presetType: {
type: String,
required: true,
},
props: {
presetType: {
type: String,
required: true,
},
epic: {
type: Object,
required: true,
},
timeframe: {
type: Array,
required: true,
},
currentGroupId: {
type: Number,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
epic: {
type: Object,
required: true,
},
};
timeframe: {
type: Array,
required: true,
},
currentGroupId: {
type: Number,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
};
</script>
<template>
......
<script>
import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
props: {
epic: {
type: Object,
required: true,
},
props: {
epic: {
type: Object,
required: true,
},
currentGroupId: {
type: Number,
required: true,
},
currentGroupId: {
type: Number,
required: true,
},
computed: {
isEpicGroupDifferent() {
return this.currentGroupId !== this.epic.groupId;
},
/**
* In case Epic start date is out of range
* we need to use original date instead of proxy date
*/
startDate() {
if (this.epic.startDateOutOfRange) {
return this.epic.originalStartDate;
}
},
computed: {
isEpicGroupDifferent() {
return this.currentGroupId !== this.epic.groupId;
},
/**
* In case Epic start date is out of range
* we need to use original date instead of proxy date
*/
startDate() {
if (this.epic.startDateOutOfRange) {
return this.epic.originalStartDate;
}
return this.epic.startDate;
},
/**
* In case Epic end date is out of range
* we need to use original date instead of proxy date
*/
endDate() {
if (this.epic.endDateOutOfRange) {
return this.epic.originalEndDate;
}
return this.epic.endDate;
},
/**
* Compose timeframe string to show on UI
* based on start and end date availability
*/
timeframeString() {
if (this.epic.startDateUndefined) {
return sprintf(s__('GroupRoadmap|Until %{dateWord}'), {
dateWord: dateInWords(this.endDate, true),
});
} else if (this.epic.endDateUndefined) {
return sprintf(s__('GroupRoadmap|From %{dateWord}'), {
dateWord: dateInWords(this.startDate, true),
});
}
return this.epic.startDate;
},
/**
* In case Epic end date is out of range
* we need to use original date instead of proxy date
*/
endDate() {
if (this.epic.endDateOutOfRange) {
return this.epic.originalEndDate;
}
return this.epic.endDate;
},
/**
* Compose timeframe string to show on UI
* based on start and end date availability
*/
timeframeString() {
if (this.epic.startDateUndefined) {
return sprintf(s__('GroupRoadmap|Until %{dateWord}'), {
dateWord: dateInWords(this.endDate, true),
});
} else if (this.epic.endDateUndefined) {
return sprintf(s__('GroupRoadmap|From %{dateWord}'), {
dateWord: dateInWords(this.startDate, true),
});
}
// In case both start and end date fall in same year
// We should hide year from start date
const startDateInWords = dateInWords(
this.startDate,
true,
this.startDate.getFullYear() === this.endDate.getFullYear(),
);
// In case both start and end date fall in same year
// We should hide year from start date
const startDateInWords = dateInWords(
this.startDate,
true,
this.startDate.getFullYear() === this.endDate.getFullYear(),
);
return `${startDateInWords} &ndash; ${dateInWords(this.endDate, true)}`;
},
return `${startDateInWords} &ndash; ${dateInWords(this.endDate, true)}`;
},
};
},
};
</script>
<template>
......
......@@ -17,11 +17,7 @@ export default {
directives: {
tooltip,
},
mixins: [
QuartersPresetMixin,
MonthsPresetMixin,
WeeksPresetMixin,
],
mixins: [QuartersPresetMixin, MonthsPresetMixin, WeeksPresetMixin],
props: {
presetType: {
type: String,
......
<script>
import eventHub from '../event_hub';
import eventHub from '../event_hub';
import SectionMixin from '../mixins/section_mixin';
import SectionMixin from '../mixins/section_mixin';
import epicItem from './epic_item.vue';
import epicItem from './epic_item.vue';
export default {
components: {
epicItem,
export default {
components: {
epicItem,
},
mixins: [SectionMixin],
props: {
presetType: {
type: String,
required: true,
},
mixins: [
SectionMixin,
],
props: {
presetType: {
type: String,
required: true,
},
epics: {
type: Array,
required: true,
},
timeframe: {
type: Array,
required: true,
},
currentGroupId: {
type: Number,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
listScrollable: {
type: Boolean,
required: true,
},
epics: {
type: Array,
required: true,
},
data() {
timeframe: {
type: Array,
required: true,
},
currentGroupId: {
type: Number,
required: true,
},
shellWidth: {
type: Number,
required: true,
},
listScrollable: {
type: Boolean,
required: true,
},
},
data() {
return {
shellHeight: 0,
emptyRowHeight: 0,
showEmptyRow: false,
offsetLeft: 0,
showBottomShadow: false,
};
},
computed: {
emptyRowContainerStyles() {
return {
shellHeight: 0,
emptyRowHeight: 0,
showEmptyRow: false,
offsetLeft: 0,
showBottomShadow: false,
height: `${this.emptyRowHeight}px`,
};
},
computed: {
emptyRowContainerStyles() {
return {
height: `${this.emptyRowHeight}px`,
};
},
emptyRowCellStyles() {
return {
width: `${this.sectionItemWidth}px`,
};
},
shadowCellStyles() {
return {
left: `${this.offsetLeft}px`,
};
},
},
watch: {
shellWidth: function shellWidth() {
// Scroll view to today indicator only when shellWidth is updated.
this.scrollToTodayIndicator();
// Initialize offsetLeft when shellWidth is updated
this.offsetLeft = this.$el.parentElement.offsetLeft;
},
emptyRowCellStyles() {
return {
width: `${this.sectionItemWidth}px`,
};
},
mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
this.$nextTick(() => {
this.initMounted();
});
shadowCellStyles() {
return {
left: `${this.offsetLeft}px`,
};
},
beforeDestroy() {
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
},
watch: {
shellWidth: function shellWidth() {
// Scroll view to today indicator only when shellWidth is updated.
this.scrollToTodayIndicator();
// Initialize offsetLeft when shellWidth is updated
this.offsetLeft = this.$el.parentElement.offsetLeft;
},
methods: {
initMounted() {
// Get available shell height based on viewport height
this.shellHeight = window.innerHeight - this.$el.offsetTop;
},
mounted() {
eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
this.$nextTick(() => {
this.initMounted();
});
},
beforeDestroy() {
eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
},
methods: {
initMounted() {
// Get available shell height based on viewport height
this.shellHeight = window.innerHeight - this.$el.offsetTop;
// In case there are epics present, initialize empty row
if (this.epics.length) {
this.initEmptyRow();
}
// In case there are epics present, initialize empty row
if (this.epics.length) {
this.initEmptyRow();
}
eventHub.$emit('epicsListRendered', {
width: this.$el.clientWidth,
height: this.shellHeight,
});
},
/**
* In case number of epics in the list are not sufficient
* to fill in full page height, we need to show an empty row
* at the bottom with fixed absolute height such that the
* column rulers expand to full page height
*
* This method calculates absolute height for empty column in pixels
* based on height of available list items and sets it to component
* props.
*/
initEmptyRow() {
const children = this.$children;
let approxChildrenHeight = children[0].$el.clientHeight * this.epics.length;
eventHub.$emit('epicsListRendered', {
width: this.$el.clientWidth,
height: this.shellHeight,
});
},
/**
* In case number of epics in the list are not sufficient
* to fill in full page height, we need to show an empty row
* at the bottom with fixed absolute height such that the
* column rulers expand to full page height
*
* This method calculates absolute height for empty column in pixels
* based on height of available list items and sets it to component
* props.
*/
initEmptyRow() {
const children = this.$children;
let approxChildrenHeight = children[0].$el.clientHeight * this.epics.length;
// Check if approximate height is greater than shell height
if (approxChildrenHeight < this.shellHeight) {
// reset approximate height and recalculate actual height
approxChildrenHeight = 0;
children.forEach((child) => {
// accumulate children height
// compensate for bottom border
approxChildrenHeight += child.$el.clientHeight;
});
// Check if approximate height is greater than shell height
if (approxChildrenHeight < this.shellHeight) {
// reset approximate height and recalculate actual height
approxChildrenHeight = 0;
children.forEach(child => {
// accumulate children height
// compensate for bottom border
approxChildrenHeight += child.$el.clientHeight;
});
// set height and show empty row reducing horizontal scrollbar size
this.emptyRowHeight = (this.shellHeight - approxChildrenHeight);
this.showEmptyRow = true;
} else {
this.showBottomShadow = true;
}
},
/**
* `clientWidth` is full width of list section, and we need to
* scroll up to 60% of the view where today indicator is present.
*
* Reason for 60% is that "today" always falls in the middle of timeframe range.
*/
scrollToTodayIndicator() {
const uptoTodayIndicator = Math.ceil((this.$el.clientWidth * 60) / 100);
this.$el.scrollTo(uptoTodayIndicator, 0);
},
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = (Math.ceil(scrollTop) + clientHeight) < scrollHeight;
},
// set height and show empty row reducing horizontal scrollbar size
this.emptyRowHeight = this.shellHeight - approxChildrenHeight;
this.showEmptyRow = true;
} else {
this.showBottomShadow = true;
}
},
/**
* `clientWidth` is full width of list section, and we need to
* scroll up to 60% of the view where today indicator is present.
*
* Reason for 60% is that "today" always falls in the middle of timeframe range.
*/
scrollToTodayIndicator() {
const uptoTodayIndicator = Math.ceil((this.$el.clientWidth * 60) / 100);
this.$el.scrollTo(uptoTodayIndicator, 0);
},
handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
},
};
},
};
</script>
<template>
......
<script>
import { monthInWords } from '~/lib/utils/datetime_utility';
import { monthInWords } from '~/lib/utils/datetime_utility';
import MonthsHeaderSubItem from './months_header_sub_item.vue';
import MonthsHeaderSubItem from './months_header_sub_item.vue';
export default {
components: {
MonthsHeaderSubItem,
export default {
components: {
MonthsHeaderSubItem,
},
props: {
timeframeIndex: {
type: Number,
required: true,
},
props: {
timeframeIndex: {
type: Number,
required: true,
},
timeframeItem: {
type: Date,
required: true,
},
timeframe: {
type: Array,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
timeframeItem: {
type: Date,
required: true,
},
data() {
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
timeframe: {
type: Array,
required: true,
},
itemWidth: {
type: Number,
required: true,
},
},
data() {
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
return {
currentDate,
currentYear: currentDate.getFullYear(),
currentMonth: currentDate.getMonth(),
};
},
computed: {
itemStyles() {
return {
currentDate,
currentYear: currentDate.getFullYear(),
currentMonth: currentDate.getMonth(),
width: `${this.itemWidth}px`,
};
},
computed: {
itemStyles() {
return {
width: `${this.itemWidth}px`,
};
},
timelineHeaderLabel() {
const year = this.timeframeItem.getFullYear();
const month = monthInWords(this.timeframeItem, true);
timelineHeaderLabel() {
const year = this.timeframeItem.getFullYear();
const month = monthInWords(this.timeframeItem, true);
// Show Year only if current timeframe has months between
// two years and current timeframe item is first month
// from one of the two years.
//
// End result of doing this is;
// 2017 Nov, Dec, 2018 Jan, Feb, Mar
if (this.timeframeIndex !== 0 &&
this.timeframe[this.timeframeIndex - 1].getFullYear() === year) {
return month;
}
// Show Year only if current timeframe has months between
// two years and current timeframe item is first month
// from one of the two years.
//
// End result of doing this is;
// 2017 Nov, Dec, 2018 Jan, Feb, Mar
if (
this.timeframeIndex !== 0 &&
this.timeframe[this.timeframeIndex - 1].getFullYear() === year
) {
return month;
}
return `${year} ${month}`;
},
timelineHeaderClass() {
let itemLabelClass = '';
return `${year} ${month}`;
},
timelineHeaderClass() {
let itemLabelClass = '';
const timeframeYear = this.timeframeItem.getFullYear();
const timeframeMonth = this.timeframeItem.getMonth();
const timeframeYear = this.timeframeItem.getFullYear();
const timeframeMonth = this.timeframeItem.getMonth();
// Show dark color text only if timeframe item year & month
// are greater than current year.
if (timeframeYear >= this.currentYear &&
timeframeMonth >= this.currentMonth) {
itemLabelClass += 'label-dark';
}
// Show dark color text only if timeframe item year & month
// are greater than current year.
if (timeframeYear >= this.currentYear && timeframeMonth >= this.currentMonth) {
itemLabelClass += 'label-dark';
}
// Show bold text only if timeframe item year & month
// is current year & month
if (timeframeYear === this.currentYear &&
timeframeMonth === this.currentMonth) {
itemLabelClass += ' label-bold';
}
// Show bold text only if timeframe item year & month
// is current year & month
if (timeframeYear === this.currentYear && timeframeMonth === this.currentMonth) {
itemLabelClass += ' label-bold';
}
return itemLabelClass;
},
return itemLabelClass;
},
};
},
};
</script>
<template>
......
<script>
import { getSundays } from '~/lib/utils/datetime_utility';
import { getSundays } from '~/lib/utils/datetime_utility';
import { PRESET_TYPES } from '../../constants';
import { PRESET_TYPES } from '../../constants';
import timelineTodayIndicator from '../timeline_today_indicator.vue';
import timelineTodayIndicator from '../timeline_today_indicator.vue';
export default {
presetType: PRESET_TYPES.MONTHS,
components: {
timelineTodayIndicator,
export default {
presetType: PRESET_TYPES.MONTHS,
components: {
timelineTodayIndicator,
},
props: {
currentDate: {
type: Date,
required: true,
},
props: {
currentDate: {
type: Date,
required: true,
},
timeframeItem: {
type: Date,
required: true,
},
timeframeItem: {
type: Date,
required: true,
},
computed: {
headerSubItems() {
return getSundays(this.timeframeItem);
},
headerSubItemClass() {
const currentYear = this.currentDate.getFullYear();
const currentMonth = this.currentDate.getMonth();
const timeframeYear = this.timeframeItem.getFullYear();
const timeframeMonth = this.timeframeItem.getMonth();
},
computed: {
headerSubItems() {
return getSundays(this.timeframeItem);
},
headerSubItemClass() {
const currentYear = this.currentDate.getFullYear();
const currentMonth = this.currentDate.getMonth();
const timeframeYear = this.timeframeItem.getFullYear();
const timeframeMonth = this.timeframeItem.getMonth();
// Show dark color text only for dates from current month and future months.
return timeframeYear >= currentYear && timeframeMonth >= currentMonth ? 'label-dark' : '';
},
hasToday() {
const timeframeYear = this.timeframeItem.getFullYear();
const timeframeMonth = this.timeframeItem.getMonth();
// Show dark color text only for dates from current month and future months.
return timeframeYear >= currentYear && timeframeMonth >= currentMonth ? 'label-dark' : '';
},
hasToday() {
const timeframeYear = this.timeframeItem.getFullYear();
const timeframeMonth = this.timeframeItem.getMonth();
return this.currentDate.getMonth() === timeframeMonth &&
this.currentDate.getFullYear() === timeframeYear;
},
return (
this.currentDate.getMonth() === timeframeMonth &&
this.currentDate.getFullYear() === timeframeYear
);
},
methods: {
getSubItemValueClass(subItem) {
const daysToClosestWeek = this.currentDate.getDate() - subItem.getDate();
// Show dark color text only for upcoming dates
// and current week date
if (daysToClosestWeek <= 6 &&
this.currentDate.getDate() >= subItem.getDate() &&
this.currentDate.getFullYear() === subItem.getFullYear() &&
this.currentDate.getMonth() === subItem.getMonth()) {
return 'label-dark label-bold';
} else if (subItem >= this.currentDate) {
return 'label-dark';
}
return '';
},
},
methods: {
getSubItemValueClass(subItem) {
const daysToClosestWeek = this.currentDate.getDate() - subItem.getDate();
// Show dark color text only for upcoming dates
// and current week date
if (
daysToClosestWeek <= 6 &&
this.currentDate.getDate() >= subItem.getDate() &&
this.currentDate.getFullYear() === subItem.getFullYear() &&
this.currentDate.getMonth() === subItem.getMonth()
) {
return 'label-dark label-bold';
} else if (subItem >= this.currentDate) {
return 'label-dark';
}
return '';
},
};
},
};
</script>
<template>
......
......@@ -73,7 +73,7 @@ export default {
}
// Calculate proportional offset based on startDate and total days in
// current month.
return `left: ${startDate / daysInMonth * 100}%;`;
return `left: ${(startDate / daysInMonth) * 100}%;`;
},
/**
* This method is externally only called when current timeframe cell has timeline
......
......@@ -65,7 +65,7 @@ export default {
return `right: ${TIMELINE_END_OFFSET_HALF}px;`;
}
return `left: ${startDay / daysInQuarter * 100}%;`;
return `left: ${(startDay / daysInQuarter) * 100}%;`;
},
/**
* This method is externally only called when current timeframe cell has timeline
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment