@spinnaker/core 0.11.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/cloudProvider/CloudProviderLogo.d.ts +0 -5
  3. package/dist/config/settings.d.ts +1 -0
  4. package/dist/core.module.d.ts +1 -1
  5. package/dist/index.js +40 -32
  6. package/dist/index.js.map +1 -1
  7. package/dist/managed/constraints/registry.d.ts +1 -1
  8. package/dist/managed/environmentBaseElements/EnvironmentsRender.d.ts +3 -2
  9. package/dist/managed/graphql/graphql-sdk.d.ts +35 -0
  10. package/dist/managed/overview/PreviewEnvironments.d.ts +8 -0
  11. package/dist/managed/resources/ToggleResourceManagement.d.ts +9 -0
  12. package/dist/managed/resources/resourceRegistry.d.ts +1 -1
  13. package/dist/managed/utils/defaults.d.ts +1 -1
  14. package/dist/managed/utils/useNotifyOnError.hook.d.ts +6 -0
  15. package/dist/pipeline/service/execution.service.d.ts +2 -1
  16. package/package.json +6 -6
  17. package/src/cloudProvider/CloudProviderLogo.tsx +0 -5
  18. package/src/cloudProvider/CloudProviderRegistry.ts +6 -65
  19. package/src/cloudProvider/providerSelection/ProviderSelectionService.spec.ts +96 -0
  20. package/src/cloudProvider/providerSelection/ProviderSelectionService.ts +6 -8
  21. package/src/cluster/allClusters.html +1 -0
  22. package/src/config/settings.ts +2 -0
  23. package/src/core.module.ts +1 -1
  24. package/src/function/function.dataSource.ts +1 -0
  25. package/src/instance/details/multipleInstances.controller.js +6 -0
  26. package/src/instance/details/multipleInstances.view.html +1 -0
  27. package/src/managed/config/Configuration.tsx +8 -1
  28. package/src/managed/config/DeliveryConfig.tsx +3 -9
  29. package/src/managed/config/GitIntegration.tsx +8 -1
  30. package/src/managed/environmentBaseElements/EnvironmentsRender.tsx +7 -1
  31. package/src/managed/externals/toggleResourceManagement.tsx +12 -59
  32. package/src/managed/graphql/graphql-sdk.ts +54 -0
  33. package/src/managed/graphql/schema.graphql +1 -0
  34. package/src/managed/overview/EnvironmentsOverview.tsx +8 -11
  35. package/src/managed/overview/PreviewEnvironments.tsx +47 -0
  36. package/src/managed/overview/Resource.tsx +76 -34
  37. package/src/managed/overview/artifact/ArtifactVersionTasks.tsx +3 -10
  38. package/src/managed/overview/artifact/Constraints.tsx +3 -11
  39. package/src/managed/overview/queries.graphql +8 -0
  40. package/src/managed/resources/ToggleResourceManagement.tsx +53 -0
  41. package/src/managed/utils/defaults.ts +1 -1
  42. package/src/managed/utils/useNotifyOnError.hook.ts +22 -0
  43. package/src/modal/modals.less +1 -1
  44. package/src/pipeline/details/SingleExecutionDetails.tsx +1 -1
  45. package/src/pipeline/service/execution.service.ts +7 -2
  46. package/src/presentation/main.less +3 -3
  47. package/src/serverGroup/details/multipleServerGroups.controller.js +6 -0
  48. package/src/serverGroup/details/multipleServerGroups.view.html +1 -0
  49. package/src/utils/clipboard/CopyToClipboard.tsx +1 -1
@@ -5,29 +5,10 @@ import { ManagedWriter } from '../ManagedWriter';
5
5
  import { Application } from '../../application';
6
6
  import { ConfirmationModalService } from '../../confirmationModal';
7
7
  import { IManagedResource, IManagedResourceSummary, ManagedResourceStatus } from '../../domain';
8
+ import { ActuationWarning, MultiRegionWarning, ToggleResourceManagement } from '../resources/ToggleResourceManagement';
8
9
 
9
10
  import './ManagedResourceStatusIndicator.less';
10
11
 
