Skip to content

Commit

Permalink
Add no devices modal and update installation guides
Browse files Browse the repository at this point in the history
Introduced a "No Device Modal" to provide an initial guide for users with no devices. Updated the technical guide with new sections for NodeJS Vanilla Agent, Dockerized Agent, and troubleshooting information. Adjusted various components for better Ansible playbook and Docker connection test handling.
  • Loading branch information
SquirrelDevelopper committed Oct 14, 2024
1 parent b5b47fb commit 418c105
Show file tree
Hide file tree
Showing 16 changed files with 449 additions and 92 deletions.
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ssm-client",
"version": "0.1.20",
"version": "0.1.21",
"private": true,
"description": "SSM Client - A simple way to manage all your servers",
"author": "Squirrel Team",
Expand Down
6 changes: 6 additions & 0 deletions client/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import DocumentationWidget from '@/components/HeaderComponents/DocumentationWidg
import { HealthWidget } from '@/components/HeaderComponents/HealthWidget';
import NotificationsWidget from '@/components/HeaderComponents/NotificationsWidget';
import UpdateAvailableWidget from '@/components/HeaderComponents/UpdateAvailableWidget';
import NoDeviceModal from '@/components/NoDevice/NoDeviceModal';
import { currentUser as queryCurrentUser, hasUser } from '@/services/rest/user';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
// @ts-ignore
Expand Down Expand Up @@ -123,8 +124,13 @@ export const layout: RunTimeLayoutConfig = ({
description={`The server version (${initialState?.currentUser?.settings?.server.version}) does not match the client version (${version}). You may need to retry a docker compose pull to update SSM.`}
type="warning"
showIcon
banner
/>
)}
{initialState?.currentUser?.devices?.overview &&
initialState?.currentUser?.devices?.overview?.length === 0 && (
<NoDeviceModal />
)}
{children}
</>
);
Expand Down
215 changes: 147 additions & 68 deletions client/src/components/DeviceConfiguration/CheckDeviceConnection.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import TerminalHandler from '@/components/PlaybookExecutionModal/PlaybookExecutionHandler';
import SwitchConnexionMethod from '@/components/NewDeviceModal/SwitchConnexionMethod';
import TerminalHandler, {
TaskStatusTimelineType,
} from '@/components/PlaybookExecutionModal/PlaybookExecutionHandler';
import { getAnsibleSmartFailure } from '@/services/rest/ansible';
import {
CheckCircleOutlined,
ClockCircleOutlined,
CloseOutlined,
InfoCircleFilled,
LoadingOutlined,
SwitcherOutlined,
} from '@ant-design/icons';
import { Popover, Steps, Typography } from 'antd';
import { Alert, Button, Popover, Steps, Typography } from 'antd';
import { motion } from 'framer-motion';
import React, { useEffect, useRef, useState } from 'react';
import { API } from 'ssm-shared-lib';

Expand All @@ -15,16 +22,44 @@ export type CheckDeviceConnectionProps = {
dockerConnErrorMessage?: string;
};

const taskInit: TaskStatusTimelineType = {
_status: 'created',
status: 'finish',
icon: <ClockCircleOutlined />,
title: 'created',
};

const animationVariants = {
hidden: { opacity: 0, y: -20 },
visible: { opacity: 1, y: 0 },
};

