Commit c84f08e4 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'ee-34312-eslint-vue-plugin' into 'master'

Ee 34312 eslint vue plugin

See merge request gitlab-org/gitlab-ee!3983
parents 527ee20b eb336d71
...@@ -13,7 +13,8 @@ engines: ...@@ -13,7 +13,8 @@ engines:
exclude_paths: exclude_paths:
- "lib/api/v3/*" - "lib/api/v3/*"
eslint: eslint:
enabled: true # eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4
enabled: false
rubocop: rubocop:
enabled: true enabled: true
channel: "gitlab-rubocop-0-52" channel: "gitlab-rubocop-0-52"
......
...@@ -4,7 +4,10 @@ ...@@ -4,7 +4,10 @@
"browser": true, "browser": true,
"es6": true "es6": true
}, },
"extends": "airbnb-base", "extends": [
"airbnb-base",
"plugin:vue/recommended"
],
"globals": { "globals": {
"__webpack_public_path__": true, "__webpack_public_path__": true,
"_": false, "_": false,
...@@ -12,7 +15,9 @@ ...@@ -12,7 +15,9 @@
"gon": false, "gon": false,
"localStorage": false "localStorage": false
}, },
"parser": "babel-eslint", "parserOptions": {
"parser": "babel-eslint"
},
"plugins": [ "plugins": [
"filenames", "filenames",
"import", "import",
...@@ -20,7 +25,7 @@ ...@@ -20,7 +25,7 @@
"promise" "promise"
], ],
"settings": { "settings": {
"html/html-extensions": [".html", ".html.raw", ".vue"], "html/html-extensions": [".html", ".html.raw"],
"import/resolver": { "import/resolver": {
"webpack": { "webpack": {
"config": "./config/webpack.config.js" "config": "./config/webpack.config.js"
...@@ -32,6 +37,15 @@ ...@@ -32,6 +37,15 @@
"import/no-commonjs": "error", "import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }], "no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error", "promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__"]}] "no-underscore-dangle": ["error", { "allow": ["__"]}],
"vue/html-self-closing": ["error", {
"html": {
"void": "always",
"normal": "never",
"component": "always"
},
"svg": "always",
"math": "always"
}]
} }
} }
...@@ -89,40 +89,53 @@ ...@@ -89,40 +89,53 @@
<p>Track your GitLab projects with GitLab for Slack.</p> <p>Track your GitLab projects with GitLab for Slack.</p>
</div> </div>
<div class="append-bottom-20 center" v-once> <div
class="append-bottom-20 center"
v-once
>
<img <img
class="gitlab-slack-logo" class="gitlab-slack-logo"
:src="gitlabLogoPath"></img> :src="gitlabLogoPath"
/>
<div <div
class="gitlab-slack-double-headed-arrow inline prepend-left-20 append-right-20" class="gitlab-slack-double-headed-arrow inline prepend-left-20 append-right-20"
v-html="doubleHeadedArrowSvg"></div> v-html="doubleHeadedArrowSvg"
>
</div>
<img <img
class="gitlab-slack-logo" class="gitlab-slack-logo"
:src="slackLogoPath"></img> :src="slackLogoPath"
/>
</div> </div>
<button <button
type="button" type="button"
class="btn btn-red center-block js-popup-button" class="btn btn-red center-block js-popup-button"
@click="togglePopup"> @click="togglePopup"
>
Add GitLab to Slack Add GitLab to Slack
</button> </button>
<div <div
class="popup gitlab-slack-popup center-block prepend-top-20 text-center js-popup" class="popup gitlab-slack-popup center-block prepend-top-20 text-center js-popup"
v-if="popupOpen"> v-if="popupOpen"
>
<div <div
class="inline" class="inline"
v-if="isSignedIn && hasProjects"> v-if="isSignedIn && hasProjects"
>
<strong>Select GitLab project to link with your Slack team</strong> <strong>Select GitLab project to link with your Slack team</strong>
<select <select
class="gitlab-slack-project-select js-project-select form-control prepend-top-10 append-bottom-10" class="gitlab-slack-project-select
v-model="selectedProjectId"> js-project-select form-control prepend-top-10 append-bottom-10"
v-model="selectedProjectId"
>
<option <option
v-for="project in projects" v-for="project in projects"
:key="project.id" :key="project.id"
:value="project.id"> :value="project.id
">
{{ project.name }} {{ project.name }}
</option> </option>
</select> </select>
...@@ -130,14 +143,16 @@ ...@@ -130,14 +143,16 @@
<button <button
type="button" type="button"
class="btn btn-red pull-right js-add-button" class="btn btn-red pull-right js-add-button"
@click="addToSlack"> @click="addToSlack"
>
Add to Slack Add to Slack
</button> </button>
</div> </div>
<span <span
class="js-no-projects" class="js-no-projects"
v-else-if="isSignedIn && !hasProjects"> v-else-if="isSignedIn && !hasProjects"
>
You don't have any projects available. You don't have any projects available.
</span> </span>
...@@ -146,7 +161,8 @@ ...@@ -146,7 +161,8 @@
<a <a
class="js-gitlab-slack-sign-in-link" class="js-gitlab-slack-sign-in-link"
v-once v-once
:href="signInPath"> :href="signInPath"
>
log in log in
</a> </a>
</span> </span>
...@@ -156,20 +172,26 @@ ...@@ -156,20 +172,26 @@
<img <img
v-once v-once
class="gitlab-slack-gif" class="gitlab-slack-gif"
:src="gitlabForSlackGifPath"> :src="gitlabForSlackGifPath"
/>
</div> </div>
<div <div
class="gitlab-slack-example" class="gitlab-slack-example"
v-once> v-once
>
<h3 class="center">How it works</h3> <h3 class="center">How it works</h3>
<div class="well gitlab-slack-well center-block"> <div class="well gitlab-slack-well center-block">
<code class="code center-block append-bottom-10">/gitlab &lt;project-alias&gt; issue show &lt;id&gt;</code> <code
class="code center-block append-bottom-10"
>/gitlab &lt;project-alias&gt; issue show &lt;id&gt;</code>
<span> <span>
<div <div
class="gitlab-slack-right-arrow inline append-right-5" class="gitlab-slack-right-arrow inline append-right-5"
v-html="arrowRightSvg"></div> v-html="arrowRightSvg"
>
</div>
Shows the issue with id Shows the issue with id
<strong>&lt;id&gt;</strong> <strong>&lt;id&gt;</strong>
</span> </span>
...@@ -177,7 +199,7 @@ ...@@ -177,7 +199,7 @@
<div class="center"> <div class="center">
<a :href="docsPath"> <a :href="docsPath">
More Slack commands More Slack commands
</a> </a>
</div> </div>
</div> </div>
......
...@@ -8,6 +8,9 @@ export default () => { ...@@ -8,6 +8,9 @@ export default () => {
new Vue({ new Vue({
el, el,
components: {
notebookLab,
},
data() { data() {
return { return {
error: false, error: false,
...@@ -16,8 +19,41 @@ export default () => { ...@@ -16,8 +19,41 @@ export default () => {
json: {}, json: {},
}; };
}, },
components: { mounted() {
notebookLab, if (gon.katex_css_url) {
const katexStyles = document.createElement('link');
katexStyles.setAttribute('rel', 'stylesheet');
katexStyles.setAttribute('href', gon.katex_css_url);
document.head.appendChild(katexStyles);
}
if (gon.katex_js_url) {
const katexScript = document.createElement('script');
katexScript.addEventListener('load', () => {
this.loadFile();
});
katexScript.setAttribute('src', gon.katex_js_url);
document.head.appendChild(katexScript);
} else {
this.loadFile();
}
},
methods: {
loadFile() {
axios.get(el.dataset.endpoint)
.then(res => res.data)
.then((data) => {
this.json = data;
this.loading = false;
})
.catch((e) => {
if (e.status !== 200) {
this.loadError = true;
}
this.error = true;
});
},
}, },
template: ` template: `
<div class="container-fluid md prepend-top-default append-bottom-default"> <div class="container-fluid md prepend-top-default append-bottom-default">
...@@ -46,41 +82,5 @@ export default () => { ...@@ -46,41 +82,5 @@ export default () => {
</p> </p>
</div> </div>
`, `,
methods: {
loadFile() {
axios.get(el.dataset.endpoint)
.then(res => res.data)
.then((data) => {
this.json = data;
this.loading = false;
})
.catch((e) => {
if (e.status !== 200) {
this.loadError = true;
}
this.error = true;
});
},
},
mounted() {
if (gon.katex_css_url) {
const katexStyles = document.createElement('link');
katexStyles.setAttribute('rel', 'stylesheet');
katexStyles.setAttribute('href', gon.katex_css_url);
document.head.appendChild(katexStyles);
}
if (gon.katex_js_url) {
const katexScript = document.createElement('script');
katexScript.addEventListener('load', () => {
this.loadFile();
});
katexScript.setAttribute('src', gon.katex_js_url);
document.head.appendChild(katexScript);
} else {
this.loadFile();
}
},
}); });
}; };
...@@ -7,6 +7,9 @@ export default () => { ...@@ -7,6 +7,9 @@ export default () => {
return new Vue({ return new Vue({
el, el,
components: {
pdfLab,
},
data() { data() {
return { return {
error: false, error: false,
...@@ -15,9 +18,6 @@ export default () => { ...@@ -15,9 +18,6 @@ export default () => {
pdf: el.dataset.endpoint, pdf: el.dataset.endpoint,
}; };
}, },
components: {
pdfLab,
},
methods: { methods: {
onLoad() { onLoad() {
this.loading = false; this.loading = false;
......
...@@ -208,6 +208,9 @@ $(() => { ...@@ -208,6 +208,9 @@ $(() => {
if (configEl) { if (configEl) {
gl.boardConfigToggle = new Vue({ gl.boardConfigToggle = new Vue({
el: configEl, el: configEl,
directives: {
tooltip,
},
data() { data() {
return { return {
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
...@@ -215,12 +218,6 @@ $(() => { ...@@ -215,12 +218,6 @@ $(() => {
state: Store.state, state: Store.state,
}; };
}, },
directives: {
tooltip,
},
methods: {
showPage: page => gl.issueBoards.BoardsStore.showPage(page),
},
computed: { computed: {
buttonText() { buttonText() {
return this.canAdminList ? 'Edit board' : 'View scope'; return this.canAdminList ? 'Edit board' : 'View scope';
...@@ -229,6 +226,9 @@ $(() => { ...@@ -229,6 +226,9 @@ $(() => {
return this.hasScope ? __('This board\'s scope is reduced') : ''; return this.hasScope ? __('This board\'s scope is reduced') : '';
} }
}, },
methods: {
showPage: page => gl.issueBoards.BoardsStore.showPage(page),
},
template: ` template: `
<div class="prepend-left-10"> <div class="prepend-left-10">
<button <button
...@@ -247,8 +247,8 @@ $(() => { ...@@ -247,8 +247,8 @@ $(() => {
} }
gl.IssueBoardsModalAddBtn = new Vue({ gl.IssueBoardsModalAddBtn = new Vue({
mixins: [gl.issueBoards.ModalMixins],
el: document.getElementById('js-add-issues-btn'), el: document.getElementById('js-add-issues-btn'),
mixins: [gl.issueBoards.ModalMixins],
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
...@@ -258,11 +258,6 @@ $(() => { ...@@ -258,11 +258,6 @@ $(() => {
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
}; };
}, },
watch: {
disabled() {
this.updateTooltip();
},
},
computed: { computed: {
disabled() { disabled() {
if (!this.store) { if (!this.store) {
...@@ -278,6 +273,14 @@ $(() => { ...@@ -278,6 +273,14 @@ $(() => {
return ''; return '';
}, },
}, },
watch: {
disabled() {
this.updateTooltip();
},
},
mounted() {
this.updateTooltip();
},
methods: { methods: {
updateTooltip() { updateTooltip() {
const $tooltip = $(this.$refs.addIssuesButton); const $tooltip = $(this.$refs.addIssuesButton);
...@@ -296,9 +299,6 @@ $(() => { ...@@ -296,9 +299,6 @@ $(() => {
} }
}, },
}, },
mounted() {
this.updateTooltip();
},
template: ` template: `
<div class="board-extra-actions"> <div class="board-extra-actions">
<button <button
......
...@@ -4,6 +4,10 @@ import loadingIcon from '~/vue_shared/components/loading_icon.vue'; ...@@ -4,6 +4,10 @@ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default { export default {
components: {
loadingIcon,
UserAvatarImage,
},
props: { props: {
anyUserText: { anyUserText: {
type: String, type: String,
...@@ -52,10 +56,6 @@ export default { ...@@ -52,10 +56,6 @@ export default {
default: '', default: '',
}, },
}, },
components: {
loadingIcon,
UserAvatarImage,
},
computed: { computed: {
hasValue() { hasValue() {
return this.selected.id > 0; return this.selected.id > 0;
...@@ -69,6 +69,9 @@ export default { ...@@ -69,6 +69,9 @@ export default {
this.initSelect(); this.initSelect();
}, },
}, },
mounted() {
this.initSelect();
},
methods: { methods: {
initSelect() { initSelect() {
this.userDropdown = new UsersSelect(null, this.$refs.dropdown, { this.userDropdown = new UsersSelect(null, this.$refs.dropdown, {
...@@ -87,9 +90,6 @@ export default { ...@@ -87,9 +90,6 @@ export default {
this.board.assignee = assignee; this.board.assignee = assignee;
}, },
}, },
mounted() {
this.initSelect();
},
}; };
</script> </script>
...@@ -161,27 +161,33 @@ export default { ...@@ -161,27 +161,33 @@ export default {
aria-hidden="true" aria-hidden="true"
class="fa fa-chevron-down" class="fa fa-chevron-down"
data-hidden="true" data-hidden="true"
/> >
</i>
</button> </button>
<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-user dropdown-menu-selectable dropdown-menu-author"> <div
class="dropdown-menu dropdown-select dropdown-menu-paging
dropdown-menu-user dropdown-menu-selectable dropdown-menu-author"
>
<div class="dropdown-input"> <div class="dropdown-input">
<input <input
autocomplete="off" autocomplete="off"
class="dropdown-input-field" class="dropdown-input-field"
placeholder="Search" placeholder="Search"
type="search" type="search"
> />
<i <i
aria-hidden="true" aria-hidden="true"
class="fa fa-search dropdown-input-search" class="fa fa-search dropdown-input-search"
data-hidden="true" data-hidden="true"
/> >
</i>
<i <i
aria-hidden="true" aria-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
data-hidden="true" data-hidden="true"
role="button" role="button"
/> >
</i>
</div> </div>
<div class="dropdown-content"></div> <div class="dropdown-content"></div>
<div class="dropdown-loading"> <div class="dropdown-loading">
......
<script> <script>
/* eslint-disable vue/require-default-prop */
import './issue_card_inner'; import './issue_card_inner';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
...@@ -10,13 +11,33 @@ export default { ...@@ -10,13 +11,33 @@ export default {
'issue-card-inner': gl.issueBoards.IssueCardInner, 'issue-card-inner': gl.issueBoards.IssueCardInner,
}, },
props: { props: {
list: Object, list: {
issue: Object, type: Object,
issueLinkBase: String, default: () => ({}),
disabled: Boolean, },
index: Number, issue: {
rootPath: String, type: Object,
groupId: Number, default: () => ({}),
},
issueLinkBase: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
},
rootPath: {
type: String,
default: '',
},
groupId: {
type: Number,
},
}, },
data() { data() {
return { return {
...@@ -55,8 +76,13 @@ export default { ...@@ -55,8 +76,13 @@ export default {
</script> </script>
<template> <template>
<li class="card" <li
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }" class="card"
:class="{
'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id,
'is-active': issueDetailVisible
}"
:index="index" :index="index"
:data-issue-id="issue.id" :data-issue-id="issue.id"
@mousedown="mouseDown" @mousedown="mouseDown"
...@@ -68,6 +94,7 @@ export default { ...@@ -68,6 +94,7 @@ export default {
:issue-link-base="issueLinkBase" :issue-link-base="issueLinkBase"
:group-id="groupId" :group-id="groupId"
:root-path="rootPath" :root-path="rootPath"
:update-filters="true" /> :update-filters="true"
/>
</li> </li>
</template> </template>
...@@ -24,6 +24,13 @@ const boardDefaults = { ...@@ -24,6 +24,13 @@ const boardDefaults = {
}; };
export default { export default {
components: {
AssigneeSelect,
BoardLabelsSelect,
BoardMilestoneSelect,
BoardWeightSelect,
modal,
},
props: { props: {
canAdminBoard: { canAdminBoard: {
type: Boolean, type: Boolean,
...@@ -55,6 +62,7 @@ export default { ...@@ -55,6 +62,7 @@ export default {
weights: { weights: {
type: String, type: String,
required: false, required: false,
default: '',
}, },
}, },
data() { data() {
...@@ -69,13 +77,6 @@ export default { ...@@ -69,13 +77,6 @@ export default {
isLoading: false, isLoading: false,
}; };
}, },
components: {
AssigneeSelect,
BoardLabelsSelect,
BoardMilestoneSelect,
BoardWeightSelect,
modal,
},
computed: { computed: {
isNewForm() { isNewForm() {
return this.currentPage === 'new'; return this.currentPage === 'new';
...@@ -132,6 +133,12 @@ export default { ...@@ -132,6 +133,12 @@ export default {
return this.isLoading || this.board.name.length === 0; return this.isLoading || this.board.name.length === 0;
}, },
}, },
mounted() {
this.resetFormState();
if (this.$refs.name) {
this.$refs.name.focus();
}
},
methods: { methods: {
submit() { submit() {
if (this.board.name.length === 0) return; if (this.board.name.length === 0) return;
...@@ -169,12 +176,6 @@ export default { ...@@ -169,12 +176,6 @@ export default {
} }
}, },
}, },
mounted() {
this.resetFormState();
if (this.$refs.name) {
this.$refs.name.focus();
}
},
}; };
</script> </script>
...@@ -217,7 +218,7 @@ export default { ...@@ -217,7 +218,7 @@ export default {
v-model="board.name" v-model="board.name"
@keyup.enter="submit" @keyup.enter="submit"
placeholder="Enter board name" placeholder="Enter board name"
> />
</div> </div>
<div v-if="scopedIssueBoardFeatureEnabled"> <div v-if="scopedIssueBoardFeatureEnabled">
<div <div
......
<script> <script>
/* global ListLabel */ /* global ListLabel */
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
export default { export default {
props: { components: {
board: { loadingIcon,
type: Object,
required: true,
}, },
labelsPath: { props: {
type: String, board: {
required: true, type: Object,
required: true,
},
labelsPath: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
}, },
canEdit: { computed: {
type: Boolean, labelIds() {
required: false, return this.board.labels.map(label => label.id);
default: false, },
isEmpty() {
return this.board.labels.length === 0;
},
}, },
}, mounted() {
components: { this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
loadingIcon, handleClick: this.handleClick,
}, });
computed: {
labelIds() {
return this.board.labels.map(label => label.id);
}, },
isEmpty() { methods: {
return this.board.labels.length === 0; labelStyle(label) {
return {
color: label.textColor,
backgroundColor: label.color,
};
},
handleClick(label) {
if (label.isAny) {
this.board.labels = [];
} else if (!this.board.labels.find(l => l.id === label.id)) {
this.board.labels.push(new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
textColor: label.text_color,
}));
} else {
let labels = this.board.labels;
labels = labels.filter(selected => selected.id !== label.id);
this.board.labels = labels;
}
},
}, },
}, };
mounted() {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
},
methods: {
labelStyle(label) {
return {
color: label.textColor,
backgroundColor: label.color,
};
},
handleClick(label) {
if (label.isAny) {
this.board.labels = [];
} else if (!this.board.labels.find(l => l.id === label.id)) {
this.board.labels.push(new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
textColor: label.text_color,
}));
} else {
let labels = this.board.labels;
labels = labels.filter(selected => selected.id !== label.id);
this.board.labels = labels;
}
},
},
};
</script> </script>
<template> <template>
...@@ -106,12 +106,13 @@ export default { ...@@ -106,12 +106,13 @@ export default {
v-for="labelId in labelIds" v-for="labelId in labelIds"
:key="labelId" :key="labelId"
:value="labelId" :value="labelId"
> />
<div class="dropdown"> <div class="dropdown">
<button <button
ref="dropdownButton" ref="dropdownButton"
:data-labels="labelsPath" :data-labels="labelsPath"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-extra-options js-board-config-modal" class="dropdown-menu-toggle wide js-label-select
js-multiselect js-extra-options js-board-config-modal"
data-field-name="label_id[]" data-field-name="label_id[]"
:data-show-any="true" :data-show-any="true"
data-toggle="dropdown" data-toggle="dropdown"
...@@ -124,27 +125,33 @@ export default { ...@@ -124,27 +125,33 @@ export default {
aria-hidden="true" aria-hidden="true"
class="fa fa-chevron-down" class="fa fa-chevron-down"
data-hidden="true" data-hidden="true"
/> >
</i>
</button> </button>
<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> <div
class="dropdown-menu dropdown-select
dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"
>
<div class="dropdown-input"> <div class="dropdown-input">
<input <input
autocomplete="off" autocomplete="off"
class="dropdown-input-field" class="dropdown-input-field"
placeholder="Search" placeholder="Search"
type="search" type="search"
> />
<i <i
aria-hidden="true" aria-hidden="true"
class="fa fa-search dropdown-input-search" class="fa fa-search dropdown-input-search"
data-hidden="true" data-hidden="true"
/> >
</i>
<i <i
aria-hidden="true" aria-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
data-hidden="true" data-hidden="true"
role="button" role="button"
/> >
</i>
</div> </div>
<div class="dropdown-content"></div> <div class="dropdown-content"></div>
<div class="dropdown-loading"> <div class="dropdown-loading">
......
<script> <script>
/* global BoardService */ /* global BoardService */
import MilestoneSelect from '~/milestone_select'; import MilestoneSelect from '~/milestone_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
const ANY_MILESTONE = 'Any Milestone'; const ANY_MILESTONE = 'Any Milestone';
const NO_MILESTONE = 'No Milestone'; const NO_MILESTONE = 'No Milestone';
export default { export default {
props: { components: {
board: { loadingIcon,
type: Object,
required: true,
}, },
milestonePath: { props: {
type: String, board: {
required: true, type: Object,
required: true,
},
milestonePath: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
}, },
canEdit: {
type: Boolean, computed: {
required: false, milestoneTitle() {
default: false, if (this.noMilestone) return NO_MILESTONE;
}, return this.board.milestone ? this.board.milestone.title : ANY_MILESTONE;
}, },
components: { noMilestone() {
loadingIcon, return this.milestoneId === 0;
}, },
computed: { milestoneId() {
milestoneTitle() { return this.board.milestone_id;
if (this.noMilestone) return NO_MILESTONE; },
return this.board.milestone ? this.board.milestone.title : ANY_MILESTONE; milestoneTitleClass() {
}, return this.milestoneTitle === ANY_MILESTONE ? 'text-secondary' : 'bold';
noMilestone() { },
return this.milestoneId === 0; selected() {
}, if (this.noMilestone) return NO_MILESTONE;
milestoneId() { return this.board.milestone ? this.board.milestone.name : '';
return this.board.milestone_id; },
},
milestoneTitleClass() {
return this.milestoneTitle === ANY_MILESTONE ? 'text-secondary' : 'bold';
}, },
selected() { mounted() {
if (this.noMilestone) return NO_MILESTONE; this.milestoneDropdown = new MilestoneSelect(null, this.$refs.dropdownButton, {
return this.board.milestone ? this.board.milestone.name : ''; handleClick: this.selectMilestone,
});
}, },
}, methods: {
methods: { selectMilestone(milestone) {
selectMilestone(milestone) { let id = milestone.id;
let id = milestone.id; // swap the IDs of 'Any' and 'No' milestone to what backend requires
// swap the IDs of 'Any' and 'No' milestone to what backend requires if (milestone.title === ANY_MILESTONE) {
if (milestone.title === ANY_MILESTONE) { id = -1;
id = -1; } else if (milestone.title === NO_MILESTONE) {
} else if (milestone.title === NO_MILESTONE) { id = 0;
id = 0; }
} this.board.milestone_id = id;
this.board.milestone_id = id; this.board.milestone = {
this.board.milestone = { ...milestone,
...milestone, id,
id, };
}; },
}, },
}, };
mounted() {
this.milestoneDropdown = new MilestoneSelect(null, this.$refs.dropdownButton, {
handleClick: this.selectMilestone,
});
},
};
</script> </script>
<template> <template>
...@@ -95,7 +96,7 @@ export default { ...@@ -95,7 +96,7 @@ export default {
:value="milestoneId" :value="milestoneId"
name="milestone_id" name="milestone_id"
type="hidden" type="hidden"
> />
<div class="dropdown"> <div class="dropdown">
<button <button
ref="dropdownButton" ref="dropdownButton"
...@@ -115,7 +116,8 @@ export default { ...@@ -115,7 +116,8 @@ export default {
aria-hidden="true" aria-hidden="true"
data-hidden="true" data-hidden="true"
class="fa fa-chevron-down" class="fa fa-chevron-down"
/> >
</i>
</button> </button>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable"> <div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div <div
...@@ -126,20 +128,23 @@ export default { ...@@ -126,20 +128,23 @@ export default {
class="dropdown-input-field" class="dropdown-input-field"
placeholder="Search milestones" placeholder="Search milestones"
autocomplete="off" autocomplete="off"
> />
<i <i
aria-hidden="true" aria-hidden="true"
data-hidden="true" data-hidden="true"
class="fa fa-search dropdown-input-search" class="fa fa-search dropdown-input-search"
/> >
</i>
<i <i
role="button" role="button"
aria-hidden="true" aria-hidden="true"
data-hidden="true" data-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
/> >
</i>
</div>
<div class="dropdown-content">
</div> </div>
<div class="dropdown-content" />
<div class="dropdown-loading"> <div class="dropdown-loading">
<loading-icon /> <loading-icon />
</div> </div>
......
<script>
/* global ListIssue */
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
export default {
name: 'BoardProjectSelect',
components: {
loadingIcon,
},
props: {
groupId: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
loading: true,
selectedProject: {},
};
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
},
},
mounted() {
$(this.$refs.projectsDropdown).glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace'],
},
clicked: ({ $el, e }) => {
e.preventDefault();
this.selectedProject = {
id: $el.data('project-id'),
name: $el.data('project-name'),
};
eventHub.$emit('setSelectedProject', this.selectedProject);
},
selectable: true,
data: (term, callback) => {
this.loading = true;
return Api.groupProjects(this.groupId, term, (projects) => {
this.loading = false;
callback(projects);
});
},
renderRow(project) {
return `
<li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
`;
},
text: project => project.name,
});
},
};
</script>
<template> <template>
<div> <div>
<label class="label-light prepend-top-10"> <label class="label-light prepend-top-10">
Project Project
</label> </label>
<div ref="projectsDropdown" class="dropdown"> <div
ref="projectsDropdown"
class="dropdown"
>
<button <button
class="dropdown-menu-toggle wide" class="dropdown-menu-toggle wide"
type="button" type="button"
data-toggle="dropdown" data-toggle="dropdown"
aria-expanded="false"> aria-expanded="false"
>
{{ selectedProjectName }} {{ selectedProjectName }}
<i class="fa fa-chevron-down" aria-hidden="true"></i> <i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width"> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title"> <div class="dropdown-title">
<span>Projects</span> <span>Projects</span>
<button aria-label="Close" type="button" class="dropdown-title-button dropdown-menu-close"> <button
<i aria-hidden="true" data-hidden="true" class="fa fa-times dropdown-menu-close-icon"></i> aria-label="Close"
type="button"
class="dropdown-title-button dropdown-menu-close"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
>
</i>
</button> </button>
</div> </div>
<div class="dropdown-input"> <div class="dropdown-input">
<input <input
class="dropdown-input-field" class="dropdown-input-field"
type="search" type="search"
placeholder="Search projects"> placeholder="Search projects"
<i aria-hidden="true" data-hidden="true" class="fa fa-search dropdown-input-search"></i> />
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search"
>
</i>
</div> </div>
<div class="dropdown-content"></div> <div class="dropdown-content"></div>
<div class="dropdown-loading"> <div class="dropdown-loading">
...@@ -34,70 +125,3 @@ ...@@ -34,70 +125,3 @@
</div> </div>
</div> </div>
</template> </template>
<script>
/* global ListIssue */
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
export default {
name: 'BoardProjectSelect',
props: {
groupId: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
loading: true,
selectedProject: {},
};
},
components: {
loadingIcon,
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
},
},
mounted() {
$(this.$refs.projectsDropdown).glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace'],
},
clicked: ({ $el, e }) => {
e.preventDefault();
this.selectedProject = {
id: $el.data('project-id'),
name: $el.data('project-name'),
};
eventHub.$emit('setSelectedProject', this.selectedProject);
},
selectable: true,
data: (term, callback) => {
this.loading = true;
return Api.groupProjects(this.groupId, term, (projects) => {
this.loading = false;
callback(projects);
});
},
renderRow(project) {
return `
<li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
`;
},
text: project => project.name,
});
},
};
</script>
<script> <script>
/* global BoardService */ /* eslint-disable vue/require-default-prop */
/* global BoardService */
import WeightSelect from 'ee/weight_select'; import WeightSelect from 'ee/weight_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
const ANY_WEIGHT = 'Any Weight'; const ANY_WEIGHT = 'Any Weight';
const NO_WEIGHT = 'No Weight'; const NO_WEIGHT = 'No Weight';
export default { export default {
props: { components: {
board: { loadingIcon,
type: Object,
required: true,
}, },
value: { props: {
type: [Number, String], board: {
required: false, type: Object,
required: true,
},
value: {
type: [Number, String],
required: false,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
weights: {
type: Array,
required: true,
},
}, },
canEdit: { data() {
type: Boolean, return {
required: false, fieldName: 'weight',
default: false, };
}, },
weights: { computed: {
type: Array, valueClass() {
required: true, 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() {
data() { this.weightDropdown = new WeightSelect(this.$refs.dropdownButton, {
return { handleClick: this.selectWeight,
fieldName: 'weight', selected: this.value,
}; fieldName: this.fieldName,
}, });
components: {
loadingIcon,
},
computed: {
valueClass() {
if (this.valueText === ANY_WEIGHT) {
return 'text-secondary';
}
return 'bold';
}, },
valueText() { methods: {
if (this.value > 0) return this.value; selectWeight(weight) {
if (this.value === 0) return NO_WEIGHT; this.board.weight = this.weightInt(weight);
return ANY_WEIGHT; },
weightInt(weight) {
if (weight > 0) {
return weight;
}
if (weight === NO_WEIGHT) {
return 0;
}
return -1;
},
}, },
}, };
methods: {
selectWeight(weight) {
this.board.weight = this.weightInt(weight);
},
weightInt(weight) {
if (weight > 0) {
return weight;
}
if (weight === NO_WEIGHT) {
return 0;
}
return -1;
},
},
mounted() {
this.weightDropdown = new WeightSelect(this.$refs.dropdownButton, {
handleClick: this.selectWeight,
selected: this.value,
fieldName: this.fieldName,
});
},
};
</script> </script>
<template> <template>
...@@ -96,9 +97,9 @@ export default { ...@@ -96,9 +97,9 @@ export default {
> >
<input <input
type="hidden" type="hidden"
:name="this.fieldName" :name="fieldName"
/> />
<div class="dropdown "> <div class="dropdown">
<button <button
ref="dropdownButton" ref="dropdownButton"
class="dropdown-menu-toggle js-weight-select wide" class="dropdown-menu-toggle js-weight-select wide"
...@@ -113,7 +114,8 @@ export default { ...@@ -113,7 +114,8 @@ export default {
aria-hidden="true" aria-hidden="true"
data-hidden="true" data-hidden="true"
class="fa fa-chevron-down" class="fa fa-chevron-down"
/> >
</i>
</button> </button>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-weight"> <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-weight">
<div class="dropdown-content "> <div class="dropdown-content ">
......
<script> <script>
import { s__, sprintf } from '../../locale'; /* eslint-disable vue/require-default-prop */
import eventHub from '../event_hub'; import { s__, sprintf } from '../../locale';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import eventHub from '../event_hub';
import { import loadingButton from '../../vue_shared/components/loading_button.vue';
APPLICATION_NOT_INSTALLABLE, import {
APPLICATION_SCHEDULED, APPLICATION_NOT_INSTALLABLE,
APPLICATION_INSTALLABLE, APPLICATION_SCHEDULED,
APPLICATION_INSTALLING, APPLICATION_INSTALLABLE,
APPLICATION_INSTALLED, APPLICATION_INSTALLING,
APPLICATION_ERROR, APPLICATION_INSTALLED,
REQUEST_LOADING, APPLICATION_ERROR,
REQUEST_SUCCESS, REQUEST_LOADING,
REQUEST_FAILURE, REQUEST_SUCCESS,
} from '../constants'; REQUEST_FAILURE,
} from '../constants';
export default { export default {
props: { components: {
id: { loadingButton,
type: String,
required: true,
}, },
title: { props: {
type: String, id: {
required: true, type: String,
required: true,
},
title: {
type: String,
required: true,
},
titleLink: {
type: String,
required: false,
},
description: {
type: String,
required: true,
},
status: {
type: String,
required: false,
},
statusReason: {
type: String,
required: false,
},
requestStatus: {
type: String,
required: false,
},
requestReason: {
type: String,
required: false,
},
}, },
titleLink: { computed: {
type: String, rowJsClass() {
required: false, return `js-cluster-application-row-${this.id}`;
}, },
description: { installButtonLoading() {
type: String, return !this.status ||
required: true, this.status === APPLICATION_SCHEDULED ||
}, this.status === APPLICATION_INSTALLING ||
status: { this.requestStatus === REQUEST_LOADING;
type: String, },
required: false, installButtonDisabled() {
}, // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
statusReason: { // we already made a request to install and are just waiting for the real-time
type: String, // to sync up.
required: false, return (this.status !== APPLICATION_INSTALLABLE &&
}, this.status !== APPLICATION_ERROR) ||
requestStatus: { this.requestStatus === REQUEST_LOADING ||
type: String, this.requestStatus === REQUEST_SUCCESS;
required: false, },
}, installButtonLabel() {
requestReason: { let label;
type: String, if (
required: false, this.status === APPLICATION_NOT_INSTALLABLE ||
}, this.status === APPLICATION_INSTALLABLE ||
}, this.status === APPLICATION_ERROR
components: { ) {
loadingButton, label = s__('ClusterIntegration|Install');
}, } else if (this.status === APPLICATION_SCHEDULED ||
computed: { this.status === APPLICATION_INSTALLING) {
rowJsClass() { label = s__('ClusterIntegration|Installing');
return `js-cluster-application-row-${this.id}`; } else if (this.status === APPLICATION_INSTALLED) {
}, label = s__('ClusterIntegration|Installed');
installButtonLoading() { }
return !this.status ||
this.status === APPLICATION_SCHEDULED ||
this.status === APPLICATION_INSTALLING ||
this.requestStatus === REQUEST_LOADING;
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) ||
this.requestStatus === REQUEST_LOADING ||
this.requestStatus === REQUEST_SUCCESS;
},
installButtonLabel() {
let label;
if (
this.status === APPLICATION_NOT_INSTALLABLE ||
this.status === APPLICATION_INSTALLABLE ||
this.status === APPLICATION_ERROR
) {
label = s__('ClusterIntegration|Install');
} else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) {
label = s__('ClusterIntegration|Installing');
} else if (this.status === APPLICATION_INSTALLED) {
label = s__('ClusterIntegration|Installed');
}
return label; return label;
}, },
hasError() { hasError() {
return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE; return this.status === APPLICATION_ERROR ||
}, this.requestStatus === REQUEST_FAILURE;
generalErrorDescription() { },
return sprintf( generalErrorDescription() {
s__('ClusterIntegration|Something went wrong while installing %{title}'), { return sprintf(
title: this.title, s__('ClusterIntegration|Something went wrong while installing %{title}'), {
}, title: this.title,
); },
);
},
}, },
}, methods: {
methods: { installClicked() {
installClicked() { eventHub.$emit('installApplication', this.id);
eventHub.$emit('installApplication', this.id); },
}, },
}, };
};
</script> </script>
<template> <template>
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue'; import applicationRow from './application_row.vue';
export default { export default {
props: { components: {
applications: { applicationRow,
type: Object,
required: false,
default: () => ({}),
}, },
helpPath: { props: {
type: String, applications: {
required: false, type: Object,
required: false,
default: () => ({}),
},
helpPath: {
type: String,
required: false,
default: '',
},
}, },
}, computed: {
components: { generalApplicationDescription() {
applicationRow, return sprintf(
}, _.escape(s__(`ClusterIntegration|Install applications on your cluster.
computed: { Read more about %{helpLink}`)),
generalApplicationDescription() { {
return sprintf( helpLink: `<a href="${this.helpPath}">
_.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), { ${_.escape(s__('ClusterIntegration|installing applications'))}
helpLink: `<a href="${this.helpPath}"> </a>`,
${_.escape(s__('ClusterIntegration|installing applications'))} },
</a>`, false,
}, );
false, },
); helmTillerDescription() {
}, return _.escape(s__(
helmTillerDescription() { `ClusterIntegration|Helm streamlines installing and managing Kubernets applications.
return _.escape(s__( Tiller runs inside of your Kubernetes Cluster, and manages
`ClusterIntegration|Helm streamlines installing and managing Kubernets applications. releases of your charts.`,
Tiller runs inside of your Kubernetes Cluster, and manages ));
releases of your charts.`, },
)); ingressDescription() {
}, const descriptionParagraph = _.escape(s__(
ingressDescription() { `ClusterIntegration|Ingress gives you a way to route requests to services based on the
const descriptionParagraph = _.escape(s__( request host or path, centralizing a number of services into a single entrypoint.`,
`ClusterIntegration|Ingress gives you a way to route requests to services based on the ));
request host or path, centralizing a number of services into a single entrypoint.`,
));
const extraCostParagraph = sprintf( const extraCostParagraph = sprintf(
_.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), { _.escape(s__(`ClusterIntegration|%{boldNotice} This will add some
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, extra resources like a load balancer,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> which incur additional costs. See %{pricingLink}`)),
${_.escape(s__('ClusterIntegration|GKE pricing'))} {
</a>`, boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
}, pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
false, ${_.escape(s__('ClusterIntegration|GKE pricing'))}
); </a>`,
},
false,
);
return ` return `
<p> <p>
${descriptionParagraph} ${descriptionParagraph}
</p> </p>
<p class="append-bottom-0"> <p class="append-bottom-0">
${extraCostParagraph} ${extraCostParagraph}
</p> </p>
`; `;
}, },
gitlabRunnerDescription() { gitlabRunnerDescription() {
return _.escape(s__( return _.escape(s__(
`ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs
and send the results back to GitLab.`, and send the results back to GitLab.`,
)); ));
}, },
prometheusDescription() { prometheusDescription() {
return sprintf( return sprintf(
_.escape(s__('ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications.')), { _.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html", target="_blank" rel="noopener noreferrer"> with %{gitlabIntegrationLink} to monitor deployed applications.`)),
${_.escape(s__('ClusterIntegration|Gitlab Integration'))} {
</a>`, gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
}, target="_blank" rel="noopener noreferrer">
false, ${_.escape(s__('ClusterIntegration|Gitlab Integration'))}
); </a>`,
},
false,
);
},
}, },
}, };
};
</script> </script>
<template> <template>
...@@ -107,26 +116,29 @@ export default { ...@@ -107,26 +116,29 @@ export default {
:request-reason="applications.helm.requestReason" :request-reason="applications.helm.requestReason"
/> />
<application-row <application-row
id="ingress" id="ingress"
:title="applications.ingress.title" :title="applications.ingress.title"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
:description="ingressDescription" :description="ingressDescription"
:status="applications.ingress.status" :status="applications.ingress.status"
:status-reason="applications.ingress.statusReason" :status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus" :request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason" :request-reason="applications.ingress.requestReason"
/> />
<application-row <application-row
id="prometheus" id="prometheus"
:title="applications.prometheus.title" :title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/" title-link="https://prometheus.io/docs/introduction/overview/"
:description="prometheusDescription" :description="prometheusDescription"
:status="applications.prometheus.status" :status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason" :status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus" :request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason" :request-reason="applications.prometheus.requestReason"
/> />
<!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests --> <!--
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests
-->
<!-- Add GitLab Runner row, all other plumbing is complete --> <!-- Add GitLab Runner row, all other plumbing is complete -->
</div> </div>
</div> </div>
......
...@@ -94,7 +94,7 @@ export default class ImageFile { ...@@ -94,7 +94,7 @@ export default class ImageFile {
}); });
return [maxWidth, maxHeight]; return [maxWidth, maxHeight];
} }
// eslint-disable-next-line
views = { views = {
'two-up': function() { 'two-up': function() {
return $('.two-up.view .wrap', this.file).each((function(_this) { return $('.two-up.view .wrap', this.file).each((function(_this) {
......
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
import pipelinesMixin from '../../pipelines/mixins/pipelines'; import pipelinesMixin from '../../pipelines/mixins/pipelines';
export default { export default {
mixins: [
pipelinesMixin,
],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
...@@ -31,9 +34,6 @@ ...@@ -31,9 +34,6 @@
default: 'child', default: 'child',
}, },
}, },
mixins: [
pipelinesMixin,
],
data() { data() {
const store = new PipelineStore(); const store = new PipelineStore();
...@@ -95,28 +95,29 @@ ...@@ -95,28 +95,29 @@
label="Loading pipelines" label="Loading pipelines"
size="3" size="3"
v-if="isLoading" v-if="isLoading"
/> />
<empty-state <empty-state
v-if="shouldRenderEmptyState" v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath" :empty-state-svg-path="emptyStateSvgPath"
/> />
<error-state <error-state
v-if="shouldRenderErrorState" v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath" :error-state-svg-path="errorStateSvgPath"
/> />
<div <div
class="table-holder" class="table-holder"
v-if="shouldRenderTable"> v-if="shouldRenderTable"
>
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType" :view-type="viewType"
/> />
</div> </div>
</div> </div>
</template> </template>
...@@ -26,28 +26,34 @@ ...@@ -26,28 +26,34 @@
class="js-ca-dismiss-button dismiss-button" class="js-ca-dismiss-button dismiss-button"
type="button" type="button"
:aria-label="__('Dismiss Cycle Analytics introduction box')" :aria-label="__('Dismiss Cycle Analytics introduction box')"
@click="dismissOverviewDialog"> @click="dismissOverviewDialog"
>
<i <i
class="fa fa-times" class="fa fa-times"
aria-hidden="true"> aria-hidden="true">
</i> </i>
</button> </button>
<div class="svg-container" v-html="iconCycleAnalyticsSplash"> <div
class="svg-container"
v-html="iconCycleAnalyticsSplash"
>
</div> </div>
<div class="inner-content"> <div class="inner-content">
<h4> <h4>
{{__('Introducing Cycle Analytics')}} {{ __('Introducing Cycle Analytics') }}
</h4> </h4>
<p> <p>
{{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} {{ __(`Cycle Analytics gives an overview
of how much time it takes to go from idea to production in your project.`) }}
</p> </p>
<p> <p>
<a <a
:href="documentationLink" :href="documentationLink"
target="_blank" target="_blank"
rel="nofollow" rel="nofollow"
class="btn"> class="btn"
{{__('Read more')}} >
{{ __('Read more') }}
</a> </a>
</p> </p>
</div> </div>
......
...@@ -2,25 +2,34 @@ ...@@ -2,25 +2,34 @@
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: {
tooltip,
},
props: { props: {
count: { count: {
type: Number, type: Number,
required: true, required: true,
}, },
}, },
directives: {
tooltip,
},
}; };
</script> </script>
<template> <template>
<span v-if="count === 50" class="events-info pull-right"> <span
v-if="count === 50"
class="events-info pull-right"
>
<i <i
class="fa fa-warning" class="fa fa-warning"
v-tooltip v-tooltip
aria-hidden="true" aria-hidden="true"
:title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)" :title="n__(
data-placement="top"></i> 'Limited to showing %d event at most',
'Limited to showing %d events at most',
50
)"
data-placement="top"
>
</i>
{{ n__('Showing %d event', 'Showing %d events', 50) }} {{ n__('Showing %d event', 'Showing %d events', 50) }}
</span> </span>
</template> </template>
...@@ -4,15 +4,21 @@ ...@@ -4,15 +4,21 @@
import totalTime from './total_time_component.vue'; import totalTime from './total_time_component.vue';
export default { export default {
props: {
items: Array,
stage: Object,
},
components: { components: {
userAvatarImage, userAvatarImage,
limitWarning, limitWarning,
totalTime, totalTime,
}, },
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
}; };
</script> </script>
<template> <template>
...@@ -22,28 +28,47 @@ ...@@ -22,28 +28,47 @@
<limit-warning :count="items.length" /> <limit-warning :count="items.length" />
</div> </div>
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item"> <li
v-for="(mergeRequest, i) in items"
:key="i"
class="stage-event-item"
>
<div class="item-details"> <div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility --> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> <user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
<h5 class="item-title merge-merquest-title"> <h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url"> <a :href="mergeRequest.url">
{{ mergeRequest.title }} {{ mergeRequest.title }}
</a> </a>
</h5> </h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> <a
:href="mergeRequest.url"
class="issue-link"
>
!{{ mergeRequest.iid }}
</a>
&middot; &middot;
<span> <span>
{{ s__('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> <a
:href="mergeRequest.url"
class="issue-date"
>
{{ mergeRequest.createdAt }}
</a>
</span> </span>
<span> <span>
{{ s__('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> <a
:href="mergeRequest.author.webUrl"
class="issue-author-link"
>
{{ mergeRequest.author.name }}
</a>
</span> </span>
</div> </div>
<div class="item-time"> <div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time> <total-time :time="mergeRequest.totalTime" />
</div> </div>
</li> </li>
</ul> </ul>
......
...@@ -4,15 +4,21 @@ ...@@ -4,15 +4,21 @@
import totalTime from './total_time_component.vue'; import totalTime from './total_time_component.vue';
export default { export default {
props: {
items: Array,
stage: Object,
},
components: { components: {
userAvatarImage, userAvatarImage,
limitWarning, limitWarning,
totalTime, totalTime,
}, },
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
}; };
</script> </script>
<template> <template>
...@@ -25,30 +31,47 @@ ...@@ -25,30 +31,47 @@
<li <li
v-for="(issue, i) in items" v-for="(issue, i) in items"
:key="i" :key="i"
class="stage-event-item"> class="stage-event-item"
>
<div class="item-details"> <div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility --> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/> <user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title"> <h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url"> <a
class="issue-title"
:href="issue.url"
>
{{ issue.title }} {{ issue.title }}
</a> </a>
</h5> </h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> <a
:href="issue.url"
class="issue-link"
>
#{{ issue.iid }}
</a>
&middot; &middot;
<span> <span>
{{ s__('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> <a
:href="issue.url"
class="issue-date"
>
{{ issue.createdAt }}
</a>
</span> </span>
<span> <span>
{{ s__('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link"> <a
:href="issue.author.webUrl"
class="issue-author-link"
>
{{ issue.author.name }} {{ issue.author.name }}
</a> </a>
</span> </span>
</div> </div>
<div class="item-time"> <div class="item-time">
<total-time :time="issue.totalTime"/> <total-time :time="issue.totalTime" />
</div> </div>
</li> </li>
</ul> </ul>
......
...@@ -5,15 +5,21 @@ ...@@ -5,15 +5,21 @@
import totalTime from './total_time_component.vue'; import totalTime from './total_time_component.vue';
export default { export default {
props: {
items: Array,
stage: Object,
},
components: { components: {
userAvatarImage, userAvatarImage,
totalTime, totalTime,
limitWarning, limitWarning,
}, },
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
computed: { computed: {
iconCommit() { iconCommit() {
return iconCommit; return iconCommit;
...@@ -31,10 +37,11 @@ ...@@ -31,10 +37,11 @@
<li <li
v-for="(commit, i) in items" v-for="(commit, i) in items"
:key="i" :key="i"
class="stage-event-item"> class="stage-event-item"
>
<div class="item-details item-conmmit-component"> <div class="item-details item-conmmit-component">
<!-- FIXME: Pass an alt attribute here for accessibility --> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/> <user-avatar-image :img-src="commit.author.avatarUrl" />
<h5 class="item-title commit-title"> <h5 class="item-title commit-title">
<a :href="commit.commitUrl"> <a :href="commit.commitUrl">
{{ commit.title }} {{ commit.title }}
...@@ -42,10 +49,22 @@ ...@@ -42,10 +49,22 @@
</h5> </h5>
<span> <span>
{{ s__('FirstPushedBy|First') }} {{ s__('FirstPushedBy|First') }}
<span class="commit-icon" v-html="iconCommit"></span> <span
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a> class="commit-icon"
v-html="iconCommit"
>
</span>
<a
:href="commit.commitUrl"
class="commit-hash-link commit-sha"
>
{{ commit.shortSha }}
</a>
{{ s__('FirstPushedBy|pushed by') }} {{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link"> <a
:href="commit.author.webUrl"
class="commit-author-link"
>
{{ commit.author.name }} {{ commit.author.name }}
</a> </a>
</span> </span>
......
...@@ -5,16 +5,22 @@ ...@@ -5,16 +5,22 @@
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
export default { export default {
props: {
items: Array,
stage: Object,
},
components: { components: {
userAvatarImage, userAvatarImage,
totalTime, totalTime,
limitWarning, limitWarning,
icon, icon,
}, },
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
}; };
</script> </script>
<template> <template>
...@@ -27,43 +33,64 @@ ...@@ -27,43 +33,64 @@
<li <li
v-for="(mergeRequest, i) in items" v-for="(mergeRequest, i) in items"
:key="i" :key="i"
class="stage-event-item"> class="stage-event-item"
>
<div class="item-details"> <div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility --> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> <user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
<h5 class="item-title merge-merquest-title"> <h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url"> <a :href="mergeRequest.url">
{{ mergeRequest.title }} {{ mergeRequest.title }}
</a> </a>
</h5> </h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> <a
:href="mergeRequest.url"
class="issue-link"
>
!{{ mergeRequest.iid }}
</a>
&middot; &middot;
<span> <span>
{{ s__('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> <a
:href="mergeRequest.url"
class="issue-date"
>{{ mergeRequest.createdAt }}</a>
</span> </span>
<span> <span>
{{ s__('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> <a
:href="mergeRequest.author.webUrl"
class="issue-author-link"
>{{ mergeRequest.author.name }}</a>
</span> </span>
<template v-if="mergeRequest.state === 'closed'"> <template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state"> <span class="merge-request-state">
<i class="fa fa-ban"></i> <i
class="fa fa-ban"
aria-hidden="true"
>
</i>
{{ mergeRequest.state.toUpperCase() }} {{ mergeRequest.state.toUpperCase() }}
</span> </span>
</template> </template>
<template v-else> <template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch"> <span
class="merge-request-branch"
v-if="mergeRequest.branch"
>
<icon <icon
name="fork" name="fork"
:size="16"> :size="16"
</icon> />
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> <a :href="mergeRequest.branch.url">
{{ mergeRequest.branch.name }}
</a>
</span> </span>
</template> </template>
</div> </div>
<div class="item-time"> <div class="item-time">
<total-time :time="mergeRequest.totalTime"/> <total-time :time="mergeRequest.totalTime" />
</div> </div>
</li> </li>
</ul> </ul>
......
...@@ -6,16 +6,22 @@ ...@@ -6,16 +6,22 @@
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
export default { export default {
props: {
items: Array,
stage: Object,
},
components: { components: {
userAvatarImage, userAvatarImage,
totalTime, totalTime,
limitWarning, limitWarning,
icon, icon,
}, },
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
computed: { computed: {
iconBranch() { iconBranch() {
return iconBranch; return iconBranch;
...@@ -33,30 +39,57 @@ ...@@ -33,30 +39,57 @@
<li <li
v-for="(build, i) in items" v-for="(build, i) in items"
class="stage-event-item item-build-component" class="stage-event-item item-build-component"
:key="i"> :key="i"
>
<div class="item-details"> <div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility --> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/> <user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title"> <h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <a
:href="build.url"
class="pipeline-id"
>
#{{ build.id }}
</a>
<icon <icon
name="fork" name="fork"
:size="16"> :size="16"
</icon> />
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> <a
<span class="icon-branch" v-html="iconBranch"></span> :href="build.branch.url"
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> class="ref-name"
>
{{ build.branch.name }}
</a>
<span
class="icon-branch"
v-html="iconBranch">
</span>
<a
:href="build.commitUrl"
class="commit-sha"
>
{{ build.shortSha }}
</a>
</h5> </h5>
<span> <span>
<a :href="build.url" class="build-date">{{ build.date }}</a> <a
:href="build.url"
class="build-date"
>
{{ build.date }}
</a>
{{ s__('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link"> <a
:href="build.author.webUrl"
class="issue-author-link"
>
{{ build.author.name }} {{ build.author.name }}
</a> </a>
</span> </span>
</div> </div>
<div class="item-time"> <div class="item-time">
<total-time :time="build.totalTime"/> <total-time :time="build.totalTime" />
</div> </div>
</li> </li>
</ul> </ul>
......
...@@ -6,15 +6,21 @@ ...@@ -6,15 +6,21 @@
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
export default { export default {
props: {
items: Array,
stage: Object,
},
components: { components: {
totalTime, totalTime,
limitWarning, limitWarning,
icon, icon,
}, },
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
computed: { computed: {
iconBuildStatus() { iconBuildStatus() {
return iconBuildStatus; return iconBuildStatus;
...@@ -35,29 +41,61 @@ ...@@ -35,29 +41,61 @@
<li <li
v-for="(build, i) in items" v-for="(build, i) in items"
:key="i" :key="i"
class="stage-event-item item-build-component"> class="stage-event-item item-build-component"
>
<div class="item-details"> <div class="item-details">
<h5 class="item-title"> <h5 class="item-title">
<span class="icon-build-status" v-html="iconBuildStatus"></span> <span
<a :href="build.url" class="item-build-name">{{ build.name }}</a> class="icon-build-status"
v-html="iconBuildStatus"
>
</span>
<a
:href="build.url"
class="item-build-name"
>
{{ build.name }}
</a>
&middot; &middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <a
:href="build.url"
class="pipeline-id"
>
#{{ build.id }}
</a>
<icon <icon
name="fork" name="fork"
:size="16"> :size="16"
</icon> />
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> <a
<span class="icon-branch" v-html="iconBranch"></span> :href="build.branch.url"
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> class="ref-name"
>
{{ build.branch.name }}
</a>
<span
class="icon-branch"
v-html="iconBranch"
>
</span>
<a
:href="build.commitUrl"
class="commit-sha"
>
{{ build.shortSha }}
</a>
</h5> </h5>
<span> <span>
<a :href="build.url" class="issue-date"> <a
:href="build.url"
class="issue-date"
>
{{ build.date }} {{ build.date }}
</a> </a>
</span> </span>
</div> </div>
<div class="item-time"> <div class="item-time">
<total-time :time="build.totalTime"/> <total-time :time="build.totalTime" />
</div> </div>
</li> </li>
</ul> </ul>
......
...@@ -17,13 +17,33 @@ ...@@ -17,13 +17,33 @@
<template> <template>
<span class="total-time"> <span class="total-time">
<template v-if="hasData"> <template v-if="hasData">
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template> <template v-if="time.days">
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template> {{ time.days }}
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template> <span>
<template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> {{ n__('day', 'days', time.days) }}
</span>
</template>
<template v-if="time.hours">
{{ time.hours }}
<span>
{{ n__('Time|hr', 'Time|hrs', time.hours) }}
</span>
</template>
<template v-if="time.mins && !time.days">
{{ time.mins }}
<span>
{{ n__('Time|min', 'Time|mins', time.mins) }}
</span>
</template>
<template v-if="time.seconds && hasData === 1 || time.seconds === 0">
{{ time.seconds }}
<span>
{{ s__('Time|s') }}
</span>
</template>
</template> </template>
<template v-else> <template v-else>
-- --
</template> </template>
</span> </span>
</template> </template>
...@@ -20,6 +20,16 @@ $(() => { ...@@ -20,6 +20,16 @@ $(() => {
gl.cycleAnalyticsApp = new Vue({ gl.cycleAnalyticsApp = new Vue({
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
components: {
banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stagePlanComponent,
'stage-code-component': stageCodeComponent,
'stage-test-component': stageTestComponent,
'stage-review-component': stageReviewComponent,
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
},
data() { data() {
const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsService = new CycleAnalyticsService({ const cycleAnalyticsService = new CycleAnalyticsService({
...@@ -43,16 +53,6 @@ $(() => { ...@@ -43,16 +53,6 @@ $(() => {
return this.store.currentActiveStage(); return this.store.currentActiveStage();
}, },
}, },
components: {
banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stagePlanComponent,
'stage-code-component': stageCodeComponent,
'stage-test-component': stageTestComponent,
'stage-review-component': stageReviewComponent,
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
},
created() { created() {
this.fetchCycleAnalyticsData(); this.fetchCycleAnalyticsData();
}, },
......
...@@ -3,10 +3,8 @@ ...@@ -3,10 +3,8 @@
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
data() { components: {
return { loadingIcon,
isLoading: false,
};
}, },
props: { props: {
deployKey: { deployKey: {
...@@ -23,11 +21,16 @@ ...@@ -23,11 +21,16 @@
default: 'btn-default', default: 'btn-default',
}, },
}, },
data() {
components: { return {
loadingIcon, isLoading: false,
};
},
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
}, },
methods: { methods: {
doAction() { doAction() {
this.isLoading = true; this.isLoading = true;
...@@ -37,11 +40,6 @@ ...@@ -37,11 +40,6 @@
}); });
}, },
}, },
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
},
}; };
</script> </script>
......
...@@ -7,11 +7,9 @@ ...@@ -7,11 +7,9 @@
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
data() { components: {
return { keysPanel,
isLoading: false, loadingIcon,
store: new DeployKeysStore(),
};
}, },
props: { props: {
endpoint: { endpoint: {
...@@ -19,6 +17,12 @@ ...@@ -19,6 +17,12 @@
required: true, required: true,
}, },
}, },
data() {
return {
isLoading: false,
store: new DeployKeysStore(),
};
},
computed: { computed: {
hasKeys() { hasKeys() {
return Object.keys(this.keys).length; return Object.keys(this.keys).length;
...@@ -27,9 +31,20 @@ ...@@ -27,9 +31,20 @@
return this.store.keys; return this.store.keys;
}, },
}, },
components: { created() {
keysPanel, this.service = new DeployKeysService(this.endpoint);
loadingIcon,
eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
}, },
methods: { methods: {
fetchKeys() { fetchKeys() {
...@@ -59,21 +74,6 @@ ...@@ -59,21 +74,6 @@
} }
}, },
}, },
created() {
this.service = new DeployKeysService(this.endpoint);
eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
},
}; };
</script> </script>
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
import { getTimeago } from '../../lib/utils/datetime_utility'; import { getTimeago } from '../../lib/utils/datetime_utility';
export default { export default {
components: {
actionBtn,
},
props: { props: {
deployKey: { deployKey: {
type: Object, type: Object,
...@@ -17,9 +20,6 @@ ...@@ -17,9 +20,6 @@
required: true, required: true,
}, },
}, },
components: {
actionBtn,
},
computed: { computed: {
timeagoDate() { timeagoDate() {
return getTimeago().format(this.deployKey.created_at); return getTimeago().format(this.deployKey.created_at);
...@@ -61,9 +61,10 @@ ...@@ -61,9 +61,10 @@
</div> </div>
<div class="deploy-key-content prepend-left-default deploy-key-projects"> <div class="deploy-key-content prepend-left-default deploy-key-projects">
<a <a
v-for="project in deployKey.projects" v-for="(project, i) in deployKey.projects"
class="label deploy-project-label" class="label deploy-project-label"
:href="project.full_path" :href="project.full_path"
:key="i"
> >
{{ project.full_name }} {{ project.full_name }}
</a> </a>
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
import key from './key.vue'; import key from './key.vue';
export default { export default {
components: {
key,
},
props: { props: {
title: { title: {
type: String, type: String,
...@@ -25,9 +28,6 @@ ...@@ -25,9 +28,6 @@
required: true, required: true,
}, },
}, },
components: {
key,
},
}; };
</script> </script>
...@@ -37,12 +37,14 @@ ...@@ -37,12 +37,14 @@
{{ title }} {{ title }}
({{ keys.length }}) ({{ keys.length }})
</h5> </h5>
<ul class="well-list" <ul
class="well-list"
v-if="keys.length" v-if="keys.length"
> >
<li <li
v-for="deployKey in keys" v-for="deployKey in keys"
:key="deployKey.id"> :key="deployKey.id"
>
<key <key
:deploy-key="deployKey" :deploy-key="deployKey"
:store="store" :store="store"
......
...@@ -3,14 +3,14 @@ import deployKeysApp from './components/app.vue'; ...@@ -3,14 +3,14 @@ import deployKeysApp from './components/app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: document.getElementById('js-deploy-keys'), el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
},
data() { data() {
return { return {
endpoint: this.$options.el.dataset.endpoint, endpoint: this.$options.el.dataset.endpoint,
}; };
}, },
components: {
deployKeysApp,
},
render(createElement) { render(createElement) {
return createElement('deploy-keys-app', { return createElement('deploy-keys-app', {
props: { props: {
......
...@@ -4,6 +4,11 @@ ...@@ -4,6 +4,11 @@
import environmentTable from '../components/environments_table.vue'; import environmentTable from '../components/environments_table.vue';
export default { export default {
components: {
environmentTable,
loadingIcon,
tablePagination,
},
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -26,12 +31,6 @@ ...@@ -26,12 +31,6 @@
required: true, required: true,
}, },
}, },
components: {
environmentTable,
loadingIcon,
tablePagination,
},
methods: { methods: {
onChangePage(page) { onChangePage(page) {
this.$emit('onChangePage', page); this.$emit('onChangePage', page);
...@@ -47,7 +46,7 @@ ...@@ -47,7 +46,7 @@
label="Loading environments" label="Loading environments"
v-if="isLoading" v-if="isLoading"
size="3" size="3"
/> />
<slot name="emptyState"></slot> <slot name="emptyState"></slot>
...@@ -59,13 +58,13 @@ ...@@ -59,13 +58,13 @@
:environments="environments" :environments="environments"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
/> />
<table-pagination <table-pagination
v-if="pagination && pagination.totalPages > 1" v-if="pagination && pagination.totalPages > 1"
:change="onChangePage" :change="onChangePage"
:pageInfo="pagination" :page-info="pagination"
/> />
</div> </div>
</div> </div>
</template> </template>
...@@ -61,28 +61,30 @@ ...@@ -61,28 +61,30 @@
<section class="deploy-board-information"> <section class="deploy-board-information">
<span> <span>
<span class="percentage">{{deployBoardData.completion}}%</span> <span class="percentage">{{ deployBoardData.completion }}%</span>
<span class="text">Complete</span> <span class="text">Complete</span>
</span> </span>
</section> </section>
<section class="deploy-board-instances"> <section class="deploy-board-instances">
<p class="text">{{instanceTitle}}</p> <p class="text">{{ instanceTitle }}</p>
<div class="deploy-board-instances-container"> <div class="deploy-board-instances-container">
<template v-for="instance in deployBoardData.instances"> <template v-for="(instance, i) in deployBoardData.instances">
<instance-component <instance-component
:status="instance.status" :status="instance.status"
:tooltip-text="instance.tooltip" :tooltip-text="instance.tooltip"
:stable="instance.stable" :stable="instance.stable"
/> :key="i"
/>
</template> </template>
</div> </div>
</section> </section>
<section <section
class="deploy-board-actions" class="deploy-board-actions"
v-if="deployBoardData.rollback_url || deployBoardData.abort_url"> v-if="deployBoardData.rollback_url || deployBoardData.abort_url"
>
<a <a
class="btn" class="btn"
data-method="post" data-method="post"
...@@ -112,10 +114,11 @@ ...@@ -112,10 +114,11 @@
<section class="deploy-board-empty-state-text"> <section class="deploy-board-empty-state-text">
<span class="title">Kubernetes deployment not found</span> <span class="title">Kubernetes deployment not found</span>
<span> <span>
To see deployment progress for your environments, make sure your deployments are in Kubernetes namespace To see deployment progress for your environments,
<code>{{projectName}}</code> and labeled with <code>app=$CI_ENVIRONMENT_SLUG</code>. make sure your deployments are in Kubernetes namespace
<code>{{ projectName }}</code> and labeled with <code>app=$CI_ENVIRONMENT_SLUG</code>.
</span> </span>
</section> </section>
</div> </div>
</div> </div>
</script> </template>
<script> <script>
/** /**
* An instance in deploy board is represented by a square in this mockup: * 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 * https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png
* *
* Each instance has a state and a tooltip. * Each instance has a state and a tooltip.
* The state needs to be represented in different colors, * The state needs to be represented in different colors,
* see more information about this in * see more information about this in
* https://gitlab.com/gitlab-org/gitlab-ee/uploads/5fff049fd88336d9ee0c6ef77b1ba7e3/monitoring__deployboard--key.png * 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 * 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. * this information in the tooltip and the colors.
* Mockup is https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1551#note_26595150 * Mockup is https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1551#note_26595150
*/ */
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { directives: {
/** tooltip,
* 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: { props: {
type: String, /**
required: false, * Represents the status of the pod. Each state is represented with a different
default: '', * color.
}, * It should be one of the following:
* finished || deploying || failed || ready || preparing || waiting
*/
status: {
type: String,
required: true,
default: 'finished',
},
stable: { tooltipText: {
type: Boolean, type: String,
required: false, required: false,
default: true, default: '',
}, },
},
directives: { stable: {
tooltip, type: Boolean,
}, required: false,
default: true,
},
},
computed: { computed: {
cssClass() { cssClass() {
let cssClassName = `deploy-board-instance-${this.status}`; let cssClassName = `deploy-board-instance-${this.status}`;
if (!this.stable) { if (!this.stable) {
cssClassName = `${cssClassName} deploy-board-instance-canary`; cssClassName = `${cssClassName} deploy-board-instance-canary`;
} }
return cssClassName; return cssClassName;
},
}, },
}, };
};
</script> </script>
<template> <template>
<div <div
...@@ -64,6 +64,7 @@ export default { ...@@ -64,6 +64,7 @@ export default {
class="deploy-board-instance" class="deploy-board-instance"
:class="cssClass" :class="cssClass"
:data-title="tooltipText" :data-title="tooltipText"
data-placement="top"> data-placement="top"
>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'environmentsEmptyState', name: 'EnvironmentsEmptyState',
props: { props: {
newPath: { newPath: {
type: String, type: String,
...@@ -21,21 +21,23 @@ ...@@ -21,21 +21,23 @@
<div class="blank-state-row"> <div class="blank-state-row">
<div class="blank-state-center"> <div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title"> <h2 class="blank-state-title js-blank-state-title">
{{s__("Environments|You don't have any environments right now.")}} {{ s__("Environments|You don't have any environments right now.") }}
</h2> </h2>
<p class="blank-state-text"> <p class="blank-state-text">
{{s__("Environments|Environments are places where code gets deployed, such as staging or production.")}} {{ s__(`Environments|Environments are places where
code gets deployed, such as staging or production.`) }}
<br /> <br />
<a :href="helpPath"> <a :href="helpPath">
{{s__("Environments|Read more about environments")}} {{ s__("Environments|Read more about environments") }}
</a> </a>
</p> </p>
<a <a
v-if="canCreateEnvironment" v-if="canCreateEnvironment"
:href="newPath" :href="newPath"
class="btn btn-create js-new-environment-button"> class="btn btn-create js-new-environment-button"
{{s__("Environments|New environment")}} >
{{ s__("Environments|New environment") }}
</a> </a>
</div> </div>
</div> </div>
......
<script> <script>
import playIconSvg from 'icons/_icon_play.svg'; import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { directives: {
actions: { tooltip,
type: Array,
required: false,
default: () => [],
}, },
},
directives: { components: {
tooltip, loadingIcon,
}, },
components: { props: {
loadingIcon, actions: {
}, type: Array,
required: false,
default: () => [],
},
},
data() { data() {
return { return {
playIconSvg, playIconSvg,
isLoading: false, isLoading: false,
}; };
}, },
computed: { computed: {
title() { title() {
return 'Deploy to...'; return 'Deploy to...';
},
}, },
},
methods: { methods: {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
eventHub.$emit('postAction', endpoint); eventHub.$emit('postAction', endpoint);
}, },
isActionDisabled(action) { isActionDisabled(action) {
if (action.playable === undefined) { if (action.playable === undefined) {
return false; return false;
} }
return !action.playable; return !action.playable;
},
}, },
}, };
};
</script> </script>
<template> <template>
<div <div
...@@ -63,27 +63,33 @@ export default { ...@@ -63,27 +63,33 @@ export default {
data-toggle="dropdown" data-toggle="dropdown"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:disabled="isLoading"> :disabled="isLoading"
>
<span> <span>
<span v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true"/> aria-hidden="true">
</i>
<loading-icon v-if="isLoading" /> <loading-icon v-if="isLoading" />
</span> </span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions"> <li
v-for="(action, i) in actions"
:key="i"
>
<button <button
type="button" type="button"
class="js-manual-action-link no-btn btn" class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)" @click="onClickAction(action.play_path)"
:class="{ disabled: isActionDisabled(action) }" :class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"> :disabled="isActionDisabled(action)"
>
<span v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
<span> <span>
{{action.name}} {{ action.name }}
</span> </span>
</button> </button>
</li> </li>
......
<script> <script>
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
export default { export default {
props: { directives: {
externalUrl: { tooltip,
type: String,
required: true,
}, },
},
directives: { props: {
tooltip, externalUrl: {
}, type: String,
required: true,
},
},
computed: { computed: {
title() { title() {
return s__('Environments|Open'); return s__('Environments|Open');
},
}, },
}, };
};
</script> </script>
<template> <template>
<a <a
...@@ -33,9 +33,12 @@ export default { ...@@ -33,9 +33,12 @@ export default {
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:href="externalUrl"> :href="externalUrl"
>
<i <i
class="fa fa-external-link" class="fa fa-external-link"
aria-hidden="true" /> aria-hidden="true"
>
</i>
</a> </a>
</template> </template>
<script> <script>
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { directives: {
monitoringUrl: { tooltip,
type: String, },
required: true, props: {
monitoringUrl: {
type: String,
required: true,
},
}, },
},
directives: {
tooltip,
},
computed: { computed: {
title() { title() {
return 'Monitoring'; return 'Monitoring';
},
}, },
}, };
};
</script> </script>
<template> <template>
<a <a
...@@ -31,10 +30,12 @@ export default { ...@@ -31,10 +30,12 @@ export default {
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
:href="monitoringUrl" :href="monitoringUrl"
:title="title" :title="title"
:aria-label="title"> :aria-label="title"
>
<i <i
class="fa fa-area-chart" class="fa fa-area-chart"
aria-hidden="true" aria-hidden="true"
/> >
</i>
</a> </a>
</template> </template>
<script> <script>
/** /**
* Renders Rollback or Re deploy button in environments table depending * Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`. * of the provided property `isLastDeployment`.
* *
* Makes a post request when the button is clicked. * Makes a post request when the button is clicked.
*/ */
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
props: { components: {
retryUrl: { loadingIcon,
type: String,
default: '',
}, },
props: {
isLastDeployment: { retryUrl: {
type: Boolean, type: String,
default: true, default: '',
},
isLastDeployment: {
type: Boolean,
default: true,
},
}, },
},
components: { data() {
loadingIcon, return {
}, isLoading: false,
};
data() { },
return {
isLoading: false,
};
},
methods: { methods: {
onClick() { onClick() {
this.isLoading = true; this.isLoading = true;
eventHub.$emit('postAction', this.retryUrl); eventHub.$emit('postAction', this.retryUrl);
},
}, },
}, };
};
</script> </script>
<template> <template>
<button <button
type="button" type="button"
class="btn hidden-xs hidden-sm" class="btn hidden-xs hidden-sm"
@click="onClick" @click="onClick"
:disabled="isLoading"> :disabled="isLoading"
>
<span v-if="isLastDeployment"> <span v-if="isLastDeployment">
{{s__("Environments|Re-deploy")}} {{ s__("Environments|Re-deploy") }}
</span> </span>
<span v-else> <span v-else>
{{s__("Environments|Rollback")}} {{ s__("Environments|Rollback") }}
</span> </span>
<loading-icon v-if="isLoading" /> <loading-icon v-if="isLoading" />
......
<script> <script>
/** /**
* Renders the stop "button" that allows stop an environment. * Renders the stop "button" that allows stop an environment.
* Used in environments table. * Used in environments table.
*/ */
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { components: {
stopUrl: { loadingIcon,
type: String, },
default: '', directives: {
tooltip,
}, },
},
directives: {
tooltip,
},
data() { props: {
return { stopUrl: {
isLoading: false, type: String,
}; default: '',
}, },
},
components: { data() {
loadingIcon, return {
}, isLoading: false,
};
},
computed: { computed: {
title() { title() {
return 'Stop'; return 'Stop';
},
}, },
},
methods: { methods: {
onClick() { onClick() {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (confirm('Are you sure you want to stop this environment?')) { if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy'); $(this.$el).tooltip('destroy');
eventHub.$emit('postAction', this.stopUrl); eventHub.$emit('postAction', this.stopUrl);
} }
},
}, },
}, };
};
</script> </script>
<template> <template>
<button <button
...@@ -58,10 +57,13 @@ export default { ...@@ -58,10 +57,13 @@ export default {
@click="onClick" @click="onClick"
:disabled="isLoading" :disabled="isLoading"
:title="title" :title="title"
:aria-label="title"> :aria-label="title"
>
<i <i
class="fa fa-stop stop-env-icon" class="fa fa-stop stop-env-icon"
aria-hidden="true" /> aria-hidden="true"
>
</i>
<loading-icon v-if="isLoading" /> <loading-icon v-if="isLoading" />
</button> </button>
</template> </template>
<script> <script>
/** /**
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
*/ */
import terminalIconSvg from 'icons/_icon_terminal.svg'; import terminalIconSvg from 'icons/_icon_terminal.svg';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { directives: {
terminalPath: { tooltip,
type: String, },
required: false, props: {
default: '', terminalPath: {
type: String,
required: false,
default: '',
},
},
data() {
return {
terminalIconSvg,
};
}, },
},
directives: {
tooltip,
},
data() {
return {
terminalIconSvg,
};
},
computed: { computed: {
title() { title() {
return 'Terminal'; return 'Terminal';
},
}, },
}, };
};
</script> </script>
<template> <template>
<a <a
...@@ -40,6 +38,7 @@ export default { ...@@ -40,6 +38,7 @@ export default {
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:href="terminalPath" :href="terminalPath"
v-html="terminalIconSvg"> v-html="terminalIconSvg"
>
</a> </a>
</template> </template>
...@@ -7,6 +7,15 @@ ...@@ -7,6 +7,15 @@
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default { export default {
components: {
emptyState,
},
mixins: [
CIPaginationMixin,
environmentsMixin,
],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
...@@ -37,14 +46,6 @@ ...@@ -37,14 +46,6 @@
required: true, required: true,
}, },
}, },
components: {
emptyState,
},
mixins: [
CIPaginationMixin,
environmentsMixin,
],
created() { created() {
eventHub.$on('toggleFolder', this.toggleFolder); eventHub.$on('toggleFolder', this.toggleFolder);
...@@ -105,15 +106,17 @@ ...@@ -105,15 +106,17 @@
:tabs="tabs" :tabs="tabs"
@onChangeTab="onChangeTab" @onChangeTab="onChangeTab"
scope="environments" scope="environments"
/> />
<div <div
v-if="canCreateEnvironment && !isLoading" v-if="canCreateEnvironment && !isLoading"
class="nav-controls"> class="nav-controls"
>
<a <a
:href="newEnvironmentPath" :href="newEnvironmentPath"
class="btn btn-create"> class="btn btn-create"
{{s__("Environments|New environment")}} >
{{ s__("Environments|New environment") }}
</a> </a>
</div> </div>
</div> </div>
...@@ -126,13 +129,13 @@ ...@@ -126,13 +129,13 @@
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
@onChangePage="onChangePage" @onChangePage="onChangePage"
> >
<empty-state <empty-state
slot="emptyState" slot="emptyState"
v-if="!isLoading && state.environments.length === 0" v-if="!isLoading && state.environments.length === 0"
:new-path="newEnvironmentPath" :new-path="newEnvironmentPath"
:help-path="helpPagePath" :help-path="helpPagePath"
:can-create-environment="canCreateEnvironment" :can-create-environment="canCreateEnvironment"
/> />
</container> </container>
</div> </div>
</template> </template>
...@@ -37,68 +37,108 @@ export default { ...@@ -37,68 +37,108 @@ export default {
folderUrl(model) { folderUrl(model) {
return `${window.location.pathname}/folders/${model.folderName}`; return `${window.location.pathname}/folders/${model.folderName}`;
}, },
shouldRenderFolderContent(env) {
return env.isFolder &&
env.isOpen &&
env.children &&
env.children.length > 0;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="ci-table" role="grid"> <div
<div class="gl-responsive-table-row table-row-header" role="row"> class="ci-table"
<div class="table-section section-10 environments-name" role="columnheader"> role="grid"
{{s__("Environments|Environment")}} >
<div
class="gl-responsive-table-row table-row-header"
role="row"
>
<div
class="table-section section-10 environments-name"
role="columnheader"
>
{{ s__("Environments|Environment") }}
</div> </div>
<div class="table-section section-10 environments-deploy" role="columnheader"> <div
{{s__("Environments|Deployment")}} class="table-section section-10 environments-deploy"
role="columnheader"
>
{{ s__("Environments|Deployment") }}
</div> </div>
<div class="table-section section-15 environments-build" role="columnheader"> <div
{{s__("Environments|Job")}} class="table-section section-15 environments-build"
role="columnheader"
>
{{ s__("Environments|Job") }}
</div> </div>
<div class="table-section section-25 environments-commit" role="columnheader"> <div
{{s__("Environments|Commit")}} class="table-section section-25 environments-commit"
role="columnheader"
>
{{ s__("Environments|Commit") }}
</div> </div>
<div class="table-section section-10 environments-date" role="columnheader"> <div
{{s__("Environments|Updated")}} class="table-section section-10 environments-date"
role="columnheader"
>
{{ s__("Environments|Updated") }}
</div> </div>
</div> </div>
<template <template
v-for="model in environments" v-for="(model, i) in environments"
v-bind:model="model"> :model="model"
>
<div <div
is="environment-item" is="environment-item"
:model="model" :model="model"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
/> :key="i"
/>
<div v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row"> <div
v-if="model.hasDeployBoard && model.isDeployBoardVisible"
class="js-deploy-board-row"
:key="i"
>
<div class="deploy-board-container"> <div class="deploy-board-container">
<deploy-board <deploy-board
:deploy-board-data="model.deployBoardData" :deploy-board-data="model.deployBoardData"
:is-loading="model.isLoadingDeployBoard" :is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard" :is-empty="model.isEmptyDeployBoard"
/> />
</div> </div>
</div> </div>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> <template
<div v-if="model.isLoadingFolderContent"> v-if="shouldRenderFolderContent(model)"
>
<div
v-if="model.isLoadingFolderContent"
:key="i"
>
<loading-icon size="2" /> <loading-icon size="2" />
</div> </div>
<template v-else> <template v-else>
<div <div
is="environment-item" is="environment-item"
v-for="children in model.children" v-for="(children, index) in model.children"
:model="children" :model="children"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
/> :key="index"
/>
<div> <div :key="i">
<div class="text-center prepend-top-10"> <div class="text-center prepend-top-10">
<a <a
:href="folderUrl(model)" :href="folderUrl(model)"
class="btn btn-default"> class="btn btn-default"
{{s__("Environments|Show all")}} >
{{ s__("Environments|Show all") }}
</a> </a>
</div> </div>
</div> </div>
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default { export default {
mixins: [
environmentsMixin,
CIPaginationMixin,
],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
...@@ -26,11 +30,6 @@ ...@@ -26,11 +30,6 @@
}, },
}, },
mixins: [
environmentsMixin,
CIPaginationMixin,
],
methods: { methods: {
successCallback(resp) { successCallback(resp) {
this.saveData(resp); this.saveData(resp);
...@@ -42,17 +41,18 @@ ...@@ -42,17 +41,18 @@
<div :class="cssContainerClass"> <div :class="cssContainerClass">
<div <div
class="top-area" class="top-area"
v-if="!isLoading"> v-if="!isLoading"
>
<h4 class="js-folder-name environments-folder-name"> <h4 class="js-folder-name environments-folder-name">
{{s__("Environments|Environments")}} / <b>{{folderName}}</b> {{ s__("Environments|Environments") }} / <b>{{ folderName }}</b>
</h4> </h4>
<tabs <tabs
:tabs="tabs" :tabs="tabs"
@onChangeTab="onChangeTab" @onChangeTab="onChangeTab"
scope="environments" scope="environments"
/> />
</div> </div>
<container <container
...@@ -62,6 +62,6 @@ ...@@ -62,6 +62,6 @@
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
@onChangePage="onChangePage" @onChangePage="onChangePage"
/> />
</div> </div>
</template> </template>
...@@ -32,6 +32,9 @@ class RecentSearchesRoot { ...@@ -32,6 +32,9 @@ class RecentSearchesRoot {
const state = this.store.state; const state = this.store.state;
this.vm = new Vue({ this.vm = new Vue({
el: this.wrapperElement, el: this.wrapperElement,
components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
},
data() { return state; }, data() { return state; },
template: ` template: `
<recent-searches-dropdown-content <recent-searches-dropdown-content
...@@ -40,9 +43,6 @@ class RecentSearchesRoot { ...@@ -40,9 +43,6 @@ class RecentSearchesRoot {
:allowed-keys="allowedKeys" :allowed-keys="allowedKeys"
/> />
`, `,
components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
},
}); });
} }
......
...@@ -42,6 +42,26 @@ export default { ...@@ -42,6 +42,26 @@ export default {
return this.store.getPaginationInfo(); return this.store.getPaginationInfo();
}, },
}, },
created() {
this.searchEmptyMessage = this.hideProjects ?
COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleChildren', this.toggleChildren);
eventHub.$on('leaveGroup', this.leaveGroup);
eventHub.$on('updatePagination', this.updatePagination);
eventHub.$on('updateGroups', this.updateGroups);
},
mounted() {
this.fetchAllGroups();
},
beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleChildren', this.toggleChildren);
eventHub.$off('leaveGroup', this.leaveGroup);
eventHub.$off('updatePagination', this.updatePagination);
eventHub.$off('updateGroups', this.updateGroups);
},
methods: { methods: {
fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) { fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived) return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
...@@ -152,26 +172,6 @@ export default { ...@@ -152,26 +172,6 @@ export default {
} }
}, },
}, },
created() {
this.searchEmptyMessage = this.hideProjects ?
COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleChildren', this.toggleChildren);
eventHub.$on('leaveGroup', this.leaveGroup);
eventHub.$on('updatePagination', this.updatePagination);
eventHub.$on('updateGroups', this.updateGroups);
},
mounted() {
this.fetchAllGroups();
},
beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleChildren', this.toggleChildren);
eventHub.$off('leaveGroup', this.leaveGroup);
eventHub.$off('updatePagination', this.updatePagination);
eventHub.$off('updateGroups', this.updateGroups);
},
}; };
</script> </script>
......
...@@ -20,7 +20,11 @@ export default { ...@@ -20,7 +20,11 @@ export default {
return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT; return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT;
}, },
moreChildrenStats() { moreChildrenStats() {
return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length); return n__(
'One more item',
'%d more items',
this.parentGroup.childrenCount - this.parentGroup.children.length,
);
}, },
}, },
}; };
...@@ -43,8 +47,9 @@ export default { ...@@ -43,8 +47,9 @@ export default {
<i <i
class="fa fa-external-link" class="fa fa-external-link"
aria-hidden="true" aria-hidden="true"
/> >
{{moreChildrenStats}} </i>
{{ moreChildrenStats }}
</a> </a>
</li> </li>
</ul> </ul>
......
...@@ -75,7 +75,7 @@ export default { ...@@ -75,7 +75,7 @@ export default {
:id="groupDomId" :id="groupDomId"
:class="rowClass" :class="rowClass"
class="group-row" class="group-row"
> >
<div <div
class="group-row-contents" class="group-row-contents"
:class="{ 'project-row-contents': !isGroup }"> :class="{ 'project-row-contents': !isGroup }">
...@@ -88,7 +88,8 @@ export default { ...@@ -88,7 +88,8 @@ export default {
:item="group" :item="group"
/> />
<div <div
class="folder-toggle-wrap"> class="folder-toggle-wrap"
>
<item-caret <item-caret
:is-group-open="group.isOpen" :is-group-open="group.isOpen"
/> />
...@@ -113,13 +114,14 @@ export default { ...@@ -113,13 +114,14 @@ export default {
<identicon <identicon
v-else v-else
size-class="s24" size-class="s24"
:entity-id=group.id :entity-id="group.id"
:entity-name="group.name" :entity-name="group.name"
/> />
</a> </a>
</div> </div>
<div <div
class="title namespace-title"> class="title namespace-title"
>
<a <a
v-tooltip v-tooltip
:href="group.relativePath" :href="group.relativePath"
...@@ -135,7 +137,7 @@ export default { ...@@ -135,7 +137,7 @@ export default {
v-if="group.permission" v-if="group.permission"
class="user-access-role" class="user-access-role"
> >
{{group.permission}} {{ group.permission }}
</span> </span>
</div> </div>
<div <div
......
<script> <script>
import tablePagination from '~/vue_shared/components/table_pagination.vue'; import tablePagination from '~/vue_shared/components/table_pagination.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils'; import { getParameterByName } from '../../lib/utils/common_utils';
export default { export default {
components: { components: {
tablePagination, tablePagination,
},
props: {
groups: {
type: Array,
required: true,
}, },
pageInfo: { props: {
type: Object, groups: {
required: true, type: Array,
required: true,
},
pageInfo: {
type: Object,
required: true,
},
searchEmpty: {
type: Boolean,
required: true,
},
searchEmptyMessage: {
type: String,
required: true,
},
}, },
searchEmpty: { methods: {
type: Boolean, change(page) {
required: true, const filterGroupsParam = getParameterByName('filter_groups');
const sortParam = getParameterByName('sort');
const archivedParam = getParameterByName('archived');
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
},
}, },
searchEmptyMessage: { };
type: String,
required: true,
},
},
methods: {
change(page) {
const filterGroupsParam = getParameterByName('filter_groups');
const sortParam = getParameterByName('sort');
const archivedParam = getParameterByName('archived');
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
},
},
};
</script> </script>
<template> <template>
<div class="groups-list-tree-container"> <div class="groups-list-tree-container">
<div <div
v-if="searchEmpty" v-if="searchEmpty"
class="has-no-search-results"> class="has-no-search-results"
{{searchEmptyMessage}} >
{{ searchEmptyMessage }}
</div> </div>
<group-folder <group-folder
v-if="!searchEmpty" v-if="!searchEmpty"
...@@ -50,7 +51,7 @@ export default { ...@@ -50,7 +51,7 @@ export default {
<table-pagination <table-pagination
v-if="!searchEmpty" v-if="!searchEmpty"
:change="change" :change="change"
:pageInfo="pageInfo" :page-info="pageInfo"
/> />
</div> </div>
</template> </template>
<script> <script>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import modal from '~/vue_shared/components/modal.vue'; import modal from '~/vue_shared/components/modal.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { COMMON_STR } from '../constants'; import { COMMON_STR } from '../constants';
export default { export default {
components: { components: {
icon, icon,
modal, modal,
},
directives: {
tooltip,
},
props: {
parentGroup: {
type: Object,
required: false,
default: () => ({}),
}, },
group: { directives: {
type: Object, tooltip,
required: true,
}, },
}, props: {
data() { parentGroup: {
return { type: Object,
modalStatus: false, required: false,
}; default: () => ({}),
}, },
computed: { group: {
leaveBtnTitle() { type: Object,
return COMMON_STR.LEAVE_BTN_TITLE; required: true,
},
}, },
editBtnTitle() { data() {
return COMMON_STR.EDIT_BTN_TITLE; return {
modalStatus: false,
};
}, },
leaveConfirmationMessage() { computed: {
return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`); leaveBtnTitle() {
return COMMON_STR.LEAVE_BTN_TITLE;
},
editBtnTitle() {
return COMMON_STR.EDIT_BTN_TITLE;
},
leaveConfirmationMessage() {
return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`);
},
}, },
}, methods: {
methods: { onLeaveGroup() {
onLeaveGroup() { this.modalStatus = true;
this.modalStatus = true; },
leaveGroup() {
this.modalStatus = false;
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
},
}, },
leaveGroup() { };
this.modalStatus = false;
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
},
},
};
</script> </script>
<template> <template>
......
...@@ -32,7 +32,6 @@ ...@@ -32,7 +32,6 @@
this.$emit('toggleCollapsed'); this.$emit('toggleCollapsed');
}, },
}, },
}; };
</script> </script>
......
...@@ -40,7 +40,7 @@ export default { ...@@ -40,7 +40,7 @@ export default {
aria-hidden="true"> aria-hidden="true">
</i> </i>
<span> <span>
{{buttonLabel}} {{ buttonLabel }}
</span> </span>
</button> </button>
<modal <modal
......
This diff is collapsed.
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