Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/organizations')
-rw-r--r--app/assets/javascripts/organizations/constants.js4
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/app.vue21
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue43
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue46
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/constants.js2
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/index.js21
-rw-r--r--app/assets/javascripts/organizations/mock_data.js258
-rw-r--r--app/assets/javascripts/organizations/shared/components/groups_view.vue82
-rw-r--r--app/assets/javascripts/organizations/shared/components/projects_view.vue86
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql (renamed from app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql)0
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql (renamed from app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql)0
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js (renamed from app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js)6
-rw-r--r--app/assets/javascripts/organizations/shared/utils.js (renamed from app/assets/javascripts/organizations/groups_and_projects/utils.js)9
-rw-r--r--app/assets/javascripts/organizations/show/components/app.vue37
-rw-r--r--app/assets/javascripts/organizations/show/components/association_count_card.vue54
-rw-r--r--app/assets/javascripts/organizations/show/components/association_counts.vue71
-rw-r--r--app/assets/javascripts/organizations/show/components/groups_and_projects.vue110
-rw-r--r--app/assets/javascripts/organizations/show/components/organization_avatar.vue71
-rw-r--r--app/assets/javascripts/organizations/show/constants.js1
-rw-r--r--app/assets/javascripts/organizations/show/index.js63
-rw-r--r--app/assets/javascripts/organizations/show/utils.js4
21 files changed, 877 insertions, 112 deletions
diff --git a/app/assets/javascripts/organizations/constants.js b/app/assets/javascripts/organizations/constants.js
new file mode 100644
index 00000000000..8ade37b169e
--- /dev/null
+++ b/app/assets/javascripts/organizations/constants.js
@@ -0,0 +1,4 @@
+export const RESOURCE_TYPE_GROUPS = 'groups';
+export const RESOURCE_TYPE_PROJECTS = 'projects';
+
+export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
index 10471cc1fdd..dba738de5e1 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
@@ -13,9 +13,10 @@ import {
FILTERED_SEARCH_TERM,
TOKEN_EMPTY_SEARCH_TERM,
} from '~/vue_shared/components/filtered_search_bar/constants';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
+import GroupsView from '../../shared/components/groups_view.vue';
+import ProjectsView from '../../shared/components/projects_view.vue';
import {
- DISPLAY_QUERY_GROUPS,
- DISPLAY_QUERY_PROJECTS,
DISPLAY_LISTBOX_ITEMS,
SORT_DIRECTION_ASC,
SORT_DIRECTION_DESC,
@@ -23,8 +24,6 @@ import {
SORT_ITEM_CREATED,
FILTERED_SEARCH_TERM_KEY,
} from '../constants';
-import GroupsPage from './groups_page.vue';
-import ProjectsPage from './projects_page.vue';
export default {
i18n: {
@@ -45,14 +44,14 @@ export default {
const { display } = this.$route.query;
switch (display) {
- case DISPLAY_QUERY_GROUPS:
- return GroupsPage;
+ case RESOURCE_TYPE_GROUPS:
+ return GroupsView;
- case DISPLAY_QUERY_PROJECTS:
- return ProjectsPage;
+ case RESOURCE_TYPE_PROJECTS:
+ return ProjectsView;
default:
- return GroupsPage;
+ return GroupsView;
}
},
activeSortItem() {
@@ -80,9 +79,9 @@ export default {
displayListboxSelected() {
const { display } = this.$route.query;
- return [DISPLAY_QUERY_GROUPS, DISPLAY_QUERY_PROJECTS].includes(display)
+ return [RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS].includes(display)
? display
- : DISPLAY_QUERY_GROUPS;
+ : RESOURCE_TYPE_GROUPS;
},
},
methods: {
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue
deleted file mode 100644
index 20db38403f7..00000000000
--- a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { s__ } from '~/locale';
-import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
-import groupsQuery from '../graphql/queries/groups.query.graphql';
-import { formatGroups } from '../utils';
-
-export default {
- i18n: {
- errorMessage: s__(
- 'Organization|An error occurred loading the groups. Please refresh the page to try again.',
- ),
- },
- components: { GlLoadingIcon, GroupsList },
- data() {
- return {
- groups: [],
- };
- },
- apollo: {
- groups: {
- query: groupsQuery,
- update(data) {
- return formatGroups(data.organization.groups.nodes);
- },
- error(error) {
- createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
- },
- },
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.groups.loading;
- },
- },
-};
-</script>
-
-<template>
- <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <groups-list v-else :groups="groups" show-group-icon />
-</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue
deleted file mode 100644
index d6958ee996e..00000000000
--- a/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
-import { createAlert } from '~/alert';
-import projectsQuery from '../graphql/queries/projects.query.graphql';
-import { formatProjects } from '../utils';
-
-export default {
- i18n: {
- errorMessage: s__(
- 'Organization|An error occurred loading the projects. Please refresh the page to try again.',
- ),
- },
- components: {
- ProjectsList,
- GlLoadingIcon,
- },
- data() {
- return {
- projects: [],
- };
- },
- apollo: {
- projects: {
- query: projectsQuery,
- update(data) {
- return formatProjects(data.organization.projects.nodes);
- },
- error(error) {
- createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
- },
- },
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.projects.loading;
- },
- },
-};
-</script>
-
-<template>
- <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <projects-list v-else :projects="projects" show-project-icon />
-</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/constants.js b/app/assets/javascripts/organizations/groups_and_projects/constants.js
index 529caa666a0..d79b632f6fb 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/constants.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/constants.js
@@ -3,8 +3,6 @@ import { __ } from '~/locale';
export const DISPLAY_QUERY_GROUPS = 'groups';
export const DISPLAY_QUERY_PROJECTS = 'projects';
-export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
-
export const FILTERED_SEARCH_TERM_KEY = 'search';
export const DISPLAY_LISTBOX_ITEMS = [
diff --git a/app/assets/javascripts/organizations/groups_and_projects/index.js b/app/assets/javascripts/organizations/groups_and_projects/index.js
index f3f15c635f1..3e05e4d0a4c 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/index.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/index.js
@@ -2,9 +2,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
-import resolvers from './graphql/resolvers';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { ORGANIZATION_ROOT_ROUTE_NAME } from '../constants';
+import resolvers from '../shared/graphql/resolvers';
import App from './components/app.vue';
-import { ORGANIZATION_ROOT_ROUTE_NAME } from './constants';
export const createRouter = () => {
const routes = [{ path: '/', name: ORGANIZATION_ROOT_ROUTE_NAME }];
@@ -23,6 +24,16 @@ export const initOrganizationsGroupsAndProjects = () => {
if (!el) return false;
+ const {
+ dataset: { appData },
+ } = el;
+ const {
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
Vue.use(VueRouter);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers),
@@ -34,6 +45,12 @@ export const initOrganizationsGroupsAndProjects = () => {
name: 'OrganizationsGroupsAndProjects',
apolloProvider,
router,
+ provide: {
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ },
render(createElement) {
return createElement(App);
},
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
new file mode 100644
index 00000000000..17ab7bd1d34
--- /dev/null
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -0,0 +1,258 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+// This is temporary mock data that will be removed when completing the following:
+// https://gitlab.com/gitlab-org/gitlab/-/issues/420777
+// https://gitlab.com/gitlab-org/gitlab/-/issues/421441
+
+export const organization = {
+ id: 'gid://gitlab/Organization/1',
+ __typename: 'Organization',
+};
+
+export const organizationProjects = {
+ nodes: [
+ {
+ id: 'gid://gitlab/Project/8',
+ nameWithNamespace: 'Twitter / Typeahead.Js',
+ webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
+ topics: ['JavaScript', 'Vue.js'],
+ forksCount: 4,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'public',
+ openIssuesCount: 48,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: true,
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/7',
+ nameWithNamespace: 'Flightjs / Flight',
+ webUrl: 'http://127.0.0.1:3000/flightjs/Flight',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 37,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/6',
+ nameWithNamespace: 'Jashkenas / Underscore',
+ webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/5',
+ nameWithNamespace: 'Commit451 / Lab Coat',
+ webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 49,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/1',
+ nameWithNamespace: 'Toolbox / Gitlab Smoke Tests',
+ webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ ],
+};
+
+export const organizationGroups = {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/29',
+ fullName: 'Commit451',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/Commit451',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 0,
+ projectsCount: 3,
+ groupMembersCount: 2,
+ visibility: 'public',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/33',
+ fullName: 'Flightjs',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/flightjs',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:60" dir="auto">Ipsa reiciendis deleniti officiis illum nostrum quo aliquam.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 3,
+ groupMembersCount: 1,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/24',
+ fullName: 'Gitlab Org',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:64" dir="auto">Dolorem dolorem omnis impedit cupiditate pariatur officia velit.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 1,
+ projectsCount: 1,
+ groupMembersCount: 2,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/27',
+ fullName: 'Gnuwget',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/gnuwgetf',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:47" dir="auto">Culpa soluta aut eius dolores est vel sapiente.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 2,
+ groupMembersCount: 3,
+ visibility: 'public',
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/31',
+ fullName: 'Jashkenas',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/jashkenas',
+ descriptionHtml: '<p data-sourcepos="1:1-1:25" dir="auto">Ut ut id aliquid nostrum.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 3,
+ projectsCount: 3,
+ groupMembersCount: 10,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/22',
+ fullName: 'Toolbox',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/toolbox',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:46" dir="auto">Quo voluptatem magnam facere voluptates alias.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 2,
+ projectsCount: 3,
+ groupMembersCount: 40,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/35',
+ fullName: 'Twitter',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/twitter',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:40" dir="auto">Quae nulla consequatur assumenda id quo.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 20,
+ projectsCount: 30,
+ groupMembersCount: 100,
+ visibility: 'public',
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/73',
+ fullName: 'test',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/test',
+ descriptionHtml: '',
+ avatarUrl: null,
+ descendantGroupsCount: 1,
+ projectsCount: 1,
+ groupMembersCount: 1,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/74',
+ fullName: 'Twitter / test subgroup',
+ parent: {
+ id: 'gid://gitlab/Group/35',
+ },
+ webUrl: 'http://127.0.0.1:3000/groups/twitter/test-subgroup',
+ descriptionHtml: '',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 4,
+ groupMembersCount: 4,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ ],
+};
diff --git a/app/assets/javascripts/organizations/shared/components/groups_view.vue b/app/assets/javascripts/organizations/shared/components/groups_view.vue
new file mode 100644
index 00000000000..eaa3017ef97
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/groups_view.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { s__, __ } from '~/locale';
+import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
+import groupsQuery from '../graphql/queries/groups.query.graphql';
+import { formatGroups } from '../utils';
+
+export default {
+ i18n: {
+ errorMessage: s__(
+ 'Organization|An error occurred loading the groups. Please refresh the page to try again.',
+ ),
+ emptyState: {
+ title: s__("Organization|You don't have any groups yet."),
+ description: s__(
+ 'Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
+ ),
+ primaryButtonText: __('New group'),
+ },
+ },
+ components: { GlLoadingIcon, GlEmptyState, GroupsList },
+ inject: {
+ groupsEmptyStateSvgPath: {},
+ newGroupPath: {
+ default: null,
+ },
+ },
+ props: {
+ shouldShowEmptyStateButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ groups: [],
+ };
+ },
+ apollo: {
+ groups: {
+ query: groupsQuery,
+ update(data) {
+ return formatGroups(data.organization.groups.nodes);
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.groups.loading;
+ },
+ emptyStateProps() {
+ const baseProps = {
+ svgHeight: 144,
+ svgPath: this.groupsEmptyStateSvgPath,
+ title: this.$options.i18n.emptyState.title,
+ description: this.$options.i18n.emptyState.description,
+ };
+
+ if (this.shouldShowEmptyStateButtons && this.newGroupPath) {
+ return {
+ ...baseProps,
+ primaryButtonLink: this.newGroupPath,
+ primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
+ };
+ }
+
+ return baseProps;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
+ <groups-list v-else-if="groups.length" :groups="groups" show-group-icon />
+ <gl-empty-state v-else v-bind="emptyStateProps" />
+</template>
diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue
new file mode 100644
index 00000000000..9bf4e597884
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { createAlert } from '~/alert';
+import projectsQuery from '../graphql/queries/projects.query.graphql';
+import { formatProjects } from '../utils';
+
+export default {
+ i18n: {
+ errorMessage: s__(
+ 'Organization|An error occurred loading the projects. Please refresh the page to try again.',
+ ),
+ emptyState: {
+ title: s__("Organization|You don't have any projects yet."),
+ description: s__(
+ 'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of Gitlab.',
+ ),
+ primaryButtonText: __('New project'),
+ },
+ },
+ components: {
+ ProjectsList,
+ GlLoadingIcon,
+ GlEmptyState,
+ },
+ inject: {
+ projectsEmptyStateSvgPath: {},
+ newProjectPath: {
+ default: null,
+ },
+ },
+ props: {
+ shouldShowEmptyStateButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ projects: [],
+ };
+ },
+ apollo: {
+ projects: {
+ query: projectsQuery,
+ update(data) {
+ return formatProjects(data.organization.projects.nodes);
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.projects.loading;
+ },
+ emptyStateProps() {
+ const baseProps = {
+ svgHeight: 144,
+ svgPath: this.projectsEmptyStateSvgPath,
+ title: this.$options.i18n.emptyState.title,
+ description: this.$options.i18n.emptyState.description,
+ };
+
+ if (this.shouldShowEmptyStateButtons && this.newProjectPath) {
+ return {
+ ...baseProps,
+ primaryButtonLink: this.newProjectPath,
+ primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
+ };
+ }
+
+ return baseProps;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
+ <projects-list v-else-if="projects.length" :projects="projects" show-project-icon />
+ <gl-empty-state v-else v-bind="emptyStateProps" />
+</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql
index 842c601e326..842c601e326 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
index 2a7971e1106..2a7971e1106 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index 8a375b28797..c78266b0476 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -1,8 +1,4 @@
-import {
- organization,
- organizationProjects,
- organizationGroups,
-} from 'jest/organizations/groups_and_projects/mock_data';
+import { organization, organizationProjects, organizationGroups } from '../../mock_data';
export default {
Query: {
diff --git a/app/assets/javascripts/organizations/groups_and_projects/utils.js b/app/assets/javascripts/organizations/shared/utils.js
index d2a4e05e806..c1aafefc553 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/utils.js
+++ b/app/assets/javascripts/organizations/shared/utils.js
@@ -1,5 +1,5 @@
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/projects_list/constants';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
export const formatProjects = (projects) =>
projects.map(({ id, nameWithNamespace, accessLevel, webUrl, ...project }) => ({
@@ -13,11 +13,14 @@ export const formatProjects = (projects) =>
},
webUrl,
editPath: `${webUrl}/edit`,
- actions: [ACTION_EDIT, ACTION_DELETE],
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
}));
export const formatGroups = (groups) =>
- groups.map(({ id, ...group }) => ({
+ groups.map(({ id, webUrl, ...group }) => ({
...group,
id: getIdFromGraphQLId(id),
+ webUrl,
+ editPath: `${webUrl}/-/edit`,
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
}));
diff --git a/app/assets/javascripts/organizations/show/components/app.vue b/app/assets/javascripts/organizations/show/components/app.vue
new file mode 100644
index 00000000000..47264d80454
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/app.vue
@@ -0,0 +1,37 @@
+<script>
+import OrganizationAvatar from './organization_avatar.vue';
+import GroupsAndProjects from './groups_and_projects.vue';
+import AssociationCounts from './association_counts.vue';
+
+export default {
+ name: 'OrganizationShowApp',
+ components: { OrganizationAvatar, GroupsAndProjects, AssociationCounts },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ groupsAndProjectsOrganizationPath: {
+ type: String,
+ required: true,
+ },
+ associationCounts: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-6">
+ <organization-avatar :organization="organization" />
+ <association-counts
+ :association-counts="associationCounts"
+ :groups-and-projects-organization-path="groupsAndProjectsOrganizationPath"
+ />
+ <groups-and-projects
+ :groups-and-projects-organization-path="groupsAndProjectsOrganizationPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/association_count_card.vue b/app/assets/javascripts/organizations/show/components/association_count_card.vue
new file mode 100644
index 00000000000..0567f43132f
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/association_count_card.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlIcon, GlLink, GlCard } from '@gitlab/ui';
+import { numberToMetricPrefix } from '~/lib/utils/number_utils';
+import { __ } from '~/locale';
+
+export default {
+ name: 'AssociationCountCard',
+ components: { GlIcon, GlLink, GlCard },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ iconName: {
+ type: String,
+ required: true,
+ },
+ count: {
+ type: Number,
+ required: true,
+ },
+ linkHref: {
+ type: String,
+ required: true,
+ },
+ linkText: {
+ type: String,
+ required: false,
+ default: __('View all'),
+ },
+ },
+ computed: {
+ formattedCount() {
+ return numberToMetricPrefix(this.count, 0);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-card>
+ <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
+ <div class="gl-display-flex gl-align-items-center gl-text-gray-700">
+ <gl-icon :name="iconName" />
+ <span class="gl-ml-2">{{ title }}</span>
+ </div>
+ <gl-link :href="linkHref">{{ linkText }}</gl-link>
+ </div>
+ <span
+ class="gl-font-size-h-display gl-font-weight-bold gl-line-height-ratio-1000 gl-mt-2 gl-display-block"
+ >{{ formattedCount }}</span
+ >
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/association_counts.vue b/app/assets/javascripts/organizations/show/components/association_counts.vue
new file mode 100644
index 00000000000..3b312924bd2
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/association_counts.vue
@@ -0,0 +1,71 @@
+<script>
+import { __, s__ } from '~/locale';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
+import AssociationCountCard from './association_count_card.vue';
+
+export default {
+ name: 'AssociationCounts',
+ i18n: {
+ groups: __('Groups'),
+ projects: __('Projects'),
+ users: __('Users'),
+ viewAll: __('View all'),
+ manage: s__('Organization|Manage'),
+ },
+ components: { AssociationCountCard },
+ props: {
+ associationCounts: {
+ type: Object,
+ required: true,
+ },
+ groupsAndProjectsOrganizationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ groupsLinkHref() {
+ return `${this.groupsAndProjectsOrganizationPath}?display=${RESOURCE_TYPE_GROUPS}`;
+ },
+ projectsLinkHref() {
+ return `${this.groupsAndProjectsOrganizationPath}?display=${RESOURCE_TYPE_PROJECTS}`;
+ },
+ associationCountCards() {
+ return [
+ {
+ title: this.$options.i18n.groups,
+ iconName: 'group',
+ count: this.associationCounts.groups,
+ linkHref: this.groupsLinkHref,
+ },
+ {
+ title: this.$options.i18n.projects,
+ iconName: 'project',
+ count: this.associationCounts.projects,
+ linkHref: this.projectsLinkHref,
+ },
+ {
+ title: this.$options.i18n.users,
+ iconName: 'users',
+ count: this.associationCounts.users,
+ linkText: this.$options.i18n.manage,
+ // TODO: update `linkHref` prop to point to users route
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/409313
+ linkHref: '/',
+ },
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-grid gl-lg-grid-template-columns-4 gl-mt-5 gl-gap-5">
+ <association-count-card
+ v-for="props in associationCountCards"
+ :key="props.title"
+ v-bind="props"
+ class="gl-w-full"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/groups_and_projects.vue b/app/assets/javascripts/organizations/show/components/groups_and_projects.vue
new file mode 100644
index 00000000000..e8972f3b380
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/groups_and_projects.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlCollapsibleListbox, GlLink } from '@gitlab/ui';
+import { isEqual } from 'lodash';
+import { s__, __ } from '~/locale';
+import GroupsView from '../../shared/components/groups_view.vue';
+import ProjectsView from '../../shared/components/projects_view.vue';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
+import { FILTER_FREQUENTLY_VISITED } from '../constants';
+import { buildDisplayListboxItem } from '../utils';
+
+export default {
+ name: 'OrganizationFrontPageGroupsAndProjects',
+ i18n: {
+ displayListboxLabel: __('Display'),
+ viewAll: s__('Organization|View all'),
+ },
+ displayListboxLabelId: 'display-listbox-label',
+ components: { GlCollapsibleListbox, GlLink },
+ displayListboxItems: [
+ buildDisplayListboxItem({
+ filter: FILTER_FREQUENTLY_VISITED,
+ resourceType: RESOURCE_TYPE_PROJECTS,
+ text: s__('Organization|Frequently visited projects'),
+ }),
+ buildDisplayListboxItem({
+ filter: FILTER_FREQUENTLY_VISITED,
+ resourceType: RESOURCE_TYPE_GROUPS,
+ text: s__('Organization|Frequently visited groups'),
+ }),
+ ],
+ props: {
+ groupsAndProjectsOrganizationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ displayListboxSelected() {
+ const { display } = this.$route.query;
+ const [{ value: fallbackSelected }] = this.$options.displayListboxItems;
+
+ return (
+ this.$options.displayListboxItems.find(({ value }) => value === display)?.value ||
+ fallbackSelected
+ );
+ },
+ resourceTypeSelected() {
+ return [RESOURCE_TYPE_PROJECTS, RESOURCE_TYPE_GROUPS].find((resourceType) =>
+ this.displayListboxSelected.endsWith(resourceType),
+ );
+ },
+ routerView() {
+ switch (this.resourceTypeSelected) {
+ case RESOURCE_TYPE_GROUPS:
+ return GroupsView;
+
+ case RESOURCE_TYPE_PROJECTS:
+ return ProjectsView;
+
+ default:
+ return ProjectsView;
+ }
+ },
+ groupsAndProjectsOrganizationPathWithQueryParam() {
+ return `${this.groupsAndProjectsOrganizationPath}?display=${this.resourceTypeSelected}`;
+ },
+ },
+ methods: {
+ pushQuery(query) {
+ const currentQuery = this.$route.query;
+
+ if (isEqual(currentQuery, query)) {
+ return;
+ }
+
+ this.$router.push({ query });
+ },
+ onDisplayListboxSelect(display) {
+ this.pushQuery({ display });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-mt-7">
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div>
+ <label
+ :id="$options.displayListboxLabelId"
+ class="gl-display-block gl-mb-2"
+ data-testid="label"
+ >{{ $options.i18n.displayListboxLabel }}</label
+ >
+ <gl-collapsible-listbox
+ block
+ toggle-class="gl-w-30"
+ :selected="displayListboxSelected"
+ :items="$options.displayListboxItems"
+ :toggle-aria-labelled-by="$options.displayListboxLabelId"
+ @select="onDisplayListboxSelect"
+ />
+ </div>
+ <gl-link class="gl-mt-5" :href="groupsAndProjectsOrganizationPathWithQueryParam">{{
+ $options.i18n.viewAll
+ }}</gl-link>
+ </div>
+ <component :is="routerView" should-show-empty-state-buttons class="gl-mt-5" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/organization_avatar.vue b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
new file mode 100644
index 00000000000..c57ee0ea5b5
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlAvatar, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import {
+ VISIBILITY_TYPE_ICON,
+ ORGANIZATION_VISIBILITY_TYPE,
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+} from '~/visibility_level/constants';
+
+export default {
+ name: 'OrganizationAvatar',
+ AVATAR_SHAPE_OPTION_RECT,
+ i18n: {
+ copyButtonText: s__('Organization|Copy organization ID'),
+ orgId: s__('Organization|Org ID'),
+ },
+ components: { GlAvatar, GlIcon, ClipboardButton },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ visibilityIcon() {
+ return VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PUBLIC_STRING];
+ },
+ visibilityTooltip() {
+ return ORGANIZATION_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-avatar
+ :entity-id="organization.id"
+ :entity-name="organization.name"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :size="64"
+ />
+ <div class="gl-ml-3">
+ <div class="gl-display-flex gl-align-items-center">
+ <h1 class="gl-m-0 gl-font-size-h1">{{ organization.name }}</h1>
+ <gl-icon
+ v-gl-tooltip="visibilityTooltip"
+ :name="visibilityIcon"
+ class="gl-text-secondary gl-ml-3"
+ />
+ </div>
+ <div class="gl-display-flex gl-align-items-center">
+ <span class="gl-text-secondary gl-font-sm"
+ >{{ $options.i18n.orgId }}: {{ organization.id }}</span
+ >
+ <clipboard-button
+ class="gl-ml-2"
+ category="tertiary"
+ size="small"
+ :title="$options.i18n.copyButtonText"
+ :text="organization.id.toString()"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/constants.js b/app/assets/javascripts/organizations/show/constants.js
new file mode 100644
index 00000000000..fe29af67f6b
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/constants.js
@@ -0,0 +1 @@
+export const FILTER_FREQUENTLY_VISITED = 'frequently_visited';
diff --git a/app/assets/javascripts/organizations/show/index.js b/app/assets/javascripts/organizations/show/index.js
new file mode 100644
index 00000000000..83a9c37e325
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/index.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import VueApollo from 'vue-apollo';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import createDefaultClient from '~/lib/graphql';
+import { ORGANIZATION_ROOT_ROUTE_NAME } from '../constants';
+import resolvers from '../shared/graphql/resolvers';
+import App from './components/app.vue';
+
+export const createRouter = () => {
+ const routes = [{ path: '/', name: ORGANIZATION_ROOT_ROUTE_NAME }];
+
+ const router = new VueRouter({
+ routes,
+ base: '/',
+ mode: 'history',
+ });
+
+ return router;
+};
+
+export const initOrganizationsShow = () => {
+ const el = document.getElementById('js-organizations-show');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const {
+ organization,
+ groupsAndProjectsOrganizationPath,
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ associationCounts,
+ } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
+ Vue.use(VueRouter);
+ const router = createRouter();
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'OrganizationShowRoot',
+ apolloProvider,
+ router,
+ provide: {
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ },
+ render(createElement) {
+ return createElement(App, {
+ props: { organization, groupsAndProjectsOrganizationPath, associationCounts },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/show/utils.js b/app/assets/javascripts/organizations/show/utils.js
new file mode 100644
index 00000000000..b4f935563aa
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/utils.js
@@ -0,0 +1,4 @@
+export const buildDisplayListboxItem = ({ filter, resourceType, text }) => ({
+ text,
+ value: `${filter}_${resourceType}`,
+});