11
- interface IToggleConfiguration {
12
- pauseWarning?: JSX.Element;
13
- }
14
-
15
- const viewConfigurationByStatus: { [status in ManagedResourceStatus]?: IToggleConfiguration } = {
16
- ACTUATING: {
17
- pauseWarning: (
18
- <p>
19
- <div className="horizontal top sp-padding-m alert alert-warning">
20
- <i className="fa fa-exclamation-triangle sp-margin-m-right sp-margin-xs-top" />
21
- <span>
22
- Pausing management will not interrupt the action Spinnaker is currently performing to resolve the difference
23
- from desired state.
24
- </span>
25
- </div>
26
- </p>
27
- ),
28
- },
29
- };
30
-
31
12
  /***
32
13
  * If the resource is not managed, or management is paused, this will return an immediate promise with a true value.
33
14
  * If the resource is managed, an interstitial modal will prompt the user to pause resource management. The promise will
@@ -71,49 +52,21 @@ export const toggleResourcePause = (
71
52
 
72
53
  return ConfirmationModalService.confirm({
73
54
  header: `Really ${isPaused ? 'resume' : 'pause'} resource management?`,
74
- bodyContent: <PopoverToggleBodyText resourceSummary={resourceSummary} />,
55
+ bodyContent: (
56
+ <ToggleResourceManagement
57
+ isActuating={resourceSummary.status === ManagedResourceStatus.ACTUATING}
58
+ isPaused={isPaused}
59
+ regions={getRegions(resourceSummary)}
60
+ />
61
+ ),
75
62
  account: resourceSummary.locations.account,
76
63
  buttonText: `${isPaused ? 'Resume' : 'Pause'} management`,
77
64
  submitMethod,
78
65
  });
79
66
  };
80
67
 
81
- const PopoverToggleBodyText = ({ resourceSummary }: { resourceSummary: IManagedResourceSummary }) => {
82
- const { isPaused, status } = resourceSummary;
83
- if (isPaused) {
84
- return (
85
- <>
86
- <p>Spinnaker will resume taking action to correct differences from the desired state.</p>
87
- <MultiRegionWarning resourceSummary={resourceSummary} />
88
- </>
89
- );
90
- } else {
91
- return (
92
- <>
93
- <p>While a resource is paused, Spinnaker will not take action to correct differences from the desired state.</p>
94
- {viewConfigurationByStatus[status]?.pauseWarning}
95
- <MultiRegionWarning resourceSummary={resourceSummary} />
96
- </>
97
- );
98
- }
99
- };
100
-
101
- const MultiRegionWarning = ({ resourceSummary }: { resourceSummary: IManagedResourceSummary }) => {
102
- const { isPaused, locations } = resourceSummary;
103
- const regions = locations.regions.map((r) => r.name).sort();
104
- if (regions.length < 2) {
105
- return null;
106
- }
107
- return (
108
- <div className="horizontal top sp-padding-m alert alert-warning">
109
- <i className="fa fa-exclamation-triangle sp-margin-m-right sp-margin-xs-top" />
110
- <span>
111
- {isPaused ? 'Resuming' : 'Pausing'} management of this resource will affect the following regions:{' '}
112
- <b>{regions.join(', ')}</b>.
113
- </span>
114
- </div>
115
- );
116
- };
68
+ const getRegions = (resourceSummary: IManagedResourceSummary) =>
69
+ resourceSummary.locations.regions.map((r) => r.name).sort();
117
70
 
118
71
  const BodyText = ({ resourceSummary }: { resourceSummary: IManagedResourceSummary }) => {
119
72
  const { status } = resourceSummary;
@@ -126,8 +79,8 @@ const BodyText = ({ resourceSummary }: { resourceSummary: IManagedResourceSummar
126
79
  If you need to temporarily stop Spinnaker from managing this resource — for example, if something is wrong and
127
80
  manual intervention is required — you can pause management and resume it later.
128
81
  </p>
129
- {viewConfigurationByStatus[status]?.pauseWarning}
130
- <MultiRegionWarning resourceSummary={resourceSummary} />
82
+ {status === ManagedResourceStatus.ACTUATING && <ActuationWarning />}
83
+ <MultiRegionWarning isPaused regions={getRegions(resourceSummary)} />
131
84
  </>
132
85
  );
133
86
  };
@@ -120,6 +120,7 @@ export interface MdConfig {
120
120
  updatedAt?: Maybe<Scalars['InstantTime']>;
121
121
  rawConfig?: Maybe<Scalars['String']>;
122
122
  processedConfig?: Maybe<Scalars['String']>;
123
+ previewEnvironmentsConfigured?: Maybe<Scalars['Boolean']>;
123
124
  }
124
125
 
125
126
  export interface MdConstraint {
@@ -545,6 +546,7 @@ export type FetchApplicationQueryVariables = Exact<{
545
546
  export type FetchApplicationQuery = { __typename?: 'Query' } & {
546
547
  application?: Maybe<
547
548
  { __typename?: 'MdApplication' } & Pick<MdApplication, 'id' | 'name' | 'account'> & {
549
+ config?: Maybe<{ __typename?: 'MdConfig' } & Pick<MdConfig, 'id' | 'previewEnvironmentsConfigured'>>;
548
550
  environments: Array<
549
551
  { __typename?: 'MdEnvironment' } & Pick<MdEnvironment, 'isDeleting'> & {
550
552
  state: { __typename?: 'MdEnvironmentState' } & Pick<MdEnvironmentState, 'id'> & {
@@ -828,6 +830,12 @@ export type ImportDeliveryConfigMutationVariables = Exact<{
828
830
 
829
831
  export type ImportDeliveryConfigMutation = { __typename?: 'Mutation' } & Pick<Mutation, 'importDeliveryConfig'>;
830
832
 
833
+ export type ToggleResourceManagementMutationVariables = Exact<{
834
+ payload?: Maybe<MdToggleResourceManagementPayload>;
835
+ }>;
836
+
837
+ export type ToggleResourceManagementMutation = { __typename?: 'Mutation' } & Pick<Mutation, 'toggleResourceManagement'>;
838
+
831
839
  export const ActionDetailsFragmentDoc = gql`
832
840
  fragment actionDetails on MdAction {
833
841
  id
@@ -931,6 +939,10 @@ export const FetchApplicationDocument = gql`
931
939
  id
932
940
  name
933
941
  account
942
+ config {
943
+ id
944
+ previewEnvironmentsConfigured
945
+ }
934
946
  environments {
935
947
  ...baseEnvironmentFields
936
948
  isDeleting
@@ -1888,3 +1900,45 @@ export type ImportDeliveryConfigMutationOptions = Apollo.BaseMutationOptions<
1888
1900
  ImportDeliveryConfigMutation,
1889
1901
  ImportDeliveryConfigMutationVariables
1890
1902
  >;
1903
+ export const ToggleResourceManagementDocument = gql`
1904
+ mutation ToggleResourceManagement($payload: MdToggleResourceManagementPayload) {
1905
+ toggleResourceManagement(payload: $payload)
1906
+ }
1907
+ `;
1908
+ export type ToggleResourceManagementMutationFn = Apollo.MutationFunction<
1909
+ ToggleResourceManagementMutation,
1910
+ ToggleResourceManagementMutationVariables
1911
+ >;
1912
+
1913
+ /**
1914
+ * __useToggleResourceManagementMutation__
1915
+ *
1916
+ * To run a mutation, you first call `useToggleResourceManagementMutation` within a React component and pass it any options that fit your needs.
1917
+ * When your component renders, `useToggleResourceManagementMutation` returns a tuple that includes:
1918
+ * - A mutate function that you can call at any time to execute the mutation
1919
+ * - An object with fields that represent the current status of the mutation's execution
1920
+ *
1921
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
1922
+ *
1923
+ * @example
1924
+ * const [toggleResourceManagementMutation, { data, loading, error }] = useToggleResourceManagementMutation({
1925
+ * variables: {
1926
+ * payload: // value for 'payload'
1927
+ * },
1928
+ * });
1929
+ */
1930
+ export function useToggleResourceManagementMutation(
1931
+ baseOptions?: Apollo.MutationHookOptions<ToggleResourceManagementMutation, ToggleResourceManagementMutationVariables>,
1932
+ ) {
1933
+ const options = { ...defaultOptions, ...baseOptions };
1934
+ return Apollo.useMutation<ToggleResourceManagementMutation, ToggleResourceManagementMutationVariables>(
1935
+ ToggleResourceManagementDocument,
1936
+ options,
1937
+ );
1938
+ }
1939
+ export type ToggleResourceManagementMutationHookResult = ReturnType<typeof useToggleResourceManagementMutation>;
1940
+ export type ToggleResourceManagementMutationResult = Apollo.MutationResult<ToggleResourceManagementMutation>;
1941
+ export type ToggleResourceManagementMutationOptions = Apollo.BaseMutationOptions<
1942
+ ToggleResourceManagementMutation,
1943
+ ToggleResourceManagementMutationVariables
1944
+ >;
@@ -31,6 +31,7 @@ type MdConfig {
31
31
  updatedAt: InstantTime
32
32
  rawConfig: String
33
33
  processedConfig: String
34
+ previewEnvironmentsConfigured: Boolean
34
35
  }
35
36
 
36
37
  type MdEnvironment {
@@ -2,6 +2,7 @@ import React from 'react';
2
2
 
3
3
  import { ApplicationQueryError } from '../ApplicationQueryError';
4
4
  import { EnvironmentOverview } from './EnvironmentOverview';
5
+ import { PreviewEnvironments } from './PreviewEnvironments';
5
6
  import { EnvironmentsRender, useOrderedEnvironment } from '../environmentBaseElements/EnvironmentsRender';
6
7
  import { useFetchApplicationQuery } from '../graphql/graphql-sdk';
7
8
  import { Messages } from '../messages/Messages';
@@ -25,11 +26,13 @@ export const EnvironmentsOverview = () => {
25
26
  environments.filter((env) => !env.isPreview),
26
27
  );
27
28
 
28
- const { environments: previewEnvironments, ...previewEnvironmentsProps } = useOrderedEnvironment(
29
+ const previewEnvironments = useOrderedEnvironment(
29
30
  wrapperRef,
30
31
  environments.filter((env) => env.isPreview),
31
32
  );
32
33
 
34
+ const previewEnvironmentsConfigured = data?.application?.config?.previewEnvironmentsConfigured;
35
+
33
36
  let content;
34
37
  if (loading && !data) {
35
38
  content = <Spinner {...spinnerProps} message="Loading environments..." />;
@@ -46,16 +49,10 @@ export const EnvironmentsOverview = () => {
46
49
  <EnvironmentOverview key={env.name} environment={env} />
47
50
  ))}
48
51
  </EnvironmentsRender>
49
- {Boolean(previewEnvironments.length) && (
50
- <h4 className="sp-margin-2xl-top sp-margin-m-bottom self-left">
51
- <b>Preview Environments</b>
52
- </h4>
53
- )}
54
- <EnvironmentsRender {...previewEnvironmentsProps}>
55
- {previewEnvironments.map((env) => (
56
- <EnvironmentOverview key={env.name} environment={env} />
57
- ))}
58
- </EnvironmentsRender>
52
+ <PreviewEnvironments
53
+ orderedEnvironments={previewEnvironments}
54
+ isConfigured={previewEnvironmentsConfigured}
55
+ />
59
56
  </>
60
57
  ) : (
61
58
  <div className="error-message">No environments found</div>
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+
3
+ import { EnvironmentOverview } from './EnvironmentOverview';
4
+ import { EnvironmentsRender, OrderedEnvironments } from '../environmentBaseElements/EnvironmentsRender';
5
+ import { QueryEnvironment } from './types';
6
+ import { getDocsUrl } from '../utils/defaults';
7
+ import { useLogEvent } from '../utils/logging';
8
+
9
+ interface IPreviewEnvironmentsProps {
10
+ orderedEnvironments: OrderedEnvironments<QueryEnvironment>;
11
+ isConfigured?: boolean;
12
+ }
13
+
14
+ export const PreviewEnvironments = ({ orderedEnvironments, isConfigured }: IPreviewEnvironmentsProps) => {
15
+ const { environments: previewEnvironments, ...previewEnvironmentsProps } = orderedEnvironments;
16
+ const logClick = useLogEvent('PreviewEnvironments');
17
+ const docsLink = getDocsUrl('previewEnvironments');
18
+ const hasActivePreviewEnvironments = previewEnvironments.length > 0;
19
+
20
+ return (
21
+ <>
22
+ {(isConfigured || hasActivePreviewEnvironments) && (
23
+ <h4 className="sp-margin-2xl-top sp-margin-m-bottom self-left">
24
+ <b>Preview Environments</b>
25
+ </h4>
26
+ )}
27
+ {isConfigured && !hasActivePreviewEnvironments && (
28
+ <div className="self-left">
29
+ No PRs matching your branch filter were found.
30
+ <br />
31
+ Your preview environments will automatically appear here once you create a PR from a branch matching your
32
+ config definition.{' '}
33
+ {docsLink && (
34
+ <a target="_blank" onClick={() => logClick({ action: 'Learn More' })} href={docsLink}>
35
+ Learn More
36
+ </a>
37
+ )}
38
+ </div>
39
+ )}
40
+ <EnvironmentsRender {...previewEnvironmentsProps}>
41
+ {previewEnvironments.map((env) => (
42
+ <EnvironmentOverview key={env.name} environment={env} />
43
+ ))}
44
+ </EnvironmentsRender>
45
+ </>
46
+ );
47
+ };
@@ -1,12 +1,19 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { ResourceTask } from './ResourceTask';
4
+ import { ConfirmationModalService } from '../../confirmationModal/confirmationModal.service';
4
5
  import { EnvironmentItem } from '../environmentBaseElements/EnvironmentItem';
5
- import { MdResourceActuationState, useFetchResourceStatusQuery } from '../graphql/graphql-sdk';
6
+ import {
7
+ FetchResourceStatusDocument,
8
+ MdResourceActuationState,
9
+ useFetchResourceStatusQuery,
10
+ useToggleResourceManagementMutation,
11
+ } from '../graphql/graphql-sdk';
6
12
  import { Icon, Markdown, useApplicationContextSafe } from '../../presentation';
7
13
  import { showManagedResourceHistoryModal } from '../resourceHistory/ManagedResourceHistoryModal';
8
14
  import { showResourceDefinitionModal } from '../resources/ResourceDefinitionModal';
9
15
  import { ResourceTitle } from '../resources/ResourceTitle';
16
+ import { ToggleResourceManagement } from '../resources/ToggleResourceManagement';
10
17
  import { IResourceLinkProps, resourceManager } from '../resources/resourceRegistry';
11
18
  import { QueryResource } from './types';
12
19
  import { getIsDebugMode } from '../utils/debugMode';
@@ -23,22 +30,30 @@ const statusUtils: {
23
30
  };
24
31
  } = {
25
32
  ERROR: { color: 'var(--color-status-error)', icon: 'fas fa-times', defaultReason: 'Failed to update resource' },
26
- NOT_MANAGED: { color: 'var(--color-status-warning)', icon: 'fas fa-pause', defaultReason: 'Resource is not managed' },
33
+ NOT_MANAGED: {
34
+ color: 'var(--color-status-warning)',
35
+ icon: 'fas fa-pause',
36
+ defaultReason: 'Resource management is paused',
37
+ },
27
38
  WAITING: { icon: 'far fa-hourglass', defaultReason: 'Resource is currently locked and can not be updated' },
28
39
  PROCESSING: { icon: 'far fa-hourglass', defaultReason: 'Resource is being updated' },
29
40
  DELETING: { icon: 'far fa-trash-alt', defaultReason: 'Resource is being deleted' },
30
41
  };
