diff --git a/api/datastore/migrate_data.go b/api/datastore/migrate_data.go
index 1279b3834..f5ec42f72 100644
--- a/api/datastore/migrate_data.go
+++ b/api/datastore/migrate_data.go
@@ -50,7 +50,7 @@ func (store *Store) MigrateData() error {
if err != nil {
err = errors.Wrap(err, "failed to migrate database")
- log.Warn().Msg("migration failed, restoring database to previous version")
+ log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
if err != nil {
return errors.Wrap(err, "failed to restore database")
diff --git a/api/datastore/migrator/migrate_dbversion100.go b/api/datastore/migrator/migrate_dbversion100.go
index cf280b984..2cdd259ef 100644
--- a/api/datastore/migrator/migrate_dbversion100.go
+++ b/api/datastore/migrator/migrate_dbversion100.go
@@ -115,10 +115,16 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
}
if environmentStatus.Details.Ok {
- statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
- Type: portainer.EdgeStackStatusRunning,
- Time: time.Now().Unix(),
- })
+ statusArray = append(statusArray,
+ portainer.EdgeStackDeploymentStatus{
+ Type: portainer.EdgeStackStatusDeploymentReceived,
+ Time: time.Now().Unix(),
+ },
+ portainer.EdgeStackDeploymentStatus{
+ Type: portainer.EdgeStackStatusRunning,
+ Time: time.Now().Unix(),
+ },
+ )
}
if environmentStatus.Details.ImagesPulled {
diff --git a/app/react/portainer/environments/update-schedules/common/utils.test.ts b/app/react/common/semver-utils.test.ts
similarity index 98%
rename from app/react/portainer/environments/update-schedules/common/utils.test.ts
rename to app/react/common/semver-utils.test.ts
index 5f78b1826..0f8ae1ae5 100644
--- a/app/react/portainer/environments/update-schedules/common/utils.test.ts
+++ b/app/react/common/semver-utils.test.ts
@@ -1,4 +1,4 @@
-import { semverCompare } from './utils';
+import { semverCompare } from './semver-utils';
describe('semverCompare', () => {
test('sort array', () => {
diff --git a/app/react/common/semver-utils.ts b/app/react/common/semver-utils.ts
new file mode 100644
index 000000000..364d26761
--- /dev/null
+++ b/app/react/common/semver-utils.ts
@@ -0,0 +1,27 @@
+/**
+ * Compares two semver strings.
+ *
+ * returns:
+ * - `-1` if `a < b`
+ * - `0` if `a == b`
+ * - `1` if `a > b`
+ */
+export function semverCompare(a: string, b: string) {
+ if (a.startsWith(`${b}-`)) {
+ return -1;
+ }
+
+ if (b.startsWith(`${a}-`)) {
+ return 1;
+ }
+
+ return a.localeCompare(b, undefined, {
+ numeric: true,
+ sensitivity: 'case',
+ caseFirst: 'upper',
+ });
+}
+
+export function isVersionSmaller(a: string, b: string) {
+ return semverCompare(a, b) < 0;
+}
diff --git a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx
index 99ab8ee23..edeb18fc6 100644
--- a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx
+++ b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx
@@ -5,9 +5,14 @@ import {
type Icon as IconType,
Loader2,
XCircle,
+ MinusCircle,
} from 'lucide-react';
+import { useEnvironmentList } from '@/react/portainer/environments/queries';
+import { isVersionSmaller } from '@/react/common/semver-utils';
+
import { Icon, IconMode } from '@@/Icon';
+import { Tooltip } from '@@/Tip/Tooltip';
import { DeploymentStatus, EdgeStack, StatusType } from '../../types';
@@ -15,28 +20,51 @@ export function EdgeStackStatus({ edgeStack }: { edgeStack: EdgeStack }) {
const status = Object.values(edgeStack.Status);
const lastStatus = _.compact(status.map((s) => _.last(s.Status)));
- const { icon, label, mode, spin } = getStatus(
+ const environmentsQuery = useEnvironmentList({ edgeStackId: edgeStack.Id });
+
+ if (environmentsQuery.isLoading) {
+ return null;
+ }
+
+ const hasOldVersion = environmentsQuery.environments.some((env) =>
+ isVersionSmaller(env.Agent.Version, '2.19.0')
+ );
+
+ const { icon, label, mode, spin, tooltip } = getStatus(
edgeStack.NumDeployments,
- lastStatus
+ lastStatus,
+ hasOldVersion
);
return (
{icon && }
{label}
+ {tooltip && }
);
}
function getStatus(
numDeployments: number,
- envStatus: Array
+ envStatus: Array,
+ hasOldVersion: boolean
): {
label: string;
icon?: IconType;
spin?: boolean;
mode?: IconMode;
+ tooltip?: string;
} {
+ if (!numDeployments || hasOldVersion) {
+ return {
+ label: 'Unavailable',
+ icon: MinusCircle,
+ mode: 'secondary',
+ tooltip: getUnavailableTooltip(),
+ };
+ }
+
if (envStatus.length < numDeployments) {
return {
label: 'Deploying',
@@ -56,7 +84,11 @@ function getStatus(
};
}
- const allRunning = envStatus.every((s) => s.Type === StatusType.Running);
+ const allRunning = envStatus.every(
+ (s) =>
+ s.Type === StatusType.Running ||
+ (s.Type === StatusType.DeploymentReceived && hasOldVersion)
+ );
if (allRunning) {
return {
@@ -84,4 +116,16 @@ function getStatus(
spin: true,
mode: 'primary',
};
+
+ function getUnavailableTooltip() {
+ if (!numDeployments) {
+ return 'Your edge stack is currently unavailable due to the absence of an available environment in your edge group';
+ }
+
+ if (hasOldVersion) {
+ return 'Please note that the new status feature for the Edge stack is only available for Edge Agent versions 2.19.0 and above. To access the status of your edge stack, it is essential to upgrade your Edge Agent to a corresponding version that is compatible with your Portainer server.';
+ }
+
+ return '';
+ }
}
diff --git a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/columns.tsx b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/columns.tsx
index b140de240..354b9669a 100644
--- a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/columns.tsx
+++ b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/columns.tsx
@@ -45,13 +45,19 @@ export const columns = _.compact([
(item) => item.aggregatedStatus[StatusType.ImagesPulled] || 0,
{
header: 'Images pre-pulled',
- cell: ({ getValue, row }) => (
-
- ),
+ cell: ({ getValue, row: { original: item } }) => {
+ if (!item.PrePullImage) {
+ return -
;
+ }
+
+ return (
+
+ );
+ },
enableSorting: false,
enableHiding: false,
meta: {
diff --git a/app/react/portainer/environments/update-schedules/common/UpdateScheduleDetailsFieldset.tsx b/app/react/portainer/environments/update-schedules/common/UpdateScheduleDetailsFieldset.tsx
index 45d7dc465..c7af44164 100644
--- a/app/react/portainer/environments/update-schedules/common/UpdateScheduleDetailsFieldset.tsx
+++ b/app/react/portainer/environments/update-schedules/common/UpdateScheduleDetailsFieldset.tsx
@@ -1,12 +1,12 @@
import _ from 'lodash';
import { Environment } from '@/react/portainer/environments/types';
+import { semverCompare } from '@/react/common/semver-utils';
import { TextTip } from '@@/Tip/TextTip';
import { VersionSelect } from './VersionSelect';
import { ScheduledTimeField } from './ScheduledTimeField';
-import { semverCompare } from './utils';
interface Props {
environments: Environment[];
diff --git a/app/react/portainer/environments/update-schedules/common/utils.ts b/app/react/portainer/environments/update-schedules/common/utils.ts
index 32696ce1b..0e02742ec 100644
--- a/app/react/portainer/environments/update-schedules/common/utils.ts
+++ b/app/react/portainer/environments/update-schedules/common/utils.ts
@@ -1,18 +1,4 @@
-export function semverCompare(a: string, b: string) {
- if (a.startsWith(`${b}-`)) {
- return -1;
- }
-
- if (b.startsWith(`${a}-`)) {
- return 1;
- }
-
- return a.localeCompare(b, undefined, {
- numeric: true,
- sensitivity: 'case',
- caseFirst: 'upper',
- });
-}
+import { semverCompare } from '@/react/common/semver-utils';
export function compareVersion(
currentVersion: string,
diff --git a/app/react/portainer/environments/update-schedules/queries/useSupportedAgentVersions.ts b/app/react/portainer/environments/update-schedules/queries/useSupportedAgentVersions.ts
index 38cd616c3..31be18a7b 100644
--- a/app/react/portainer/environments/update-schedules/queries/useSupportedAgentVersions.ts
+++ b/app/react/portainer/environments/update-schedules/queries/useSupportedAgentVersions.ts
@@ -2,8 +2,7 @@ import { useQuery } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { withError } from '@/react-tools/react-query';
-
-import { semverCompare } from '../common/utils';
+import { semverCompare } from '@/react/common/semver-utils';
import { queryKeys } from './query-keys';
import { buildUrl } from './urls';