summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorShinya Maeda <shinya@gitlab.com>2017-10-05 14:49:16 +0900
committerShinya Maeda <shinya@gitlab.com>2017-10-05 14:49:16 +0900
commit88cc9d5294198cfa748d602236729abcd73f56a6 (patch)
tree6eb5be9ed46ec720ba195702d80e166980868146 /app
parentd6e22e83d1fe8ffaa71c3bcb7906731733a89f0e (diff)
parent8921af39e74976e37e92c786bd957883110f6522 (diff)
downloadgitlab-ce-88cc9d5294198cfa748d602236729abcd73f56a6.tar.gz
Merge branch 'master' into feature/sm/35954-create-kubernetes-cluster-on-gke-from-k8s-service
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/locale/index.js20
-rw-r--r--app/assets/javascripts/merge_request.js15
-rw-r--r--app/assets/javascripts/notebook/cells/code.vue30
-rw-r--r--app/assets/javascripts/notebook/cells/code/index.vue28
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue14
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue14
-rw-r--r--app/assets/javascripts/notebook/cells/output/image.vue16
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue18
-rw-r--r--app/assets/javascripts/notebook/cells/prompt.vue16
-rw-r--r--app/assets/javascripts/notebook/index.vue22
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue1
-rw-r--r--app/assets/javascripts/pdf/index.vue20
-rw-r--r--app/assets/javascripts/pdf/page/index.vue14
-rw-r--r--app/assets/javascripts/project_fork.js5
-rw-r--r--app/assets/javascripts/registry/components/app.vue62
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue131
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue137
-rw-r--r--app/assets/javascripts/registry/constants.js15
-rw-r--r--app/assets/javascripts/registry/index.js25
-rw-r--r--app/assets/javascripts/registry/stores/actions.js39
-rw-r--r--app/assets/javascripts/registry/stores/getters.js2
-rw-r--r--app/assets/javascripts/registry/stores/index.js39
-rw-r--r--app/assets/javascripts/registry/stores/mutation_types.js7
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js54
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js33
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue32
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/gfm.scss11
-rw-r--r--app/assets/stylesheets/framework/selects.scss145
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/pages/container_registry.scss8
-rw-r--r--app/assets/stylesheets/pages/projects.scss99
-rw-r--r--app/assets/stylesheets/pages/settings_ci_cd.scss4
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/registry/repositories_controller.rb21
-rw-r--r--app/controllers/projects/registry/tags_controller.rb27
-rw-r--r--app/helpers/events_helper.rb20
-rw-r--r--app/models/key.rb1
-rw-r--r--app/models/merge_request.rb27
-rw-r--r--app/models/personal_access_token.rb6
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/presenters/merge_request_presenter.rb2
-rw-r--r--app/serializers/container_repositories_serializer.rb3
-rw-r--r--app/serializers/container_repository_entity.rb25
-rw-r--r--app/serializers/container_tag_entity.rb23
-rw-r--r--app/serializers/container_tags_serializer.rb17
-rw-r--r--app/serializers/merge_request_entity.rb2
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/projects/forks/new.html.haml78
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml4
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/registry/repositories/_image.html.haml32
-rw-r--r--app/views/projects/registry/repositories/index.html.haml93
-rw-r--r--app/views/projects/services/_form.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml7
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml4
68 files changed, 1033 insertions, 478 deletions
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index af718e894cf..ce05b3eabec 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -1,30 +1,12 @@
import Jed from 'jed';
-
import sprintf from './sprintf';
-/**
- This is required to require all the translation folders in the current directory
- this saves us having to do this manually & keep up to date with new languages
-**/
-function requireAll(requireContext) { return requireContext.keys().map(requireContext); }
-
-const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/));
-const locales = allLocales.reduce((d, obj) => {
- const data = d;
- const localeKey = Object.keys(obj)[0];
-
- data[localeKey] = obj[localeKey];
-
- return data;
-}, {});
-
const langAttribute = document.querySelector('html').getAttribute('lang');
const lang = (langAttribute || 'en').replace(/-/g, '_');
-const locale = new Jed(locales[lang]);
+const locale = new Jed(window.translations || {});
/**
Translates `text`
-
@param text The text to be translated
@returns {String} The translated text
**/
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 0db2abe507d..af0658eb668 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper';
$el.text(gl.text.addDelimiter(count));
};
+ MergeRequest.prototype.hideCloseButton = function() {
+ const el = document.querySelector('.merge-request .issuable-actions');
+ const closeDropdownItem = el.querySelector('li.close-item');
+ if (closeDropdownItem) {
+ closeDropdownItem.classList.add('hidden');
+ // Selects the next dropdown item
+ el.querySelector('li.report-item').click();
+ } else {
+ // No dropdown just hide the Close button
+ el.querySelector('.btn-close').classList.add('hidden');
+ }
+ // Dropdown for mobile screen
+ el.querySelector('li.js-close-item').classList.add('hidden');
+ };
+
return MergeRequest;
})();
}).call(window);
diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue
index b8a16356576..b4067d229aa 100644
--- a/app/assets/javascripts/notebook/cells/code.vue
+++ b/app/assets/javascripts/notebook/cells/code.vue
@@ -1,18 +1,3 @@
-<template>
- <div class="cell">
- <code-cell
- type="input"
- :raw-code="rawInputCode"
- :count="cell.execution_count"
- :code-css-class="codeCssClass" />
- <output-cell
- v-if="hasOutput"
- :count="cell.execution_count"
- :output="output"
- :code-css-class="codeCssClass" />
- </div>
-</template>
-
<script>
import CodeCell from './code/index.vue';
import OutputCell from './output/index.vue';
@@ -51,6 +36,21 @@ export default {
};
</script>
+<template>
+ <div class="cell">
+ <code-cell
+ type="input"
+ :raw-code="rawInputCode"
+ :count="cell.execution_count"
+ :code-css-class="codeCssClass" />
+ <output-cell
+ v-if="hasOutput"
+ :count="cell.execution_count"
+ :output="output"
+ :code-css-class="codeCssClass" />
+ </div>
+</template>
+
<style scoped>
.cell {
flex-direction: column;
diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue
index 31b30f601e2..0f3083f05b2 100644
--- a/app/assets/javascripts/notebook/cells/code/index.vue
+++ b/app/assets/javascripts/notebook/cells/code/index.vue
@@ -1,17 +1,3 @@
-<template>
- <div :class="type">
- <prompt
- :type="promptType"
- :count="count" />
- <pre
- class="language-python"
- :class="codeCssClass"
- ref="code"
- v-text="code">
- </pre>
- </div>
-</template>
-
<script>
import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue';
@@ -55,3 +41,17 @@
},
};
</script>
+
+<template>
+ <div :class="type">
+ <prompt
+ :type="promptType"
+ :count="count" />
+ <pre
+ class="language-python"
+ :class="codeCssClass"
+ ref="code"
+ v-text="code">
+ </pre>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index 814d2ea92b4..82c51a1068c 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,10 +1,3 @@
-<template>
- <div class="cell text-cell">
- <prompt />
- <div class="markdown" v-html="markdown"></div>
- </div>
-</template>
-
<script>
/* global katex */
import marked from 'marked';
@@ -95,6 +88,13 @@
};
</script>
+<template>
+ <div class="cell text-cell">
+ <prompt />
+ <div class="markdown" v-html="markdown"></div>
+ </div>
+</template>
+
<style>
.markdown .katex {
display: block;
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index 0f39cd138df..2110a9de7ed 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -1,10 +1,3 @@
-<template>
- <div class="output">
- <prompt />
- <div v-html="rawCode"></div>
- </div>
-</template>
-
<script>
import Prompt from '../prompt.vue';
@@ -20,3 +13,10 @@ export default {
},
};
</script>
+
+<template>
+ <div class="output">
+ <prompt />
+ <div v-html="rawCode"></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue
index f3b873bbc0f..fbb39ea6e2d 100644
--- a/app/assets/javascripts/notebook/cells/output/image.vue
+++ b/app/assets/javascripts/notebook/cells/output/image.vue
@@ -1,11 +1,3 @@
-<template>
- <div class="output">
- <prompt />
- <img
- :src="'data:' + outputType + ';base64,' + rawCode" />
- </div>
-</template>
-
<script>
import Prompt from '../prompt.vue';
@@ -25,3 +17,11 @@ export default {
},
};
</script>
+
+<template>
+ <div class="output">
+ <prompt />
+ <img
+ :src="'data:' + outputType + ';base64,' + rawCode" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index 23c9ea78939..05af0bf1e8e 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -1,12 +1,3 @@
-<template>
- <component :is="componentName"
- type="output"
- :outputType="outputType"
- :count="count"
- :raw-code="rawCode"
- :code-css-class="codeCssClass" />
-</template>
-
<script>
import CodeCell from '../code/index.vue';
import Html from './html.vue';
@@ -81,3 +72,12 @@ export default {
},
};
</script>
+
+<template>
+ <component :is="componentName"
+ type="output"
+ :outputType="outputType"
+ :count="count"
+ :raw-code="rawCode"
+ :code-css-class="codeCssClass" />
+</template>
diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue
index 4540e4248d8..039fb99293d 100644
--- a/app/assets/javascripts/notebook/cells/prompt.vue
+++ b/app/assets/javascripts/notebook/cells/prompt.vue
@@ -1,11 +1,3 @@
-<template>
- <div class="prompt">
- <span v-if="type && count">
- {{ type }} [{{ count }}]:
- </span>
- </div>
-</template>
-
<script>
export default {
props: {
@@ -21,6 +13,14 @@
};
</script>
+<template>
+ <div class="prompt">
+ <span v-if="type && count">
+ {{ type }} [{{ count }}]:
+ </span>
+ </div>
+</template>
+
<style scoped>
.prompt {
padding: 0 10px;
diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue
index fd62c1231ef..e88806431af 100644
--- a/app/assets/javascripts/notebook/index.vue
+++ b/app/assets/javascripts/notebook/index.vue
@@ -1,14 +1,3 @@
-<template>
- <div v-if="hasNotebook">
- <component
- v-for="(cell, index) in cells"
- :is="cellType(cell.cell_type)"
- :cell="cell"
- :key="index"
- :code-css-class="codeCssClass" />
- </div>
-</template>
-
<script>
import {
MarkdownCell,
@@ -59,6 +48,17 @@
};
</script>
+<template>
+ <div v-if="hasNotebook">
+ <component
+ v-for="(cell, index) in cells"
+ :is="cellType(cell.cell_type)"
+ :cell="cell"
+ :key="index"
+ :code-css-class="codeCssClass" />
+ </div>
+</template>
+
<style>
.cell,
.input,
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index fa7ac994058..1a7da84a424 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -272,6 +272,7 @@
v-model="note"
ref="textarea"
slot="textarea"
+ :disabled="isSubmitting"
placeholder="Write a comment or drag your files here..."
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()">
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index b874e484d45..c8a2f778ee8 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -1,13 +1,3 @@
-<template>
- <div class="pdf-viewer" v-if="hasPDF">
- <page v-for="(page, index) in pages"
- :key="index"
- :v-if="!loading"
- :page="page"
- :number="index + 1" />
- </div>
-</template>
-
<script>
import pdfjsLib from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min';
@@ -64,6 +54,16 @@
};
</script>
+<template>
+ <div class="pdf-viewer" v-if="hasPDF">
+ <page v-for="(page, index) in pages"
+ :key="index"
+ :v-if="!loading"
+ :page="page"
+ :number="index + 1" />
+ </div>
+</template>
+
<style>
.pdf-viewer {
background: url('./assets/img/bg.gif');
diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue
index 7b74ee4eb2e..be38f7cc129 100644
--- a/app/assets/javascripts/pdf/page/index.vue
+++ b/app/assets/javascripts/pdf/page/index.vue
@@ -1,10 +1,3 @@
-<template>
- <canvas
- class="pdf-page"
- ref="canvas"
- :data-page="number" />
-</template>
-
<script>
export default {
props: {
@@ -48,6 +41,13 @@
};
</script>
+<template>
+ <canvas
+ class="pdf-page"
+ ref="canvas"
+ :data-page="number" />
+</template>
+
<style>
.pdf-page {
margin: 8px auto 0 auto;
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index 68cf47fd54e..65d46fa9a73 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -1,8 +1,7 @@
export default () => {
- $('.fork-thumbnail a').on('click', function forkThumbnailClicked() {
+ $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
if ($(this).hasClass('disabled')) return false;
- $('.fork-namespaces').hide();
- return $('.save-project-loader').show();
+ return $('.js-fork-content').toggle();
});
};
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
new file mode 100644
index 00000000000..2d8ca443ea7
--- /dev/null
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -0,0 +1,62 @@
+<script>
+ /* globals Flash */
+ import { mapGetters, mapActions } from 'vuex';
+ import '../../flash';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import store from '../stores';
+ import collapsibleContainer from './collapsible_container.vue';
+ import { errorMessages, errorMessagesTypes } from '../constants';
+
+ export default {
+ name: 'registryListApp',
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ store,
+ components: {
+ collapsibleContainer,
+ loadingIcon,
+ },
+ computed: {
+ ...mapGetters([
+ 'isLoading',
+ 'repos',
+ ]),
+ },
+ methods: {
+ ...mapActions([
+ 'setMainEndpoint',
+ 'fetchRepos',
+ ]),
+ },
+ created() {
+ this.setMainEndpoint(this.endpoint);
+ },
+ mounted() {
+ this.fetchRepos()
+ .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
+ },
+ };
+</script>
+<template>
+ <div>
+ <loading-icon
+ v-if="isLoading"
+ size="3"
+ />
+
+ <collapsible-container
+ v-else-if="!isLoading && repos.length"
+ v-for="(item, index) in repos"
+ :key="index"
+ :repo="item"
+ />
+
+ <p v-else-if="!isLoading && !repos.length">
+ {{__("No container images stored for this project. Add one by following the instructions above.")}}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
new file mode 100644
index 00000000000..41ea9742406
--- /dev/null
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -0,0 +1,131 @@
+<script>
+ /* globals Flash */
+ import { mapActions } from 'vuex';
+ import '../../flash';
+ import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import tableRegistry from './table_registry.vue';
+ import { errorMessages, errorMessagesTypes } from '../constants';
+
+ export default {
+ name: 'collapsibeContainerRegisty',
+ props: {
+ repo: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ clipboardButton,
+ loadingIcon,
+ tableRegistry,
+ },
+ directives: {
+ tooltip,
+ },
+ data() {
+ return {
+ isOpen: false,
+ };
+ },
+ computed: {
+ clipboardText() {
+ return `docker pull ${this.repo.location}`;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'fetchRepos',
+ 'fetchList',
+ 'deleteRepo',
+ ]),
+
+ toggleRepo() {
+ this.isOpen = !this.isOpen;
+
+ if (this.isOpen) {
+ this.fetchList({ repo: this.repo })
+ .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
+ }
+ },
+
+ handleDeleteRepository() {
+ this.deleteRepo(this.repo)
+ .then(() => this.fetchRepos())
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
+ },
+
+ showError(message) {
+ Flash((errorMessages[message]));
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="container-image">
+ <div
+ class="container-image-head">
+ <button
+ type="button"
+ @click="toggleRepo"
+ class="js-toggle-repo btn-link">
+ <i
+ class="fa"
+ :class="{
+ 'fa-chevron-right': !isOpen,
+ 'fa-chevron-up': isOpen,
+ }"
+ aria-hidden="true">
+ </i>
+ {{repo.name}}
+ </button>
+
+ <clipboard-button
+ v-if="repo.location"
+ :text="clipboardText"
+ :title="repo.location"
+ />
+
+ <div class="controls hidden-xs pull-right">
+ <button
+ v-if="repo.canDelete"
+ type="button"
+ class="js-remove-repo btn btn-danger"
+ :title="s__('ContainerRegistry|Remove repository')"
+ :aria-label="s__('ContainerRegistry|Remove repository')"
+ v-tooltip
+ @click="handleDeleteRepository">
+ <i
+ class="fa fa-trash"
+ aria-hidden="true">
+ </i>
+ </button>
+ </div>
+
+ </div>
+
+ <loading-icon
+ v-if="repo.isLoading"
+ class="append-bottom-20"
+ size="2"
+ />
+
+ <div
+ v-else-if="!repo.isLoading && isOpen"
+ class="container-image-tags">
+
+ <table-registry
+ v-if="repo.list.length"
+ :repo="repo"
+ />
+
+ <div
+ v-else
+ class="nothing-here-block">
+ {{s__("ContainerRegistry|No tags in Container Registry for this container image.")}}
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
new file mode 100644
index 00000000000..4ce1571b0aa
--- /dev/null
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -0,0 +1,137 @@
+<script>
+ /* globals Flash */
+ import { mapActions } from 'vuex';
+ import { n__ } from '../../locale';
+ import '../../flash';
+ import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+ import tablePagination from '../../vue_shared/components/table_pagination.vue';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import timeagoMixin from '../../vue_shared/mixins/timeago';
+ import { errorMessages, errorMessagesTypes } from '../constants';
+
+ export default {
+ props: {
+ repo: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ clipboardButton,
+ tablePagination,
+ },
+ mixins: [
+ timeagoMixin,
+ ],
+ directives: {
+ tooltip,
+ },
+ computed: {
+ shouldRenderPagination() {
+ return this.repo.pagination.total > this.repo.pagination.perPage;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'fetchList',
+ 'deleteRegistry',
+ ]),
+
+ layers(item) {
+ return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
+ },
+
+ handleDeleteRegistry(registry) {
+ this.deleteRegistry(registry)
+ .then(() => this.fetchList({ repo: this.repo }))
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
+ },
+
+ onPageChange(pageNumber) {
+ this.fetchList({ repo: this.repo, page: pageNumber })
+ .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
+ },
+
+ clipboardText(text) {
+ return `docker pull ${text}`;
+ },
+
+ showError(message) {
+ Flash((errorMessages[message]));
+ },
+ },
+ };
+</script>
+<template>
+<div>
+ <table class="table tags">
+ <thead>
+ <tr>
+ <th>{{s__('ContainerRegistry|Tag')}}</th>
+ <th>{{s__('ContainerRegistry|Tag ID')}}</th>
+ <th>{{s__("ContainerRegistry|Size")}}</th>
+ <th>{{s__("ContainerRegistry|Created")}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ v-for="(item, i) in repo.list"
+ :key="i">
+ <td>
+
+ {{item.tag}}
+
+ <clipboard-button
+ v-if="item.location"
+ :title="item.location"
+ :text="clipboardText(item.location)"
+ />
+ </td>
+ <td>
+ <span
+ v-tooltip
+ :title="item.revision"
+ data-placement="bottom">
+ {{item.shortRevision}}
+ </span>
+ </td>
+ <td>
+ {{item.size}}
+ <template v-if="item.size && item.layers">
+ &middot;
+ </template>
+ {{layers(item)}}
+ </td>
+
+ <td>
+ {{timeFormated(item.createdAt)}}
+ </td>
+
+ <td class="content">
+ <button
+ v-if="item.canDelete"
+ type="button"
+ class="js-delete-registry btn btn-danger hidden-xs pull-right"
+ :title="s__('ContainerRegistry|Remove tag')"
+ :aria-label="s__('ContainerRegistry|Remove tag')"
+ data-container="body"
+ v-tooltip
+ @click="handleDeleteRegistry(item)">
+ <i
+ class="fa fa-trash"
+ aria-hidden="true">
+ </i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table-pagination
+ v-if="shouldRenderPagination"
+ :change="onPageChange"
+ :page-info="repo.pagination"
+ />
+</div>
+</template>
diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/constants.js
new file mode 100644
index 00000000000..712b0fade3d
--- /dev/null
+++ b/app/assets/javascripts/registry/constants.js
@@ -0,0 +1,15 @@
+import { __ } from '../locale';
+
+export const errorMessagesTypes = {
+ FETCH_REGISTRY: 'FETCH_REGISTRY',
+ FETCH_REPOS: 'FETCH_REPOS',
+ DELETE_REPO: 'DELETE_REPO',
+ DELETE_REGISTRY: 'DELETE_REGISTRY',
+};
+
+export const errorMessages = {
+ [errorMessagesTypes.FETCH_REGISTRY]: __('Something went wrong while fetching the registry list.'),
+ [errorMessagesTypes.FETCH_REPOS]: __('Something went wrong while fetching the projects.'),
+ [errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'),
+ [errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'),
+};
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
new file mode 100644
index 00000000000..d8edff73f72
--- /dev/null
+++ b/app/assets/javascripts/registry/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import registryApp from './components/app.vue';
+import Translate from '../vue_shared/translate';
+
+Vue.use(Translate);
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#js-vue-registry-images',
+ components: {
+ registryApp,
+ },
+ data() {
+ const dataset = document.querySelector(this.$options.el).dataset;
+ return {
+ endpoint: dataset.endpoint,
+ };
+ },
+ render(createElement) {
+ return createElement('registry-app', {
+ props: {
+ endpoint: this.endpoint,
+ },
+ });
+ },
+}));
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
new file mode 100644
index 00000000000..34ed40b8b65
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import * as types from './mutation_types';
+
+Vue.use(VueResource);
+
+export const fetchRepos = ({ commit, state }) => {
+ commit(types.TOGGLE_MAIN_LOADING);
+
+ return Vue.http.get(state.endpoint)
+ .then(res => res.json())
+ .then((response) => {
+ commit(types.TOGGLE_MAIN_LOADING);
+ commit(types.SET_REPOS_LIST, response);
+ });
+};
+
+export const fetchList = ({ commit }, { repo, page }) => {
+ commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
+
+ return Vue.http.get(repo.tagsPath, { params: { page } })
+ .then((response) => {
+ const headers = response.headers;
+
+ return response.json().then((resp) => {
+ commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
+ commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
+ });
+ });
+};
+
+export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath)
+ .then(res => res.json());
+
+export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath)
+ .then(res => res.json());
+
+export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
+export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
new file mode 100644
index 00000000000..588f479c492
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -0,0 +1,2 @@
+export const isLoading = state => state.isLoading;
+export const repos = state => state.repos;
diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js
new file mode 100644
index 00000000000..78b67881210
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/index.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: {
+ isLoading: false,
+ endpoint: '', // initial endpoint to fetch the repos list
+ /**
+ * Each object in `repos` has the following strucure:
+ * {
+ * name: String,
+ * isLoading: Boolean,
+ * tagsPath: String // endpoint to request the list
+ * destroyPath: String // endpoit to delete the repo
+ * list: Array // List of the registry images
+ * }
+ *
+ * Each registry image inside `list` has the following structure:
+ * {
+ * tag: String,
+ * revision: String
+ * shortRevision: String
+ * size: Number
+ * layers: Number
+ * createdAt: String
+ * destroyPath: String // endpoit to delete each image
+ * }
+ */
+ repos: [],
+ },
+ actions,
+ getters,
+ mutations,
+});
diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js
new file mode 100644
index 00000000000..2c69bf11807
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/mutation_types.js
@@ -0,0 +1,7 @@
+export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT';
+
+export const SET_REPOS_LIST = 'SET_REPOS_LIST';
+export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING';
+
+export const SET_REGISTRY_LIST = 'SET_REGISTRY_LIST';
+export const TOGGLE_REGISTRY_LIST_LOADING = 'TOGGLE_REGISTRY_LIST_LOADING';
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
new file mode 100644
index 00000000000..e40382e7afc
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -0,0 +1,54 @@
+import * as types from './mutation_types';
+import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
+
+export default {
+
+ [types.SET_MAIN_ENDPOINT](state, endpoint) {
+ Object.assign(state, { endpoint });
+ },
+
+ [types.SET_REPOS_LIST](state, list) {
+ Object.assign(state, {
+ repos: list.map(el => ({
+ canDelete: !!el.destroy_path,
+ destroyPath: el.destroy_path,
+ id: el.id,
+ isLoading: false,
+ list: [],
+ location: el.location,
+ name: el.path,
+ tagsPath: el.tags_path,
+ })),
+ });
+ },
+
+ [types.TOGGLE_MAIN_LOADING](state) {
+ Object.assign(state, { isLoading: !state.isLoading });
+ },
+
+ [types.SET_REGISTRY_LIST](state, { repo, resp, headers }) {
+ const listToUpdate = state.repos.find(el => el.id === repo.id);
+
+ const normalizedHeaders = normalizeHeaders(headers);
+ const pagination = parseIntPagination(normalizedHeaders);
+
+ listToUpdate.pagination = pagination;
+
+ listToUpdate.list = resp.map(element => ({
+ tag: element.name,
+ revision: element.revision,
+ shortRevision: element.short_revision,
+ size: element.size,
+ layers: element.layers,
+ location: element.location,
+ createdAt: element.created_at,
+ destroyPath: element.destroy_path,
+ canDelete: !!element.destroy_path,
+ }));
+ },
+
+ [types.TOGGLE_REGISTRY_LIST_LOADING](state, list) {
+ const listToUpdate = state.repos.find(el => el.id === list.id);
+ listToUpdate.isLoading = !listToUpdate.isLoading;
+ },
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
index aaf9d3304a4..09561694939 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="loading" showDisabledButton />
+ <status-icon status="loading" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Checking ability to merge automatically
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
index dc252f8a9b7..5d468a085cb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
@@ -12,7 +12,7 @@ export default {
<div class="mr-widget-body media">
<status-icon
status="failed"
- showDisabledButton />
+ :show-disabled-button="true" />
<div class="media-body space-children">
<span
v-if="mr.shouldBeRebased"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
index 1cb24549d53..c25d6c359bb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
@@ -51,7 +51,7 @@ export default {
</span>
</template>
<template v-else>
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
index 9f0a359d01a..1bc0b7e0819 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -24,7 +24,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold js-branch-text">
<span class="capitalize">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
index 797511d4e3a..00047718201 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="success" showDisabledButton />
+ <status-icon status="success" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Ready to be merged automatically.
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
index 167a0d4613a..1cedf86e811 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
index c5be9a0530a..6853ba4b9f8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index f83d3ca00dd..edc1d191bf2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -38,24 +38,40 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
- mergeButtonClass() {
- const defaultClass = 'btn btn-sm btn-success accept-merge-request';
- const failedClass = `${defaultClass} btn-danger`;
- const inActionClass = `${defaultClass} btn-info`;
+ status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) {
- return failedClass;
+ return 'failed';
} else if (!pipeline) {
- return defaultClass;
+ return 'success';
} else if (isPipelineActive) {
- return inActionClass;
+ return 'pending';
} else if (isPipelineFailed) {
+ return 'failed';
+ }
+
+ return 'success';
+ },
+ mergeButtonClass() {
+ const defaultClass = 'btn btn-sm btn-success accept-merge-request';
+ const failedClass = `${defaultClass} btn-danger`;
+ const inActionClass = `${defaultClass} btn-info`;
+
+ if (this.status === 'failed') {
return failedClass;
+ } else if (this.status === 'pending') {
+ return inActionClass;
}
return defaultClass;
},
+ iconClass() {
+ if (this.status === 'failed' || !this.commitMessage.length || !this.isMergeAllowed() || this.mr.preventMerge) {
+ return 'failed';
+ }
+ return 'success';
+ },
mergeButtonText() {
if (this.isMergingImmediately) {
return 'Merge in progress';
@@ -156,6 +172,7 @@ export default {
eventHub.$emit('FetchActionsContent');
if (window.mergeRequest) {
window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged');
+ window.mergeRequest.hideCloseButton();
window.mergeRequest.decreaseCounter();
}
stopPolling();
@@ -208,7 +225,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="success" />
+ <status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
index 89f38e5bd2a..af19cf6ab87 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
index d762ca6e640..a119ecbbdfe 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
@@ -10,7 +10,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
There are unresolved discussions. Please resolve these discussions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index b11a06899cf..54be1fbe675 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -38,7 +38,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" />
+ <status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
new file mode 100644
index 00000000000..3a7143c450e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -0,0 +1,32 @@
+<script>
+ /**
+ * Falls back to the code used in `copy_to_clipboard.js`
+ */
+
+ export default {
+ name: 'clipboardButton',
+ props: {
+ text: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <button
+ type="button"
+ class="btn btn-transparent btn-clipboard"
+ :data-title="title"
+ :data-clipboard-text="text">
+ <i
+ aria-hidden="true"
+ class="fa fa-clipboard">
+ </i>
+ </button>
+</template>
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index bdcbd4021b3..f1aedc227f3 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -23,6 +23,7 @@
&.s60 { @include avatar-size(60px, 12px); }
&.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); }
+ &.s100 { @include avatar-size(100px, 15px); }
&.s110 { @include avatar-size(110px, 15px); }
&.s140 { @include avatar-size(140px, 15px); }
&.s160 { @include avatar-size(160px, 20px); }
@@ -78,6 +79,7 @@
&.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
+ &.s100 { font-size: 36px; line-height: 98px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
&.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; }
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 706a9cffe87..96f9dda26c4 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -11,6 +11,7 @@
.prepend-top-10 { margin-top: 10px; }
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px; }
+.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
@@ -129,11 +130,6 @@ span.update-author {
}
}
-.user-mention {
- color: $user-mention-color;
- font-weight: $gl-font-weight-bold;
-}
-
.field_with_errors {
display: inline;
}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index dbdd5a4464b..34a35734acc 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -6,3 +6,14 @@
.gfm-commit_range {
@extend .commit-sha;
}
+
+.gfm-project_member {
+ padding: 0 2px;
+ border-radius: #{$border-radius-default / 2};
+ background-color: $user-mention-bg;
+
+ &:hover {
+ background-color: $user-mention-bg-hover;
+ text-decoration: none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 6c14e8b97e0..50f1445bc2e 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -48,31 +48,24 @@
}
&:hover {
- background-color: $white-normal;
- border-color: $border-white-normal;
+ border-color: $gray-darkest;
color: $gl-text-color;
}
}
}
-.select2-drop {
- box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0;
- border-radius: $border-radius-default;
- border: none;
+.select2-drop,
+.select2-drop.select2-drop-above {
+ box-shadow: 0 2px 4px $dropdown-shadow-color;
+ border-radius: $border-radius-base;
+ border: 1px solid $dropdown-border-color;
min-width: 175px;
+ color: $gl-text-color;
}
-.select2-results .select2-result-label,
-.select2-more-results {
- padding: 10px 15px;
-}
-
-.select2-drop {
- color: $gl-grayish-blue;
-}
-
-.select2-highlighted {
- background: $gl-link-color !important;
+.select2-drop.select2-drop-above.select2-drop-active {
+ border-top: 1px solid $dropdown-border-color;
+ margin-top: -6px;
}
.select2-results li.select2-result-with-children > .select2-result-label {
@@ -87,13 +80,11 @@
}
}
-.select2-dropdown-open {
+.select2-dropdown-open,
+.select2-dropdown-open.select2-drop-above {
.select2-choice {
- border-color: $border-white-normal;
+ border-color: $gray-darkest;
outline: 0;
- background-image: none;
- background-color: $white-dark;
- box-shadow: $gl-btn-active-gradient;
}
}
@@ -131,28 +122,14 @@
}
}
}
-
- &.select2-container-active .select2-choices,
- &.select2-dropdown-open .select2-choices {
- border-color: $border-white-normal;
- box-shadow: $gl-btn-active-gradient;
- }
}
.select2-drop-active {
- margin-top: 6px;
+ margin-top: $dropdown-vertical-offset;
font-size: 14px;
- &.select2-drop-above {
- margin-bottom: 8px;
- }
-
.select2-results {
max-height: 350px;
-
- .select2-highlighted {
- background: $gl-primary;
- }
}
}
@@ -186,19 +163,35 @@
background-size: 16px 16px !important;
}
-.select2-results .select2-no-results,
-.select2-results .select2-searching,
-.select2-results .select2-ajax-error,
-.select2-results .select2-selection-limit {
- background: $gray-light;
- display: list-item;
- padding: 10px 15px;
-}
-
-
.select2-results {
margin: 0;
- padding: 10px 0;
+ padding: #{$gl-padding / 2} 0;
+
+ .select2-no-results,
+ .select2-searching,
+ .select2-ajax-error,
+ .select2-selection-limit {
+ background: transparent;
+ padding: #{$gl-padding / 2} $gl-padding;
+ }
+
+ .select2-result-label,
+ .select2-more-results {
+ padding: #{$gl-padding / 2} $gl-padding;
+ }
+
+ .select2-highlighted {
+ background: transparent;
+ color: $gl-text-color;
+
+ .select2-result-label {
+ background: $dropdown-item-hover-bg;
+ }
+ }
+
+ .select2-result {
+ padding: 0 1px;
+ }
}
.ajax-users-select {
@@ -265,56 +258,10 @@
min-width: 250px !important;
}
-// TODO: change global style
-.ajax-project-dropdown,
-.ajax-users-dropdown,
-body[data-page="projects:edit"] #select2-drop,
-body[data-page="projects:new"] #select2-drop,
-body[data-page="projects:merge_requests:edit"] #select2-drop,
-body[data-page="projects:blob:new"] #select2-drop,
-body[data-page="profiles:show"] #select2-drop,
-body[data-page="admin:groups:show"] #select2-drop,
-body[data-page="projects:issues:show"] #select2-drop,
-body[data-page="projects:blob:edit"] #select2-drop {
- &.select2-drop {
- border: 1px solid $dropdown-border-color;
- border-radius: $border-radius-base;
- color: $gl-text-color;
- }
-
- &.select2-drop-above {
- border-top: none;
- margin-top: -4px;
- }
-
- .select2-results {
- .select2-no-results,
- .select2-searching,
- .select2-ajax-error,
- .select2-selection-limit {
- background: transparent;
- }
-
- .select2-result {
- padding: 0 1px;
-
- .select2-match {
- font-weight: $gl-font-weight-bold;
- text-decoration: none;
- }
-
- .select2-result-label {
- padding: #{$gl-padding / 2} $gl-padding;
- }
-
- &.select2-highlighted {
- background-color: transparent !important;
- color: $gl-text-color;
-
- .select2-result-label {
- background-color: $dropdown-item-hover-bg;
- }
- }
- }
+.select2-result-selectable,
+.select2-result-unselectable {
+ .select2-match {
+ font-weight: $gl-font-weight-bold;
+ text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 9bbda87dec9..60260355765 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -262,7 +262,8 @@ $well-pre-bg: #eee;
$well-pre-color: #555;
$loading-color: #555;
$update-author-color: #999;
-$user-mention-color: #2fa0bb;
+$user-mention-bg: rgba($blue-500, 0.044);
+$user-mention-bg-hover: rgba($blue-500, 0.15);
$time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss
index 3266714396e..dfff3e15556 100644
--- a/app/assets/stylesheets/pages/container_registry.scss
+++ b/app/assets/stylesheets/pages/container_registry.scss
@@ -9,6 +9,14 @@
.container-image-head {
padding: 0 16px;
line-height: 4em;
+
+ .btn-link {
+ padding: 0;
+
+ &:focus {
+ outline: none;
+ }
+ }
}
.table.tags {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 1f7b6703909..a086c11324d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -499,73 +499,56 @@ a.deploy-project-label {
}
}
-.fork-namespaces {
- .row {
- -webkit-flex-wrap: wrap;
- display: -webkit-flex;
- display: flex;
- flex-wrap: wrap;
- justify-content: flex-start;
+.fork-thumbnail {
+ height: 200px;
+ width: calc((100% / 2) - #{$gl-padding * 2});
- .fork-thumbnail {
- border-radius: $border-radius-base;
- background-color: $white-light;
- border: 1px solid $border-white-light;
- height: 202px;
- margin: $gl-padding;
- text-align: center;
- width: 169px;
+ @media (min-width: $screen-md-min) {
+ width: calc((100% / 4) - #{$gl-padding * 2});
+ }
- &:hover:not(.disabled),
- &.forked {
- background-color: $row-hover;
- border-color: $row-hover-border;
- }
+ @media (min-width: $screen-lg-min) {
+ width: calc((100% / 5) - #{$gl-padding * 2});
+ }
- .no-avatar {
- width: 100px;
- height: 100px;
- background-color: $gray-light;
- border: 1px solid $white-normal;
- margin: 0 auto;
- border-radius: 50%;
-
- i {
- font-size: 100px;
- color: $white-normal;
- }
- }
+ &:hover:not(.disabled),
+ &.forked {
+ background-color: $row-hover;
+ border-color: $row-hover-border;
+ }
- a {
- display: block;
- width: 100%;
- height: 100%;
- padding-top: $gl-padding;
- color: $gl-text-color;
-
- &.disabled {
- opacity: .3;
- cursor: not-allowed;
-
- &:hover {
- text-decoration: none;
- }
- }
-
- .caption {
- min-height: 30px;
- padding: $gl-padding 0;
- }
- }
+ .avatar-container,
+ .identicon {
+ float: none;
+ margin-left: auto;
+ margin-right: auto;
+ }
- img {
- border-radius: 50%;
- max-width: 100px;
- }
+ a {
+ display: block;
+ width: 100%;
+ height: 100%;
+ padding-top: $gl-padding;
+ text-decoration: none;
+
+ &.disabled {
+ opacity: .3;
+ cursor: not-allowed;
}
}
}
+.fork-thumbnail-container {
+ display: flex;
+ flex-wrap: wrap;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+
+ > h5 {
+ width: 100%;
+ }
+}
+
.project-template,
.project-import {
.form-group {
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
index fe22d186af1..a355e2dee24 100644
--- a/app/assets/stylesheets/pages/settings_ci_cd.scss
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -12,3 +12,7 @@
margin-left: 10px;
}
}
+
+.registry-placeholder {
+ min-height: 60px;
+}
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index c1cc509a748..4146deefa89 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -1,6 +1,7 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def index
set_index_vars
+ @personal_access_token = finder.build
end
def create
@@ -40,7 +41,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def set_index_vars
@scopes = Gitlab::Auth.available_scopes
- @personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index a9cce578366..7f03ce07dec 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort].presence || sort_value_recently_updated
- @branches = BranchesFinder.new(@repository, params).execute
+ @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
respond_to do |format|
diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb
index 71e7dc70a4d..32c0fc6d14a 100644
--- a/app/controllers/projects/registry/repositories_controller.rb
+++ b/app/controllers/projects/registry/repositories_controller.rb
@@ -6,17 +6,26 @@ module Projects
def index
@images = project.container_repositories
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: ContainerRepositoriesSerializer
+ .new(project: project, current_user: current_user)
+ .represent(@images)
+ end
+ end
end
def destroy
if image.destroy
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- notice: 'Image repository has been removed successfully!'
+ respond_to do |format|
+ format.json { head :no_content }
+ end
else
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- alert: 'Failed to remove image repository!'
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
end
end
diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb
index ae72bd03cfb..e602aa3f393 100644
--- a/app/controllers/projects/registry/tags_controller.rb
+++ b/app/controllers/projects/registry/tags_controller.rb
@@ -3,20 +3,35 @@ module Projects
class TagsController < ::Projects::Registry::ApplicationController
before_action :authorize_update_container_image!, only: [:destroy]
+ def index
+ respond_to do |format|
+ format.json do
+ render json: ContainerTagsSerializer
+ .new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
+ .represent(tags)
+ end
+ end
+ end
+
def destroy
if tag.delete
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- notice: 'Registry tag has been removed successfully!'
+ respond_to do |format|
+ format.json { head :no_content }
+ end
else
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- alert: 'Failed to remove registry tag!'
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
end
end
private
+ def tags
+ Kaminari::PaginatableArray.new(image.tags, limit: 15)
+ end
+
def image
@image ||= project.container_repositories
.find(params[:repository_id])
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index b331693c789..fd88e0d794a 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -1,13 +1,15 @@
module EventsHelper
ICON_NAMES_BY_EVENT_TYPE = {
- 'pushed to' => 'icon_commit',
- 'pushed new' => 'icon_commit',
- 'created' => 'icon_status_open',
- 'opened' => 'icon_status_open',
- 'closed' => 'icon_status_closed',
- 'accepted' => 'icon_code_fork',
- 'commented on' => 'icon_comment_o',
- 'deleted' => 'icon_trash_o'
+ 'pushed to' => 'commit',
+ 'pushed new' => 'commit',
+ 'created' => 'status_open',
+ 'opened' => 'status_open',
+ 'closed' => 'status_closed',
+ 'accepted' => 'fork',
+ 'commented on' => 'comment',
+ 'deleted' => 'remove',
+ 'imported' => 'import',
+ 'joined' => 'users'
}.freeze
def link_to_author(event, self_added: false)
@@ -197,7 +199,7 @@ module EventsHelper
def icon_for_event(note)
icon_name = ICON_NAMES_BY_EVENT_TYPE[note]
- custom_icon(icon_name) if icon_name
+ sprite_icon(icon_name) if icon_name
end
def icon_for_profile_event(event)
diff --git a/app/models/key.rb b/app/models/key.rb
index 0c41e34d969..f119b15c737 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -34,6 +34,7 @@ class Key < ActiveRecord::Base
value&.delete!("\n\r")
value.strip! unless value.blank?
write_attribute(:key, value)
+ @public_key = nil
end
def publishable_key
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e85b83daf9e..0ba00d447e8 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -560,14 +560,20 @@ class MergeRequest < ActiveRecord::Base
commits_for_notes_limit = 100
commit_ids = commit_shas.take(commits_for_notes_limit)
- Note.where(
- "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
- "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
- mr_id: id,
- commit_ids: commit_ids,
- target_project_id: target_project_id,
- source_project_id: source_project_id
- )
+ commit_notes = Note
+ .except(:order)
+ .where(project_id: [source_project_id, target_project_id])
+ .where(noteable_type: 'Commit', commit_id: commit_ids)
+
+ # We're using a UNION ALL here since this results in better performance
+ # compared to using OR statements. We're using UNION ALL since the queries
+ # used won't produce any duplicates (e.g. a note for a commit can't also be
+ # a note for an MR).
+ union = Gitlab::SQL::Union
+ .new([notes, commit_notes], remove_duplicates: false)
+ .to_sql
+
+ Note.from("(#{union}) #{Note.table_name}")
end
alias_method :discussion_notes, :related_notes
@@ -742,10 +748,9 @@ class MergeRequest < ActiveRecord::Base
end
def has_ci?
- has_ci_integration = source_project.try(:ci_service)
- uses_gitlab_ci = all_pipelines.any?
+ return false if has_no_commits?
- (has_ci_integration || uses_gitlab_ci) && commits.any?
+ !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
end
def branch_missing?
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 1f9d712ef84..cfcb03138b7 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -17,6 +17,8 @@ class PersonalAccessToken < ActiveRecord::Base
validates :scopes, presence: true
validate :validate_scopes
+ after_initialize :set_default_scopes, if: :persisted?
+
def revoke!
update!(revoked: true)
end
@@ -32,4 +34,8 @@ class PersonalAccessToken < ActiveRecord::Base
errors.add :scopes, "can only contain available scopes"
end
end
+
+ def set_default_scopes
+ self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index d47dc9a05cd..d725c65081d 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -989,7 +989,7 @@ class Repository
end
def create_ref(ref, ref_path)
- fetch_ref(path_to_repo, ref, ref_path)
+ raw_repository.write_ref(ref_path, ref)
end
def ls_files(ref)
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 2df84e58575..a25882cbb62 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -31,7 +31,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def remove_wip_path
- if can?(current_user, :update_merge_request, merge_request.project)
+ if work_in_progress? && can?(current_user, :update_merge_request, merge_request.project)
remove_wip_project_merge_request_path(project, merge_request)
end
end
diff --git a/app/serializers/container_repositories_serializer.rb b/app/serializers/container_repositories_serializer.rb
new file mode 100644
index 00000000000..56dc70b5687
--- /dev/null
+++ b/app/serializers/container_repositories_serializer.rb
@@ -0,0 +1,3 @@
+class ContainerRepositoriesSerializer < BaseSerializer
+ entity ContainerRepositoryEntity
+end
diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb
new file mode 100644
index 00000000000..1103cf30a07
--- /dev/null
+++ b/app/serializers/container_repository_entity.rb
@@ -0,0 +1,25 @@
+class ContainerRepositoryEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id, :path, :location
+
+ expose :tags_path do |repository|
+ project_registry_repository_tags_path(project, repository, format: :json)
+ end
+
+ expose :destroy_path, if: -> (*) { can_destroy? } do |repository|
+ project_container_registry_path(project, repository, format: :json)
+ end
+
+ private
+
+ alias_method :repository, :object
+
+ def project
+ request.project
+ end
+
+ def can_destroy?
+ can?(request.current_user, :update_container_image, project)
+ end
+end
diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb
new file mode 100644
index 00000000000..ec1fc349586
--- /dev/null
+++ b/app/serializers/container_tag_entity.rb
@@ -0,0 +1,23 @@
+class ContainerTagEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name, :location, :revision, :total_size, :created_at
+
+ expose :destroy_path, if: -> (*) { can_destroy? } do |tag|
+ project_registry_repository_tag_path(project, tag.repository, tag.name, format: :json)
+ end
+
+ private
+
+ alias_method :tag, :object
+
+ def project
+ request.project
+ end
+
+ def can_destroy?
+ # TODO: We check permission against @project, not tag,
+ # as tag is no AR object that is attached to project
+ can?(request.current_user, :update_container_image, project)
+ end
+end
diff --git a/app/serializers/container_tags_serializer.rb b/app/serializers/container_tags_serializer.rb
new file mode 100644
index 00000000000..6ff3adff135
--- /dev/null
+++ b/app/serializers/container_tags_serializer.rb
@@ -0,0 +1,17 @@
+class ContainerTagsSerializer < BaseSerializer
+ entity ContainerTagEntity
+
+ def with_pagination(request, response)
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def paginated?
+ @paginator.present?
+ end
+
+ def represent(resource, opts = {})
+ resource = @paginator.paginate(resource) if paginated?
+
+ super(resource, opts)
+ end
+end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 7d3c752b8e6..36537c5bd02 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -23,7 +23,6 @@ class MergeRequestEntity < IssuableEntity
expose :closed_event, using: EventEntity
# User entities
- expose :author, using: UserEntity
expose :merge_user, using: UserEntity
# Diff sha's
@@ -31,7 +30,6 @@ class MergeRequestEntity < IssuableEntity
merge_request.diff_head_sha if merge_request.diff_head_commit
end
- expose :merge_commit_sha
expose :merge_commit_message
expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index e3a9e99250e..0d5350f873b 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -37,9 +37,9 @@
- if content_for?(:library_javascripts)
= yield :library_javascripts
+ = javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js")
= webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common"
- = webpack_bundle_tag "locale"
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 906774a21e3..e9613534dde 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -9,50 +9,36 @@
%br
Forking a repository allows you to make changes without affecting the original project.
.col-lg-9
- .fork-namespaces
- - if @namespaces.present?
- %label.label-light
- %span
- Click to fork the project
- - @namespaces.in_groups_of(6, false) do |group|
- .row
- - group.each do |namespace|
- - avatar = namespace_icon(namespace, 100)
- - if fork = namespace.find_fork_of(@project)
- .fork-thumbnail.forked
- = link_to project_path(fork) do
- - if /no_((\w*)_)*avatar/.match(avatar)
- .no-avatar
- = icon 'question'
- - else
- = image_tag avatar
- .caption
- = namespace.human_name
- - else
- - can_create_project = current_user.can?(:create_projects, namespace)
- .fork-thumbnail{ class: ("disabled" unless can_create_project) }
- = link_to project_forks_path(@project, namespace_key: namespace.id),
- method: "POST",
- class: ("disabled has-tooltip" unless can_create_project),
- title: (_('You have reached your project limit') unless can_create_project) do
- - if /no_((\w*)_)*avatar/.match(avatar)
- .no-avatar
- = icon 'question'
- - else
- = image_tag avatar
- .caption
- = namespace.human_name
- - else
- %label.label-light
- %span
- No available namespaces to fork the project.
- %br
- %small
- You must have permission to create a project in a namespace before forking.
+ - if @namespaces.present?
+ .fork-thumbnail-container.js-fork-content
+ %h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
+ Click to fork the project
+ - @namespaces.each do |namespace|
+ - avatar = namespace_icon(namespace, 100)
+ - can_create_project = current_user.can?(:create_projects, namespace)
+ - forked_project = namespace.find_fork_of(@project)
+ - fork_path = forked_project ? project_path(forked_project) : project_forks_path(@project, namespace_key: namespace.id)
+ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: [("disabled" unless can_create_project), ("forked" if forked_project)] }
+ = link_to fork_path,
+ method: "POST",
+ class: [("js-fork-thumbnail" unless forked_project), ("disabled has-tooltip" unless can_create_project)],
+ title: (_('You have reached your project limit') unless can_create_project) do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ = project_identicon(namespace, class: "avatar s100 identicon")
+ - else
+ .avatar-container.s100
+ = image_tag(avatar, class: "avatar s100")
+ %h5.prepend-top-default
+ = namespace.human_name
+ - else
+ %strong
+ No available namespaces to fork the project.
+ %p.prepend-top-default
+ You must have permission to create a project in a namespace before forking.
- .save-project-loader.hide
- .center
- %h2
- %i.fa.fa-spinner.fa-spin
- Forking repository
- %p Please wait a moment, this page will automatically refresh when ready.
+ .save-project-loader.hide.js-fork-content
+ %h2.text-center
+ = icon('spinner spin')
+ Forking repository
+ %p.text-center
+ Please wait a moment, this page will automatically refresh when ready.
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 6a567487514..5f97d31f610 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -2,13 +2,13 @@
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list.related-merge-requests
- - has_any_ci = @merge_requests.any?(&:head_pipeline)
+ - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- @merge_requests.each do |merge_request|
%li
%span.merge-request-ci-status
- if merge_request.head_pipeline
= render_pipeline_status(merge_request.head_pipeline)
- - elsif has_any_ci
+ - elsif has_any_head_pipeline
= icon('blank fw')
%span.merge-request-id
= merge_request.to_reference
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index f3c44c94a5c..9ff85c2ee4c 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -29,7 +29,7 @@
- unless current_user == @merge_request.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- if can_update_merge_request
- %li{ class: merge_request_button_visibility(@merge_request, true) }
+ %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml
deleted file mode 100644
index a0535edafc3..00000000000
--- a/app/views/projects/registry/repositories/_image.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-.container-image.js-toggle-container
- .container-image-head
- = link_to "#", class: "js-toggle-button" do
- = icon('chevron-down', 'aria-hidden': 'true')
- = escape_once(image.path)
-
- = clipboard_button(clipboard_text: "docker pull #{image.location}")
-
- - if can?(current_user, :update_container_image, @project)
- .controls.hidden-xs.pull-right
- = link_to project_container_registry_path(@project, image),
- class: 'btn btn-remove has-tooltip',
- title: 'Remove repository',
- data: { confirm: 'Are you sure?' },
- method: :delete do
- = icon('trash cred', 'aria-hidden': 'true')
-
- .container-image-tags.js-toggle-content.hide
- - if image.has_tags?
- .table-holder
- %table.table.tags
- %thead
- %tr
- %th Tag
- %th Tag ID
- %th Size
- %th Created
- - if can?(current_user, :update_container_image, @project)
- %th
- = render partial: 'tag', collection: image.tags
- - else
- .nothing-here-block No tags in Container Registry for this container image.
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 5661af01302..36ea5e013e4 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,60 +1,49 @@
- page_title "Container Registry"
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- %h4.prepend-top-0
+%section
+ .settings-header
+ %h4
= page_title
%p
- With the Docker Container Registry integrated into GitLab, every project
- can have its own space to store its Docker images.
+ = s_('ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.')
%p.append-bottom-0
= succeed '.' do
- Learn more about
- = link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank'
+ = s_('ContainerRegistry|Learn more about')
+ = link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank'
+ .row.registry-placeholder.prepend-bottom-10
+ .col-lg-12
+ #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
- .col-lg-9
- .panel.panel-default
- .panel-heading
- %h4.panel-title
- How to use the Container Registry
- .panel-body
- %p
- First log in to GitLab&rsquo;s Container Registry using your GitLab username
- and password. If you have
- = link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank'
- you need to use a
- = succeed ':' do
- = link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank'
- %pre
- docker login #{Gitlab.config.registry.host_port}
- %br
- %p
- Once you log in, you&rsquo;re free to create and upload a container image
- using the common
- %code build
- and
- %code push
- commands:
- %pre
- :plain
- docker build -t #{escape_once(@project.container_registry_url)} .
- docker push #{escape_once(@project.container_registry_url)}
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('registry_list')
- %hr
- %h5.prepend-top-default
- Use different image names
- %p.light
- GitLab supports up to 3 levels of image names. The following
- examples of images are valid for your project:
- %pre
- :plain
- #{escape_once(@project.container_registry_url)}:tag
- #{escape_once(@project.container_registry_url)}/optional-image-name:tag
- #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
-
- - if @images.blank?
- %p.settings-message.text-center.append-bottom-default
- No container images stored for this project. Add one by following the
- instructions above.
- - else
- = render partial: 'image', collection: @images
+ .row.prepend-top-10
+ .col-lg-12
+ .panel.panel-default
+ .panel-heading
+ %h4.panel-title
+ = s_('ContainerRegistry|How to use the Container Registry')
+ .panel-body
+ %p
+ - link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank')
+ - link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank')
+ = s_('ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:').html_safe % { link_2fa: link_2fa, link_token: link_token }
+ %pre
+ docker login #{Gitlab.config.registry.host_port}
+ %br
+ %p
+ = s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
+ %pre
+ :plain
+ docker build -t #{escape_once(@project.container_registry_url)} .
+ docker push #{escape_once(@project.container_registry_url)}
+ %hr
+ %h5.prepend-top-default
+ = s_('ContainerRegistry|Use different image names')
+ %p.light
+ = s_('ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:')
+ %pre
+ :plain
+ #{escape_once(@project.container_registry_url)}:tag
+ #{escape_once(@project.container_registry_url)}/optional-image-name:tag
+ #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index b842fd57cf3..c0b1c62e8ef 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -23,7 +23,7 @@
- disabled_class = 'disabled'
- disabled_title = @service.disabled_title
- = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel'
+ = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel'
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
%hr
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 468ab922542..1927216e191 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -2,12 +2,11 @@
- release = @releases.find { |release| release.tag == tag.name }
%li.flex-row
.row-main-content.str-truncated
- = link_to project_tag_path(@project, tag.name), class: 'item-title ref-name' do
- = icon('tag')
- = tag.name
+ = icon('tag')
+ = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name prepend-left-4'
- if protected_tag?(@project, tag)
- %span.label.label-success
+ %span.label.label-success.prepend-left-4
protected
- if tag.message.present?
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index e415ec64c38..b8b1f4ca42f 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -1,9 +1,9 @@
- type = impersonation ? "impersonation" : "personal access"
%h5.prepend-top-0
- Add a #{type} Token
+ Add a #{type} token
%p.profile-settings-content
- Pick a name for the application, and we'll give you a unique #{type} Token.
+ Pick a name for the application, and we'll give you a unique #{type} token.
= form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f|