const CheckDeviceConnection: React.FC<CheckDeviceConnectionProps> = (props) => {
const { execId, dockerConnRes, dockerConnErrorMessage } = props;
const timerIdRef = useRef();
const [isPollingEnabled, setIsPollingEnabled] = useState(false);
const [playbookStatus, setPlaybookStatus] = useState('running...');
const [dockerStatus, setDockerStatus] = useState('running...');
const [execRes, setExecRes] = useState(<></>);
const [smartFailure, setSmartFailure] = useState<
API.SmartFailure | undefined
>();
const statusesType: TaskStatusTimelineType[] = [taskInit];
const [savedStatuses, setSavedStatuses] = useState(statusesType);
const statusCallback = (status: string) => {
setPlaybookStatus(status);
};
const [count, setCount] = useState(0);

const isFinalStatusFailed = async () => {
if (savedStatuses?.find((status) => status._status === 'failed')) {
const res = await getAnsibleSmartFailure({ execId: execId });
if (res.data) {
setSmartFailure(res.data);
}
return true;
}
};
const execLogsCallBack = (execLog: API.ExecLog) => {
setExecRes((previous) => (
<>
Expand All @@ -43,8 +78,13 @@ const CheckDeviceConnection: React.FC<CheckDeviceConnectionProps> = (props) => {
undefined,
execLogsCallBack,
statusCallback,
setSavedStatuses,
);

useEffect(() => {
void isFinalStatusFailed();
}, [savedStatuses]);

useEffect(() => {
if (execId) {
terminalHandler.resetTerminal();
Expand All @@ -66,13 +106,17 @@ const CheckDeviceConnection: React.FC<CheckDeviceConnectionProps> = (props) => {
}, [dockerConnRes]);

useEffect(() => {
const pollingCallback = () => terminalHandler.pollingCallback(execId || '');
const pollingCallback = () => {
terminalHandler.pollingCallback(execId || '');
setCount((prevCount) => prevCount + 1);
};

const startPolling = () => {
// pollingCallback(); // To immediately start fetching data
setCount(0);
// Polling every 3 seconds
// @ts-ignore
timerIdRef.current = setInterval(pollingCallback, 3000);
setSmartFailure(undefined);
};

const stopPolling = () => {
Expand All @@ -90,72 +134,107 @@ const CheckDeviceConnection: React.FC<CheckDeviceConnectionProps> = (props) => {
};
}, [isPollingEnabled]);
return (
<Steps
direction="vertical"
items={[
{
title: 'Ansible Playbook : Ansible Ping & Call SSM API URL',
description: (
<>
{playbookStatus}{' '}
{playbookStatus === 'failed' && (
<Popover
title={'Ansible Connection Logs'}
content={
<div style={{ overflowY: 'scroll', height: '400px' }}>
{execRes}
</div>
}
overlayStyle={{
width: '400px',
height: '400px',
overflowY: 'scroll',
}}
>
<InfoCircleFilled />
</Popover>
)}
</>
),
icon:
playbookStatus === 'successful' ? (
<CheckCircleOutlined />
) : playbookStatus === 'failed' ? (
<CloseOutlined style={{ color: 'red' }} />
) : (
<LoadingOutlined />
<>
<Steps
direction="vertical"
items={[
{
title: 'Ansible Playbook : Ansible Ping & Call SSM API URL',
description: (
<>
{playbookStatus}{' '}
{playbookStatus === 'failed' && (
<Popover
title={'Ansible Connection Logs'}
content={
<div style={{ overflowY: 'scroll', height: '400px' }}>
{execRes}
</div>
}
overlayStyle={{
width: '400px',
height: '400px',
overflowY: 'scroll',
}}
>
<InfoCircleFilled />
</Popover>
)}
</>
),
},
{
title: 'Docker Connection test',
description: (
<>
{dockerStatus}{' '}
{dockerStatus === 'failed' && (
<Popover
content={
<Typography.Text type="danger">
{dockerConnErrorMessage}
</Typography.Text>
}
title={'Docker Connection Logs'}
>
<InfoCircleFilled />
</Popover>
)}
</>
),
icon:
dockerStatus === 'successful' ? (
<CheckCircleOutlined />
) : dockerStatus === 'failed' ? (
<CloseOutlined style={{ color: 'red' }} />
) : (
<LoadingOutlined />
icon:
playbookStatus === 'successful' ? (
<CheckCircleOutlined />
) : playbookStatus === 'failed' ? (
<CloseOutlined style={{ color: 'red' }} />
) : (
<LoadingOutlined />
),
},
{
title: 'Docker Connection test',
description: (
<>
{dockerStatus}{' '}
{dockerStatus === 'failed' && (
<Popover
content={
<Typography.Text type="danger">
{dockerConnErrorMessage}
</Typography.Text>
}
title={'Docker Connection Logs'}
>
<InfoCircleFilled />
</Popover>
)}
</>
),
},
]}
/>
icon:
dockerStatus === 'successful' ? (
<CheckCircleOutlined />
) : dockerStatus === 'failed' ? (
<CloseOutlined style={{ color: 'red' }} />
) : (
<LoadingOutlined />
),
},
]}
/>
{smartFailure && (
<motion.div
initial="hidden"
animate="visible"
variants={animationVariants}
transition={{ duration: 0.5 }}
>
<Alert
message={'Ansible failed'}
description={
<>
<Typography.Paragraph>
<b>Probable cause</b>: {smartFailure.cause}
<br />
<b>Probable Resolution</b>: {smartFailure.resolution}
</Typography.Paragraph>
</>
}
showIcon
type={'error'}
/>
</motion.div>
)}
{(playbookStatus === 'failed' || count > 10) && (
<motion.div
initial="hidden"
animate="visible"
variants={animationVariants}
transition={{ duration: 0.5 }}
>
<SwitchConnexionMethod />
</motion.div>
)}
</>
);
};

Expand Down
6 changes: 6 additions & 0 deletions client/src/components/NewDeviceModal/NewDeviceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ const NewDeviceModal: React.FC<NewDeviceModalProps> = (props) => {
},
]}
/>
<Alert
message={
'Ensure that the server running Squirrel Servers Manager has port *8000* opened and accessible from the device you wish to add.'
}
showIcon
/>
</StepsForm.StepForm>
<StepsForm.StepForm
name="test"
Expand Down
45 changes: 45 additions & 0 deletions client/src/components/NewDeviceModal/SwitchConnexionMethod.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { SwapOutlined, SwitcherOutlined } from '@ant-design/icons';
import { Alert, Button, Space, Typography } from 'antd';
import React from 'react';

const SwitchConnexionMethod = () => {
const [showDetails, setShowDetails] = React.useState(false);

return (
<Alert
style={{ marginTop: 15 }}
message={'Encountering problems?'}
description={
showDetails ? (
<Space direction={'vertical'}>
<Typography.Text>
Try switching to the classic Ansible SSH connexion method instead
of paramiko (not available when using a passphrase protected key).
</Typography.Text>
<Typography.Text>
SSH &rarr; Show advanced &rarr; Connection Method &rarr; *SSH*
</Typography.Text>
<Typography.Link
href={
'https://squirrelserversmanager.io/docs/technical-guide/troubleshoot'
}
target={'_blank'}
>
[More details]
</Typography.Link>
</Space>
) : undefined
}
action={
showDetails ? undefined : (
<Button size="small" onClick={() => setShowDetails(true)}>
Details
</Button>
)
}
showIcon
/>
);
};

export default SwitchConnexionMethod;
Loading

0 comments on commit 418c105

Please sign in to comment.