31
42
 
32
- const Status = ({
33
- appName,
34
- environmentName,
35
- resourceId,
36
- }: {
43
+ interface IStatusProps {
37
44
  appName: string;
38
45
  environmentName: string;
39
46
  resourceId: string;
40
- }) => {
47
+ regions: string[];
48
+ account?: string;
49
+ }
50
+
51
+ const Status = ({ appName, environmentName, resourceId, regions, account }: IStatusProps) => {
41
52
  const { data: resourceStatuses, error, loading } = useFetchResourceStatusQuery({ variables: { appName } });
53
+ const [enableResourceManagement] = useToggleResourceManagementMutation({
54
+ variables: { payload: { id: resourceId, isPaused: false } },
55
+ refetchQueries: [{ query: FetchResourceStatusDocument, variables: { appName } }],
56
+ });
42
57
  const state = resourceStatuses?.application?.environments
43
58
  .find((env) => env.name === environmentName)
44
59
  ?.state.resources?.find((resource) => resource.id === resourceId)?.state;
@@ -51,32 +66,53 @@ const Status = ({
51
66
  </div>
52
67
  );
53
68
  }
54
-
55
- if (state) {
56
- if (state.status === 'UP_TO_DATE') return null;
57
-
58
- return (
59
- <div className="resource-status">
60
- <i
61
- className={statusUtils[state.status].icon}
62
- style={{ color: statusUtils[state.status].color || 'var(--color-titanium)' }}
63
- />
64
- <div>
65
- <div>{state.reason || statusUtils[state.status].defaultReason}</div>
66
- {state.event && state.event !== state.reason && <Markdown className="event" message={state.event} />}
67
- {Boolean(state.tasks?.length) && (
68
- <ul className="tasks-list">
69
- {state.tasks?.map(({ id, name }) => (
70
- <ResourceTask key={id} id={id} name={name} />
71
- ))}
72
- </ul>
73
- )}
74
- </div>
69
+ if (!state) return <Spinner className="sp-margin-xs-top" mode="circular" size="nano" color="var(--color-accent)" />;
70
+ if (state.status === 'UP_TO_DATE') return null;
71
+ const isNotManaged = state.status === 'NOT_MANAGED';
72
+ const reasonElem = (
73
+ <div>
74
+ {state.reason || statusUtils[state.status].defaultReason}
75
+ {isNotManaged ? ' (click to enable...)' : undefined}
76
+ </div>
77
+ );
78
+ return (
79
+ <div className="resource-status">
80
+ <i
81
+ className={statusUtils[state.status].icon}
82
+ style={{
83
+ color: statusUtils[state.status].color || 'var(--color-titanium)',
84
+ }}
85
+ />
86
+ <div>
87
+ {isNotManaged ? (
88
+ <button
89
+ className="as-link"
90
+ onClick={() => {
91
+ ConfirmationModalService.confirm({
92
+ header: `Really resume resource management?`,
93
+ bodyContent: <ToggleResourceManagement isPaused regions={regions} />,
94
+ account: account,
95
+ buttonText: `Resume management`,
96
+ submitMethod: enableResourceManagement,
97
+ });
98
+ }}
99
+ >
100
+ {reasonElem}
101
+ </button>
102
+ ) : (
103
+ reasonElem
104
+ )}
105
+ {state.event && state.event !== state.reason && <Markdown className="event" message={state.event} />}
106
+ {Boolean(state.tasks?.length) && (
107
+ <ul className="tasks-list">
108
+ {state.tasks?.map(({ id, name }) => (
109
+ <ResourceTask key={id} id={id} name={name} />
110
+ ))}
111
+ </ul>
112
+ )}
75
113
  </div>
76
- );
77
- }
78
-
79
- return <Spinner className="sp-margin-xs-top" mode="circular" size="nano" color="var(--color-accent)" />;
114
+ </div>
115
+ );
80
116
  };
81
117
 
82
118
  export const Resource = ({ resource, environment }: { resource: QueryResource; environment: string }) => {
@@ -142,7 +178,13 @@ export const Resource = ({ resource, environment }: { resource: QueryResource; e
142
178
  )}
143
179
  </div>
144
180
  <div>
145
- <Status appName={app.name} environmentName={environment} resourceId={resource.id} />
181
+ <Status
182
+ appName={app.name}
183
+ environmentName={environment}
184
+ resourceId={resource.id}
185
+ account={account}
186
+ regions={regions}
187
+ />
146
188
  </div>
147
189
  </EnvironmentItem>
148
190
  );
@@ -7,7 +7,8 @@ import { Tooltip, useApplicationContextSafe } from '../../../presentation';
7
7
  import { QueryArtifactVersionTask, QueryArtifactVersionTaskStatus } from '../types';
8
8
  import { TOOLTIP_DELAY_SHOW } from '../../utils/defaults';
9
9
  import { useLogEvent } from '../../utils/logging';
10
- import { NotifierService, Spinner } from '../../../widgets';
10
+ import { useNotifyOnError } from '../../utils/useNotifyOnError.hook';
11
+ import { Spinner } from '../../../widgets';
11
12
 
12
13
  import './ArtifactVersionTasks.less';
13
14
 
@@ -55,15 +56,7 @@ const ArtifactVersionTask = ({ type, artifact, task }: IArtifactVersionTaskProps
55
56
  },
56
57
  });
