Commit 70a950f7 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'master' into ce-to-ee

parents 520c2817 d482a08e
/* eslint-disable no-new*/ /* eslint-disable no-new*/
import './smart_interval'; import axios from 'axios';
import SmartInterval from '~/smart_interval';
import { parseSeconds, stringifyTime } from './lib/utils/pretty_time'; import { parseSeconds, stringifyTime } from './lib/utils/pretty_time';
const healthyClass = 'geo-node-healthy'; const healthyClass = 'geo-node-healthy';
...@@ -31,7 +32,7 @@ class GeoNodeStatus { ...@@ -31,7 +32,7 @@ class GeoNodeStatus {
this.$advancedStatus = $('.js-advanced-geo-node-status-toggler', this.$status); this.$advancedStatus = $('.js-advanced-geo-node-status-toggler', this.$status);
this.$advancedStatus.on('click', GeoNodeStatus.toggleShowAdvancedStatus); this.$advancedStatus.on('click', GeoNodeStatus.toggleShowAdvancedStatus);
this.statusInterval = new gl.SmartInterval({ this.statusInterval = new SmartInterval({
callback: this.getStatus.bind(this), callback: this.getStatus.bind(this),
startingInterval: 30000, startingInterval: 30000,
maxInterval: 120000, maxInterval: 120000,
...@@ -59,78 +60,105 @@ class GeoNodeStatus { ...@@ -59,78 +60,105 @@ class GeoNodeStatus {
static formatCount(count) { static formatCount(count) {
if (count !== null) { if (count !== null) {
gl.text.addDelimiter(count); return gl.text.addDelimiter(count);
} }
return notAvailable; return notAvailable;
} }
getStatus() { getStatus() {
$.getJSON(this.endpoint, (status) => { return axios.get(this.endpoint)
this.setStatusIcon(status.healthy); .then((response) => {
this.setHealthStatus(status.healthy); this.handleStatus(response.data);
return response;
})
.catch((err) => {
this.handleError(err);
});
}
handleStatus(status) {
this.setStatusIcon(status.healthy);
this.setHealthStatus(status.healthy);
// Replication lag can be nil if the secondary isn't actually streaming // Replication lag can be nil if the secondary isn't actually streaming
if (status.db_replication_lag_seconds !== null && status.db_replication_lag_seconds >= 0) { if (status.db_replication_lag_seconds !== null && status.db_replication_lag_seconds >= 0) {
const parsedTime = parseSeconds(status.db_replication_lag_seconds, { const parsedTime = parseSeconds(status.db_replication_lag_seconds, {
hoursPerDay: 24, hoursPerDay: 24,
daysPerWeek: 7, daysPerWeek: 7,
}); });
this.$dbReplicationLag.text(stringifyTime(parsedTime)); this.$dbReplicationLag.text(stringifyTime(parsedTime));
} else { } else {
this.$dbReplicationLag.text('UNKNOWN'); this.$dbReplicationLag.text('UNKNOWN');
} }
const repoText = GeoNodeStatus.formatCountAndPercentage( const repoText = GeoNodeStatus.formatCountAndPercentage(
status.repositories_synced_count, status.repositories_synced_count,
status.repositories_count, status.repositories_count,
status.repositories_synced_in_percentage); status.repositories_synced_in_percentage);
const repoFailedText = GeoNodeStatus.formatCount(status.repositories_failed_count); const repoFailedText = GeoNodeStatus.formatCount(status.repositories_failed_count);
const lfsText = GeoNodeStatus.formatCountAndPercentage( const lfsText = GeoNodeStatus.formatCountAndPercentage(
status.lfs_objects_synced_count, status.lfs_objects_synced_count,
status.lfs_objects_count, status.lfs_objects_count,
status.lfs_objects_synced_in_percentage); status.lfs_objects_synced_in_percentage);
const lfsFailedText = GeoNodeStatus.formatCount(status.lfs_objects_failed_count); const lfsFailedText = GeoNodeStatus.formatCount(status.lfs_objects_failed_count);
const attachmentText = GeoNodeStatus.formatCountAndPercentage( const attachmentText = GeoNodeStatus.formatCountAndPercentage(
status.attachments_synced_count, status.attachments_synced_count,
status.attachments_count, status.attachments_count,
status.attachments_synced_in_percentage); status.attachments_synced_in_percentage);
const attachmentFailedText = GeoNodeStatus.formatCount(status.attachments_failed_count); const attachmentFailedText = GeoNodeStatus.formatCount(status.attachments_failed_count);
this.$repositoriesSynced.text(repoText); this.$repositoriesSynced.text(repoText);
this.$repositoriesFailed.text(repoFailedText); this.$repositoriesFailed.text(repoFailedText);
this.$lfsObjectsSynced.text(lfsText); this.$lfsObjectsSynced.text(lfsText);
this.$lfsObjectsFailed.text(lfsFailedText); this.$lfsObjectsFailed.text(lfsFailedText);
this.$attachmentsSynced.text(attachmentText); this.$attachmentsSynced.text(attachmentText);
this.$attachmentsFailed.text(attachmentFailedText); this.$attachmentsFailed.text(attachmentFailedText);
let eventDate = notAvailable; let eventDate = notAvailable;
let cursorDate = notAvailable; let cursorDate = notAvailable;
let lastEventSeen = notAvailable;
if (status.last_event_timestamp !== null && status.last_event_timestamp > 0) { let lastCursorEvent = notAvailable;
eventDate = gl.utils.formatDate(new Date(status.last_event_timestamp * 1000));
} if (status.last_event_timestamp !== null && status.last_event_timestamp > 0) {
eventDate = gl.utils.formatDate(new Date(status.last_event_timestamp * 1000));
if (status.cursor_last_event_timestamp !== null && status.cursor_last_event_timestamp > 0) { }
cursorDate = gl.utils.formatDate(new Date(status.cursor_last_event_timestamp * 1000));
} if (status.cursor_last_event_timestamp !== null && status.cursor_last_event_timestamp > 0) {
cursorDate = gl.utils.formatDate(new Date(status.cursor_last_event_timestamp * 1000));
this.$lastEventSeen.text(`${status.last_event_id} (${eventDate})`); }
this.$lastCursorEvent.text(`${status.cursor_last_event_id} (${cursorDate})`);
if (status.health === 'Healthy') { if (status.last_event_id !== null) {
this.$health.text(''); lastEventSeen = `${status.last_event_id} (${eventDate})`;
} else { }
const strippedData = $('<div>').html(`${status.health}`).text();
this.$health.html(`<code class="geo-health">${strippedData}</code>`); if (status.cursor_last_event_id !== null) {
} lastCursorEvent = `${status.cursor_last_event_id} (${cursorDate})`;
}
this.$status.show();
}); this.$lastEventSeen.text(lastEventSeen);
this.$lastCursorEvent.text(lastCursorEvent);
if (status.health === 'Healthy') {
this.$health.text('');
} else {
const strippedData = $('<div>').html(`${status.health}`).text();
this.$health.html(`<code class="geo-health">${strippedData}</code>`);
}
this.$status.show();
}
handleError(err) {
this.setStatusIcon(false);
this.setHealthStatus(false);
this.$health.html(`<code class="geo-health">${err}</code>`);
this.$status.show();
} }
setStatusIcon(healthy) { setStatusIcon(healthy) {
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
Geo nodes (#{@nodes.count}) Geo nodes (#{@nodes.count})
%ul.well-list.geo-nodes %ul.well-list.geo-nodes
- @nodes.each do |node| - @nodes.each do |node|
%li{ id: dom_id(node), class: node_class(node), data: { status_url: status_admin_geo_node_path(node) } } %li{ id: dom_id(node), class: node_class(node), data: { status_url: status_admin_geo_node_path(node, format: :json) } }
.node-block .node-block
= node_status_icon(node) = node_status_icon(node)
%strong= node.url %strong= node.url
......
---
title: 'Geo: Fix handling of nil values on advanced section in admin screen'
merge_request:
author:
type: fixed
---
title: Add /groups/:id/subgroups endpoint to API
merge_request: 15142
author: marbemac
type: added
...@@ -9,13 +9,13 @@ Parameters: ...@@ -9,13 +9,13 @@ Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `skip_groups` | array of integers | no | Skip the group IDs passes | | `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to | | `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
| `search` | string | no | Return list of authorized groups matching the search criteria | | `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` | | `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) | | `statistics` | boolean | no | Include group statistics (admins only) |
| `owned` | boolean | no | Limit by groups owned by the current user | | `owned` | boolean | no | Limit to groups owned by the current user |
``` ```
GET /groups GET /groups
...@@ -80,6 +80,47 @@ You can filter by [custom attributes](custom_attributes.md) with: ...@@ -80,6 +80,47 @@ You can filter by [custom attributes](custom_attributes.md) with:
GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_value GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_value
``` ```
## List a groups's subgroups
Get a list of visible direct subgroups in this group.
When accessed without authentication, only public groups are returned.
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `owned` | boolean | no | Limit to groups owned by the current user |
```
GET /groups/:id/subgroups
```
```json
[
{
"id": 1,
"name": "Foobar Group",
"path": "foo-bar",
"description": "An interesting group",
"visibility": "public",
"lfs_enabled": true,
"avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/foo.jpg",
"web_url": "http://gitlab.example.com/groups/foo-bar",
"request_access_enabled": false,
"full_name": "Foobar Group",
"full_path": "foo-bar",
"parent_id": 123
}
]
```
## List a group's projects ## List a group's projects
Get a list of projects in this group. When accessed without authentication, only Get a list of projects in this group. When accessed without authentication, only
......
...@@ -34,24 +34,7 @@ module API ...@@ -34,24 +34,7 @@ module API
optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
end end
def present_groups(groups, options = {}) params :group_list_params do
options = options.reverse_merge(
with: Entities::Group,
current_user: current_user
)
groups = groups.with_statistics if options[:statistics]
present paginate(groups), options
end
end
resource :groups do
include CustomAttributesEndpoints
desc 'Get a groups list' do
success Entities::Group
end
params do
use :statistics_params use :statistics_params
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to' optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
...@@ -61,19 +44,47 @@ module API ...@@ -61,19 +44,47 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination use :pagination
end end
get do
def find_groups(params)
find_params = { find_params = {
all_available: params[:all_available], all_available: params[:all_available],
owned: params[:owned], custom_attributes: params[:custom_attributes],
custom_attributes: params[:custom_attributes] owned: params[:owned]
} }
find_params[:parent] = find_group!(params[:id]) if params[:id]
groups = GroupsFinder.new(current_user, find_params).execute groups = GroupsFinder.new(current_user, find_params).execute
groups = groups.search(params[:search]) if params[:search].present? groups = groups.search(params[:search]) if params[:search].present?
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort]) groups = groups.reorder(params[:order_by] => params[:sort])
present_groups groups, statistics: params[:statistics] && current_user.admin? groups
end
def present_groups(params, groups)
options = {
with: Entities::Group,
current_user: current_user,
statistics: params[:statistics] && current_user.admin?
}
groups = groups.with_statistics if options[:statistics]
present paginate(groups), options
end
end
resource :groups do
include CustomAttributesEndpoints
desc 'Get a groups list' do
success Entities::Group
end
params do
use :group_list_params
end
get do
groups = find_groups(params)
present_groups params, groups
end end
desc 'Create a group. Available only for users who can create groups.' do desc 'Create a group. Available only for users who can create groups.' do
...@@ -199,6 +210,17 @@ module API ...@@ -199,6 +210,17 @@ module API
present paginate(projects), with: entity, current_user: current_user present paginate(projects), with: entity, current_user: current_user
end end
desc 'Get a list of subgroups in this group.' do
success Entities::Group
end
params do
use :group_list_params
end
get ":id/subgroups" do
groups = find_groups(params)
present_groups params, groups
end
desc 'Transfer a project to the group namespace. Available only for admin.' do desc 'Transfer a project to the group namespace. Available only for admin.' do
success Entities::GroupDetail success Entities::GroupDetail
end end
......
...@@ -41,7 +41,7 @@ describe "Git HTTP requests (Geo)" do ...@@ -41,7 +41,7 @@ describe "Git HTTP requests (Geo)" do
it { travel_to(2.minutes.ago) { is_expected.to have_gitlab_http_status(:unauthorized) } } it { travel_to(2.minutes.ago) { is_expected.to have_gitlab_http_status(:unauthorized) } }
end end
xcontext 'expired Geo JWT token' do context 'expired Geo JWT token' do
let(:env) { valid_geo_env } let(:env) { valid_geo_env }
it { travel_to(Time.now + 2.minutes) { is_expected.to have_gitlab_http_status(:unauthorized) } } it { travel_to(Time.now + 2.minutes) { is_expected.to have_gitlab_http_status(:unauthorized) } }
......
...@@ -171,7 +171,7 @@ describe('Api', () => { ...@@ -171,7 +171,7 @@ describe('Api', () => {
it('creates a new group label', (done) => { it('creates a new group label', (done) => {
const namespace = 'some namespace'; const namespace = 'some namespace';
const labelData = { some: 'data' }; const labelData = { some: 'data' };
const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/labels`; const expectedUrl = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespace);
const expectedData = { const expectedData = {
label: labelData, label: labelData,
}; };
......
...@@ -471,6 +471,142 @@ describe API::Groups do ...@@ -471,6 +471,142 @@ describe API::Groups do
end end
end end
describe 'GET /groups/:id/subgroups', :nested_groups do
let!(:subgroup1) { create(:group, parent: group1) }
let!(:subgroup2) { create(:group, :private, parent: group1) }
let!(:subgroup3) { create(:group, :private, parent: group2) }
context 'when unauthenticated' do
it 'returns only public subgroups' do
get api("/groups/#{group1.id}/subgroups")
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(subgroup1.id)
expect(json_response.first['parent_id']).to eq(group1.id)
end
it 'returns 404 for a private group' do
get api("/groups/#{group2.id}/subgroups")
expect(response).to have_gitlab_http_status(404)
end
end
context 'when authenticated as user' do
context 'when user is not member of a public group' do
it 'returns no subgroups for the public group' do
get api("/groups/#{group1.id}/subgroups", user2)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
context 'when using all_available in request' do
it 'returns public subgroups' do
get api("/groups/#{group1.id}/subgroups", user2), all_available: true
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response[0]['id']).to eq(subgroup1.id)
expect(json_response[0]['parent_id']).to eq(group1.id)
end
end
end
context 'when user is not member of a private group' do
it 'returns 404 for the private group' do
get api("/groups/#{group2.id}/subgroups", user1)
expect(response).to have_gitlab_http_status(404)
end
end
context 'when user is member of public group' do
before do
group1.add_guest(user2)
end
it 'returns private subgroups' do
get api("/groups/#{group1.id}/subgroups", user2)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
private_subgroups = json_response.select { |group| group['visibility'] == 'private' }
expect(private_subgroups.length).to eq(1)
expect(private_subgroups.first['id']).to eq(subgroup2.id)
expect(private_subgroups.first['parent_id']).to eq(group1.id)
end
context 'when using statistics in request' do
it 'does not include statistics' do
get api("/groups/#{group1.id}/subgroups", user2), statistics: true
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
end
end
end
context 'when user is member of private group' do
before do
group2.add_guest(user1)
end
it 'returns subgroups' do
get api("/groups/#{group2.id}/subgroups", user1)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(subgroup3.id)
expect(json_response.first['parent_id']).to eq(group2.id)
end
end
end
context 'when authenticated as admin' do
it 'returns private subgroups of a public group' do
get api("/groups/#{group1.id}/subgroups", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
it 'returns subgroups of a private group' do
get api("/groups/#{group2.id}/subgroups", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
it 'does not include statistics by default' do
get api("/groups/#{group1.id}/subgroups", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
it 'includes statistics if requested' do
get api("/groups/#{group1.id}/subgroups", admin), statistics: true
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).to include('statistics')
end
end
end
describe "POST /groups" do describe "POST /groups" do
context "when authenticated as user without group permissions" do context "when authenticated as user without group permissions" do
it "does not create group" do it "does not create group" do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment