diff options
Diffstat (limited to 'app/assets/javascripts/admin')
22 files changed, 408 insertions, 17 deletions
diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue new file mode 100644 index 00000000000..a4211002f71 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue @@ -0,0 +1,41 @@ +<script> +import { getTimeago } from '~/lib/utils/datetime_utility'; +import { __, sprintf } from '~/locale'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; + +export default { + name: 'AbuseReportRow', + components: { + ListItem, + }, + props: { + report: { + type: Object, + required: true, + }, + }, + computed: { + updatedAt() { + const template = __('Updated %{timeAgo}'); + return sprintf(template, { timeAgo: getTimeago().format(this.report.updatedAt) }); + }, + title() { + const { reportedUser, reporter, category } = this.report; + const template = __('%{reported} reported for %{category} by %{reporter}'); + return sprintf(template, { reported: reportedUser.name, reporter: reporter.name, category }); + }, + }, +}; +</script> + +<template> + <list-item data-testid="abuse-report-row"> + <template #left-primary> + <div class="gl-font-weight-normal" data-testid="title">{{ title }}</div> + </template> + + <template #right-secondary> + <div data-testid="updated-at">{{ updatedAt }}</div> + </template> + </list-item> +</template> diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue new file mode 100644 index 00000000000..b60fe3ae9b8 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue @@ -0,0 +1,109 @@ +<script> +import { setUrlParams, redirectTo, queryToObject, updateHistory } from '~/lib/utils/url_utility'; +import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import { + FILTERED_SEARCH_TOKENS, + DEFAULT_SORT, + SORT_OPTIONS, + isValidSortKey, +} from '~/admin/abuse_reports/constants'; +import { buildFilteredSearchCategoryToken } from '~/admin/abuse_reports/utils'; + +export default { + name: 'AbuseReportsFilteredSearchBar', + components: { FilteredSearchBar }, + sortOptions: SORT_OPTIONS, + inject: ['categories'], + data() { + return { + initialFilterValue: [], + initialSortBy: DEFAULT_SORT, + }; + }, + computed: { + tokens() { + return [...FILTERED_SEARCH_TOKENS, buildFilteredSearchCategoryToken(this.categories)]; + }, + }, + created() { + const query = queryToObject(window.location.search); + + // Backend shows open reports by default if status param is not specified. + // To match that behavior, update the current URL to include status=open + // query when no status query is specified on load. + if (!query.status) { + query.status = 'open'; + updateHistory({ url: setUrlParams(query), replace: true }); + } + + const sort = this.currentSortKey(); + if (sort) { + this.initialSortBy = query.sort; + } + + const tokens = this.tokens + .filter((token) => query[token.type]) + .map((token) => ({ + type: token.type, + value: { + data: query[token.type], + operator: '=', + }, + })); + + this.initialFilterValue = tokens; + }, + methods: { + currentSortKey() { + const { sort } = queryToObject(window.location.search); + + return isValidSortKey(sort) ? sort : undefined; + }, + handleFilter(tokens) { + let params = tokens.reduce((accumulator, token) => { + const { type, value } = token; + + // We don't support filtering reports by search term for now + if (!value || !type || type === FILTERED_SEARCH_TERM) { + return accumulator; + } + + return { + ...accumulator, + [type]: value.data, + }; + }, {}); + + const sort = this.currentSortKey(); + if (sort) { + params = { ...params, sort }; + } + + redirectTo(setUrlParams(params, window.location.href, true)); + }, + handleSort(sort) { + const { page, ...query } = queryToObject(window.location.search); + + redirectTo(setUrlParams({ ...query, sort }, window.location.href, true)); + }, + }, + filteredSearchNamespace: 'abuse_reports', + recentSearchesStorageKey: 'abuse_reports', +}; +</script> + +<template> + <filtered-search-bar + :namespace="$options.filteredSearchNamespace" + :tokens="tokens" + :recent-searches-storage-key="$options.recentSearchesStorageKey" + :search-input-placeholder="__('Filter reports')" + :initial-filter-value="initialFilterValue" + :initial-sort-by="initialSortBy" + :sort-options="$options.sortOptions" + data-testid="abuse-reports-filtered-search-bar" + @onFilter="handleFilter" + @onSort="handleSort" + /> +</template> diff --git a/app/assets/javascripts/admin/abuse_reports/components/app.vue b/app/assets/javascripts/admin/abuse_reports/components/app.vue new file mode 100644 index 00000000000..e1e75a4f8d0 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_reports/components/app.vue @@ -0,0 +1,63 @@ +<script> +import { GlEmptyState, GlPagination } from '@gitlab/ui'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; +import FilteredSearchBar from './abuse_reports_filtered_search_bar.vue'; +import AbuseReportRow from './abuse_report_row.vue'; + +export default { + name: 'AbuseReportsApp', + components: { + AbuseReportRow, + FilteredSearchBar, + GlEmptyState, + GlPagination, + }, + props: { + abuseReports: { + type: Array, + required: true, + }, + pagination: { + type: Object, + required: true, + }, + }, + computed: { + showPagination() { + return this.pagination.totalItems > this.pagination.perPage; + }, + }, + methods: { + paginationLinkGenerator(page) { + return mergeUrlParams({ page }, window.location.href); + }, + }, +}; +</script> +<template> + <div> + <filtered-search-bar /> + + <gl-empty-state v-if="abuseReports.length == 0" :title="s__('AbuseReports|No reports found')" /> + <abuse-report-row + v-for="(report, index) in abuseReports" + v-else + :key="index" + :report="report" + /> + + <gl-pagination + v-if="showPagination" + :value="pagination.currentPage" + :per-page="pagination.perPage" + :total-items="pagination.totalItems" + :link-gen="paginationLinkGenerator" + :prev-text="__('Prev')" + :next-text="__('Next')" + :label-next-page="__('Go to next page')" + :label-prev-page="__('Go to previous page')" + align="center" + class="gl-mt-3" + /> + </div> +</template> diff --git a/app/assets/javascripts/admin/abuse_reports/constants.js b/app/assets/javascripts/admin/abuse_reports/constants.js new file mode 100644 index 00000000000..ee2e9ab2cbf --- /dev/null +++ b/app/assets/javascripts/admin/abuse_reports/constants.js @@ -0,0 +1,81 @@ +import { getUsers } from '~/rest_api'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue'; +import { + OPERATORS_IS, + TOKEN_TITLE_STATUS, +} from '~/vue_shared/components/filtered_search_bar/constants'; +import { __ } from '~/locale'; + +const STATUS_OPTIONS = [ + { value: 'closed', title: __('Closed') }, + { value: 'open', title: __('Open') }, +]; + +export const FILTERED_SEARCH_TOKEN_USER = { + type: 'user', + icon: 'user', + title: __('User'), + token: UserToken, + unique: true, + operators: OPERATORS_IS, + fetchUsers: getUsers, + defaultUsers: [], +}; + +export const FILTERED_SEARCH_TOKEN_REPORTER = { + ...FILTERED_SEARCH_TOKEN_USER, + type: 'reporter', + title: __('Reporter'), +}; + +export const FILTERED_SEARCH_TOKEN_STATUS = { + type: 'status', + icon: 'status', + title: TOKEN_TITLE_STATUS, + token: BaseToken, + unique: true, + options: STATUS_OPTIONS, + operators: OPERATORS_IS, +}; + +export const DEFAULT_SORT = 'created_at_desc'; + +export const SORT_OPTIONS = [ + { + id: 10, + title: __('Created date'), + sortDirection: { + descending: DEFAULT_SORT, + ascending: 'created_at_asc', + }, + }, + { + id: 20, + title: __('Updated date'), + sortDirection: { + descending: 'updated_at_desc', + ascending: 'updated_at_asc', + }, + }, +]; + +export const isValidSortKey = (key) => + SORT_OPTIONS.some( + (sort) => sort.sortDirection.ascending === key || sort.sortDirection.descending === key, + ); + +export const FILTERED_SEARCH_TOKEN_CATEGORY = { + type: 'category', + icon: 'label', + title: __('Category'), + token: BaseToken, + unique: true, + operators: OPERATORS_IS, +}; + +export const FILTERED_SEARCH_TOKENS = [ + FILTERED_SEARCH_TOKEN_USER, + FILTERED_SEARCH_TOKEN_REPORTER, + FILTERED_SEARCH_TOKEN_STATUS, +]; diff --git a/app/assets/javascripts/admin/abuse_reports/index.js b/app/assets/javascripts/admin/abuse_reports/index.js new file mode 100644 index 00000000000..dbc466af2d2 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_reports/index.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import AbuseReportsApp from './components/app.vue'; + +export const initAbuseReportsApp = () => { + const el = document.querySelector('#js-abuse-reports-list-app'); + + if (!el) { + return null; + } + + const { abuseReportsData } = el.dataset; + const { categories, reports, pagination } = convertObjectPropsToCamelCase( + JSON.parse(abuseReportsData), + { + deep: true, + }, + ); + + return new Vue({ + el, + provide: { categories }, + render: (createElement) => + createElement(AbuseReportsApp, { + props: { + abuseReports: reports, + pagination, + }, + }), + }); +}; diff --git a/app/assets/javascripts/admin/abuse_reports/utils.js b/app/assets/javascripts/admin/abuse_reports/utils.js new file mode 100644 index 00000000000..84221901089 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_reports/utils.js @@ -0,0 +1,6 @@ +import { FILTERED_SEARCH_TOKEN_CATEGORY } from './constants'; + +export const buildFilteredSearchCategoryToken = (categories) => { + const options = categories.map((c) => ({ value: c, title: c })); + return { ...FILTERED_SEARCH_TOKEN_CATEGORY, options }; +}; diff --git a/app/assets/javascripts/admin/application_settings/network_outbound.js b/app/assets/javascripts/admin/application_settings/network_outbound.js new file mode 100644 index 00000000000..ad7ed85131c --- /dev/null +++ b/app/assets/javascripts/admin/application_settings/network_outbound.js @@ -0,0 +1,28 @@ +export default () => { + const denyAllRequests = document.querySelector('.js-deny-all-requests'); + + if (!denyAllRequests) { + return; + } + + denyAllRequests.addEventListener('change', () => { + const denyAll = denyAllRequests.checked; + const allowLocalRequests = document.querySelectorAll('.js-allow-local-requests'); + const denyAllRequestsWarning = document.querySelector('.js-deny-all-requests-warning'); + + if (denyAll) { + denyAllRequestsWarning.classList.remove('gl-display-none'); + } else { + denyAllRequestsWarning.classList.add('gl-display-none'); + } + + allowLocalRequests.forEach((allowLocalRequest) => { + /* eslint-disable no-param-reassign */ + if (denyAll) { + allowLocalRequest.checked = false; + } + allowLocalRequest.disabled = denyAll; + /* eslint-enable no-param-reassign */ + }); + }); +}; diff --git a/app/assets/javascripts/admin/broadcast_messages/components/base.vue b/app/assets/javascripts/admin/broadcast_messages/components/base.vue index f869d21d55f..c28cd266617 100644 --- a/app/assets/javascripts/admin/broadcast_messages/components/base.vue +++ b/app/assets/javascripts/admin/broadcast_messages/components/base.vue @@ -2,7 +2,7 @@ import { GlPagination } from '@gitlab/ui'; import { redirectTo } from '~/lib/utils/url_utility'; import { buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; -import { createAlert, VARIANT_DANGER } from '~/flash'; +import { createAlert, VARIANT_DANGER } from '~/alert'; import { s__ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import { NEW_BROADCAST_MESSAGE } from '../constants'; diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue index 36796708e78..65aa4cba074 100644 --- a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue +++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue @@ -12,16 +12,25 @@ import { } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; -import { createAlert, VARIANT_DANGER } from '~/flash'; +import { createAlert, VARIANT_DANGER } from '~/alert'; import { redirectTo } from '~/lib/utils/url_utility'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { BROADCAST_MESSAGES_PATH, THEMES, TYPES, TYPE_BANNER } from '../constants'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { + BROADCAST_MESSAGES_PATH, + MESSAGES_PREVIEW_PATH, + THEMES, + TYPES, + TYPE_BANNER, +} from '../constants'; import MessageFormGroup from './message_form_group.vue'; import DatetimePicker from './datetime_picker.vue'; const FORM_HEADERS = { headers: { 'Content-Type': 'application/json; charset=utf-8' } }; export default { + DEFAULT_DEBOUNCE_AND_THROTTLE_MS, name: 'MessageForm', components: { DatetimePicker, @@ -36,6 +45,9 @@ export default { GlFormTextarea, MessageFormGroup, }, + directives: { + SafeHtml, + }, mixins: [glFeatureFlagsMixin()], inject: ['targetAccessLevelOptions'], i18n: { @@ -81,6 +93,7 @@ export default { })), startsAt: new Date(this.broadcastMessage.startsAt.getTime()), endsAt: new Date(this.broadcastMessage.endsAt.getTime()), + renderedMessage: '', }; }, computed: { @@ -91,7 +104,7 @@ export default { return this.message.trim() === ''; }, messagePreview() { - return this.messageBlank ? this.$options.i18n.messagePlaceholder : this.message; + return this.messageBlank ? this.$options.i18n.messagePlaceholder : this.renderedMessage; }, isAddForm() { return !this.broadcastMessage.id; @@ -114,6 +127,11 @@ export default { }); }, }, + watch: { + message() { + this.renderPreview(); + }, + }, methods: { async onSubmit() { this.loading = true; @@ -140,13 +158,25 @@ export default { } return true; }, + + async renderPreview() { + try { + const res = await axios.post(MESSAGES_PREVIEW_PATH, this.formPayload, FORM_HEADERS); + this.renderedMessage = res.data; + } catch (e) { + this.renderedMessage = ''; + } + }, + }, + safeHtmlConfig: { + ADD_TAGS: ['use'], }, }; </script> <template> <gl-form @submit.prevent="onSubmit"> <gl-broadcast-message class="gl-my-6" :type="type" :theme="theme" :dismissible="dismissable"> - {{ messagePreview }} + <div v-safe-html:[$options.safeHtmlConfig]="messagePreview"></div> </gl-broadcast-message> <message-form-group :label="$options.i18n.message" label-for="message-textarea"> @@ -154,6 +184,7 @@ export default { id="message-textarea" v-model="message" size="sm" + :debounce="$options.DEFAULT_DEBOUNCE_AND_THROTTLE_MS" :placeholder="$options.i18n.messagePlaceholder" /> </message-form-group> diff --git a/app/assets/javascripts/admin/broadcast_messages/constants.js b/app/assets/javascripts/admin/broadcast_messages/constants.js index 6250d5a943d..323ac6857f6 100644 --- a/app/assets/javascripts/admin/broadcast_messages/constants.js +++ b/app/assets/javascripts/admin/broadcast_messages/constants.js @@ -1,6 +1,7 @@ import { s__ } from '~/locale'; export const BROADCAST_MESSAGES_PATH = '/admin/broadcast_messages'; +export const MESSAGES_PREVIEW_PATH = '/admin/broadcast_messages/preview'; export const TYPE_BANNER = 'banner'; export const TYPE_NOTIFICATION = 'notification'; diff --git a/app/assets/javascripts/admin/deploy_keys/components/table.vue b/app/assets/javascripts/admin/deploy_keys/components/table.vue index be85ee43891..134498af348 100644 --- a/app/assets/javascripts/admin/deploy_keys/components/table.vue +++ b/app/assets/javascripts/admin/deploy_keys/components/table.vue @@ -5,7 +5,7 @@ import { __ } from '~/locale'; import Api, { DEFAULT_PER_PAGE } from '~/api'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { cleanLeadingSeparator } from '~/lib/utils/url_utility'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import csrf from '~/lib/utils/csrf'; export default { diff --git a/app/assets/javascripts/admin/statistics_panel/store/actions.js b/app/assets/javascripts/admin/statistics_panel/store/actions.js index 4f952698d7a..7372f03ec0b 100644 --- a/app/assets/javascripts/admin/statistics_panel/store/actions.js +++ b/app/assets/javascripts/admin/statistics_panel/store/actions.js @@ -1,5 +1,5 @@ import Api from '~/api'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import * as types from './mutation_types'; diff --git a/app/assets/javascripts/admin/users/components/actions/activate.vue b/app/assets/javascripts/admin/users/components/actions/activate.vue index 3a54035c587..0099c8da8e6 100644 --- a/app/assets/javascripts/admin/users/components/actions/activate.vue +++ b/app/assets/javascripts/admin/users/components/actions/activate.vue @@ -41,7 +41,7 @@ export default { }, actionPrimary: { text: I18N_USER_ACTIONS.activate, - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, }, messageHtml, }, diff --git a/app/assets/javascripts/admin/users/components/actions/approve.vue b/app/assets/javascripts/admin/users/components/actions/approve.vue index 5a8c675822d..52560ebe5b1 100644 --- a/app/assets/javascripts/admin/users/components/actions/approve.vue +++ b/app/assets/javascripts/admin/users/components/actions/approve.vue @@ -43,7 +43,7 @@ export default { }, actionPrimary: { text: I18N_USER_ACTIONS.approve, - attributes: [{ variant: 'confirm', 'data-qa-selector': 'approve_user_confirm_button' }], + attributes: { variant: 'confirm', 'data-qa-selector': 'approve_user_confirm_button' }, }, messageHtml, }, diff --git a/app/assets/javascripts/admin/users/components/actions/ban.vue b/app/assets/javascripts/admin/users/components/actions/ban.vue index 898a688c203..203d076914f 100644 --- a/app/assets/javascripts/admin/users/components/actions/ban.vue +++ b/app/assets/javascripts/admin/users/components/actions/ban.vue @@ -56,7 +56,7 @@ export default { }, actionPrimary: { text: I18N_USER_ACTIONS.ban, - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, }, messageHtml, }, diff --git a/app/assets/javascripts/admin/users/components/actions/block.vue b/app/assets/javascripts/admin/users/components/actions/block.vue index d25dd400f9b..d50b76aaa92 100644 --- a/app/assets/javascripts/admin/users/components/actions/block.vue +++ b/app/assets/javascripts/admin/users/components/actions/block.vue @@ -42,7 +42,7 @@ export default { }, actionPrimary: { text: I18N_USER_ACTIONS.block, - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, }, messageHtml, }, diff --git a/app/assets/javascripts/admin/users/components/actions/deactivate.vue b/app/assets/javascripts/admin/users/components/actions/deactivate.vue index c85f3f01675..ab1069601d2 100644 --- a/app/assets/javascripts/admin/users/components/actions/deactivate.vue +++ b/app/assets/javascripts/admin/users/components/actions/deactivate.vue @@ -51,7 +51,7 @@ export default { }, actionPrimary: { text: I18N_USER_ACTIONS.deactivate, - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, }, messageHtml, }, diff --git a/app/assets/javascripts/admin/users/components/actions/reject.vue b/app/assets/javascripts/admin/users/components/actions/reject.vue index bac08de1d5e..2b9c4acfcb5 100644 --- a/app/assets/javascripts/admin/users/components/actions/reject.vue +++ b/app/assets/javascripts/admin/users/components/actions/reject.vue @@ -54,7 +54,7 @@ export default { }, actionPrimary: { text: I18N_USER_ACTIONS.reject, - attributes: [{ variant: 'danger' }], + attributes: { variant: 'danger' }, }, messageHtml, }, diff --git a/app/assets/javascripts/admin/users/components/actions/unban.vue b/app/assets/javascripts/admin/users/components/actions/unban.vue index beede2d37d7..42b6fb3bdd4 100644 --- a/app/assets/javascripts/admin/users/components/actions/unban.vue +++ b/app/assets/javascripts/admin/users/components/actions/unban.vue @@ -37,7 +37,7 @@ export default { }, actionPrimary: { text: I18N_USER_ACTIONS.unban, - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, }, messageHtml, }, diff --git a/app/assets/javascripts/admin/users/components/actions/unblock.vue b/app/assets/javascripts/admin/users/components/actions/unblock.vue index 720f2efd932..f94e128a945 100644 --- a/app/assets/javascripts/admin/users/components/actions/unblock.vue +++ b/app/assets/javascripts/admin/users/components/actions/unblock.vue @@ -32,7 +32,7 @@ export default { }, actionPrimary: { text: I18N_USER_ACTIONS.unblock, - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, }, }, }); diff --git a/app/assets/javascripts/admin/users/components/actions/unlock.vue b/app/assets/javascripts/admin/users/components/actions/unlock.vue index 55ea3e0aba7..c78c260b4fe 100644 --- a/app/assets/javascripts/admin/users/components/actions/unlock.vue +++ b/app/assets/javascripts/admin/users/components/actions/unlock.vue @@ -31,7 +31,7 @@ export default { }, actionPrimary: { text: I18N_USER_ACTIONS.unlock, - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, }, }, }); diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue index f569cda0a4b..e55622d40ba 100644 --- a/app/assets/javascripts/admin/users/components/users_table.vue +++ b/app/assets/javascripts/admin/users/components/users_table.vue @@ -1,6 +1,6 @@ <script> import { GlSkeletonLoader, GlTable } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils'; import { thWidthPercent } from '~/lib/utils/table_utility'; import { s__, __ } from '~/locale'; |