diff --git a/src/features/StackScripts/SelectStackScriptPanel/CASelectStackScriptPanel.tsx b/src/features/StackScripts/SelectStackScriptPanel/CASelectStackScriptPanel.tsx deleted file mode 100644 index 9d57e98d7e6..00000000000 --- a/src/features/StackScripts/SelectStackScriptPanel/CASelectStackScriptPanel.tsx +++ /dev/null @@ -1,270 +0,0 @@ -import { pathOr } from 'ramda'; -import * as React from 'react'; -import { connect } from 'react-redux'; -import { compose } from 'recompose'; -import Button from 'src/components/Button'; -import CircleProgress from 'src/components/CircleProgress'; -import Paper from 'src/components/core/Paper'; -import { - StyleRulesCallback, - withStyles, - WithStyles -} from 'src/components/core/styles'; -import Typography from 'src/components/core/Typography'; -import Notice from 'src/components/Notice'; -import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; -import Table from 'src/components/Table'; -import { getStackScript } from 'src/services/stackscripts'; -import { MapState } from 'src/store/types'; -import { formatDate } from 'src/utilities/format-date-iso8601'; -import { getParamFromUrl } from 'src/utilities/queryParams'; -import stripImageName from 'src/utilities/stripImageName'; -import truncateText from 'src/utilities/truncateText'; -import StackScriptTableHead from '../Partials/StackScriptTableHead'; -import SelectStackScriptPanelContent from './SelectStackScriptPanelContent'; -import StackScriptSelectionRow from './StackScriptSelectionRow'; - -export interface ExtendedLinode extends Linode.Linode { - heading: string; - subHeadings: string[]; -} - -type ClassNames = - | 'root' - | 'table' - | 'link' - | 'selecting' - | 'panel' - | 'inner' - | 'header'; - -const styles: StyleRulesCallback = theme => ({ - root: { - marginBottom: theme.spacing.unit * 3 - }, - table: { - flexGrow: 1, - width: '100%', - backgroundColor: theme.color.white - }, - selecting: { - minHeight: '400px', - maxHeight: '1000px', - overflowX: 'auto', - overflowY: 'scroll', - paddingTop: 0, - marginTop: theme.spacing.unit * 2 - }, - link: { - display: 'block', - textAlign: 'right', - marginBottom: 24, - marginTop: theme.spacing.unit - }, - panel: { - flexGrow: 1, - width: '100%', - backgroundColor: theme.color.white, - marginBottom: theme.spacing.unit * 3 - }, - inner: { - padding: theme.spacing.unit * 2, - [theme.breakpoints.up('sm')]: { - padding: theme.spacing.unit * 3 - } - }, - header: { - paddingBottom: theme.spacing.unit * 2 - } -}); - -interface Props extends RenderGuardProps { - selectedId: number | undefined; - selectedUsername?: string; - error?: string; - onSelect: ( - id: number, - label: string, - username: string, - images: string[], - userDefinedFields: Linode.StackScript.UserDefinedField[] - ) => void; - publicImages: Linode.Image[]; - resetSelectedStackScript: () => void; - disabled?: boolean; - request: ( - username: string, - params?: any, - filter?: any - ) => Promise>; - category: string; - header: string; -} - -type CombinedProps = Props & - StateProps & - RenderGuardProps & - WithStyles; - -interface State { - stackScript?: Linode.StackScript.Response; - stackScriptError: boolean; - stackScriptLoading: boolean; -} - -class SelectStackScriptPanel extends React.Component { - state: State = { - stackScriptLoading: false, - stackScriptError: false - }; - - mounted: boolean = false; - - componentDidMount() { - const selected = - this.props.selectedId || - getParamFromUrl(location.search, 'stackScriptID'); - if (selected) { - this.setState({ stackScriptLoading: true }); - getStackScript(selected) - .then(stackScript => { - this.setState({ stackScript, stackScriptLoading: false }); - this.props.onSelect( - stackScript.id, - stackScript.label, - stackScript.username, - stackScript.images, - stackScript.user_defined_fields - ); - }) - .catch(e => { - this.setState({ stackScriptLoading: false, stackScriptError: true }); - }); - } - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleTabChange = () => { - /* - * if we're coming from a query string, the stackscript will be preselected - * however, we don't want the user to have their stackscript still preselected - * when they change StackScript tabs - */ - this.props.resetSelectedStackScript(); - }; - - resetStackScript = () => { - this.setState({ stackScript: undefined, stackScriptLoading: false }); - }; - - render() { - const { - category, - classes, - header, - request, - selectedId, - error - } = this.props; - const { stackScript, stackScriptLoading, stackScriptError } = this.state; - - if (selectedId) { - if (stackScriptLoading) { - return ; - } - if (stackScript) { - return ( - - - - - {}} - description={truncateText(stackScript.description, 100)} - images={stripImageName(stackScript.images)} - deploymentsActive={stackScript.deployments_active} - updated={formatDate(stackScript.updated, false)} - checked={selectedId === stackScript.id} - updateFor={[selectedId === stackScript.id]} - stackScriptID={stackScript.id} - /> - -
-
- -
-
- ); - } - } - - return ( - -
- {error && } - - {header} - - {stackScriptError && ( - - An error occured while loading the selected StackScript. - - )} - - - -
-
- ); - } -} - -interface StateProps { - username: string; -} - -const mapStateToProps: MapState = state => ({ - username: pathOr('', ['data', 'username'], state.__resources.profile) -}); - -const connected = connect(mapStateToProps); - -const styled = withStyles(styles); - -export default compose( - connected, - RenderGuard, - styled -)(SelectStackScriptPanel); diff --git a/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel.tsx b/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel.tsx index 19cafd56a35..cce14b1cad8 100644 --- a/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel.tsx +++ b/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel.tsx @@ -1,24 +1,26 @@ -import { compose, pathOr } from 'ramda'; +import { pathOr } from 'ramda'; import * as React from 'react'; import { connect } from 'react-redux'; +import { compose } from 'recompose'; import Button from 'src/components/Button'; import CircleProgress from 'src/components/CircleProgress'; +import Paper from 'src/components/core/Paper'; import { StyleRulesCallback, withStyles, WithStyles } from 'src/components/core/styles'; import Typography from 'src/components/core/Typography'; -import RenderGuard from 'src/components/RenderGuard'; -import TabbedPanel from 'src/components/TabbedPanel'; +import Notice from 'src/components/Notice'; +import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; import Table from 'src/components/Table'; import { getStackScript } from 'src/services/stackscripts'; import { MapState } from 'src/store/types'; import { formatDate } from 'src/utilities/format-date-iso8601'; +import { getParamFromUrl } from 'src/utilities/queryParams'; import stripImageName from 'src/utilities/stripImageName'; import truncateText from 'src/utilities/truncateText'; import StackScriptTableHead from '../Partials/StackScriptTableHead'; -import { StackScriptTabs } from '../stackScriptUtils'; import SelectStackScriptPanelContent from './SelectStackScriptPanelContent'; import StackScriptSelectionRow from './StackScriptSelectionRow'; @@ -27,7 +29,14 @@ export interface ExtendedLinode extends Linode.Linode { subHeadings: string[]; } -type ClassNames = 'root' | 'table' | 'link' | 'selecting'; +type ClassNames = + | 'root' + | 'table' + | 'link' + | 'selecting' + | 'panel' + | 'inner' + | 'header'; const styles: StyleRulesCallback = theme => ({ root: { @@ -51,10 +60,25 @@ const styles: StyleRulesCallback = theme => ({ textAlign: 'right', marginBottom: 24, marginTop: theme.spacing.unit + }, + panel: { + flexGrow: 1, + width: '100%', + backgroundColor: theme.color.white, + marginBottom: theme.spacing.unit * 3 + }, + inner: { + padding: theme.spacing.unit * 2, + [theme.breakpoints.up('sm')]: { + padding: theme.spacing.unit * 3 + } + }, + header: { + paddingBottom: theme.spacing.unit * 2 } }); -interface Props { +interface Props extends RenderGuardProps { selectedId: number | undefined; selectedUsername?: string; error?: string; @@ -68,9 +92,20 @@ interface Props { publicImages: Linode.Image[]; resetSelectedStackScript: () => void; disabled?: boolean; + request: ( + username: string, + params?: any, + filter?: any, + stackScriptGrants?: Linode.Grant[] + ) => Promise>; + category: string; + header: string; } -type CombinedProps = Props & StateProps & WithStyles; +type CombinedProps = Props & + StateProps & + RenderGuardProps & + WithStyles; interface State { stackScript?: Linode.StackScript.Response; @@ -87,9 +122,12 @@ class SelectStackScriptPanel extends React.Component { mounted: boolean = false; componentDidMount() { - if (this.props.selectedId) { + const selected = + this.props.selectedId || + getParamFromUrl(location.search, 'stackScriptID'); + if (selected) { this.setState({ stackScriptLoading: true }); - getStackScript(this.props.selectedId) + getStackScript(selected) .then(stackScript => { this.setState({ stackScript, stackScriptLoading: false }); this.props.onSelect( @@ -111,22 +149,6 @@ class SelectStackScriptPanel extends React.Component { this.mounted = false; } - createTabs = StackScriptTabs.map(tab => ({ - title: tab.title, - render: () => ( - - ) - })); - handleTabChange = () => { /* * if we're coming from a query string, the stackscript will be preselected @@ -141,7 +163,14 @@ class SelectStackScriptPanel extends React.Component { }; render() { - const { error, classes, selectedId } = this.props; + const { + category, + classes, + header, + request, + selectedId, + error + } = this.props; const { stackScript, stackScriptLoading, stackScriptError } = this.state; if (selectedId) { @@ -173,17 +202,13 @@ class SelectStackScriptPanel extends React.Component { deploymentsActive={stackScript.deployments_active} updated={formatDate(stackScript.updated, false)} checked={selectedId === stackScript.id} - updateFor={[selectedId === stackScript.id, classes]} + updateFor={[selectedId === stackScript.id]} stackScriptID={stackScript.id} />
-
@@ -193,23 +218,36 @@ class SelectStackScriptPanel extends React.Component { } return ( - - {stackScriptError && ( - - An error occured while loading the selected StackScript. Please - choose one from the list. + +
+ {error && } + + {header} - )} - - + {stackScriptError && ( + + An error occurred while loading the selected StackScript. + + )} + + + +
+
); } } @@ -226,12 +264,7 @@ const connected = connect(mapStateToProps); const styled = withStyles(styles); -export default compose< - Linode.TodoAny, - Linode.TodoAny, - Linode.TodoAny, - Linode.TodoAny ->( +export default compose( connected, RenderGuard, styled diff --git a/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanelContent.tsx b/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanelContent.tsx index e47481ab3bd..5f4aa7455d8 100644 --- a/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanelContent.tsx +++ b/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanelContent.tsx @@ -19,7 +19,8 @@ interface Props { request: ( username: string, params?: any, - filter?: any + filter?: any, + stackScriptGrants?: Linode.Grant[] ) => Promise>; category: string; disabled?: boolean; diff --git a/src/features/StackScripts/SelectStackScriptPanel/index.ts b/src/features/StackScripts/SelectStackScriptPanel/index.ts index d06eee18238..d39d3c670ae 100644 --- a/src/features/StackScripts/SelectStackScriptPanel/index.ts +++ b/src/features/StackScripts/SelectStackScriptPanel/index.ts @@ -1,3 +1,2 @@ import SelectStackScriptPanel from './SelectStackScriptPanel'; -// export { CommunityStackScripts, LinodeStackScripts, MyStackScripts }; export default SelectStackScriptPanel; diff --git a/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx b/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx index ab20e1fefb6..81793fa83db 100644 --- a/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx +++ b/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx @@ -13,12 +13,15 @@ import Table from 'src/components/Table'; import { isRestrictedUser } from 'src/features/Profile/permissionsHelpers'; import { MapState } from 'src/store/types'; import { sendEvent } from 'src/utilities/analytics'; +import { + getAPIErrorOrDefault, + handleUnauthorizedErrors +} from 'src/utilities/errorUtils'; import StackScriptTableHead from '../Partials/StackScriptTableHead'; import { AcceptedFilters, generateCatchAllFilter, - generateSpecificFilter, - getErrorText + generateSpecificFilter } from '../stackScriptUtils'; import withStyles, { StyleProps } from './StackScriptBase.styles'; @@ -45,7 +48,7 @@ export interface State { currentFilter: any; // @TODO type correctly currentSearchFilter: any; isSorting: boolean; - error?: Error; + error?: Linode.ApiFieldError[]; fieldError: Linode.ApiFieldError | undefined; isSearching: boolean; didSearch: boolean; @@ -98,7 +101,7 @@ const withStackScriptBase = (isSelecting: boolean) => ( componentDidMount() { this.mounted = true; - return this.getDataAtPage(0); + return this.getDataAtPage(1); } componentWillUnmount() { @@ -180,7 +183,10 @@ const withStackScriptBase = (isSelecting: boolean) => ( this.setState({ getMoreStackScriptsFailed: true }); } this.setState({ - error: e.response, + error: getAPIErrorOrDefault( + e, + 'There was an error loading StackScripts' + ), loading: false, gettingMoreStackScripts: false }); @@ -356,7 +362,13 @@ const withStackScriptBase = (isSelecting: boolean) => ( if (!this.mounted) { return; } - this.setState({ error: e, isSearching: false }); + this.setState({ + error: getAPIErrorOrDefault( + e, + 'There was an error loading StackScripts' + ), + isSearching: false + }); }); }; @@ -381,7 +393,14 @@ const withStackScriptBase = (isSelecting: boolean) => ( if (error) { return (
- +
); } diff --git a/src/features/StackScripts/StackScriptPanel/StackScriptPanel.tsx b/src/features/StackScripts/StackScriptPanel/StackScriptPanel.tsx index b24aa52d57c..29f34f98c3c 100644 --- a/src/features/StackScripts/StackScriptPanel/StackScriptPanel.tsx +++ b/src/features/StackScripts/StackScriptPanel/StackScriptPanel.tsx @@ -9,7 +9,10 @@ import { import RenderGuard from 'src/components/RenderGuard'; import TabbedPanel from 'src/components/TabbedPanel'; import { MapState } from 'src/store/types'; -import { StackScriptTabs } from '../stackScriptUtils'; +import { + getCommunityStackscripts, + getMineAndAccountStackScripts +} from '../stackScriptUtils'; import StackScriptPanelContent from './StackScriptPanelContent'; export interface ExtendedLinode extends Linode.Linode { @@ -93,6 +96,19 @@ class SelectStackScriptPanel extends React.Component { } } +export const StackScriptTabs = [ + { + title: 'Account StackScripts', + request: getMineAndAccountStackScripts, + category: 'account' + }, + { + title: 'Community StackScripts', + request: getCommunityStackscripts, + category: 'community' + } +]; + interface StateProps { username: string; } diff --git a/src/features/StackScripts/stackScriptUtils.ts b/src/features/StackScripts/stackScriptUtils.ts index fe4309c0002..d6581151437 100644 --- a/src/features/StackScripts/stackScriptUtils.ts +++ b/src/features/StackScripts/stackScriptUtils.ts @@ -1,8 +1,7 @@ -import { pathOr } from 'ramda'; import { getUsers } from 'src/services/account'; import { getStackScript, getStackscripts } from 'src/services/stackscripts'; -export const emptyResult = { +export const emptyResult: Linode.ResourcePage = { data: [], page: 1, pages: 1, @@ -19,39 +18,48 @@ export const getStackScriptsByUser = ( username }); -export const getAccountStackScripts = ( +export const getMineAndAccountStackScripts = ( currentUser: string, params?: any, filter?: any, stackScriptGrants?: Linode.Grant[] ) => { - /* - Secondary users can't see other account users but they have a list of - available account stackscripts in grant call. - If user is restricted we get the stackscripts for the list in grants. - Otherwise we pull all stackscripts for users on the account. - */ + /** + * Secondary users can't see other account users but they have a list of + * available account stackscripts in grant call. + * If user is restricted we get the stackscripts for the list in grants. + * Otherwise we pull all stackscripts for users on the account. + */ if (stackScriptGrants) { - if (params.page !== 0) { - // disable other pages loading, we got all the account stackscripts with an initial call + /** + * don't try to get another page of stackscripts because the request to /grants + * already gave us all stackscripts results, non-paginated + */ + if (params.page !== 1) { return Promise.resolve(emptyResult); } + + /** + * From the grants request, we got the entire list of StackScripts this + * user has access to, so we need to iterate over that list to get the + * meta data about each StackScript + */ return Promise.all( stackScriptGrants.map(grant => getStackScript(grant.id)) - ).then(data => { - // Filter out current user stackscripts and add to data of a sample response + ).then(response => { return { ...emptyResult, - data: data.filter(stackScript => stackScript.username !== currentUser) + data: response }; }); } else { + /** + * in this case, we are unrestricted user, so instead of getting the + * StackScripts from the /grants meta data, need to get a list of all + * users on the account and make a GET /stackscripts call with the list + * of users as a filter + */ return getUsers().then(response => { - if (response.data.length === 1) { - // there is only one user on the account. All his stackscripts are in "My StackScripts" tab. - return Promise.resolve(emptyResult); - } - return getStackscripts(params, { ...filter, '+and': [ @@ -62,7 +70,7 @@ export const getAccountStackScripts = ( user.username === currentUser ? acc : [...acc, { username: user.username }], - [] + [{ username: currentUser }] ) } ] @@ -71,49 +79,25 @@ export const getAccountStackScripts = ( } }; +/** + * Gets all StackScripts that don't belong to user "Linode" + * and do not belong to any users on the current account + */ export const getCommunityStackscripts = ( currentUser: string, params?: any, filter?: any ) => - getUsers() - .catch( - (): Promise> => - Promise.resolve(emptyResult) - ) - .then(response => - getStackscripts(params, { - ...filter, - '+and': response.data.reduce( - // pull all stackScripts except linode and account users - (acc, user) => [...acc, { username: { '+neq': user.username } }], - [{ username: { '+neq': 'linode' } }] - ) - }) - ); - -export const StackScriptTabs = [ - { - title: 'My StackScripts', - request: getStackScriptsByUser, - category: 'my' - }, - { - title: 'Account StackScripts', - request: getAccountStackScripts, - category: 'account' - }, - { - title: 'Linode StackScripts', - request: getStackScriptsByUser, - category: 'linode' - }, - { - title: 'Community StackScripts', - request: getCommunityStackscripts, - category: 'community' - } -]; + getUsers().then(response => { + return getStackscripts(params, { + ...filter, + '+and': response.data.reduce( + // pull all stackScripts except linode and account users + (acc, user) => [...acc, { username: { '+neq': user.username } }], + [{ username: { '+neq': 'linode' } }] + ) + }); + }); export type AcceptedFilters = 'username' | 'description' | 'label'; @@ -150,15 +134,6 @@ export const generateCatchAllFilter = (searchTerm: string) => { }; }; -export const getErrorText = (error: any) => { - const reason = pathOr('', ['data', 'errors', 0, 'reason'], error); - - if (reason === 'Unauthorized') { - return 'You are not authorized to view StackScripts for this account.'; - } - return 'There was an error loading your StackScripts. Please try again later.'; -}; - export const getStackScriptUrl = ( username: string, id: number, diff --git a/src/features/linodes/LinodesCreate/LinodeCreate.tsx b/src/features/linodes/LinodesCreate/LinodeCreate.tsx index 4c1514cd8b4..9ff93de51b4 100644 --- a/src/features/linodes/LinodesCreate/LinodeCreate.tsx +++ b/src/features/linodes/LinodesCreate/LinodeCreate.tsx @@ -8,7 +8,7 @@ import ErrorState from 'src/components/ErrorState'; import Grid from 'src/components/Grid'; import { getCommunityStackscripts, - getStackScriptsByUser + getMineAndAccountStackScripts } from 'src/features/StackScripts/stackScriptUtils'; import { getParamsFromUrl } from 'src/utilities/queryParams'; import SubTabs, { Tab } from './LinodeCreateSubTabs'; @@ -332,7 +332,7 @@ export class LinodeCreate extends React.PureComponent< Promise>; header: string; } @@ -212,7 +213,7 @@ export class FromStackScriptContent extends React.PureComponent { {generalError && } - & ContextProps; -type MODES = 'fromImage' | 'fromStackScript'; +type MODES = + | 'fromImage' + | 'fromCommunityStackScript' + | 'fromAccountStackScript'; const options = [ { value: 'fromImage', label: 'From Image' }, - { value: 'fromStackScript', label: 'From StackScript' } + { value: 'fromCommunityStackScript', label: 'From Community StackScript' }, + { value: 'fromAccountStackScript', label: 'From Account StackScript' } ]; const LinodeRebuild: React.StatelessComponent = props => { @@ -66,7 +70,12 @@ const LinodeRebuild: React.StatelessComponent = props => { /> {mode === 'fromImage' && } - {mode === 'fromStackScript' && } + {mode === 'fromCommunityStackScript' && ( + + )} + {mode === 'fromAccountStackScript' && ( + + )}
); }; diff --git a/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx b/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx index 8f68769f6cb..0a16db69bb9 100644 --- a/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx +++ b/src/features/linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx @@ -32,6 +32,11 @@ import { filterPublicImages } from 'src/utilities/images'; import { withLinodeDetailContext } from '../linodeDetailContext'; import { RebuildDialog } from './RebuildDialog'; +import { + getCommunityStackscripts, + getMineAndAccountStackScripts +} from 'src/features/StackScripts/stackScriptUtils'; + type ClassNames = 'root' | 'error' | 'emptyImagePanel' | 'emptyImagePanelText'; const styles: StyleRulesCallback = theme => ({ @@ -50,6 +55,10 @@ const styles: StyleRulesCallback = theme => ({ } }); +interface Props { + type: 'community' | 'account'; +} + interface ContextProps { linodeId: number; } @@ -64,7 +73,8 @@ interface RebuildFromStackScriptForm { password: string; } -export type CombinedProps = WithStyles & +export type CombinedProps = Props & + WithStyles & WithImagesProps & ContextProps & UserSSHKeyProps & @@ -198,6 +208,13 @@ export const RebuildFromStackScript: React.StatelessComponent< publicImages={filterPublicImages(imagesData)} resetSelectedStackScript={resetStackScript} data-qa-select-stackscript + category={props.type} + header="Select StackScript" + request={ + props.type === 'account' + ? getMineAndAccountStackScripts + : getCommunityStackscripts + } /> {ss.user_defined_fields && ss.user_defined_fields.length > 0 && ( ({ linodeId: linode.id })); -const enhanced = compose( +const enhanced = compose( linodeContext, userSSHKeyHoc, styled, diff --git a/src/utilities/errorUtils.ts b/src/utilities/errorUtils.ts index 4d3db65fdbd..6079385a47b 100644 --- a/src/utilities/errorUtils.ts +++ b/src/utilities/errorUtils.ts @@ -29,6 +29,42 @@ export const getAPIErrorOrDefault = ( return pathOr(_defaultError, ['response', 'data', 'errors'], errorResponse); }; +export const handleUnauthorizedErrors = ( + e: Linode.ApiFieldError[], + unauthedMessage: string +) => { + /** + * filter out errors that match the following + * { + * reason: "Unauthorized" + * } + * + * and if any of these errors exist, set the hasUnauthorizedError + * flag to true + */ + let hasUnauthorizedError = false; + const filteredErrors = e.filter(eachError => { + if (eachError.reason.toLowerCase().includes('unauthorized')) { + hasUnauthorizedError = true; + return false; + } + return true; + }); + + /** + * if we found an unauthorized error, add on the new message in the API + * Error format + */ + return hasUnauthorizedError + ? [ + { + reason: unauthedMessage + }, + ...filteredErrors + ] + : filteredErrors; +}; + export const getErrorStringOrDefault = ( errors: Linode.ApiFieldError[] | string, defaultError: string = 'An unexpected error occurred.'