Merge branch 'master' into feature/sm/35954-create-kubernetes-cluster-on-gke-from-k8s-service
|
@ -5,3 +5,4 @@ app/policies/project_policy.rb
|
|||
app/models/concerns/relative_positioning.rb
|
||||
app/workers/stuck_merge_jobs_worker.rb
|
||||
lib/gitlab/redis/*.rb
|
||||
lib/gitlab/gitaly_client/operation_service.rb
|
||||
|
|
|
@ -195,6 +195,10 @@ entry.
|
|||
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
|
||||
- [BUGIFX] Improves subgroup creation permissions. !13418
|
||||
|
||||
## 9.5.7 (2017-10-03)
|
||||
|
||||
- Fix gitlab rake:import:repos task.
|
||||
|
||||
## 9.5.6 (2017-09-29)
|
||||
|
||||
- [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242
|
||||
|
|
|
@ -910,7 +910,7 @@ GEM
|
|||
json (>= 1.8.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.2)
|
||||
unf_ext (0.0.7.4)
|
||||
unicode-display_width (1.3.0)
|
||||
unicorn (5.1.0)
|
||||
kgio (~> 2.6)
|
||||
|
|
|
@ -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
|
||||
**/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()">
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
|
62
app/assets/javascripts/registry/components/app.vue
Normal file
|
@ -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>
|
|
@ -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>
|
137
app/assets/javascripts/registry/components/table_registry.vue
Normal file
|
@ -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">
|
||||
·
|
||||
</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>
|
15
app/assets/javascripts/registry/constants.js
Normal file
|
@ -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.'),
|
||||
};
|
25
app/assets/javascripts/registry/index.js
Normal file
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
39
app/assets/javascripts/registry/stores/actions.js
Normal file
|
@ -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);
|
2
app/assets/javascripts/registry/stores/getters.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const isLoading = state => state.isLoading;
|
||||
export const repos = state => state.repos;
|
39
app/assets/javascripts/registry/stores/index.js
Normal file
|
@ -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,
|
||||
});
|
7
app/assets/javascripts/registry/stores/mutation_types.js
Normal file
|
@ -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';
|
54
app/assets/javascripts/registry/stores/mutations.js
Normal file
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -38,24 +38,40 @@ export default {
|
|||
|
||||
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
|
||||
},
|
||||
status() {
|
||||
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
|
||||
|
||||
if (hasCI && !ciStatus) {
|
||||
return 'failed';
|
||||
} else if (!pipeline) {
|
||||
return 'success';
|
||||
} else if (isPipelineActive) {
|
||||
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`;
|
||||
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
|
||||
|
||||
if (hasCI && !ciStatus) {
|
||||
if (this.status === 'failed') {
|
||||
return failedClass;
|
||||
} else if (!pipeline) {
|
||||
return defaultClass;
|
||||
} else if (isPipelineActive) {
|
||||
} else if (this.status === 'pending') {
|
||||
return inActionClass;
|
||||
} else if (isPipelineFailed) {
|
||||
return failedClass;
|
||||
}
|
||||
|
||||
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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,6 +9,14 @@
|
|||
.container-image-head {
|
||||
padding: 0 16px;
|
||||
line-height: 4em;
|
||||
|
||||
.btn-link {
|
||||
padding: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table.tags {
|
||||
|
|
|
@ -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%;
|
||||
&:hover:not(.disabled),
|
||||
&.forked {
|
||||
background-color: $row-hover;
|
||||
border-color: $row-hover-border;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 100px;
|
||||
color: $white-normal;
|
||||
}
|
||||
}
|
||||
.avatar-container,
|
||||
.identicon {
|
||||
float: none;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: $gl-padding;
|
||||
color: $gl-text-color;
|
||||
a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: $gl-padding;
|
||||
text-decoration: none;
|
||||
|
||||
&.disabled {
|
||||
opacity: .3;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.caption {
|
||||
min-height: 30px;
|
||||
padding: $gl-padding 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
max-width: 100px;
|
||||
}
|
||||
&.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 {
|
||||
|
|
|
@ -12,3 +12,7 @@
|
|||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.registry-placeholder {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
3
app/serializers/container_repositories_serializer.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class ContainerRepositoriesSerializer < BaseSerializer
|
||||
entity ContainerRepositoryEntity
|
||||
end
|
25
app/serializers/container_repository_entity.rb
Normal file
|
@ -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
|
23
app/serializers/container_tag_entity.rb
Normal file
|
@ -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
|
17
app/serializers/container_tags_serializer.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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.
|
|
@ -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’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’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’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’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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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|
|
||||
|
||||
|
|
5
changelogs/unreleased/14553-missing-space-in-log-msg.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: "Add missing space in Sidekiq memory killer log message"
|
||||
merge_request: 14553
|
||||
author: Benjamin Drung
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix the default branches sorting to actually be 'Last updated'
|
||||
merge_request: 14295
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Re-arrange <script> tags before <template> tags in .vue files
|
||||
merge_request: 14671
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Hide close MR button after merge without reloading page
|
||||
merge_request: 14122
|
||||
author: Jacopo Beschi @jacopo-beschi
|
||||
type: added
|
5
changelogs/unreleased/37229-mr-widget-status-icon.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: fix merge request widget status icon for failed CI
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use explicit boolean true attribute for show-disabled-button in Vue files
|
||||
merge_request: 14672
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/dm-pat-revoke.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Set default scope on PATs that don't have one set to allow them to be revoked
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/docs-openid-connect.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add link to OpenID Connect documentation
|
||||
merge_request: 14368
|
||||
author: Markus Koller
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix edit project service cancel button position
|
||||
merge_request: 14596
|
||||
author: Matt Coleman
|
||||
type: fixed
|
5
changelogs/unreleased/mentions-in-comments.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Makes @mentions links have a different styling for better separation
|
||||
merge_request:
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use a UNION ALL for getting merge request notes
|
||||
merge_request:
|
||||
author:
|
||||
type: other
|
5
changelogs/unreleased/tag-link-size.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adjusts tag link to avoid underlining spaces
|
||||
merge_request: 14544
|
||||
author: Guilherme Vieira
|
||||
type: fixed
|
|
@ -105,6 +105,7 @@ module Gitlab
|
|||
config.assets.precompile << "lib/ace.js"
|
||||
config.assets.precompile << "vendor/assets/fonts/*"
|
||||
config.assets.precompile << "test.css"
|
||||
config.assets.precompile << "locale/**/app.js"
|
||||
|
||||
# Version of your assets, change this if you want to expire all your assets
|
||||
config.assets.version = '1.0'
|
||||
|
|
|
@ -499,6 +499,8 @@ production: &base
|
|||
|
||||
# Gitaly settings
|
||||
gitaly:
|
||||
# Path to the directory containing Gitaly client executables.
|
||||
client_path: /home/git/gitaly
|
||||
# Default Gitaly authentication token. Can be overriden per storage. Can
|
||||
# be left blank when Gitaly is running locally on a Unix socket, which
|
||||
# is the normal way to deploy Gitaly.
|
||||
|
@ -664,7 +666,7 @@ test:
|
|||
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
|
||||
|
||||
gitaly:
|
||||
enabled: true
|
||||
client_path: tmp/tests/gitaly
|
||||
token: secret
|
||||
backup:
|
||||
path: tmp/tests/backups
|
||||
|
|
|
@ -39,3 +39,17 @@ module GettextI18nRailsJs
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class PoToJson
|
||||
# This is required to modify the JS locale file output to our import needs
|
||||
# Overwrites: https://github.com/webhippie/po_to_json/blob/master/lib/po_to_json.rb#L46
|
||||
def generate_for_jed(language, overwrite = {})
|
||||
@options = parse_options(overwrite.merge(language: language))
|
||||
@parsed ||= inject_meta(parse_document)
|
||||
|
||||
generated = build_json_for(build_jed_for(@parsed))
|
||||
[
|
||||
"window.translations = #{generated};"
|
||||
].join(" ")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -281,7 +281,7 @@ constraints(ProjectUrlConstrainer.new) do
|
|||
|
||||
namespace :registry do
|
||||
resources :repository, only: [] do
|
||||
resources :tags, only: [:destroy],
|
||||
resources :tags, only: [:index, :destroy],
|
||||
constraints: { id: Gitlab::Regex.container_registry_tag_regex }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -68,6 +68,7 @@ var config = {
|
|||
prometheus_metrics: './prometheus_metrics',
|
||||
protected_branches: './protected_branches',
|
||||
protected_tags: './protected_tags',
|
||||
registry_list: './registry/index.js',
|
||||
repo: './repo/index.js',
|
||||
sidebar: './sidebar/sidebar_bundle.js',
|
||||
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
|
||||
|
@ -121,10 +122,6 @@ var config = {
|
|||
name: '[name].[hash].[ext]',
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /locale\/\w+\/(.*)\.js$/,
|
||||
loader: 'exports-loader?locales',
|
||||
},
|
||||
{
|
||||
test: /monaco-editor\/\w+\/vs\/loader\.js$/,
|
||||
use: [
|
||||
|
@ -200,6 +197,7 @@ var config = {
|
|||
'pdf_viewer',
|
||||
'pipelines',
|
||||
'pipelines_details',
|
||||
'registry_list',
|
||||
'repo',
|
||||
'schedule_form',
|
||||
'schedules_index',
|
||||
|
@ -222,7 +220,7 @@ var config = {
|
|||
|
||||
// create cacheable common library bundles
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
names: ['main', 'locale', 'common', 'webpack_runtime'],
|
||||
names: ['main', 'common', 'webpack_runtime'],
|
||||
}),
|
||||
|
||||
// enable scope hoisting
|
||||
|
|
|
@ -7,11 +7,13 @@ class AddFastForwardOptionToProject < ActiveRecord::Migration
|
|||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def add
|
||||
def up
|
||||
add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column(:projects, :merge_requests_ff_only_enabled)
|
||||
if column_exists?(:projects, :merge_requests_ff_only_enabled)
|
||||
remove_column(:projects, :merge_requests_ff_only_enabled)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# rubocop:disable all
|
||||
class MakeSureFastForwardOptionExists < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
# Set this constant to true if this migration requires downtime.
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
# We had to fix the migration db/migrate/20150827121444_add_fast_forward_option_to_project.rb
|
||||
# And this is why it's possible that someone has ran the migrations but does
|
||||
# not have the merge_requests_ff_only_enabled column. This migration makes sure it will
|
||||
# be added
|
||||
unless column_exists?(:projects, :merge_requests_ff_only_enabled)
|
||||
add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
if column_exists?(:projects, :merge_requests_ff_only_enabled)
|
||||
remove_column(:projects, :merge_requests_ff_only_enabled)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20170928100231) do
|
||||
ActiveRecord::Schema.define(version: 20171004121444) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
|
|
@ -121,6 +121,15 @@ GET /projects/:id/merge_requests?labels=bug,reproduced
|
|||
GET /projects/:id/merge_requests?my_reaction_emoji=star
|
||||
```
|
||||
|
||||
`project_id` represents the ID of the project where the MR resides.
|
||||
`project_id` will always equal `target_project_id`.
|
||||
|
||||
In the case of a merge request from the same project,
|
||||
`source_project_id`, `target_project_id` and `project_id`
|
||||
will be the same. In the case of a merge request from a fork,
|
||||
`target_project_id` and `project_id` will be the same and
|
||||
`source_project_id` will be the fork project's ID.
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
|
@ -1,51 +1,46 @@
|
|||
## Enable or disable GitLab CI
|
||||
## Enable or disable GitLab CI/CD
|
||||
|
||||
_To effectively use GitLab CI, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
|
||||
To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
|
||||
file present at the root directory of your project and a
|
||||
[runner](runners/README.md) properly set up. You can read our
|
||||
[quick start guide](quick_start/README.md) to get you started._
|
||||
[quick start guide](quick_start/README.md) to get you started.
|
||||
|
||||
If you are using an external CI server like Jenkins or Drone CI, it is advised
|
||||
to disable GitLab CI in order to not have any conflicts with the commits status
|
||||
If you are using an external CI/CD server like Jenkins or Drone CI, it is advised
|
||||
to disable GitLab CI/CD in order to not have any conflicts with the commits status
|
||||
API.
|
||||
|
||||
---
|
||||
|
||||
GitLab CI is exposed via the `/pipelines` and `/builds` pages of a project.
|
||||
Disabling GitLab CI in a project does not delete any previous jobs.
|
||||
In fact, the `/pipelines` and `/builds` pages can still be accessed, although
|
||||
GitLab CI/CD is exposed via the `/pipelines` and `/jobs` pages of a project.
|
||||
Disabling GitLab CI/CD in a project does not delete any previous jobs.
|
||||
In fact, the `/pipelines` and `/jobs` pages can still be accessed, although
|
||||
it's hidden from the left sidebar menu.
|
||||
|
||||
GitLab CI is enabled by default on new installations and can be disabled either
|
||||
GitLab CI/CD is enabled by default on new installations and can be disabled either
|
||||
individually under each project's settings, or site-wide by modifying the
|
||||
settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
|
||||
respectively.
|
||||
|
||||
### Per-project user setting
|
||||
|
||||
The setting to enable or disable GitLab CI can be found with the name **Pipelines**
|
||||
under the **Sharing & Permissions** area of a project's settings along with
|
||||
**Merge Requests**. Choose one of **Disabled**, **Only team members** and
|
||||
**Everyone with access** and hit **Save changes** for the settings to take effect.
|
||||
The setting to enable or disable GitLab CI/CD can be found under your project's
|
||||
**Settings > General > Permissions**. Choose one of "Disabled", "Only team members"
|
||||
or "Everyone with access" and hit **Save changes** for the settings to take effect.
|
||||
|
||||
![Sharing & Permissions settings](img/permissions_settings.png)
|
||||
![Sharing & Permissions settings](../user/project/settings/img/sharing_and_permissions_settings.png)
|
||||
|
||||
---
|
||||
### Site-wide admin setting
|
||||
|
||||
### Site-wide administrator setting
|
||||
|
||||
You can disable GitLab CI site-wide, by modifying the settings in `gitlab.yml`
|
||||
You can disable GitLab CI/CD site-wide, by modifying the settings in `gitlab.yml`
|
||||
and `gitlab.rb` for source and Omnibus installations respectively.
|
||||
|
||||
Two things to note:
|
||||
|
||||
1. Disabling GitLab CI, will affect only newly-created projects. Projects that
|
||||
1. Disabling GitLab CI/CD, will affect only newly-created projects. Projects that
|
||||
had it enabled prior to this modification, will work as before.
|
||||
1. Even if you disable GitLab CI, users will still be able to enable it in the
|
||||
1. Even if you disable GitLab CI/CD, users will still be able to enable it in the
|
||||
project's settings.
|
||||
|
||||
---
|
||||
|
||||
For installations from source, open `gitlab.yml` with your editor and set
|
||||
`builds` to `false`:
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ so every environment can have one or more deployments. GitLab keeps track of
|
|||
your deployments, so you always know what is currently being deployed on your
|
||||
servers. If you have a deployment service such as [Kubernetes][kubernetes-service]
|
||||
enabled for your project, you can use it to assist with your deployments, and
|
||||
can even access a web terminal for your environment from within GitLab!
|
||||
can even access a [web terminal](#web-terminals) for your environment from within GitLab!
|
||||
|
||||
To better understand how environments and deployments work, let's consider an
|
||||
example. We assume that you have already created a project in GitLab and set up
|
||||
|
@ -119,7 +119,7 @@ where you can find information of the last deployment status of an environment.
|
|||
|
||||
Here's how the Environments page looks so far.
|
||||
|
||||
![Staging environment view](img/environments_available_staging.png)
|
||||
![Environment view](img/environments_available.png)
|
||||
|
||||
There's a bunch of information there, specifically you can see:
|
||||
|
||||
|
@ -229,7 +229,7 @@ You can find it in the pipeline, job, environment, and deployment views.
|
|||
|
||||
| Pipelines | Single pipeline | Environments | Deployments | jobs |
|
||||
| --------- | ----------------| ------------ | ----------- | -------|
|
||||
| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_builds.png) |
|
||||
| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_jobs.png) |
|
||||
|
||||
Clicking on the play button in either of these places will trigger the
|
||||
`deploy_prod` job, and the deployment will be recorded under a new
|
||||
|
@ -402,7 +402,7 @@ places within GitLab.
|
|||
|
||||
| In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button |
|
||||
| -------------------- | ------------ | ----------- |
|
||||
| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_link_url.png) | ![Environment URL in deployments](img/environments_link_url_deployments.png) |
|
||||
| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_available.png) | ![Environment URL in deployments](img/deployments_view.png) |
|
||||
|
||||
If a merge request is eventually merged to the default branch (in our case
|
||||
`master`) and that branch also deploys to an environment (in our case `staging`
|
||||
|
@ -574,7 +574,7 @@ Once configured, GitLab will attempt to retrieve [supported performance metrics]
|
|||
environment which has had a successful deployment. If monitoring data was
|
||||
successfully retrieved, a Monitoring button will appear for each environment.
|
||||
|
||||
![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png)
|
||||
![Environment Detail with Metrics](img/deployments_view.png)
|
||||
|
||||
Clicking on the Monitoring button will display a new page, showing up to the last
|
||||
8 hours of performance data. It may take a minute or two for data to appear
|
||||
|
@ -593,10 +593,11 @@ Web terminals were added in GitLab 8.15 and are only available to project
|
|||
masters and owners.
|
||||
|
||||
If you deploy to your environments with the help of a deployment service (e.g.,
|
||||
the [Kubernetes service][kubernetes-service], GitLab can open
|
||||
the [Kubernetes service][kubernetes-service]), GitLab can open
|
||||
a terminal session to your environment! This is a very powerful feature that
|
||||
allows you to debug issues without leaving the comfort of your web browser. To
|
||||
enable it, just follow the instructions given in the service documentation.
|
||||
enable it, just follow the instructions given in the service integration
|
||||
documentation.
|
||||
|
||||
Once enabled, your environments will gain a "terminal" button:
|
||||
|
||||
|
|
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 60 KiB |
BIN
doc/ci/img/environments_available.png
Normal file
After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 12 KiB |