diff --git a/api/http/handler/teams/team_list.go b/api/http/handler/teams/team_list.go
index 72fb36c81..db012e331 100644
--- a/api/http/handler/teams/team_list.go
+++ b/api/http/handler/teams/team_list.go
@@ -4,6 +4,7 @@ import (
"net/http"
httperror "github.com/portainer/libhttp/error"
+ "github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api/http/security"
)
@@ -13,6 +14,7 @@ import (
// @description List teams. For non-administrator users, will only list the teams they are member of.
// @description **Access policy**: restricted
// @tags teams
+// @param onlyLedTeams query boolean false "Only list teams that the user is leader of"
// @security ApiKeyAuth
// @security jwt
// @produce json
@@ -22,15 +24,23 @@ import (
func (handler *Handler) teamList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
teams, err := handler.DataStore.Team().Teams()
if err != nil {
- return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve teams from the database", err}
+ return httperror.InternalServerError("Unable to retrieve teams from the database", err)
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
- return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
+ return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
- filteredTeams := security.FilterUserTeams(teams, securityContext)
+ onlyLedTeams, _ := request.RetrieveBooleanQueryParameter(r, "onlyLedTeams", true)
+
+ filteredTeams := teams
+
+ if onlyLedTeams {
+ filteredTeams = security.FilterLeaderTeams(filteredTeams, securityContext)
+ }
+
+ filteredTeams = security.FilterUserTeams(filteredTeams, securityContext)
return response.JSON(w, filteredTeams)
}
diff --git a/api/http/security/filter.go b/api/http/security/filter.go
index 923ff0977..3f746c890 100644
--- a/api/http/security/filter.go
+++ b/api/http/security/filter.go
@@ -27,17 +27,22 @@ func FilterUserTeams(teams []portainer.Team, context *RestrictedRequestContext)
// FilterLeaderTeams filters teams based on user role.
// Team leaders only have access to team they lead.
func FilterLeaderTeams(teams []portainer.Team, context *RestrictedRequestContext) []portainer.Team {
- filteredTeams := teams
+ filteredTeams := []portainer.Team{}
- if context.IsTeamLeader {
- filteredTeams = make([]portainer.Team, 0)
- for _, membership := range context.UserMemberships {
- for _, team := range teams {
- if team.ID == membership.TeamID && membership.Role == portainer.TeamLeader {
- filteredTeams = append(filteredTeams, team)
- break
- }
- }
+ if !context.IsTeamLeader {
+ return filteredTeams
+ }
+
+ leaderSet := map[portainer.TeamID]bool{}
+ for _, membership := range context.UserMemberships {
+ if membership.Role == portainer.TeamLeader && membership.UserID == context.UserID {
+ leaderSet[membership.TeamID] = true
+ }
+ }
+
+ for _, team := range teams {
+ if leaderSet[team.ID] {
+ filteredTeams = append(filteredTeams, team)
}
}
diff --git a/app/portainer/__module.js b/app/portainer/__module.js
index 5b758de45..28c7e8a94 100644
--- a/app/portainer/__module.js
+++ b/app/portainer/__module.js
@@ -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);
},
]);
diff --git a/app/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx b/app/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx
index c37fd9360..0775d8999 100644
--- a/app/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx
+++ b/app/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx
@@ -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';
diff --git a/app/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx b/app/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx
index 9e43e3de9..96de9606a 100644
--- a/app/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx
+++ b/app/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx
@@ -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)
);
}
diff --git a/app/portainer/access-control/AccessControlPanel/AccessControlPanelDetails.tsx b/app/portainer/access-control/AccessControlPanel/AccessControlPanelDetails.tsx
index e9eb9b670..d2f7f8ec0 100644
--- a/app/portainer/access-control/AccessControlPanel/AccessControlPanelDetails.tsx
+++ b/app/portainer/access-control/AccessControlPanel/AccessControlPanelDetails.tsx
@@ -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;
+ })
+ );
+ },
});
}
diff --git a/app/portainer/access-control/EditDetails/TeamsField.tsx b/app/portainer/access-control/EditDetails/TeamsField.tsx
index 456b40de8..02f9f3aa3 100644
--- a/app/portainer/access-control/EditDetails/TeamsField.tsx
+++ b/app/portainer/access-control/EditDetails/TeamsField.tsx
@@ -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';
diff --git a/app/portainer/access-control/EditDetails/useLoadState.ts b/app/portainer/access-control/EditDetails/useLoadState.ts
index 0cbeadea1..991dcb6e0 100644
--- a/app/portainer/access-control/EditDetails/useLoadState.ts
+++ b/app/portainer/access-control/EditDetails/useLoadState.ts
@@ -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() {
diff --git a/app/portainer/access-control/EditDetails/useOptions.tsx b/app/portainer/access-control/EditDetails/useOptions.tsx
index 3b5f19715..3f8289fdc 100644
--- a/app/portainer/access-control/EditDetails/useOptions.tsx
+++ b/app/portainer/access-control/EditDetails/useOptions.tsx
@@ -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';
diff --git a/app/portainer/access-control/types.ts b/app/portainer/access-control/types.ts
index dce3dcccb..a9a08c856 100644
--- a/app/portainer/access-control/types.ts
+++ b/app/portainer/access-control/types.ts
@@ -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;
diff --git a/app/portainer/access-control/utils.ts b/app/portainer/access-control/utils.ts
index d692f500e..c88cca09c 100644
--- a/app/portainer/access-control/utils.ts
+++ b/app/portainer/access-control/utils.ts
@@ -1,4 +1,5 @@
-import { TeamId } from '../teams/types';
+import { TeamId } from '@/react/portainer/users/teams/types';
+
import { UserId } from '../users/types';
import {
diff --git a/app/portainer/components/datatables/teams-datatable/teamsDatatable.html b/app/portainer/components/datatables/teams-datatable/teamsDatatable.html
deleted file mode 100644
index 48de95a99..000000000
--- a/app/portainer/components/datatables/teams-datatable/teamsDatatable.html
+++ /dev/null
@@ -1,95 +0,0 @@
-
diff --git a/app/portainer/components/datatables/teams-datatable/teamsDatatable.js b/app/portainer/components/datatables/teams-datatable/teamsDatatable.js
deleted file mode 100644
index 08f290bfc..000000000
--- a/app/portainer/components/datatables/teams-datatable/teamsDatatable.js
+++ /dev/null
@@ -1,14 +0,0 @@
-angular.module('portainer.app').component('teamsDatatable', {
- templateUrl: './teamsDatatable.html',
- controller: 'GenericDatatableController',
- bindings: {
- titleText: '@',
- titleIcon: '@',
- dataset: '<',
- tableKey: '@',
- orderBy: '@',
- reverseOrder: '<',
- removeAction: '<',
- isAdmin: '<',
- },
-});
diff --git a/app/portainer/environments/environment.service/index.ts b/app/portainer/environments/environment.service/index.ts
index 0732e371e..2aaa0e40f 100644
--- a/app/portainer/environments/environment.service/index.ts
+++ b/app/portainer/environments/environment.service/index.ts
@@ -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,
diff --git a/app/portainer/environments/environment.service/registries.ts b/app/portainer/environments/environment.service/registries.ts
index 0bac8578a..2be438755 100644
--- a/app/portainer/environments/environment.service/registries.ts
+++ b/app/portainer/environments/environment.service/registries.ts
@@ -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';
diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx
index e8a11efc6..892e2d5f1 100644
--- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx
+++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx
@@ -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);
diff --git a/app/portainer/home/EnvironmentList/EnvironmentList.tsx b/app/portainer/home/EnvironmentList/EnvironmentList.tsx
index 02b63c016..0cde258dd 100644
--- a/app/portainer/home/EnvironmentList/EnvironmentList.tsx
+++ b/app/portainer/home/EnvironmentList/EnvironmentList.tsx
@@ -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[]
diff --git a/app/portainer/hooks/useUser.tsx b/app/portainer/hooks/useUser.tsx
index 58c62faa2..4f61d9502 100644
--- a/app/portainer/hooks/useUser.tsx
+++ b/app/portainer/hooks/useUser.tsx
@@ -176,8 +176,3 @@ export function UserProvider({ children }: UserProviderProps) {
setUser(user);
}
}
-
-export function useIsAdmin() {
- const { user } = useUser();
- return !!user && isAdmin(user);
-}
diff --git a/app/portainer/react/views/index.ts b/app/portainer/react/views/index.ts
index 66b654ea8..80bfb0b48 100644
--- a/app/portainer/react/views/index.ts
+++ b/app/portainer/react/views/index.ts
@@ -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, []))
diff --git a/app/portainer/react/views/teams.ts b/app/portainer/react/views/teams.ts
new file mode 100644
index 000000000..309e880b6
--- /dev/null
+++ b/app/portainer/react/views/teams.ts
@@ -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',
+ },
+ },
+ });
+}
diff --git a/app/portainer/settings/types.ts b/app/portainer/settings/types.ts
index d7d54e41b..f054dab05 100644
--- a/app/portainer/settings/types.ts
+++ b/app/portainer/settings/types.ts
@@ -1,4 +1,4 @@
-import { TeamId } from '../teams/types';
+import { TeamId } from '@/react/portainer/users/teams/types';
export interface FDOConfiguration {
enabled: boolean;
diff --git a/app/portainer/teams/CreateTeamForm/CreateTeamForm.stories.tsx b/app/portainer/teams/CreateTeamForm/CreateTeamForm.stories.tsx
deleted file mode 100644
index c7589a9b4..000000000
--- a/app/portainer/teams/CreateTeamForm/CreateTeamForm.stories.tsx
+++ /dev/null
@@ -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 (
-
- );
-
- function handleSubmit(values: FormValues) {
- return new Promise((resolve) => {
- setTimeout(() => {
- setMessage(
- `created team ${values.name} with ${values.leaders.length} leaders`
- );
- resolve();
- }, 3000);
- });
- }
-}
diff --git a/app/portainer/teams/CreateTeamForm/index.ts b/app/portainer/teams/CreateTeamForm/index.ts
deleted file mode 100644
index 5cdccb886..000000000
--- a/app/portainer/teams/CreateTeamForm/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { r2a } from '@/react-tools/react2angular';
-
-import { CreateTeamForm } from './CreateTeamForm';
-
-export { CreateTeamForm };
-
-export const CreateTeamFormAngular = r2a(CreateTeamForm, [
- 'users',
- 'onSubmit',
- 'teams',
-]);
diff --git a/app/portainer/teams/index.ts b/app/portainer/teams/index.ts
deleted file mode 100644
index 42c42eb49..000000000
--- a/app/portainer/teams/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import angular from 'angular';
-
-import { CreateTeamFormAngular } from './CreateTeamForm';
-
-export default angular
- .module('portainer.app.teams', [])
-
- .component('createTeamForm', CreateTeamFormAngular).name;
diff --git a/app/portainer/teams/queries.ts b/app/portainer/teams/queries.ts
deleted file mode 100644
index 71cb476f3..000000000
--- a/app/portainer/teams/queries.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { useQuery } from 'react-query';
-
-import { getTeams } from './teams.service';
-import { Team } from './types';
-
-export function useTeams(
- 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;
-}
diff --git a/app/portainer/teams/teams.service.ts b/app/portainer/teams/teams.service.ts
deleted file mode 100644
index c4de675b8..000000000
--- a/app/portainer/teams/teams.service.ts
+++ /dev/null
@@ -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(buildUrl());
- return data;
- } catch (error) {
- throw parseAxiosError(error as Error);
- }
-}
-
-function buildUrl(id?: TeamId) {
- let url = '/teams';
-
- if (id) {
- url += `/${id}`;
- }
-
- return url;
-}
diff --git a/app/portainer/teams/types.ts b/app/portainer/teams/types.ts
deleted file mode 100644
index 157c1e804..000000000
--- a/app/portainer/teams/types.ts
+++ /dev/null
@@ -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;
-}
diff --git a/app/portainer/users/queries.ts b/app/portainer/users/queries.ts
index 246d61e91..34b57792b 100644
--- a/app/portainer/users/queries.ts
+++ b/app/portainer/users/queries.ts
@@ -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(
- includeAdministrator: boolean,
+ includeAdministrator = false,
enabled = true,
select: (data: User[]) => T = (data) => data as unknown as T
) {
diff --git a/app/portainer/users/types.ts b/app/portainer/users/types.ts
index e2d893db7..b51673f20 100644
--- a/app/portainer/users/types.ts
+++ b/app/portainer/users/types.ts
@@ -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;
-}
+};
diff --git a/app/portainer/users/user.service.ts b/app/portainer/users/user.service.ts
index 7a7aba283..f9b2decf0 100644
--- a/app/portainer/users/user.service.ts
+++ b/app/portainer/users/user.service.ts
@@ -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';
diff --git a/app/portainer/views/teams/edit/team.html b/app/portainer/views/teams/edit/team.html
deleted file mode 100644
index 3eb223443..000000000
--- a/app/portainer/views/teams/edit/team.html
+++ /dev/null
@@ -1,223 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Name |
-
- {{ team.Name }}
-
- |
-
-
- Leaders |
-
- {{ leaderCount }}
- |
-
-
- Total users in team |
- {{ teamMembers.length }} |
-
-
-
-
-
-
-
-
-
-
-
- The team leader feature is disabled as external authentication is currently enabled with team sync.
-
-
-
-
-
-
-
-
- Items per page:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
-
-
-
- {{ user.Username }}
-
-
- Add
-
-
- |
-
-
- Loading... |
-
-
- No users. |
-
-
-
-
-
-
-
-
-
-
-
-
- Items per page:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
- |
-
-
-
-
-
- {{ user.Username }}
-
-
- Remove
-
-
- |
-
-
-
- {{ user.TeamRole }}
-
-
- Leader
-
-
- Member
-
-
- |
-
-
- Loading... |
-
-
- No team members. |
-
-
-
-
-
-
-
-
-
diff --git a/app/portainer/views/teams/edit/teamController.js b/app/portainer/views/teams/edit/teamController.js
deleted file mode 100644
index 1a2b5b52d..000000000
--- a/app/portainer/views/teams/edit/teamController.js
+++ /dev/null
@@ -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();
- },
-]);
diff --git a/app/portainer/views/teams/teams.html b/app/portainer/views/teams/teams.html
deleted file mode 100644
index f3108efe3..000000000
--- a/app/portainer/views/teams/teams.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
diff --git a/app/portainer/views/teams/teamsController.js b/app/portainer/views/teams/teamsController.js
deleted file mode 100644
index fdee63dcb..000000000
--- a/app/portainer/views/teams/teamsController.js
+++ /dev/null
@@ -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();
- },
-]);
diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts
index eefa43943..68cf61392 100644
--- a/app/react-tools/test-mocks.ts
+++ b/app/react-tools/test-mocks.ts
@@ -1,6 +1,6 @@
import _ from 'lodash';
-import { Team } from '@/portainer/teams/types';
+import { Team } from '@/react/portainer/users/teams/types';
import { Role, User, UserId } from '@/portainer/users/types';
import { Environment } from '@/portainer/environments/types';
diff --git a/app/react/azure/container-instances/CreateView/useLoadFormState.ts b/app/react/azure/container-instances/CreateView/useLoadFormState.ts
index 3bb932e0f..ffd717812 100644
--- a/app/react/azure/container-instances/CreateView/useLoadFormState.ts
+++ b/app/react/azure/container-instances/CreateView/useLoadFormState.ts
@@ -6,7 +6,7 @@ import {
Subscription,
} from '@/react/azure/types';
import { parseAccessControlFormData } from '@/portainer/access-control/utils';
-import { useIsAdmin } from '@/portainer/hooks/useUser';
+import { useUser } from '@/portainer/hooks/useUser';
import { useProvider } from '@/react/azure/queries/useProvider';
import { useResourceGroups } from '@/react/azure/queries/useResourceGroups';
import { useSubscriptions } from '@/react/azure/queries/useSubscriptions';
@@ -37,7 +37,7 @@ export function useFormState(
resourceGroups: Record = {},
providers: Record = {}
) {
- const isAdmin = useIsAdmin();
+ const { isAdmin } = useUser();
const subscriptionOptions = subscriptions.map((s) => ({
value: s.subscriptionId,
diff --git a/app/react/components/PaginationControls/PageSelector.tsx b/app/react/components/PaginationControls/PageSelector.tsx
index 728a54791..6fa3af1fd 100644
--- a/app/react/components/PaginationControls/PageSelector.tsx
+++ b/app/react/components/PaginationControls/PageSelector.tsx
@@ -1,3 +1,5 @@
+import './pagination-controls.css';
+
import { generatePagesArray } from './generatePagesArray';
import { PageButton } from './PageButton';
import { PageInput } from './PageInput';
diff --git a/app/react/components/TeamsSelector/TeamsSelector.tsx b/app/react/components/TeamsSelector/TeamsSelector.tsx
index 4bd71cdc9..e03e1ffa3 100644
--- a/app/react/components/TeamsSelector/TeamsSelector.tsx
+++ b/app/react/components/TeamsSelector/TeamsSelector.tsx
@@ -1,4 +1,4 @@
-import { Team, TeamId } from '@/portainer/teams/types';
+import { Team, TeamId } from '@/react/portainer/users/teams/types';
import { Select } from '@@/form-components/ReactSelect';
diff --git a/app/react/components/Widget/WidgetTitle.tsx b/app/react/components/Widget/WidgetTitle.tsx
index 26f40504e..a781298e5 100644
--- a/app/react/components/Widget/WidgetTitle.tsx
+++ b/app/react/components/Widget/WidgetTitle.tsx
@@ -24,7 +24,7 @@ export function WidgetTitle({
return (
-
+
{
columns: ColumnInstance[];
onChange: (value: string[]) => void;
@@ -18,8 +16,6 @@ export function ColumnVisibilityMenu({
onChange,
value,
}: Props) {
- useTableContext();
-
return (