57
58
 
58
- React.useEffect(() => {
59
- if (error) {
60
- NotifierService.publish({
61
- key: task.id,
62
- content: `Failed to re-run ${type} - ${error.message}`,
63
- options: { type: 'error' },
64
- });
65
- }
66
- }, [error]);
59
+ useNotifyOnError({ key: task.id, content: `Failed to re-run ${type}`, error });
67
60
 
68
61
  return (
69
62
  <div className="version-task">
@@ -10,7 +10,8 @@ import { CollapsibleSection, useApplicationContextSafe } from '../../../presenta
10
10
  import { ArtifactVersionProps, QueryConstraint } from '../types';
11
11
  import { getConstraintsStatusSummary } from './utils';
12
12
  import { useLogEvent } from '../../utils/logging';
13
- import { NotifierService, Spinner } from '../../../widgets';
13
+ import { useNotifyOnError } from '../../utils/useNotifyOnError.hook';
14
+ import { Spinner } from '../../../widgets';
14
15
 
15
16
  import './Constraints.less';
16
17
 
@@ -31,16 +32,7 @@ const ConstraintContent = ({ constraint, versionProps }: IConstraintContentProps
31
32
  ],
32
33
  });
33
34
 
34
- React.useEffect(() => {
35
- if (error) {
36
- NotifierService.publish({
37
- action: 'create',
38
- key: 'updateConstraintError',
39
- content: `Failed to update constraint - ${error.message}`,
40
- options: { type: 'error' },
41
- });
42
- }
43
- }, [error]);
35
+ useNotifyOnError({ key: 'updateConstraintError', content: `Failed to update constraint`, error });
44
36
 
45
37
  return (
46
38
  <dl className="constraint-content">
@@ -95,6 +95,10 @@ query fetchApplication($appName: String!, $statuses: [MdArtifactStatusInEnvironm
95
95
  id
96
96
  name
97
97
  account
98
+ config {
99
+ id
100
+ previewEnvironmentsConfigured
101
+ }
98
102
  environments {
99
103
  ...baseEnvironmentFields
100
104
  isDeleting
@@ -339,3 +343,7 @@ mutation DismissNotification($payload: MdDismissNotificationPayload!) {
339
343
  mutation ImportDeliveryConfig($application: String!) {
340
344
  importDeliveryConfig(application: $application)
341
345
  }
346
+
347
+ mutation ToggleResourceManagement($payload: MdToggleResourceManagementPayload) {
348
+ toggleResourceManagement(payload: $payload)
349
+ }
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+
3
+ interface IToggleResourceManagementProps {
4
+ isPaused: boolean;
5
+ isActuating?: boolean;
6
+ regions: string[];
7
+ }
8
+
9
+ export const ActuationWarning = () => (
10
+ <p>
11
+ <div className="horizontal top sp-padding-m alert alert-warning">
12
+ <i className="fa fa-exclamation-triangle sp-margin-m-right sp-margin-xs-top" />
13
+ <span>
14
+ Pausing management will not interrupt the action Spinnaker is currently performing to resolve the difference
15
+ from desired state.
16
+ </span>
17
+ </div>
18
+ </p>
19
+ );
20
+
21
+ export const ToggleResourceManagement = ({ isPaused, isActuating, regions }: IToggleResourceManagementProps) => {
22
+ if (isPaused) {
23
+ return (
24
+ <>
25
+ <p>Spinnaker will resume taking action to correct differences from the desired state.</p>
26
+ <MultiRegionWarning isPaused regions={regions} />
27
+ </>
28
+ );
29
+ } else {
30
+ return (
31
+ <>
32
+ <p>While a resource is paused, Spinnaker will not take action to correct differences from the desired state.</p>
33
+ {isActuating && <ActuationWarning />}
34
+ <MultiRegionWarning isPaused regions={regions} />
35
+ </>
36
+ );
37
+ }
38
+ };
39
+
40
+ export const MultiRegionWarning = ({ isPaused, regions }: Omit<IToggleResourceManagementProps, 'isActuating'>) => {
41
+ if (regions.length < 2) {
42
+ return null;
43
+ }
44
+ return (
45
+ <div className="horizontal top sp-padding-m alert alert-warning">
46
+ <i className="fa fa-exclamation-triangle sp-margin-m-right sp-margin-xs-top" />
47
+ <span>
48
+ {isPaused ? 'Resuming' : 'Pausing'} management of this resource will affect the following regions:{' '}
49
+ <b>{regions.join(', ')}</b>.
50
+ </span>
51
+ </div>
52
+ );
53
+ };
@@ -26,6 +26,6 @@ const DOCS_URLS: IManagedDeliveryURLs = {
26
26
  resourceStatus: 'https://www.spinnaker.io/guides/user/managed-delivery/resource-status/',
27
27
  };
28
28
 
29
- export const getDocsUrl = (doc: keyof IManagedDeliveryURLs): string => {
29
+ export const getDocsUrl = (doc: keyof IManagedDeliveryURLs): string | undefined => {
30
30
  return SETTINGS.managedDelivery?.urls?.[doc] || DOCS_URLS[doc];
31
31
  };
@@ -0,0 +1,22 @@
1
+ import { ApolloError } from '@apollo/client';
2
+ import React from 'react';
3
+ import { NotifierService } from '../../widgets';
4
+
5
+ export const useNotifyOnError = ({
6
+ key,
7
+ content,
8
+ error,
9
+ }: {
10
+ key: string;
11
+ content?: string;
12
+ error: ApolloError | undefined;
13
+ }) => {
14
+ React.useEffect(() => {
15
+ if (!error) return;
16
+ NotifierService.publish({
17
+ key,
18
+ content: content ? `${content} - ` : '' + error.message,
19
+ options: { type: 'error' },
20
+ });
21
+ }, [error]);
22
+ };
@@ -222,7 +222,7 @@ html .select2-container-multi {
222
222
  display: inline-block;
223
223
  border: 1px solid var(--color-alto);
224
224
  background-color: var(--color-white);
225
- font-family: 'Source Sans Pro', sans-serif;
225
+ font-family: 'Source Sans 3', sans-serif;
226
226
  border-radius: 4px;
227
227
  }
228
228
  .select2-search-choice-close {
@@ -30,7 +30,7 @@ export interface ISingleExecutionRouterStateChange extends IStateChange {
30
30
  }
31
31
 
32
32
  export function getAndTransformExecution(id: string, app: Application) {
33
- return ReactInjector.executionService.getExecution(id).then((execution) => {
33
+ return ReactInjector.executionService.getExecution(id, app.pipelineConfigs?.data).then((execution) => {
34
34
  ExecutionsTransformer.transformExecution(app, execution);
35
35
  return execution;
36
36
  });
@@ -97,7 +97,7 @@ export class ExecutionService {
97
97
  return this.getFilteredExecutions(applicationName, statuses, limit, null, expand);
98
98
  }
99
99
 
100
- public getExecution(executionId: string): PromiseLike<IExecution> {
100
+ public getExecution(executionId: string, pipelineConfigs: IPipeline[] = []): PromiseLike<IExecution> {
101
101
  return REST('/pipelines')
102
102
  .path(executionId)
103
103
  .get()
@@ -106,6 +106,11 @@ export class ExecutionService {
106
106
  execution.hydrated = true;
107
107
  this.cleanExecutionForDiffing(execution);
108
108
  if (application && name) {
109
+ const cached = pipelineConfigs.find((config) => config.id === execution.pipelineConfigId);
110
+ if (cached) {
111
+ execution.pipelineConfig = cached;
112
+ return Promise.resolve(execution);
113
+ }
109
114
  return REST('/applications')
110
115
  .path(application, 'pipelineConfigs', name)
111
116
  .get()
@@ -402,7 +407,7 @@ export class ExecutionService {
402
407
  });
403
408
  application.executions.data.forEach((execution: IExecution) => {
404
409
  if (execution.isActive && application.runningExecutions.data.every((e: IExecution) => e.id !== execution.id)) {
405
- this.getExecution(execution.id).then((updatedExecution) => {
410
+ this.getExecution(execution.id, application.pipelineConfigs?.data).then((updatedExecution) => {
406
411
  this.updateExecution(application, updatedExecution);
407
412
  });
408
413
  }