diff --git a/app/portainer/__module.js b/app/portainer/__module.js
index b43401dd5..eec3bf318 100644
--- a/app/portainer/__module.js
+++ b/app/portainer/__module.js
@@ -171,7 +171,7 @@ angular
url: '/endpoints',
views: {
'content@': {
- component: 'endpointsView',
+ component: 'environmentsListView',
},
},
};
diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html
deleted file mode 100644
index fd9db10cf..000000000
--- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html
+++ /dev/null
@@ -1,167 +0,0 @@
-
diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js
deleted file mode 100644
index 807f05192..000000000
--- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js
+++ /dev/null
@@ -1,13 +0,0 @@
-angular.module('portainer.app').component('endpointsDatatable', {
- templateUrl: './endpointsDatatable.html',
- controller: 'EndpointsDatatableController',
- bindings: {
- titleText: '@',
- titleIcon: '@',
- tableKey: '@',
- orderBy: '@',
- reverseOrder: '<',
- removeAction: '<',
- retrievePage: '<',
- },
-});
diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js
deleted file mode 100644
index ccdd3178d..000000000
--- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import _ from 'lodash-es';
-import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
-
-angular.module('portainer.app').controller('EndpointsDatatableController', [
- '$scope',
- '$controller',
- 'DatatableService',
- 'PaginationService',
- function ($scope, $controller, DatatableService, PaginationService) {
- angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
-
- this.isBE = isBE;
-
- this.state = Object.assign(this.state, {
- orderBy: this.orderBy,
- loading: true,
- filteredDataSet: [],
- totalFilteredDataset: 0,
- pageNumber: 1,
- });
-
- this.paginationChanged = async function () {
- try {
- this.state.loading = true;
- this.state.filteredDataSet = [];
- const start = (this.state.pageNumber - 1) * this.state.paginatedItemLimit + 1;
- const { endpoints, totalCount } = await this.retrievePage(start, this.state.paginatedItemLimit, this.state.textFilter);
- this.state.filteredDataSet = endpoints;
- this.state.totalFilteredDataSet = totalCount;
- this.refreshSelectedItems();
- } finally {
- this.state.loading = false;
- }
- };
-
- this.onPageChange = function (newPageNumber) {
- this.state.pageNumber = newPageNumber;
- this.paginationChanged();
- };
-
- this.setReferrer = function () {
- window.localStorage.setItem('wizardReferrer', 'environments');
- };
-
- /**
- * Overridden
- */
- this.onTextFilterChange = function () {
- var filterValue = this.state.textFilter;
- DatatableService.setDataTableTextFilters(this.tableKey, filterValue);
- this.resetSelectionState();
- this.paginationChanged();
- };
-
- /**
- * Overriden
- */
- this.uniq = function () {
- return _.uniqBy(_.concat(this.state.filteredDataSet, this.state.selectedItems), 'Id');
- };
-
- /**
- * Overridden
- */
- this.changePaginationLimit = function () {
- PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
- this.paginationChanged();
- };
-
- this.refreshSelectedItems = function () {
- _.forEach(this.state.filteredDataSet, (item) => {
- if (_.filter(this.state.selectedItems, (i) => i.Id == item.Id).length > 0) {
- item.Checked = true;
- }
- });
- };
-
- /**
- * Overridden
- */
- this.$onInit = function () {
- this.setDefaults();
- this.prepareTableFromDataset();
-
- var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
- if (storedOrder !== null) {
- this.state.reverseOrder = storedOrder.reverse;
- this.state.orderBy = storedOrder.orderBy;
- }
-
- var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
- if (textFilter !== null) {
- this.state.textFilter = textFilter;
- this.onTextFilterChange();
- }
-
- var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
- if (storedFilters !== null) {
- this.filters = storedFilters;
- }
- if (this.filters && this.filters.state) {
- this.filters.state.open = false;
- }
-
- this.paginationChanged();
- };
- },
-]);
diff --git a/app/portainer/react/views/index.ts b/app/portainer/react/views/index.ts
index 6b80ace4a..056049eaa 100644
--- a/app/portainer/react/views/index.ts
+++ b/app/portainer/react/views/index.ts
@@ -9,6 +9,7 @@ import { CreateAccessToken } from '@/react/portainer/account/CreateAccessTokenVi
import { EdgeComputeSettingsView } from '@/react/portainer/settings/EdgeComputeView/EdgeComputeSettingsView';
import { withI18nSuspense } from '@/react-tools/withI18nSuspense';
import { EdgeAutoCreateScriptView } from '@/react/portainer/environments/EdgeAutoCreateScriptView';
+import { ListView as EnvironmentsListView } from '@/react/portainer/environments/ListView';
import { wizardModule } from './wizard';
import { teamsModule } from './teams';
@@ -44,4 +45,8 @@ export const viewsModule = angular
withUIRouter(withReactQuery(withCurrentUser(EdgeComputeSettingsView))),
['onSubmit', 'settings']
)
+ )
+ .component(
+ 'environmentsListView',
+ r2a(withUIRouter(withReactQuery(withCurrentUser(EnvironmentsListView))), [])
).name;
diff --git a/app/portainer/views/endpoints/endpoints.html b/app/portainer/views/endpoints/endpoints.html
deleted file mode 100644
index 72ac0b64f..000000000
--- a/app/portainer/views/endpoints/endpoints.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
diff --git a/app/portainer/views/endpoints/endpoints.js b/app/portainer/views/endpoints/endpoints.js
deleted file mode 100644
index 7b42d0936..000000000
--- a/app/portainer/views/endpoints/endpoints.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { EndpointsController } from './endpointsController';
-
-angular.module('portainer.app').component('endpointsView', {
- templateUrl: './endpoints.html',
- controller: EndpointsController,
-});
diff --git a/app/portainer/views/endpoints/endpointsController.js b/app/portainer/views/endpoints/endpointsController.js
deleted file mode 100644
index acc2142ef..000000000
--- a/app/portainer/views/endpoints/endpointsController.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import { map } from 'lodash';
-import EndpointHelper from '@/portainer/helpers/endpointHelper';
-import { getEnvironments } from '@/react/portainer/environments/environment.service';
-import { confirmDelete } from '@@/modals/confirm';
-
-export class EndpointsController {
- /* @ngInject */
- constructor($state, $async, EndpointService, GroupService, Notifications, EndpointProvider, StateManager) {
- Object.assign(this, {
- $state,
- $async,
- EndpointService,
- GroupService,
- Notifications,
- EndpointProvider,
- StateManager,
- });
-
- this.state = {
- loadingMessage: '',
- };
-
- this.setLoadingMessage = this.setLoadingMessage.bind(this);
- this.getPaginatedEndpoints = this.getPaginatedEndpoints.bind(this);
- this.removeAction = this.removeAction.bind(this);
- }
-
- setLoadingMessage(message) {
- this.state.loadingMessage = message;
- }
-
- removeAction(endpoints) {
- confirmDelete('This action will remove all configurations associated to your environment(s). Continue?').then((confirmed) => {
- if (!confirmed) {
- return;
- }
- return this.$async(async () => {
- try {
- await Promise.all(endpoints.map(({ Id }) => this.EndpointService.deleteEndpoint(Id)));
- this.Notifications.success('Environments successfully removed', map(endpoints, 'Name').join(', '));
- } catch (err) {
- this.Notifications.error('Failure', err, 'Unable to remove environment');
- }
-
- const id = this.EndpointProvider.endpointID();
- // If the current endpoint was deleted, then clean endpoint store
- if (endpoints.some((e) => e.Id === id)) {
- this.StateManager.cleanEndpoint();
- }
-
- this.$state.reload();
- });
- });
- }
-
- getPaginatedEndpoints(start, limit, search) {
- return this.$async(async () => {
- try {
- const [{ value: endpoints, totalCount }, groups] = await Promise.all([
- getEnvironments({ start, limit, query: { search, excludeSnapshots: true } }),
- this.GroupService.groups(),
- ]);
- EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
- return { endpoints, totalCount };
- } catch (err) {
- this.Notifications.error('Failure', err, 'Unable to retrieve environment information');
- }
- });
- }
-}
diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts
index 6efc2bb0b..0972f50cd 100644
--- a/app/react-tools/test-mocks.ts
+++ b/app/react-tools/test-mocks.ts
@@ -116,5 +116,9 @@ export function createMockEnvironment(): Environment {
EndTime: '',
StartTime: '',
},
+ StatusMessage: {
+ Detail: '',
+ Summary: '',
+ },
};
}
diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx
index 770c4652e..f7b33e17a 100644
--- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx
+++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx
@@ -126,7 +126,7 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
pageLimit,
...queryWithSort,
},
- refetchIfAnyOffline
+ { refetchInterval: refetchIfAnyOffline }
);
useEffect(() => {
diff --git a/app/react/portainer/environments/ListView/.keep b/app/react/portainer/environments/ListView/.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx b/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx
new file mode 100644
index 000000000..eb2b6f3a6
--- /dev/null
+++ b/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx
@@ -0,0 +1,104 @@
+import { HardDrive, Plus, Trash2 } from 'lucide-react';
+import { useState } from 'react';
+
+import { useEnvironmentList } from '@/react/portainer/environments/queries';
+import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
+
+import { Datatable } from '@@/datatables';
+import { createPersistedStore } from '@@/datatables/types';
+import { Button } from '@@/buttons';
+import { Link } from '@@/Link';
+import { useTableState } from '@@/datatables/useTableState';
+
+import { isBE } from '../../feature-flags/feature-flags.service';
+import { refetchIfAnyOffline } from '../queries/useEnvironmentList';
+
+import { columns } from './columns';
+import { EnvironmentListItem } from './types';
+import { ImportFdoDeviceButton } from './ImportFdoDeviceButton';
+
+const tableKey = 'environments';
+const settingsStore = createPersistedStore(tableKey, 'Name');
+
+export function EnvironmentsDatatable({
+ onRemove,
+}: {
+ onRemove: (environments: Array) => void;
+}) {
+ const tableState = useTableState(settingsStore, tableKey);
+
+ const [page, setPage] = useState(0);
+
+ const groupsQuery = useGroups();
+ const { environments, isLoading, totalCount } = useEnvironmentList(
+ {
+ search: tableState.search,
+ excludeSnapshots: true,
+ page: page + 1,
+ pageLimit: tableState.pageSize,
+ sort: tableState.sortBy.id,
+ order: tableState.sortBy.desc ? 'desc' : 'asc',
+ },
+ { enabled: groupsQuery.isSuccess, refetchInterval: refetchIfAnyOffline }
+ );
+
+ const environmentsWithGroups = environments.map(
+ (env) => {
+ const groupId = env.GroupId;
+ const group = groupsQuery.data?.find((g) => g.Id === groupId);
+ return {
+ ...env,
+ GroupName: group?.Name,
+ };
+ }
+ );
+
+ return (
+ (
+
+
+
+
+
+ {isBE && (
+
+ )}
+
+
+
+ )}
+ />
+ );
+}
diff --git a/app/react/portainer/environments/ListView/ListView.tsx b/app/react/portainer/environments/ListView/ListView.tsx
new file mode 100644
index 000000000..94f01b591
--- /dev/null
+++ b/app/react/portainer/environments/ListView/ListView.tsx
@@ -0,0 +1,58 @@
+import { useStore } from 'zustand';
+import _ from 'lodash';
+
+import { environmentStore } from '@/react/hooks/current-environment-store';
+import { notifySuccess } from '@/portainer/services/notifications';
+
+import { PageHeader } from '@@/PageHeader';
+import { confirmDelete } from '@@/modals/confirm';
+
+import { Environment } from '../types';
+
+import { EnvironmentsDatatable } from './EnvironmentsDatatable';
+import { useDeleteEnvironmentsMutation } from './useDeleteEnvironmentsMutation';
+
+export function ListView() {
+ const constCurrentEnvironmentStore = useStore(environmentStore);
+ const deletionMutation = useDeleteEnvironmentsMutation();
+
+ return (
+ <>
+
+
+
+ >
+ );
+
+ async function handleRemove(environments: Array) {
+ const confirmed = await confirmDelete(
+ 'This action will remove all configurations associated to your environment(s). Continue?'
+ );
+
+ if (!confirmed) {
+ return;
+ }
+
+ const id = constCurrentEnvironmentStore.environmentId;
+ // If the current endpoint was deleted, then clean endpoint store
+ if (environments.some((e) => e.Id === id)) {
+ constCurrentEnvironmentStore.clear();
+ }
+
+ deletionMutation.mutate(
+ environments.map((e) => e.Id),
+ {
+ onSuccess() {
+ notifySuccess(
+ 'Environments successfully removed',
+ _.map(environments, 'Name').join(', ')
+ );
+ },
+ }
+ );
+ }
+}
diff --git a/app/react/portainer/environments/ListView/columns/actions.tsx b/app/react/portainer/environments/ListView/columns/actions.tsx
new file mode 100644
index 000000000..a52bad72e
--- /dev/null
+++ b/app/react/portainer/environments/ListView/columns/actions.tsx
@@ -0,0 +1,41 @@
+import { CellContext } from '@tanstack/react-table';
+import { Users } from 'lucide-react';
+
+import { EnvironmentStatus } from '@/react/portainer/environments/types';
+
+import { Button } from '@@/buttons';
+import { Link } from '@@/Link';
+
+import { EnvironmentListItem } from '../types';
+
+import { columnHelper } from './helper';
+
+export const actions = columnHelper.display({
+ header: 'Actions',
+ cell: Cell,
+});
+
+function Cell({
+ row: { original: environment },
+}: CellContext) {
+ if (
+ environment.Status === EnvironmentStatus.Provisioning ||
+ environment.Status === EnvironmentStatus.Error
+ ) {
+ return <>->;
+ }
+
+ return (
+
+ );
+}
diff --git a/app/react/portainer/environments/ListView/columns/helper.ts b/app/react/portainer/environments/ListView/columns/helper.ts
new file mode 100644
index 000000000..3d35eef7f
--- /dev/null
+++ b/app/react/portainer/environments/ListView/columns/helper.ts
@@ -0,0 +1,5 @@
+import { createColumnHelper } from '@tanstack/react-table';
+
+import { EnvironmentListItem } from '../types';
+
+export const columnHelper = createColumnHelper();
diff --git a/app/react/portainer/environments/ListView/columns/index.ts b/app/react/portainer/environments/ListView/columns/index.ts
new file mode 100644
index 000000000..7a68d2272
--- /dev/null
+++ b/app/react/portainer/environments/ListView/columns/index.ts
@@ -0,0 +1,15 @@
+import { actions } from './actions';
+import { columnHelper } from './helper';
+import { name } from './name';
+import { type } from './type';
+import { url } from './url';
+
+export const columns = [
+ name,
+ type,
+ url,
+ columnHelper.accessor('GroupName', {
+ header: 'Group Name',
+ }),
+ actions,
+];
diff --git a/app/react/portainer/environments/ListView/columns/name.tsx b/app/react/portainer/environments/ListView/columns/name.tsx
new file mode 100644
index 000000000..92821e226
--- /dev/null
+++ b/app/react/portainer/environments/ListView/columns/name.tsx
@@ -0,0 +1,21 @@
+import { EnvironmentStatus } from '@/react/portainer/environments/types';
+
+import { Link } from '@@/Link';
+
+import { columnHelper } from './helper';
+
+export const name = columnHelper.accessor('Name', {
+ header: 'Name',
+ cell: ({ getValue, row: { original: environment } }) => {
+ const name = getValue();
+ if (environment.Status === EnvironmentStatus.Provisioning) {
+ return name;
+ }
+
+ return (
+
+ {name}
+
+ );
+ },
+});
diff --git a/app/react/portainer/environments/ListView/columns/type.tsx b/app/react/portainer/environments/ListView/columns/type.tsx
new file mode 100644
index 000000000..5f82deace
--- /dev/null
+++ b/app/react/portainer/environments/ListView/columns/type.tsx
@@ -0,0 +1,28 @@
+import { CellContext } from '@tanstack/react-table';
+
+import { environmentTypeIcon } from '@/portainer/filters/filters';
+import {
+ Environment,
+ EnvironmentType,
+} from '@/react/portainer/environments/types';
+import { getPlatformTypeName } from '@/react/portainer/environments/utils';
+
+import { Icon } from '@@/Icon';
+
+import { columnHelper } from './helper';
+
+export const type = columnHelper.accessor('Type', {
+ header: 'Type',
+ cell: Cell,
+});
+
+function Cell({ getValue }: CellContext) {
+ const type = getValue();
+
+ return (
+
+
+ {getPlatformTypeName(type)}
+
+ );
+}
diff --git a/app/react/portainer/environments/ListView/columns/url.tsx b/app/react/portainer/environments/ListView/columns/url.tsx
new file mode 100644
index 000000000..a7c7635d7
--- /dev/null
+++ b/app/react/portainer/environments/ListView/columns/url.tsx
@@ -0,0 +1,116 @@
+import { CellContext } from '@tanstack/react-table';
+import { AlertCircle, HelpCircle, Settings } from 'lucide-react';
+
+import {
+ EnvironmentStatus,
+ EnvironmentStatusMessage,
+ EnvironmentType,
+} from '@/react/portainer/environments/types';
+import { notifySuccess } from '@/portainer/services/notifications';
+
+import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
+import { Button } from '@@/buttons';
+
+import { EnvironmentListItem } from '../types';
+import { useUpdateEnvironmentMutation } from '../../queries/useUpdateEnvironmentMutation';
+
+import { columnHelper } from './helper';
+
+export const url = columnHelper.accessor('URL', {
+ header: 'URL',
+ cell: Cell,
+});
+
+function Cell({
+ row: { original: environment },
+}: CellContext) {
+ const mutation = useUpdateEnvironmentMutation();
+
+ if (
+ environment.Type !== EnvironmentType.EdgeAgentOnDocker &&
+ environment.Status !== EnvironmentStatus.Provisioning
+ ) {
+ return (
+ <>
+ {environment.URL}
+ {environment.StatusMessage.Summary &&
+ environment.StatusMessage.Detail && (
+
+
+
+ {environment.StatusMessage.Summary}
+
+
+ {environment.StatusMessage.Detail}
+ {environment.URL && (
+
+
+
+ )}
+
+ }
+ position="bottom"
+ >
+
+
+
+
+
+ )}
+ >
+ );
+ }
+
+ if (environment.Type === 4) {
+ return <>->;
+ }
+
+ if (environment.Status === 3) {
+ const status = (
+
+
+ {environment.StatusMessage.Summary}
+
+ );
+ if (!environment.StatusMessage.Detail) {
+ return status;
+ }
+ return (
+
+ {status}
+
+ );
+ }
+
+ return <>->;
+
+ function handleDismissButton() {
+ mutation.mutate(
+ {
+ id: environment.Id,
+ payload: {
+ IsSetStatusMessage: true,
+ StatusMessage: {} as EnvironmentStatusMessage,
+ },
+ },
+ {
+ onSuccess: () => {
+ notifySuccess('Success', 'Error dismissed successfully');
+ },
+ }
+ );
+ }
+}
diff --git a/app/react/portainer/environments/ListView/index.ts b/app/react/portainer/environments/ListView/index.ts
new file mode 100644
index 000000000..dd06dfd19
--- /dev/null
+++ b/app/react/portainer/environments/ListView/index.ts
@@ -0,0 +1 @@
+export { ListView } from './ListView';
diff --git a/app/react/portainer/environments/ListView/types.ts b/app/react/portainer/environments/ListView/types.ts
new file mode 100644
index 000000000..e0d7dfa48
--- /dev/null
+++ b/app/react/portainer/environments/ListView/types.ts
@@ -0,0 +1,5 @@
+import { Environment } from '@/react/portainer/environments/types';
+
+export type EnvironmentListItem = {
+ GroupName?: string;
+} & Environment;
diff --git a/app/react/portainer/environments/ListView/useDeleteEnvironmentsMutation.ts b/app/react/portainer/environments/ListView/useDeleteEnvironmentsMutation.ts
new file mode 100644
index 000000000..8c6df6c99
--- /dev/null
+++ b/app/react/portainer/environments/ListView/useDeleteEnvironmentsMutation.ts
@@ -0,0 +1,36 @@
+import { useMutation, useQueryClient } from 'react-query';
+
+import { promiseSequence } from '@/portainer/helpers/promise-utils';
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+import {
+ mutationOptions,
+ withError,
+ withInvalidate,
+} from '@/react-tools/react-query';
+
+import { buildUrl } from '../environment.service/utils';
+import { EnvironmentId } from '../types';
+
+export function useDeleteEnvironmentsMutation() {
+ const queryClient = useQueryClient();
+ return useMutation(
+ (environments: EnvironmentId[]) =>
+ promiseSequence(
+ environments.map(
+ (environmentId) => () => deleteEnvironment(environmentId)
+ )
+ ),
+ mutationOptions(
+ withError('Unable to delete environment(s)'),
+ withInvalidate(queryClient, [['environments']])
+ )
+ );
+}
+
+async function deleteEnvironment(id: EnvironmentId) {
+ try {
+ await axios.delete(buildUrl(id));
+ } catch (e) {
+ throw parseAxiosError(e as Error, 'Unable to delete environment');
+ }
+}
diff --git a/app/react/portainer/environments/queries/useEnvironmentList.ts b/app/react/portainer/environments/queries/useEnvironmentList.ts
index 6fbd9cb2b..00675b8e4 100644
--- a/app/react/portainer/environments/queries/useEnvironmentList.ts
+++ b/app/react/portainer/environments/queries/useEnvironmentList.ts
@@ -39,12 +39,18 @@ export function refetchIfAnyOffline(data?: GetEndpointsResponse) {
export function useEnvironmentList(
{ page = 1, pageLimit = 100, sort, order, ...query }: Query = {},
- refetchInterval?:
- | number
- | false
- | ((data?: GetEndpointsResponse) => false | number),
- staleTime = 0,
- enabled = true
+ {
+ enabled,
+ refetchInterval,
+ staleTime,
+ }: {
+ refetchInterval?:
+ | number
+ | false
+ | ((data?: GetEndpointsResponse) => false | number);
+ staleTime?: number;
+ enabled?: boolean;
+ } = {}
) {
const { isLoading, data } = useQuery(
[
diff --git a/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts b/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts
index d9330c14c..ecb474124 100644
--- a/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts
+++ b/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts
@@ -1,21 +1,23 @@
-import { useMutation, useQueryClient } from 'react-query';
+import { useQueryClient, useMutation } from 'react-query';
+import { withError, withInvalidate } from '@/react-tools/react-query';
+import {
+ EnvironmentId,
+ EnvironmentStatusMessage,
+ Environment,
+} from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { TagId } from '@/portainer/tags/types';
-import { withError } from '@/react-tools/react-query';
import { EnvironmentGroupId } from '../environment-groups/types';
import { buildUrl } from '../environment.service/utils';
-import { EnvironmentId, Environment } from '../types';
import { queryKeys } from './query-keys';
export function useUpdateEnvironmentMutation() {
const queryClient = useQueryClient();
return useMutation(updateEnvironment, {
- onSuccess(data, { id }) {
- queryClient.invalidateQueries(queryKeys.item(id));
- },
+ ...withInvalidate(queryClient, [queryKeys.base()]),
...withError('Unable to update environment'),
});
}
@@ -38,6 +40,9 @@ export interface UpdatePayload {
AzureApplicationID: string;
AzureTenantID: string;
AzureAuthenticationKey: string;
+
+ IsSetStatusMessage: boolean;
+ StatusMessage: Partial;
}
async function updateEnvironment({
diff --git a/app/react/portainer/environments/types.ts b/app/react/portainer/environments/types.ts
index 3536bd2d6..f93ce9338 100644
--- a/app/react/portainer/environments/types.ts
+++ b/app/react/portainer/environments/types.ts
@@ -131,6 +131,10 @@ interface EndpointChangeWindow {
StartTime: string;
EndTime: string;
}
+export interface EnvironmentStatusMessage {
+ Summary: string;
+ Detail: string;
+}
export type Environment = {
Agent: { Version: string };
@@ -163,6 +167,10 @@ export type Environment = {
/** GitOps update change window restriction for stacks and apps */
ChangeWindow: EndpointChangeWindow;
+ /**
+ * A message that describes the status. Should be included for Status Provisioning or Error.
+ */
+ StatusMessage: EnvironmentStatusMessage;
};
/**
diff --git a/app/react/portainer/environments/update-schedules/common/useEnvironments.ts b/app/react/portainer/environments/update-schedules/common/useEnvironments.ts
index ee1f91d7b..e5f776765 100644
--- a/app/react/portainer/environments/update-schedules/common/useEnvironments.ts
+++ b/app/react/portainer/environments/update-schedules/common/useEnvironments.ts
@@ -4,9 +4,9 @@ import { EdgeTypes, EnvironmentId } from '@/react/portainer/environments/types';
export function useEnvironments(environmentsIds: Array) {
const environmentsQuery = useEnvironmentList(
{ endpointIds: environmentsIds, types: EdgeTypes },
- undefined,
- undefined,
- environmentsIds.length > 0
+ {
+ enabled: environmentsIds.length > 0,
+ }
);
return environmentsQuery.environments;
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx
index a4ea97eb8..50a884f28 100644
--- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx
+++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx
@@ -28,19 +28,21 @@ interface Props {
export function WizardEndpointsList({ environmentIds }: Props) {
const { environments } = useEnvironmentList(
{ endpointIds: environmentIds },
- (environments) => {
- if (!environments) {
- return false;
- }
+ {
+ refetchInterval: (environments) => {
+ if (!environments) {
+ return false;
+ }
- if (!environments.value.some(isUnassociatedEdgeEnvironment)) {
- return false;
- }
+ if (!environments.value.some(isUnassociatedEdgeEnvironment)) {
+ return false;
+ }
- return ENVIRONMENTS_POLLING_INTERVAL;
- },
- 0,
- environmentIds.length > 0
+ return ENVIRONMENTS_POLLING_INTERVAL;
+ },
+
+ enabled: environmentIds.length > 0,
+ }
);
return (
diff --git a/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts b/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts
index 72faf893d..6437472a8 100644
--- a/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts
+++ b/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts
@@ -76,8 +76,10 @@ function useFetchLocalEnvironment() {
pageLimit: 1,
types: [EnvironmentType.Docker, EnvironmentType.KubernetesLocal],
},
- false,
- Infinity
+ {
+ refetchInterval: false,
+ staleTime: Infinity,
+ }
);
return {