• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender-server / 1568834739

02 Dec 2024 10:01AM UTC coverage: 73.562% (+0.8%) from 72.786%
1568834739

Pull #211

gitlab-ci

mineralsfree
test: added upgrade unit tests

Ticket: MEN-7469
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #211: MEN-7469-feat: updated upgrades and add-on page

4251 of 6156 branches covered (69.05%)

Branch coverage included in aggregate %.

166 of 200 new or added lines in 18 files covered. (83.0%)

47 existing lines in 4 files now uncovered.

40029 of 54038 relevant lines covered (74.08%)

17.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.45
/frontend/src/js/components/settings/AddonSelection.tsx
1
// Copyright 2021 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14
import React, { useMemo, useState } from 'react';
15

16
import { Launch as LaunchIcon } from '@mui/icons-material';
17
import { Button, Chip } from '@mui/material';
18
import { makeStyles } from 'tss-react/mui';
19

20
import { ConfirmAddon } from '@northern.tech/common-ui/dialogs/ConfirmAddon';
21
import { ADDONS, Addon, AvailablePlans, PLANS } from '@northern.tech/store/constants';
22
import { Organization } from '@northern.tech/store/organizationSlice/types';
23
import { useAppDispatch } from '@northern.tech/store/store';
24
import { requestPlanChange } from '@northern.tech/store/thunks';
25

26
import { addOnsToString, clearLocalStorageEntry, updateLocalStorage } from './Upgrade';
27

