mirror of
https://github.com/portainer/portainer.git
synced 2025-08-08 07:15:23 +02:00
refactor(teams): migrate teams to react [EE-2273] (#6691)
closes [EE-2273]
This commit is contained in:
parent
9b02f575ef
commit
f9427c8fb2
97 changed files with 1929 additions and 938 deletions
|
@ -6,7 +6,6 @@ import settingsModule from './settings';
|
|||
import featureFlagModule from './feature-flags';
|
||||
import userActivityModule from './user-activity';
|
||||
import servicesModule from './services';
|
||||
import teamsModule from './teams';
|
||||
import homeModule from './home';
|
||||
import { accessControlModule } from './access-control';
|
||||
import { reactModule } from './react';
|
||||
|
@ -40,7 +39,6 @@ angular
|
|||
userActivityModule,
|
||||
'portainer.shared.datatable',
|
||||
servicesModule,
|
||||
teamsModule,
|
||||
accessControlModule,
|
||||
reactModule,
|
||||
sidebarModule,
|
||||
|
@ -425,28 +423,6 @@ angular
|
|||
},
|
||||
};
|
||||
|
||||
var teams = {
|
||||
name: 'portainer.teams',
|
||||
url: '/teams',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/teams/teams.html',
|
||||
controller: 'TeamsController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var team = {
|
||||
name: 'portainer.teams.team',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/teams/edit/team.html',
|
||||
controller: 'TeamController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(root);
|
||||
$stateRegistryProvider.register(endpointRoot);
|
||||
$stateRegistryProvider.register(portainer);
|
||||
|
@ -478,8 +454,6 @@ angular
|
|||
$stateRegistryProvider.register(tags);
|
||||
$stateRegistryProvider.register(users);
|
||||
$stateRegistryProvider.register(user);
|
||||
$stateRegistryProvider.register(teams);
|
||||
$stateRegistryProvider.register(team);
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { server, rest } from '@/setup-tests/server';
|
|||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { renderWithQueryClient, within } from '@/react-tools/test-utils';
|
||||
import { Team, TeamId } from '@/portainer/teams/types';
|
||||
import { Team, TeamId } from '@/react/portainer/users/teams/types';
|
||||
import { createMockTeams } from '@/react-tools/test-mocks';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useReducer } from 'react';
|
|||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
import { Icon } from '@/react/components/Icon';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { TeamMembership, Role } from '@/portainer/teams/types';
|
||||
import { TeamMembership, TeamRole } from '@/react/portainer/users/teams/types';
|
||||
import { useUserMembership } from '@/portainer/users/queries';
|
||||
|
||||
import { TableContainer, TableTitle } from '@@/datatables';
|
||||
|
@ -138,7 +138,7 @@ function isLeaderOfAnyRestrictedTeams(
|
|||
) {
|
||||
return userMemberships.some(
|
||||
(membership) =>
|
||||
membership.Role === Role.TeamLeader &&
|
||||
membership.Role === TeamRole.Leader &&
|
||||
resourceControl.TeamAccesses.some((ta) => ta.TeamId === membership.TeamID)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import _ from 'lodash';
|
|||
|
||||
import { ownershipIcon, truncate } from '@/portainer/filters/filters';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import { TeamId } from '@/portainer/teams/types';
|
||||
import { useTeams } from '@/portainer/teams/queries';
|
||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||
import { useTeams } from '@/react/portainer/users/teams/queries';
|
||||
import { useUsers } from '@/portainer/users/queries';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
|
@ -178,17 +178,20 @@ function InheritanceMessage({
|
|||
}
|
||||
|
||||
function useAuthorizedTeams(authorizedTeamIds: TeamId[]) {
|
||||
return useTeams(authorizedTeamIds.length > 0, (teams) => {
|
||||
if (authorizedTeamIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return useTeams(false, {
|
||||
enabled: authorizedTeamIds.length > 0,
|
||||
select: (teams) => {
|
||||
if (authorizedTeamIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return _.compact(
|
||||
authorizedTeamIds.map((id) => {
|
||||
const team = teams.find((u) => u.Id === id);
|
||||
return team?.Name;
|
||||
})
|
||||
);
|
||||
return _.compact(
|
||||
authorizedTeamIds.map((id) => {
|
||||
const team = teams.find((u) => u.Id === id);
|
||||
return team?.Name;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Team } from '@/portainer/teams/types';
|
||||
import { Team } from '@/react/portainer/users/teams/types';
|
||||
|
||||
import { TeamsSelector } from '@@/TeamsSelector';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useTeams } from '@/portainer/teams/queries';
|
||||
import { useTeams } from '@/react/portainer/users/teams/queries';
|
||||
import { useUsers } from '@/portainer/users/queries';
|
||||
|
||||
export function useLoadState() {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
|
|||
|
||||
import { buildOption } from '@/portainer/components/BoxSelector';
|
||||
import { ownershipIcon } from '@/portainer/filters/filters';
|
||||
import { Team } from '@/portainer/teams/types';
|
||||
import { Team } from '@/react/portainer/users/teams/types';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector/types';
|
||||
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TeamId } from '@/portainer/teams/types';
|
||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
|
||||
export type ResourceControlId = number;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { TeamId } from '../teams/types';
|
||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||
|
||||
import { UserId } from '../users/types';
|
||||
|
||||
import {
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-white icon-primary icon-nested-blue'"></pr-icon>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
<pr-icon icon="'search'" feather="true"></pr-icon>
|
||||
<input
|
||||
type="text"
|
||||
class="searchInput"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search..."
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
<div class="actionBar" ng-show="$ctrl.isAdmin">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-dangerlight vertical-center"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="vertical-center">
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<table-column-header
|
||||
col-title="'Name'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Name'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Name')"
|
||||
></table-column-header>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
|
||||
ng-class="{ active: item.Checked }"
|
||||
>
|
||||
<td>
|
||||
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="portainer.teams.team({id: item.Id})">{{ item.Name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td class="text-center text-muted">No team available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span class="space-right"> Items per page </span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -1,14 +0,0 @@
|
|||
angular.module('portainer.app').component('teamsDatatable', {
|
||||
templateUrl: './teamsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
isAdmin: '<',
|
||||
},
|
||||
});
|
|
@ -2,7 +2,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|||
import { type EnvironmentGroupId } from '@/portainer/environment-groups/types';
|
||||
import { type TagId } from '@/portainer/tags/types';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import { TeamId } from '@/portainer/teams/types';
|
||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||
|
||||
import type {
|
||||
Environment,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { TeamId } from '@/portainer/teams/types';
|
||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
|
||||
import { EnvironmentId } from '../types';
|
||||
|
|
|
@ -14,8 +14,8 @@ import {
|
|||
isEdgeEnvironment,
|
||||
} from '@/portainer/environments/utils';
|
||||
import type { TagId } from '@/portainer/tags/types';
|
||||
import { useIsAdmin } from '@/portainer/hooks/useUser';
|
||||
import { useTags } from '@/portainer/tags/queries';
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
import { Link } from '@@/Link';
|
||||
|
@ -34,7 +34,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
||||
const isAdmin = useIsAdmin();
|
||||
const { isAdmin } = useUser();
|
||||
const isEdge = isEdgeEnvironment(environment.Type);
|
||||
|
||||
const snapshotTime = getSnapshotTime(environment);
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
EdgeTypes,
|
||||
} from '@/portainer/environments/types';
|
||||
import { EnvironmentGroupId } from '@/portainer/environment-groups/types';
|
||||
import { useIsAdmin } from '@/portainer/hooks/useUser';
|
||||
import {
|
||||
HomepageFilter,
|
||||
useHomePageFilter,
|
||||
|
@ -27,6 +26,7 @@ import { useTags } from '@/portainer/tags/queries';
|
|||
import { Filter } from '@/portainer/home/types';
|
||||
import { useAgentVersionsList } from '@/portainer/environments/queries/useAgentVersionsList';
|
||||
import { EnvironmentsQueryParams } from '@/portainer/environments/environment.service';
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { TableActions, TableContainer, TableTitle } from '@@/datatables';
|
||||
|
@ -69,7 +69,7 @@ enum ConnectionType {
|
|||
const storageKey = 'home_endpoints';
|
||||
|
||||
export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
||||
const isAdmin = useIsAdmin();
|
||||
const { isAdmin } = useUser();
|
||||
|
||||
const [platformTypes, setPlatformTypes] = useHomePageFilter<
|
||||
Filter<PlatformType>[]
|
||||
|
|
|
@ -176,8 +176,3 @@ export function UserProvider({ children }: UserProviderProps) {
|
|||
setUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
export function useIsAdmin() {
|
||||
const { user } = useUser();
|
||||
return !!user && isAdmin(user);
|
||||
}
|
||||
|
|
|
@ -9,9 +9,10 @@ import {
|
|||
} from '@/react/portainer/registries/ListView/DefaultRegistry';
|
||||
|
||||
import { wizardModule } from './wizard';
|
||||
import { teamsModule } from './teams';
|
||||
|
||||
export const viewsModule = angular
|
||||
.module('portainer.app.react.views', [wizardModule])
|
||||
.module('portainer.app.react.views', [wizardModule, teamsModule])
|
||||
.component('defaultRegistryName', r2a(DefaultRegistryName, []))
|
||||
.component('defaultRegistryAction', r2a(DefaultRegistryAction, []))
|
||||
.component('defaultRegistryDomain', r2a(DefaultRegistryDomain, []))
|
||||
|
|
34
app/portainer/react/views/teams.ts
Normal file
34
app/portainer/react/views/teams.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import angular from 'angular';
|
||||
import { StateRegistry } from '@uirouter/angularjs';
|
||||
|
||||
import { ItemView, ListView } from '@/react/portainer/users/teams';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
|
||||
export const teamsModule = angular
|
||||
.module('portainer.app.teams', [])
|
||||
.config(config)
|
||||
.component('teamView', r2a(ItemView, []))
|
||||
.component('teamsView', r2a(ListView, [])).name;
|
||||
|
||||
/* @ngInject */
|
||||
function config($stateRegistryProvider: StateRegistry) {
|
||||
$stateRegistryProvider.register({
|
||||
name: 'portainer.teams',
|
||||
url: '/teams',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'teamsView',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'portainer.teams.team',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'teamView',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { TeamId } from '../teams/types';
|
||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||
|
||||
export interface FDOConfiguration {
|
||||
enabled: boolean;
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
import { TeamViewModel } from '@/portainer/models/team';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
export function mockExampleData() {
|
||||
const teams: TeamViewModel[] = [
|
||||
{
|
||||
Id: 3,
|
||||
Name: 'Team 1',
|
||||
Checked: false,
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
Name: 'Team 2',
|
||||
Checked: false,
|
||||
},
|
||||
];
|
||||
|
||||
const users: UserViewModel[] = [
|
||||
{
|
||||
Id: 10,
|
||||
Username: 'user1',
|
||||
Role: 2,
|
||||
UserTheme: '',
|
||||
EndpointAuthorizations: {},
|
||||
PortainerAuthorizations: {
|
||||
PortainerDockerHubInspect: true,
|
||||
PortainerEndpointGroupInspect: true,
|
||||
PortainerEndpointGroupList: true,
|
||||
PortainerEndpointInspect: true,
|
||||
PortainerEndpointList: true,
|
||||
PortainerMOTD: true,
|
||||
PortainerRoleList: true,
|
||||
PortainerTeamList: true,
|
||||
PortainerTemplateInspect: true,
|
||||
PortainerTemplateList: true,
|
||||
PortainerUserInspect: true,
|
||||
PortainerUserList: true,
|
||||
PortainerUserMemberships: true,
|
||||
},
|
||||
RoleName: 'user',
|
||||
Checked: false,
|
||||
AuthenticationMethod: '',
|
||||
},
|
||||
{
|
||||
Id: 13,
|
||||
Username: 'user2',
|
||||
Role: 2,
|
||||
UserTheme: '',
|
||||
EndpointAuthorizations: {},
|
||||
PortainerAuthorizations: {
|
||||
PortainerDockerHubInspect: true,
|
||||
PortainerEndpointGroupInspect: true,
|
||||
PortainerEndpointGroupList: true,
|
||||
PortainerEndpointInspect: true,
|
||||
PortainerEndpointList: true,
|
||||
PortainerMOTD: true,
|
||||
PortainerRoleList: true,
|
||||
PortainerTeamList: true,
|
||||
PortainerTemplateInspect: true,
|
||||
PortainerTemplateList: true,
|
||||
PortainerUserInspect: true,
|
||||
PortainerUserList: true,
|
||||
PortainerUserMemberships: true,
|
||||
},
|
||||
RoleName: 'user',
|
||||
Checked: false,
|
||||
AuthenticationMethod: '',
|
||||
},
|
||||
];
|
||||
|
||||
return { users, teams };
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import { Meta } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { CreateTeamForm, FormValues } from './CreateTeamForm';
|
||||
import { mockExampleData } from './CreateTeamForm.mocks';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'teams/CreateTeamForm',
|
||||
component: CreateTeamForm,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export { Example };
|
||||
|
||||
function Example() {
|
||||
const [message, setMessage] = useState('');
|
||||
const { teams, users } = mockExampleData();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CreateTeamForm users={users} teams={teams} onSubmit={handleSubmit} />
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function handleSubmit(values: FormValues) {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
setMessage(
|
||||
`created team ${values.name} with ${values.leaders.length} leaders`
|
||||
);
|
||||
resolve();
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { render, waitFor } from '@/react-tools/test-utils';
|
||||
|
||||
import { CreateTeamForm } from './CreateTeamForm';
|
||||
|
||||
test('filling the name should make the submit button clickable and emptying it should make it disabled', async () => {
|
||||
const { findByLabelText, findByText } = render(
|
||||
<CreateTeamForm users={[]} teams={[]} onSubmit={() => {}} />
|
||||
);
|
||||
|
||||
const button = await findByText('Create team');
|
||||
expect(button).toBeVisible();
|
||||
|
||||
const nameField = await findByLabelText('Name*');
|
||||
expect(nameField).toBeVisible();
|
||||
expect(nameField).toHaveDisplayValue('');
|
||||
|
||||
expect(button).toBeDisabled();
|
||||
|
||||
const newValue = 'name';
|
||||
userEvent.type(nameField, newValue);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(nameField).toHaveDisplayValue(newValue);
|
||||
expect(button).toBeEnabled();
|
||||
});
|
||||
});
|
|
@ -1,119 +0,0 @@
|
|||
import { Formik, Field, Form } from 'formik';
|
||||
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { Icon } from '@/react/components/Icon';
|
||||
import { TeamViewModel } from '@/portainer/models/team';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { UsersSelector } from '@@/UsersSelector';
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
|
||||
import { validationSchema } from './CreateTeamForm.validation';
|
||||
|
||||
export interface FormValues {
|
||||
name: string;
|
||||
leaders: number[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
users: UserViewModel[];
|
||||
teams: TeamViewModel[];
|
||||
onSubmit(values: FormValues): void;
|
||||
}
|
||||
|
||||
export function CreateTeamForm({ users, teams, onSubmit }: Props) {
|
||||
const initialValues = {
|
||||
name: '',
|
||||
leaders: [],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<Widget>
|
||||
<WidgetTitle
|
||||
icon="plus"
|
||||
title="Add a new team"
|
||||
featherIcon
|
||||
className="vertical-center"
|
||||
/>
|
||||
<WidgetBody>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={() => validationSchema(teams)}
|
||||
onSubmit={onSubmit}
|
||||
validateOnMount
|
||||
>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
}) => (
|
||||
<Form
|
||||
className="form-horizontal"
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
>
|
||||
<FormControl
|
||||
inputId="team_name"
|
||||
label="Name"
|
||||
errors={errors.name}
|
||||
required
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="name"
|
||||
id="team_name"
|
||||
required
|
||||
placeholder="e.g. development"
|
||||
data-cy="team-teamNameInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{users.length > 0 && (
|
||||
<FormControl
|
||||
inputId="users-input"
|
||||
label="Select team leader(s)"
|
||||
tooltip="You can assign one or more leaders to this team. Team leaders can manage their teams users and resources."
|
||||
errors={errors.leaders}
|
||||
>
|
||||
<UsersSelector
|
||||
value={values.leaders}
|
||||
onChange={(leaders) =>
|
||||
setFieldValue('leaders', leaders)
|
||||
}
|
||||
users={users}
|
||||
dataCy="team-teamLeaderSelect"
|
||||
inputId="users-input"
|
||||
placeholder="Select one or more team leaders"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid}
|
||||
data-cy="team-createTeamButton"
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Creating team..."
|
||||
>
|
||||
<Icon icon="plus" feather size="md" />
|
||||
Create team
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { object, string, array, number } from 'yup';
|
||||
|
||||
import { TeamViewModel } from '@/portainer/models/team';
|
||||
|
||||
export function validationSchema(teams: TeamViewModel[]) {
|
||||
return object().shape({
|
||||
name: string()
|
||||
.required('This field is required.')
|
||||
.test(
|
||||
'is-unique',
|
||||
'This team already exists.',
|
||||
(name) => !!name && teams.every((team) => team.Name !== name)
|
||||
),
|
||||
leaders: array().of(number()),
|
||||
});
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { r2a } from '@/react-tools/react2angular';
|
||||
|
||||
import { CreateTeamForm } from './CreateTeamForm';
|
||||
|
||||
export { CreateTeamForm };
|
||||
|
||||
export const CreateTeamFormAngular = r2a(CreateTeamForm, [
|
||||
'users',
|
||||
'onSubmit',
|
||||
'teams',
|
||||
]);
|
|
@ -1,8 +0,0 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { CreateTeamFormAngular } from './CreateTeamForm';
|
||||
|
||||
export default angular
|
||||
.module('portainer.app.teams', [])
|
||||
|
||||
.component('createTeamForm', CreateTeamFormAngular).name;
|
|
@ -1,19 +0,0 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { getTeams } from './teams.service';
|
||||
import { Team } from './types';
|
||||
|
||||
export function useTeams<T = Team[]>(
|
||||
enabled = true,
|
||||
select: (data: Team[]) => T = (data) => data as unknown as T
|
||||
) {
|
||||
const teams = useQuery(['teams'], () => getTeams(), {
|
||||
meta: {
|
||||
error: { title: 'Failure', message: 'Unable to load teams' },
|
||||
},
|
||||
enabled,
|
||||
select,
|
||||
});
|
||||
|
||||
return teams;
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { Team, TeamId } from './types';
|
||||
|
||||
export async function getTeams() {
|
||||
try {
|
||||
const { data } = await axios.get<Team[]>(buildUrl());
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(id?: TeamId) {
|
||||
let url = '/teams';
|
||||
|
||||
if (id) {
|
||||
url += `/${id}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import { UserId } from '../users/types';
|
||||
|
||||
export type TeamId = number;
|
||||
|
||||
export enum Role {
|
||||
TeamLeader = 1,
|
||||
TeamMember,
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
Id: TeamId;
|
||||
Name: string;
|
||||
}
|
||||
|
||||
export interface TeamMembership {
|
||||
Id: number;
|
||||
UserID: UserId;
|
||||
TeamID: TeamId;
|
||||
Role: Role;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { Role as TeamRole, TeamMembership } from '../teams/types';
|
||||
import { TeamRole, TeamMembership } from '@/react/portainer/users/teams/types';
|
||||
|
||||
import { User, UserId } from './types';
|
||||
import { isAdmin } from './user.helpers';
|
||||
|
@ -26,14 +26,14 @@ export function useIsTeamLeader(user: User) {
|
|||
const query = useUserMembership(user.Id, {
|
||||
enabled: !isAdmin(user),
|
||||
select: (memberships) =>
|
||||
memberships.some((membership) => membership.Role === TeamRole.TeamLeader),
|
||||
memberships.some((membership) => membership.Role === TeamRole.Leader),
|
||||
});
|
||||
|
||||
return isAdmin(user) ? true : query.data;
|
||||
}
|
||||
|
||||
export function useUsers<T = User[]>(
|
||||
includeAdministrator: boolean,
|
||||
includeAdministrator = false,
|
||||
enabled = true,
|
||||
select: (data: User[]) => T = (data) => data as unknown as T
|
||||
) {
|
||||
|
|
|
@ -11,7 +11,7 @@ interface AuthorizationMap {
|
|||
[authorization: string]: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
export type User = {
|
||||
Id: UserId;
|
||||
Username: string;
|
||||
Role: Role;
|
||||
|
@ -29,4 +29,4 @@ export interface User {
|
|||
// this.AuthenticationMethod = data.AuthenticationMethod;
|
||||
// this.Checked = false;
|
||||
// this.EndpointAuthorizations = data.EndpointAuthorizations;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { TeamMembership } from '../teams/types';
|
||||
import { TeamMembership } from '@/react/portainer/users/teams/types';
|
||||
|
||||
import { User, UserId } from './types';
|
||||
import { filterNonAdministratorUsers } from './user.helpers';
|
||||
|
|
|
@ -1,223 +0,0 @@
|
|||
<page-header title="'Team details'" breadcrumbs="[{label:'Teams', link:'portainer.teams'}, team.Name]"> </page-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="users" feather-icon="true" title-text="Team details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<span ng-if="!settings.TeamSync">{{ team.Name }}</span>
|
||||
<button class="btn btn-xs btn-danger" ng-if="isAdmin" ng-click="deleteTeam()"> <pr-icon icon="'trash-2'" feather="true"></pr-icon>Delete this team </button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Leaders</td>
|
||||
<td>
|
||||
<span ng-if="!settings.TeamSync">{{ leaderCount }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total users in team</td>
|
||||
<td>{{ teamMembers.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="team && settings.TeamSync">
|
||||
<div class="col-sm-12 text-muted">
|
||||
<pr-icon icon="'alert-circle'" feather="true" class-name="'icon-warning'"></pr-icon>
|
||||
The team leader feature is disabled as external authentication is currently enabled with team sync.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="team">
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="users" feather-icon="true" title-text="Users">
|
||||
<div class="pull-md-right pull-lg-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count_users" ng-change="changePaginationCountUsers()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<button class="btn btn-primary btn-sm" ng-click="addAllUsers()" ng-if="isAdmin" ng-disabled="users.length === 0 || filteredUsers.length === 0 || settings.TeamSync">
|
||||
<pr-icon icon="'user-plus'" feather="true" class-name="'icon-white'" size="'sm'"></pr-icon>Add all users
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<input type="text" id="filter-users" ng-model="state.filterUsers" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Name'"
|
||||
can-sort="true"
|
||||
is-sorted="sortTypeUsers === 'Username'"
|
||||
is-sorted-desc="sortTypeUsers === 'Username' && sortReverseUsers"
|
||||
ng-click="orderUsers('Username')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
pagination-id="table1"
|
||||
dir-paginate="user in users | filter:state.filterUsers | orderBy:sortTypeUsers:sortReverseUsers | itemsPerPage: state.pagination_count_users"
|
||||
>
|
||||
<td>
|
||||
<span class="vertical-center">
|
||||
{{ user.Username }}
|
||||
<span class="space-left vertical-center">
|
||||
<a class="hyperlink vertical-center" ng-click="addUser(user)" ng-class="{ 'btn disabled py-0': settings.TeamSync }">
|
||||
<pr-icon icon="'plus-circle'" feather="true" size="'sm'"></pr-icon> Add
|
||||
</a>
|
||||
</span></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!users">
|
||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="users.length === 0 || (users | filter: state.filterUsers).length === 0">
|
||||
<td colspan="2" class="text-center text-muted">No users.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="users" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls pagination-id="table1"></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="users" feather-icon="true" title-text="Team members">
|
||||
<div class="pull-md-right pull-lg-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count_members" ng-change="changePaginationCountGroupMembers()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="removeAllUsers()"
|
||||
ng-if="isAdmin"
|
||||
ng-disabled="teamMembers.length === 0 || filteredGroupMembers.length === 0 || settings.TeamSync"
|
||||
>
|
||||
<pr-icon icon="'user-x'" feather="true" class-name="'icon-white'" size="'sm'"></pr-icon>Remove all users
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<input type="text" id="filter-group" ng-model="state.filterGroupMembers" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Name'"
|
||||
can-sort="true"
|
||||
is-sorted="sortTypeGroupMembers === 'Username'"
|
||||
is-sorted-desc="sortTypeGroupMembers === 'Username' && sortReverseGroupMembers"
|
||||
ng-click="orderGroupMembers('Username')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Team Role'"
|
||||
can-sort="true"
|
||||
is-sorted="sortTypeGroupMembers === 'TeamRole'"
|
||||
is-sorted-desc="sortTypeGroupMembers === 'TeamRole' && sortReverseGroupMembers"
|
||||
ng-click="orderGroupMembers('TeamRole')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
pagination-id="table2"
|
||||
dir-paginate="user in teamMembers | filter:state.filterGroupMembers | orderBy:sortTypeGroupMembers:sortReverseGroupMembers | itemsPerPage: state.pagination_count_groupMembers"
|
||||
>
|
||||
<td>
|
||||
<span class="vertical-center"
|
||||
>{{ user.Username }}
|
||||
<span class="space-left vertical-center" ng-if="isAdmin || user.TeamRole === 'Member'">
|
||||
<a class="hyperlink vertical-center" ng-click="removeUser(user)" ng-class="{ 'btn disabled py-0': settings.TeamSync }">
|
||||
<pr-icon icon="'minus-circle'" feather="true" size="'sm'"></pr-icon> Remove
|
||||
</a>
|
||||
</span></span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<span class="vertical-center"
|
||||
><pr-icon ng-if="user.TeamRole === 'Leader'" icon="'user-plus'" feather="true" size="'sm'"></pr-icon>
|
||||
<pr-icon ng-if="user.TeamRole === 'Member'" icon="'user'" feather="true" size="'sm'"></pr-icon>
|
||||
{{ user.TeamRole }}</span
|
||||
>
|
||||
<span class="space-left">
|
||||
<a
|
||||
class="space-left hyperlink vertical-center"
|
||||
ng-click="promoteToLeader(user)"
|
||||
ng-if="user.TeamRole === 'Member'"
|
||||
ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
||||
>
|
||||
<pr-icon icon="'user-plus'" feather="true" size="'sm'"></pr-icon> Leader
|
||||
</a>
|
||||
<a
|
||||
class="space-left hyperlink vertical-center"
|
||||
ng-click="demoteToMember(user)"
|
||||
ng-if="isAdmin && user.TeamRole === 'Leader'"
|
||||
ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
||||
>
|
||||
<pr-icon icon="'user-x'" feather="true" size="'sm'"></pr-icon> Member
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!teamMembers">
|
||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="teamMembers.length === 0 || (teamMembers | filter: state.filterGroupMembers).length === 0">
|
||||
<td colspan="2" class="text-center text-muted">No team members.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="teamMembers" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls pagination-id="table2"></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -1,214 +0,0 @@
|
|||
angular.module('portainer.app').controller('TeamController', [
|
||||
'$q',
|
||||
'$scope',
|
||||
'$state',
|
||||
'$transition$',
|
||||
'TeamService',
|
||||
'UserService',
|
||||
'TeamMembershipService',
|
||||
'ModalService',
|
||||
'Notifications',
|
||||
'PaginationService',
|
||||
'Authentication',
|
||||
'SettingsService',
|
||||
function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMembershipService, ModalService, Notifications, PaginationService, Authentication, SettingsService) {
|
||||
$scope.state = {
|
||||
pagination_count_users: PaginationService.getPaginationLimit('team_available_users'),
|
||||
pagination_count_members: PaginationService.getPaginationLimit('team_members'),
|
||||
};
|
||||
|
||||
$scope.sortTypeUsers = 'Username';
|
||||
$scope.sortReverseUsers = true;
|
||||
$scope.users = [];
|
||||
$scope.teamMembers = [];
|
||||
$scope.leaderCount = 0;
|
||||
|
||||
$scope.orderUsers = function (sortType) {
|
||||
$scope.sortReverseUsers = $scope.sortTypeUsers === sortType ? !$scope.sortReverseUsers : false;
|
||||
$scope.sortTypeUsers = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCountUsers = function () {
|
||||
PaginationService.setPaginationLimit('team_available_users', $scope.state.pagination_count_users);
|
||||
};
|
||||
|
||||
$scope.sortTypeGroupMembers = 'TeamRole';
|
||||
$scope.sortReverseGroupMembers = false;
|
||||
|
||||
$scope.orderGroupMembers = function (sortType) {
|
||||
$scope.sortReverseGroupMembers = $scope.sortTypeGroupMembers === sortType ? !$scope.sortReverseGroupMembers : false;
|
||||
$scope.sortTypeGroupMembers = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCountGroupMembers = function () {
|
||||
PaginationService.setPaginationLimit('team_members', $scope.state.pagination_count_members);
|
||||
};
|
||||
|
||||
$scope.deleteTeam = function () {
|
||||
ModalService.confirmDeletion('Do you want to delete this team? Users in this team will not be deleted.', function onConfirm(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
deleteTeam();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.promoteToLeader = function (user) {
|
||||
TeamMembershipService.updateMembership(user.MembershipId, user.Id, $scope.team.Id, 1)
|
||||
.then(function success() {
|
||||
$scope.leaderCount++;
|
||||
user.TeamRole = 'Leader';
|
||||
Notifications.success('User is now team leader', user.Username);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update user role');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.demoteToMember = function (user) {
|
||||
TeamMembershipService.updateMembership(user.MembershipId, user.Id, $scope.team.Id, 2)
|
||||
.then(function success() {
|
||||
user.TeamRole = 'Member';
|
||||
$scope.leaderCount--;
|
||||
Notifications.success('User is now team member', user.Username);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update user role');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addAllUsers = function () {
|
||||
var teamMembershipQueries = [];
|
||||
angular.forEach($scope.users, function (user) {
|
||||
teamMembershipQueries.push(TeamMembershipService.createMembership(user.Id, $scope.team.Id, 2));
|
||||
});
|
||||
$q.all(teamMembershipQueries)
|
||||
.then(function success(data) {
|
||||
var users = $scope.users;
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
var user = users[i];
|
||||
user.MembershipId = data[i].Id;
|
||||
user.TeamRole = 'Member';
|
||||
}
|
||||
$scope.teamMembers = $scope.teamMembers.concat(users);
|
||||
$scope.users = [];
|
||||
Notifications.success('Success', 'All users successfully added');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update team members');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addUser = function (user) {
|
||||
TeamMembershipService.createMembership(user.Id, $scope.team.Id, 2)
|
||||
.then(function success(data) {
|
||||
removeUserFromArray(user.Id, $scope.users);
|
||||
user.TeamRole = 'Member';
|
||||
user.MembershipId = data.Id;
|
||||
$scope.teamMembers.push(user);
|
||||
Notifications.success('User added to team', user.Username);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update team members');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeAllUsers = function () {
|
||||
var teamMembershipQueries = [];
|
||||
angular.forEach($scope.teamMembers, function (user) {
|
||||
teamMembershipQueries.push(TeamMembershipService.deleteMembership(user.MembershipId));
|
||||
});
|
||||
$q.all(teamMembershipQueries)
|
||||
.then(function success() {
|
||||
$scope.users = $scope.users.concat($scope.teamMembers);
|
||||
$scope.teamMembers = [];
|
||||
$scope.leaderCount = 0;
|
||||
Notifications.success('Success', 'All users successfully removed');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update team members');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeUser = function (user) {
|
||||
TeamMembershipService.deleteMembership(user.MembershipId)
|
||||
.then(function success() {
|
||||
removeUserFromArray(user.Id, $scope.teamMembers);
|
||||
if (user.TeamRole === 'Leader') {
|
||||
$scope.leaderCount--;
|
||||
}
|
||||
$scope.users.push(user);
|
||||
Notifications.success('User removed from team', user.Username);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update team members');
|
||||
});
|
||||
};
|
||||
|
||||
function deleteTeam() {
|
||||
TeamService.deleteTeam($scope.team.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Team successfully deleted', $scope.team.Name);
|
||||
$state.go('portainer.teams');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove team');
|
||||
});
|
||||
}
|
||||
|
||||
function removeUserFromArray(id, users) {
|
||||
for (var i = 0, l = users.length; i < l; i++) {
|
||||
if (users[i].Id === id) {
|
||||
users.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assignUsersAndMembers(users, memberships) {
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
var user = users[i];
|
||||
var member = false;
|
||||
for (var j = 0; j < memberships.length; j++) {
|
||||
var membership = memberships[j];
|
||||
if (user.Id === membership.UserId) {
|
||||
member = true;
|
||||
if (membership.Role === 1) {
|
||||
user.TeamRole = 'Leader';
|
||||
$scope.leaderCount++;
|
||||
} else {
|
||||
user.TeamRole = 'Member';
|
||||
}
|
||||
user.MembershipId = membership.Id;
|
||||
$scope.teamMembers.push(user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!member) {
|
||||
$scope.users.push(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initView() {
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
|
||||
try {
|
||||
$scope.settings = await SettingsService.publicSettings();
|
||||
|
||||
const data = await $q.all({
|
||||
team: TeamService.team($transition$.params().id),
|
||||
users: UserService.users($scope.isAdmin && $scope.settings.TeamSync),
|
||||
memberships: TeamService.userMemberships($transition$.params().id),
|
||||
});
|
||||
|
||||
$scope.team = data.team;
|
||||
assignUsersAndMembers(data.users, data.memberships);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve team details');
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
},
|
||||
]);
|
|
@ -1,9 +0,0 @@
|
|||
<page-header title="'Teams'" breadcrumbs="['Teams management']" reload="true"> </page-header>
|
||||
|
||||
<create-team-form ng-if="isAdmin && users" users="users" action-in-progress="state.actionInProgress" teams="teams" on-submit="(addTeam)"></create-team-form>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<teams-datatable title-text="Teams" title-icon="users" dataset="teams" table-key="teams" order-by="Name" remove-action="removeAction" is-admin="isAdmin"></teams-datatable>
|
||||
</div>
|
||||
</div>
|
|
@ -1,103 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.app').controller('TeamsController', [
|
||||
'$q',
|
||||
'$scope',
|
||||
'$state',
|
||||
'TeamService',
|
||||
'UserService',
|
||||
'ModalService',
|
||||
'Notifications',
|
||||
'Authentication',
|
||||
function ($q, $scope, $state, TeamService, UserService, ModalService, Notifications, Authentication) {
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
Leaders: [],
|
||||
};
|
||||
|
||||
$scope.checkNameValidity = function (form) {
|
||||
var valid = true;
|
||||
for (var i = 0; i < $scope.teams.length; i++) {
|
||||
if ($scope.formValues.Name === $scope.teams[i].Name) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
form.team_name.$setValidity('validName', valid);
|
||||
};
|
||||
|
||||
$scope.addTeam = function (formValues) {
|
||||
const teamName = formValues.name;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
TeamService.createTeam(teamName, formValues.leaders)
|
||||
.then(function success() {
|
||||
Notifications.success('Team successfully created', teamName);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create team');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
ModalService.confirmDeletion('Do you want to delete the selected team(s)? Users in the team(s) will not be deleted.', function onConfirm(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
deleteSelectedTeams(selectedItems);
|
||||
});
|
||||
};
|
||||
|
||||
function deleteSelectedTeams(selectedItems) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (team) {
|
||||
TeamService.deleteTeam(team.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Team successfully removed', team.Name);
|
||||
var index = $scope.teams.indexOf(team);
|
||||
$scope.teams.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove team');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = Authentication.isAdmin();
|
||||
$scope.isAdmin = isAdmin;
|
||||
$q.all({
|
||||
users: UserService.users(false),
|
||||
teams: isAdmin ? TeamService.teams() : UserService.userLeadingTeams(userDetails.ID),
|
||||
})
|
||||
.then(function success(data) {
|
||||
var teams = data.teams;
|
||||
$scope.teams = teams;
|
||||
$scope.users = _.orderBy(data.users, 'Username', 'asc');
|
||||
$scope.isTeamLeader = !!teams.length;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.teams = [];
|
||||
$scope.users = [];
|
||||
Notifications.error('Failure', err, 'Unable to retrieve teams');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
},
|
||||
]);
|
Loading…
Add table
Add a link
Reference in a new issue