- handleChangeItem(key, value)}
+ error={error}
/>
{movable && (
@@ -172,12 +183,15 @@ function defaultItemBuilder(): DefaultType {
return { value: '' };
}
-function DefaultItem({ item, onChange }: ItemProps) {
+function DefaultItem({ item, onChange, error }: ItemProps) {
return (
- onChange({ value: e.target.value })}
- className={styles.defaultItem}
- />
+ <>
+ onChange({ value: e.target.value })}
+ className={styles.defaultItem}
+ />
+ {error}
+ >
);
}
diff --git a/app/portainer/hooks/useUser.tsx b/app/portainer/hooks/useUser.tsx
index 1c605aba8..6979394d3 100644
--- a/app/portainer/hooks/useUser.tsx
+++ b/app/portainer/hooks/useUser.tsx
@@ -125,6 +125,6 @@ export function UserProvider({ children }: UserProviderProps) {
}
}
-function isAdmin(user?: UserViewModel | null) {
+export function isAdmin(user?: UserViewModel | null): boolean {
return !!user && user.Role === 1;
}
diff --git a/app/portainer/resource-control/helper.ts b/app/portainer/resource-control/helper.ts
new file mode 100644
index 000000000..5a4cef8f5
--- /dev/null
+++ b/app/portainer/resource-control/helper.ts
@@ -0,0 +1,51 @@
+import { ResourceControlOwnership } from 'Portainer/models/resourceControl/resourceControlOwnership';
+
+import { AccessControlFormData } from '../components/accessControlForm/model';
+import { TeamId } from '../teams/types';
+import { UserId } from '../users/types';
+
+import { OwnershipParameters } from './types';
+
+/**
+ * Transform AccessControlFormData to ResourceControlOwnershipParameters
+ * @param {int} userId ID of user performing the operation
+ * @param {AccessControlFormData} formValues Form data (generated by AccessControlForm)
+ * @param {int[]} subResources Sub Resources restricted by the ResourceControl
+ */
+export function parseOwnershipParameters(
+ userId: UserId,
+ formValues: AccessControlFormData,
+ subResources: (number | string)[] = []
+): OwnershipParameters {
+ let { ownership } = formValues;
+ if (!formValues.accessControlEnabled) {
+ ownership = ResourceControlOwnership.PUBLIC;
+ }
+
+ let adminOnly = false;
+ let publicOnly = false;
+ let users: UserId[] = [];
+ let teams: TeamId[] = [];
+ switch (ownership) {
+ case ResourceControlOwnership.PUBLIC:
+ publicOnly = true;
+ break;
+ case ResourceControlOwnership.PRIVATE:
+ users.push(userId);
+ break;
+ case ResourceControlOwnership.RESTRICTED:
+ users = formValues.authorizedUsers;
+ teams = formValues.authorizedTeams;
+ break;
+ default:
+ adminOnly = true;
+ break;
+ }
+ return {
+ administratorsOnly: adminOnly,
+ public: publicOnly,
+ users,
+ teams,
+ subResources,
+ };
+}
diff --git a/app/portainer/resource-control/resource-control.service.ts b/app/portainer/resource-control/resource-control.service.ts
new file mode 100644
index 000000000..a33eb22df
--- /dev/null
+++ b/app/portainer/resource-control/resource-control.service.ts
@@ -0,0 +1,48 @@
+import { UserId } from '@/portainer/users/types';
+import { AccessControlFormData } from '@/portainer/components/accessControlForm/model';
+import { ResourceControlResponse } from '@/portainer/models/resourceControl/resourceControl';
+
+import axios, { parseAxiosError } from '../services/axios';
+
+import { parseOwnershipParameters } from './helper';
+import { OwnershipParameters } from './types';
+
+/**
+ * Apply a ResourceControl after Resource creation
+ * @param userId ID of User performing the action
+ * @param accessControlData ResourceControl to apply
+ * @param resourceControl ResourceControl to update
+ * @param subResources SubResources managed by the ResourceControl
+ */
+export function applyResourceControl(
+ userId: UserId,
+ accessControlData: AccessControlFormData,
+ resourceControl: ResourceControlResponse,
+ subResources: (number | string)[] = []
+) {
+ const ownershipParameters = parseOwnershipParameters(
+ userId,
+ accessControlData,
+ subResources
+ );
+ return updateResourceControl(resourceControl.Id, ownershipParameters);
+}
+
+/**
+ * Update a ResourceControl
+ * @param resourceControlId ID of involved resource
+ * @param ownershipParameters Transient type from view data to payload
+ */
+async function updateResourceControl(
+ resourceControlId: string | number,
+ ownershipParameters: OwnershipParameters
+) {
+ try {
+ await axios.put(
+ `/resource_controls/${resourceControlId}`,
+ ownershipParameters
+ );
+ } catch (error) {
+ throw parseAxiosError(error as Error);
+ }
+}
diff --git a/app/portainer/resource-control/types.ts b/app/portainer/resource-control/types.ts
new file mode 100644
index 000000000..80dfa124e
--- /dev/null
+++ b/app/portainer/resource-control/types.ts
@@ -0,0 +1,13 @@
+import { TeamId } from '@/portainer/teams/types';
+import { UserId } from '@/portainer/users/types';
+
+/**
+ * Transient type from view data to payload
+ */
+export interface OwnershipParameters {
+ administratorsOnly: boolean;
+ public: boolean;
+ users: UserId[];
+ teams: TeamId[];
+ subResources: (number | string)[];
+}
diff --git a/app/portainer/services/axios.ts b/app/portainer/services/axios.ts
index 5b242a2f5..57155cfd7 100644
--- a/app/portainer/services/axios.ts
+++ b/app/portainer/services/axios.ts
@@ -1,4 +1,6 @@
-import axios, { AxiosError, AxiosRequestConfig } from 'axios';
+import axiosOrigin, { AxiosError, AxiosRequestConfig } from 'axios';
+import { loadProgressBar } from 'axios-progress-bar';
+import 'axios-progress-bar/dist/nprogress.css';
import PortainerError from '../error';
import { get as localStorageGet } from '../hooks/useLocalStorage';
@@ -8,11 +10,13 @@ import {
portainerAgentTargetHeader,
} from './http-request.helper';
-const axiosApiInstance = axios.create({ baseURL: 'api' });
+const axios = axiosOrigin.create({ baseURL: 'api' });
-export default axiosApiInstance;
+loadProgressBar(undefined, axios);
-axiosApiInstance.interceptors.request.use(async (config) => {
+export default axios;
+
+axios.interceptors.request.use(async (config) => {
const newConfig = { headers: config.headers || {}, ...config };
const jwt = localStorageGet('JWT', '');
@@ -41,18 +45,28 @@ export function agentInterceptor(config: AxiosRequestConfig) {
return newConfig;
}
-axiosApiInstance.interceptors.request.use(agentInterceptor);
+axios.interceptors.request.use(agentInterceptor);
-export function parseAxiosError(err: Error, msg = '') {
+export function parseAxiosError(
+ err: Error,
+ msg = '',
+ parseError = defaultErrorParser
+) {
let resultErr = err;
let resultMsg = msg;
if ('isAxiosError' in err) {
- const axiosError = err as AxiosError;
- resultErr = new Error(`${axiosError.response?.data.message}`);
- const msgDetails = axiosError.response?.data.details;
- resultMsg = msg ? `${msg}: ${msgDetails}` : msgDetails;
+ const { error, details } = parseError(err as AxiosError);
+ resultErr = error;
+ resultMsg = msg ? `${msg}: ${details}` : details;
}
return new PortainerError(resultMsg, resultErr);
}
+
+function defaultErrorParser(axiosError: AxiosError) {
+ const message = axiosError.response?.data.message;
+ const details = axiosError.response?.data.details || message;
+ const error = new Error(message);
+ return { error, details };
+}
diff --git a/app/setup-tests/server-handlers.ts b/app/setup-tests/server-handlers.ts
index 2525bf607..e3fec1e57 100644
--- a/app/setup-tests/server-handlers.ts
+++ b/app/setup-tests/server-handlers.ts
@@ -2,6 +2,8 @@ import { rest } from 'msw';
import { createMockTeams, createMockUsers } from '../react-tools/test-mocks';
+import { azureHandlers } from './setup-handlers/azure';
+
export const handlers = [
rest.get('/api/teams', async (req, res, ctx) =>
res(ctx.json(createMockTeams(10)))
@@ -9,4 +11,5 @@ export const handlers = [
rest.get('/api/users', async (req, res, ctx) =>
res(ctx.json(createMockUsers(10)))
),
+ ...azureHandlers,
];
diff --git a/app/setup-tests/setup-handlers/azure.ts b/app/setup-tests/setup-handlers/azure.ts
new file mode 100644
index 000000000..fef5ce472
--- /dev/null
+++ b/app/setup-tests/setup-handlers/azure.ts
@@ -0,0 +1,76 @@
+import { rest } from 'msw';
+
+export const azureHandlers = [
+ rest.get('/api/endpoints/:endpointId/azure/subscriptions', (req, res, ctx) =>
+ res(
+ ctx.json({
+ value: [
+ {
+ id: '/subscriptions/sub1',
+ authorizationSource: 'RoleBased',
+ subscriptionId: 'sub1',
+ displayName: 'Portainer',
+ state: 'Enabled',
+ },
+ ],
+ })
+ )
+ ),
+ rest.get(
+ '/api/endpoints/:endpointId/azure/subscriptions/:subscriptionId/providers/Microsoft.ContainerInstance',
+ (req, res, ctx) =>
+ res(
+ ctx.json({
+ id: `/subscriptions/${req.params.subscriptionId}/providers/Microsoft.ContainerInstance`,
+ namespace: 'Microsoft.ContainerInstance',
+ resourceTypes: [
+ {
+ resourceType: 'containerGroups',
+ locations: [
+ 'Australia East',
+ 'Australia Southeast',
+ 'Brazil South',
+ ],
+ },
+ {
+ resourceType: 'serviceAssociationLinks',
+ locations: [
+ 'Korea Central',
+ 'North Central US',
+ 'North Europe',
+ 'Norway East',
+ 'South Africa North',
+ 'South Central US',
+ ],
+ },
+ {
+ resourceType: 'locations',
+ locations: [],
+ },
+ ],
+ })
+ )
+ ),
+ rest.get(
+ '/api/endpoints/:endpointId/azure/subscriptions/:subsriptionId/resourcegroups',
+ (res, req, ctx) =>
+ req(
+ ctx.json({
+ value: [
+ {
+ id: `/subscriptions/${res.params.subscriptionId}/resourceGroups/rg1`,
+ name: 'rg1',
+ location: 'southcentralus',
+ properties: { provisioningState: 'Succeeded' },
+ },
+ {
+ id: `/subscriptions/${res.params.subscriptionId}/resourceGroups/rg2`,
+ name: 'rg2',
+ location: 'southcentralus',
+ properties: { provisioningState: 'Succeeded' },
+ },
+ ],
+ })
+ )
+ ),
+];
diff --git a/package.json b/package.json
index edefb9580..1aa351e52 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,7 @@
"angularjs-slider": "^6.4.0",
"angulartics": "^1.6.0",
"axios": "^0.24.0",
+ "axios-progress-bar": "^1.2.0",
"babel-plugin-angularjs-annotate": "^0.10.0",
"bootbox": "^5.5.2",
"bootstrap": "^3.4.0",
diff --git a/yarn.lock b/yarn.lock
index 4cfcd5ba4..a0d5b7dec 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5057,6 +5057,11 @@ axe-core@^4.3.5:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5"
integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==
+axios-progress-bar@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/axios-progress-bar/-/axios-progress-bar-1.2.0.tgz#f9ee88dc9af977246be1ef07eedfa4c990c639c5"
+ integrity sha512-PEgWb/b2SMyHnKJ/cxA46OdCuNeVlo8eqL0HxXPtz+6G/Jtpyo49icPbW+jpO1wUeDEjbqpseMoCyWxESxf5pA==
+
axios@^0.24.0:
version "0.24.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"