28
const useStyles = makeStyles()(theme => ({
6✔
29
  chip: {
30
    color: theme.palette.grey[800],
31
    textTransform: 'uppercase',
32
    background: theme.palette.tooltip.tierTipBackground,
33
    borderRadius: theme.spacing(3),
34
    fontSize: '11px',
35
    fontWeight: 'bold',
36
    height: '27px'
37
  },
38
  price: { weight: 400, flexGrow: 2 },
39
  addButton: {
40
    color: `${theme.palette.primary.main} !important`,
41
    textTransform: 'none',
42
    padding: '2px 5px'
43
  },
44
  planPanelAddon: {
45
    display: 'flex',
46

47
    justifyContent: 'space-between',
48
    alignItems: 'center',
49
    borderTop: '1px solid',
50
    borderColor: theme.palette.grey[300],
51
    minHeight: '35px',
52
    '&:first-of-type': {
53
      border: 'none'
54
    }
55
  },
56
  link: { width: '170px' },
57
  removeButton: {
58
    backgroundColor: theme.palette.background.default,
59
    textTransform: 'none',
60
    color: `${theme.palette.secondary.lighter} !important`,
61
    padding: '2px 5px'
62
  },
63
  placeholder: {
64
    width: '140px'
65
  }
66
}));
67
interface RelatableAddon extends Addon {
68
  name: string;
69
  isEnabled: boolean;
70
  pending: { isAdd: string; name: string };
71
  isEligible: boolean;
72
}
73
interface AddOnSelectionProps {
74
  org: Organization;
75
  currentPlan: AvailablePlans;
76
  addons: { enabled: boolean; name: string }[];
77
  features: string[];
78
  updatedPlan: string;
79
  isTrial: boolean;
80
}
81
export const AddOnSelection = ({ org, currentPlan, addons = [], features, updatedPlan = PLANS.os.id, isTrial }: AddOnSelectionProps) => {
6✔
82
  const [action, setAction] = useState<{ name: string; isAdd: boolean } | null>(null);
20✔
83
  const currentPlanName = PLANS[currentPlan].name;
20✔
84
  const { classes } = useStyles();
20✔
85
  const dispatch = useAppDispatch();
20✔
86
  const onAddOnClick = (name: string, isAdd: boolean) => {
20✔
87
    setAction({ name, isAdd });
2✔
88
  };
89
  const requestAddon = async () => {
20✔
90
    if (!action) return;
2!
91
    const { name: addonName, isAdd } = action;
2✔
92
    let requested_addons = addOnsToString(org.addons).split(', ');
2✔
93
    if (isAdd) {
2!
94
      requested_addons.push(addonName);
2✔
95
    } else {
NEW
96
      requested_addons = requested_addons.filter(addon => addon !== addonName);
×
97
    }
98
    try {
2✔
99
      await dispatch(
2✔
100
        requestPlanChange({
101
          tenantId: org.id,
102
          content: {
103
            current_plan: currentPlanName,
104
            requested_plan: currentPlanName,
105
            current_addons: addOnsToString(org.addons) || '-',
4✔
106
            requested_addons: requested_addons.filter(addon => !!addon).join(', ') || '-',
4!
107
            user_message: ''
108
          }
109
        })
110
      ).unwrap();
111
    } catch (error) {
NEW
112
      console.error(error);
×
NEW
113
      return;
×
114
    }
115
    updateLocalStorage(org.id, addonName, isAdd);
2✔
116
    setAction(null);
2✔
117
  };
118
  const isUpgrade = Object.keys(PLANS).indexOf(updatedPlan) < Object.keys(PLANS).length - 1;
20✔
119
  const relevantAddons = useMemo(
20✔
120
    () => {
121
      const currentState = JSON.parse(localStorage.getItem(org.id + '_upgrades') || '{}');
9✔
122
      return Object.entries(ADDONS)
9✔
123
        .reduce((acc: RelatableAddon[], [addOnName, addOn]) => {
124
          const isEnabled = addons.some(orgAddOn => orgAddOn.enabled && addOnName === orgAddOn.name);
27!
125
          let pending = currentState[addOnName];
27✔
126

127
          if (pending && pending.pending && pending.isAdd === isEnabled) {
27!
NEW
128
            clearLocalStorageEntry(org.id, addOnName);
×
NEW
129
            pending = null;
×
130
          }
131
          acc.push({ ...addOn, name: addOnName, isEnabled, pending, isEligible: addOn.eligible.indexOf(currentPlan) > -1 });
27✔
132
          return acc;
27✔
133
        }, [])
134
        .sort((a, b) => a.name.localeCompare(b.name));
36✔
135
    },
136
    // eslint-disable-next-line react-hooks/exhaustive-deps
137
    [JSON.stringify(addons), JSON.stringify(features), action, org]
138
  );
139
  return (
20✔
140
    <>
141
      {action && <ConfirmAddon name={action.name} onClose={() => setAction(null)} onConfirm={() => requestAddon()} variant={action.isAdd ? 'add' : 'remove'} />}
2!
142
      <h3 className="margin-top-large">Get more features with add-ons</h3>
143

144
      <div className="flexbox column">
145
        <p>
146
          Enhance your Mender plan with optional add-on packages – offering additional features to easily manage your software and devices over-the-air. Below
147
          you can request which add-ons you’d like to be included with your plan.
148
        </p>
149
        <div className="flexbox column">
150
          {relevantAddons.map(addOn => {
151
            return (
60✔
152
              <div key={addOn.name} className={`${classes.planPanelAddon} ${addOn.isEnabled ? 'active' : ''} ${addOn.isEligible ? '' : 'muted'}`}>
120!
153
                <a
154
                  className={`${classes.link} flexbox center-aligned bold`}
155
                  href={`https://mender.io/pricing/add-ons/${addOn.name}`}
156
                  target="_blank"
157
                  rel="noopener noreferrer"
158
                >
159
                  Mender {addOn.title}
160
                  <LaunchIcon className="link-color margin-left-x-small" fontSize="small" />
161
                </a>
162
                {isUpgrade && currentPlan !== PLANS.enterprise.id ? (
174✔
163
                  <div className={`${classes.price} margin-left`}>
164
                    {addOn.isEligible ? (
45✔
165
                      <>
166
                        starting <b>{addOn[currentPlan].price}</b>
167
                      </>
168
                    ) : (
169
                      <>not available on {currentPlanName} plan</>
170
                    )}
171
                  </div>
172
                ) : (
173
                  currentPlan === PLANS.enterprise.id && <div />
24✔
174
                )}
175
                {addOn.isEnabled ? (
60!
176
                  <>
177
                    <Chip className={`${classes.chip} margin-right-small`} label="active" />
178
                    {addOn.pending && !addOn.pending.isAdd ? (
×
179
                      <Chip label="removal pending" className={classes.chip} />
180
                    ) : (
181
                      !isTrial && (
×
NEW
182
                        <Button className={classes.removeButton} variant="text" disabled={!addOn.isEligible} onClick={() => onAddOnClick(addOn.name, false)}>
×
183
                          Remove from plan
184
                        </Button>
185
                      )
186
                    )}
187
                  </>
188
                ) : (
189
                  <>
190
                    {addOn.pending && addOn.pending.isAdd ? (
120!
191
                      <Chip className={classes.chip} label="pending" />
192
                    ) : (
193
                      <Button
194
                        className={classes.addButton}
195
                        variant="text"
196
                        disabled={!addOn.isEligible}
197
                        onClick={() => (addOn.isEligible ? onAddOnClick(addOn.name, true) : () => false)}
2!
198
                      >
199
                        Add to plan
200
                      </Button>
201
                    )}
202
                    <div className={classes.placeholder} />
203
                  </>
204
                )}
205
              </div>
206
            );
207
          })}
208
        </div>
209
      </div>
210
    </>
211
  );
212
};
213

214
export default AddOnSelection;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc