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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

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

2✔
17
// material ui
2✔
18
import { Alert, Button, Collapse, DialogActions, DialogContent, FormControlLabel, MenuItem, Select, Switch, Typography } from '@mui/material';
2✔
19
import { makeStyles } from 'tss-react/mui';
2✔
20

2✔
21
import { CopyTextToClipboard } from '@northern.tech/common-ui/CopyText';
2✔
22
import { BaseDialog } from '@northern.tech/common-ui/dialogs/BaseDialog';
2✔
23
import storeActions from '@northern.tech/store/actions';
2✔
24
import { SSO_TYPES } from '@northern.tech/store/constants';
2✔
25
import { getCurrentSession, getFeatures, getIsEnterprise, getIsPreview, getOrganization, getSsoConfig, getUserRoles } from '@northern.tech/store/selectors';
2✔
26
import { changeSsoConfig, deleteSsoConfig, downloadLicenseReport, getSsoConfigs, getUserOrganization, storeSsoConfig } from '@northern.tech/store/thunks';
2✔
27
import { createFileDownload, toggle } from '@northern.tech/utils/helpers';
2✔
28
import dayjs from 'dayjs';
2✔
29

2✔
30
import OrganizationSettingsItem from './OrganizationSettingsItem';
2✔
31
import { SSOConfig } from './SSOConfig';
2✔
32

2✔
33
const { setSnackbar } = storeActions;
7✔
34

2✔
35
const useStyles = makeStyles()(({ spacing }) => ({
7✔
36
  orgInfo: { gap: spacing(2) },
2✔
37
  ssoSelect: { minWidth: 265 },
2✔
38
  tenantToken: { wordWrap: 'break-word' }
2✔
39
}));
2✔
40

2✔
41
// unlike the ExpandableAttribute, the token should not be visible by default - thus a separate component
2✔
42
const TenantToken = ({ expanded, onClick, token }: { expanded: boolean; onClick: () => void; token: string }) => (
7✔
43
  <>
7✔
44
    <div className="tenant-token-text">{expanded ? token : `${token.substring(0, 8)}...`}</div>
2!
45
    {!expanded && (
2✔
46
      <div className="clickable link-color margin-top-x-small margin-left-x-small" onClick={onClick}>
2✔
47
        <b>Show more</b>
2✔
48
      </div>
2✔
49
    )}
2✔
50
  </>
2✔
51
);
2✔
52

2✔
53
export const Organization = () => {
7✔
54
  const [hasSingleSignOn, setHasSingleSignOn] = useState(false);
7✔
55
  const [isConfiguringSSO, setIsConfiguringSSO] = useState(false);
7✔
56
  const [isResettingSSO, setIsResettingSSO] = useState(false);
7✔
57
  const [showTokenWarning, setShowTokenWarning] = useState(false);
7✔
58
  const [newSso, setNewSso] = useState('');
7✔
59
  const [selectedSsoItem, setSelectedSsoItem] = useState(undefined);
7✔
60
  const isEnterprise = useSelector(getIsEnterprise);
7✔
61
  const { isAdmin } = useSelector(getUserRoles);
7✔
62
  const canPreview = useSelector(getIsPreview);
7✔
63
  const { isHosted } = useSelector(getFeatures);
7✔
64
  const { id: tenantId, name: orgName, tenant_token = '' } = useSelector(getOrganization);
7✔
65
  const ssoConfig = useSelector(getSsoConfig);
7✔
66
  const dispatch = useDispatch();
7✔
67
  const { token } = useSelector(getCurrentSession);
7✔
68
  const { classes } = useStyles();
7✔
69

2✔
70
  useEffect(() => {
7✔
71
    dispatch(getUserOrganization());
4✔
72
  }, [dispatch]);
2✔
73

2✔
74
  useEffect(() => {
7✔
75
    if (!isEnterprise) {
4!
76
      return;
2✔
77
    }
2✔
78
    dispatch(getSsoConfigs());
4✔
79
  }, [dispatch, isEnterprise]);
2✔
80

2✔
81
  useEffect(() => {
7✔
82
    setHasSingleSignOn(!!ssoConfig);
5✔
83
    setIsConfiguringSSO(!!ssoConfig);
5✔
84
    if (ssoConfig) {
5✔
85
      setSelectedSsoItem(SSO_TYPES[ssoConfig.type]);
3✔
86
    }
2✔
87
  }, [ssoConfig]);
2✔
88

2✔
89
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
7✔
90

2✔
91
  const onSaveSSOSettings = useCallback(
7✔
92
    (id, config) => {
2✔
93
      const { contentType } = SSO_TYPES[selectedSsoItem.type];
2✔
94
      if (isResettingSSO) {
2!
95
        return dispatch(deleteSsoConfig(ssoConfig)).then(() => setIsResettingSSO(false));
2✔
96
      }
2✔
97
      if (id) {
2!
98
        return dispatch(changeSsoConfig({ config: { config, id }, contentType }));
2✔
99
      }
2✔
100
      return dispatch(storeSsoConfig({ config, contentType }));
2✔
101
    },
2✔
102
    [isResettingSSO, dispatch, ssoConfig, selectedSsoItem]
2✔
103
  );
2✔
104

2✔
105
  const onCancelSSOSettings = () => {
7✔
106
    setIsResettingSSO(false);
2✔
107
    setIsConfiguringSSO(hasSingleSignOn);
2✔
108
  };
2✔
109

2✔
110
  const onTokenExpansion = useCallback(() => setShowTokenWarning(true), []);
7✔
111

2✔
112
  const onDownloadReportClick = () =>
7✔
113
    dispatch(downloadLicenseReport())
2✔
114
      .unwrap()
2✔
115
      .then(report => createFileDownload(report, `Mender-license-report-${dayjs().format('YYYY-MM-DD')}`, token));
2✔
116

2✔
117
  const onSSOClick = () => {
7✔
118
    if (hasSingleSignOn) {
2!
119
      setIsConfiguringSSO(false);
2✔
120
      return setIsResettingSSO(true);
2✔
121
    }
2✔
122
    setIsConfiguringSSO(toggle);
2✔
123
  };
2✔
124

2✔
125
  const onSsoSelect = useCallback(
7✔
126
    ({ target: { value: type = '' } }) => {
2!
127
      if (ssoConfig) {
2!
128
        setNewSso(type);
2✔
129
      } else {
2✔
130
        setSelectedSsoItem(SSO_TYPES[type]);
2✔
131
      }
2✔
132
    },
2✔
133
    [ssoConfig]
2✔
134
  );
2✔
135

2✔
136
  const changeSSO = () =>
7✔
137
    dispatch(deleteSsoConfig(ssoConfig)).then(() => {
2✔
138
      setSelectedSsoItem(SSO_TYPES[newSso]);
2✔
139
      setIsConfiguringSSO(true);
2✔
140
      setNewSso('');
2✔
141
    });
2✔
142

2✔
143
  return (
7✔
144
    <div style={{ maxWidth: 750 }}>
2✔
145
      <Typography variant="h6">My organization</Typography>
2✔
146
      <div className={`flexbox column ${classes.orgInfo}`}>
2✔
147
        <OrganizationSettingsItem title="Organization ID" secondary={tenantId} sideBarContent={<CopyTextToClipboard notify={false} token={tenantId} />} />
2✔
148
        <OrganizationSettingsItem title="Organization name" secondary={orgName} sideBarContent={<CopyTextToClipboard notify={false} token={orgName} />} />
2✔
149
        <OrganizationSettingsItem
2✔
150
          classes={{ base: '', content: '', main: classes.tenantToken }}
2✔
151
          title="Organization token"
2✔
152
          description="The token is unique for your organization and ensures that only devices that you own are able to connect to your account."
2✔
153
          secondary={<TenantToken expanded={showTokenWarning} onClick={onTokenExpansion} token={tenant_token} />}
2✔
154
          sideBarContent={<CopyTextToClipboard notify={false} onCopy={onTokenExpansion} token={tenant_token} />}
2✔
155
          notification={
2✔
156
            showTokenWarning && (
2!
157
              <Alert severity="warning">
2✔
158
                Do not share your organization token with others. Treat this token like a password, as it can be used to request authorization for new devices.
2✔
159
              </Alert>
2✔
160
            )
2✔
161
          }
2✔
162
        />
2✔
163
        {isEnterprise && isAdmin && (
2✔
164
          <div>
2✔
165
            <FormControlLabel
2✔
166
              className="margin-bottom-small margin-left-none"
2✔
167
              control={<Switch checked={!isResettingSSO && (hasSingleSignOn || isConfiguringSSO)} className="margin-left-small" onChange={onSSOClick} />}
2✔
168
              label="Enable Single Sign-On"
2✔
169
              labelPlacement="start"
2✔
170
            />
2✔
171
          </div>
2✔
172
        )}
2✔
173
      </div>
2✔
174

2✔
175
      {isConfiguringSSO && (
2✔
176
        <div>
2✔
177
          <Select className={classes.ssoSelect} displayEmpty onChange={onSsoSelect} value={selectedSsoItem?.type || ''}>
2!
178
            <MenuItem value="">Select type</MenuItem>
2✔
179
            {Object.values(SSO_TYPES).map(item => (
2✔
180
              <MenuItem key={item.type} value={item.type}>
4✔
181
                <div className="capitalized-start">{item.title}</div>
2✔
182
              </MenuItem>
2✔
183
            ))}
2✔
184
          </Select>
2✔
185
        </div>
2✔
186
      )}
2✔
187

2✔
188
      <div className="flexbox center-aligned">
2✔
189
        {isResettingSSO && !isConfiguringSSO && (
2!
190
          <>
2✔
191
            <Button onClick={onCancelSSOSettings}>Cancel</Button>
2✔
192
            <Button onClick={onSaveSSOSettings} disabled={!hasSingleSignOn} variant="contained">
2✔
193
              Save
2✔
194
            </Button>
2✔
195
          </>
2✔
196
        )}
2✔
197
      </div>
2✔
198
      {selectedSsoItem && (
2✔
199
        <div className="margin-top">
2✔
200
          <Collapse className="margin-left-large" in={isConfiguringSSO}>
2✔
201
            <SSOConfig
2✔
202
              ssoItem={selectedSsoItem}
2✔
203
              config={ssoConfig}
2✔
204
              onSave={onSaveSSOSettings}
2✔
205
              onCancel={onCancelSSOSettings}
2✔
206
              setSnackbar={dispatchedSetSnackbar}
2✔
207
              token={token}
2✔
208
            />
2✔
209
          </Collapse>
2✔
210
        </div>
2!
211
      )}
2✔
212
      {(canPreview || !isHosted) && isEnterprise && isAdmin && (
2✔
213
        <Button className="margin-top" onClick={onDownloadReportClick} variant="contained">
2✔
214
          Download license report
2✔
215
        </Button>
2✔
216
      )}
2✔
UNCOV
217
      <ChangeSsoDialog dismiss={() => setNewSso(undefined)} open={!!newSso} submit={changeSSO} />
2✔
218
    </div>
2✔
219
  );
2✔
220
};
2✔
221

2✔
222
const ChangeSsoDialog = ({ dismiss, open, submit }) => (
7✔
223
  <BaseDialog open={open} title="Change Single Sign-On type" onClose={dismiss}>
7✔
224
    <DialogContent style={{ overflow: 'hidden' }}>Are you sure you want to change SSO type? This will lose your current settings.</DialogContent>
2✔
225
    <DialogActions>
2✔
226
      <Button style={{ marginRight: 10 }} onClick={dismiss}>
2✔
227
        Cancel
2✔
228
      </Button>
2✔
UNCOV
229
      <Button variant="contained" color="primary" onClick={() => submit()}>
2✔
230
        Change
2✔
231
      </Button>
2✔
232
    </DialogActions>
2✔
233
  </BaseDialog>
2✔
234
);
2✔
235

2✔
236
export default Organization;
2✔
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