Skip to content

Commit e330e40

Browse files
committed
Add copy result functionality and authorization
1 parent d41c5ae commit e330e40

File tree

9 files changed

+421
-470
lines changed

9 files changed

+421
-470
lines changed

src/App.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ button:disabled {
114114

115115
.editor {
116116
display: flex;
117-
height: 85vh;
117+
height: 92vh;
118118
}
119119

120120
.logo {
@@ -128,4 +128,4 @@ button:disabled {
128128
display: flex;
129129
justify-content: center;
130130
align-items: center;
131-
}
131+
}

src/App.tsx

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,66 @@
1-
import React from 'react';
1+
import React, { useState, useEffect } from 'react';
22
import './App.css';
33
import { useFHIRPathUI } from './hooks';
44
import Editor from '@monaco-editor/react';
55
import { Allotment } from "allotment";
66
import "allotment/dist/style.css";
7-
import { Play, ShareFat, FileArrowDown } from "@phosphor-icons/react";
7+
import { Play, ShareFat, FileArrowDown, Gear, Copy, Info } from "@phosphor-icons/react";
88
import Loader from './components/loader';
99
import logo from './assets/logo.png';
1010
import { ToastContainer } from 'react-toastify';
1111
import 'react-toastify/dist/ReactToastify.css';
1212
import { ResultOutput } from './components/ResultOutput';
1313
import { WeeklyPopup } from './components/WeeklyPopup';
14+
import { DrawerButton } from './components/DrawerButton';
15+
import { SettingsContainer } from './containers/Settings'
16+
import { CredentialsContainer } from './containers/Credentials'
17+
1418

1519
const App: React.FC = () => {
16-
const { url, handleUrlChange, handleFetch,
17-
resource, expression, setExpression, setResource,
18-
handleExecute, result, handleShare, isLoading, isExecuteActive, isGetResourceActive, isShareActive } = useFHIRPathUI();
20+
const { url, handleUrlChange, handleFetch,
21+
resource, expression, setExpression, setResource,
22+
handleExecute, result, handleShare, isLoading, isExecuteActive, isGetResourceActive, isShareActive, handleShareResult, isShareResultActive } = useFHIRPathUI();
1923

20-
return (
21-
<div className="App">
22-
{isLoading ? <Loader /> : null}
23-
<WeeklyPopup />
24-
<div className='header'>
25-
<img src={logo} alt="Logo" className='logo' />
26-
<div className='searchBlock'>
27-
<input className="input" type="url" value={url} onChange={handleUrlChange} placeholder='You can paste the URL to get the FHIR Resource' />
28-
</div>
29-
<div className="buttonsBlock">
30-
<button onClick={() => handleFetch(url)} disabled={!isGetResourceActive}><FileArrowDown fontSize={24} /></button>
31-
<button onClick={() => handleExecute(resource, expression)} disabled={!isExecuteActive}><Play fontSize={24} /></button>
32-
<button onClick={handleShare} disabled={!isShareActive}><ShareFat fontSize={24} /></button>
24+
return (
25+
<div className="App">
26+
{isLoading ? <Loader /> : null}
27+
<WeeklyPopup />
28+
<div className='header'>
29+
<img src={logo} alt="Logo" className='logo' />
30+
<div className='searchBlock'>
31+
<input className="input" type="url" value={url} onChange={handleUrlChange} placeholder='You can paste the URL to get the FHIR Resource' />
32+
</div>
33+
<div className="buttonsBlock">
34+
<button onClick={() => handleFetch(url)} disabled={!isGetResourceActive}><FileArrowDown fontSize={24} /></button>
35+
<button onClick={() => handleExecute(resource, expression)} disabled={!isExecuteActive}><Play fontSize={24} /></button>
36+
<button onClick={handleShare} disabled={!isShareActive}><ShareFat fontSize={24} /></button>
37+
<button onClick={handleShareResult} disabled={!isShareResultActive}><Copy fontSize={24} /></button>
38+
<DrawerButton content={<SettingsContainer />} button={<button><Gear fontSize={24} /></button>} title="Settings" size="large" />
39+
<DrawerButton content={<CredentialsContainer />} button={<button><Info fontSize={24} /></button>} title="Credentials" />
40+
</div>
41+
</div>
42+
<div className='editor'>
43+
<Allotment defaultSizes={[550, 250]}>
44+
<div className='editorWrapper'>
45+
<Editor height="100vh" defaultLanguage="json" value={resource} onChange={(value) => setResource(value as string)} options={{ formatOnPaste: true, formatOnType: true }} />
46+
</div>
47+
<div style={{ height: '100vh' }}>
48+
<Allotment defaultSizes={[100, 300]} vertical>
49+
<div className='editorWrapper'>
50+
<Editor defaultLanguage="ruby" value={expression} onChange={(value) => setExpression(value as string)} options={{
51+
formatOnPaste: true, formatOnType: true, minimap: {
52+
enabled: false,
53+
},
54+
}} />
55+
</div>
56+
<ResultOutput resultItems={result} />
57+
</Allotment>
58+
</div>
59+
</Allotment>
60+
</div>
61+
<ToastContainer />
3362
</div>
34-
</div>
35-
<div className='editor'>
36-
<Allotment defaultSizes={[550, 250]}>
37-
<div className='editorWrapper'>
38-
<Editor height="100vh" defaultLanguage="json" value={resource} onChange={(value) => setResource(value as string)} options={{ formatOnPaste: true, formatOnType: true }} />
39-
</div>
40-
<div style={{ height: '100vh' }}>
41-
<Allotment defaultSizes={[100, 300]} vertical>
42-
<div className='editorWrapper'>
43-
<Editor defaultLanguage="ruby" value={expression} onChange={(value) => setExpression(value as string)} options={{
44-
formatOnPaste: true, formatOnType: true, minimap: {
45-
enabled: false,
46-
},
47-
}} />
48-
</div>
49-
<ResultOutput resultItems={result} />
50-
</Allotment>
51-
</div>
52-
</Allotment>
53-
</div>
54-
<ToastContainer />
55-
</div>
56-
);
63+
);
5764
};
5865

5966
export default App;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { useState } from 'react';
2+
import { Drawer } from 'antd';
3+
4+
interface DrawerButtonProps {
5+
title: string;
6+
content: React.ReactNode;
7+
button: React.ReactNode;
8+
size?: 'large' | 'default';
9+
placement?: 'top' | 'right' | 'bottom' | 'left';
10+
}
11+
12+
export function DrawerButton(props: DrawerButtonProps) {
13+
const [open, setOpen] = useState(false);
14+
const showDrawer = () => {
15+
setOpen(true);
16+
};
17+
18+
const onClose = () => {
19+
setOpen(false);
20+
};
21+
22+
return (<>
23+
<div onClick={showDrawer}>
24+
{props.button}
25+
</div>
26+
<Drawer title={props.title} onClose={onClose} open={open} size={props.size} placement={props.placement}>
27+
{props.content}
28+
</Drawer>
29+
</>)
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import version from '../../version'
2+
3+
export function CredentialsContainer() {
4+
return (
5+
<div>
6+
<h1>FHIRPath UI</h1>
7+
<h2>Version: {version}</h2>
8+
<p>
9+
This project is <b>open-source</b>, so you can <b>use it however you like</b>.
10+
</p>
11+
<p>
12+
The <b>inspiration</b> for this project came from my <b>work tasks</b>, where I spent a lot of time retrieving data from FHIR implementation guides using FHIRPath expressions.
13+
</p>
14+
<p>
15+
The project has been <b>developed</b> (or will be developed) entirely in my <b>free time</b>.
16+
</p>
17+
<p>
18+
If you have any ideas, suggestions, or would like to contribute, please reach out via <a href="https://github.com/projkov/fhirpath-ui" target="_blank" rel="noreferrer">GitHub</a>.
19+
</p>
20+
<p>
21+
You can find more information <b>about me</b> on this page: <a href="https://projkov.github.io" target="_blank" rel="noreferrer">projkov.github.io</a>
22+
</p>
23+
<p>
24+
Best regards, <br />
25+
Pavel Rozhkov from beda.software
26+
</p>
27+
</div>
28+
)
29+
}

src/containers/Settings/index.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useState, useEffect } from 'react';
2+
import { Table, Input, Button, Space } from "antd";
3+
4+
import { getFromLocalStorage, setToLocalStorage} from '../../utils/storage'
5+
6+
export interface SettingItem {
7+
id: string;
8+
name: string;
9+
value: string;
10+
}
11+
12+
export function SettingsContainer() {
13+
const [settings, setSettings] = useState<Array<SettingItem>>([]);
14+
15+
useEffect(() => {
16+
const savedSettings = getFromLocalStorage<Array<SettingItem>>("settings");
17+
if (savedSettings) {
18+
setSettings(savedSettings);
19+
} else {
20+
setSettings([{ id: 'authorization_header', name: "Authorization header", value: "" }]);
21+
}
22+
}, []);
23+
24+
const handleSave = () => {
25+
setToLocalStorage("settings", settings);
26+
alert("Saved!");
27+
};
28+
29+
const handleChange = (index: number, newValue: string) => {
30+
const updatedSettings = [...settings];
31+
updatedSettings[index].value = newValue;
32+
setSettings(updatedSettings);
33+
};
34+
35+
const columns = [
36+
{
37+
title: "Name",
38+
dataIndex: "name",
39+
key: "name",
40+
},
41+
{
42+
title: "Value",
43+
dataIndex: "value",
44+
key: "value",
45+
render: (_: string, record: SettingItem, index: number) => (
46+
<Input
47+
value={record.value}
48+
onChange={(e) => handleChange(index, e.target.value)}
49+
/>
50+
),
51+
},
52+
];
53+
54+
return (
55+
<div>
56+
<Table
57+
dataSource={settings}
58+
columns={columns}
59+
pagination={false}
60+
rowKey="name"
61+
/>
62+
<Space style={{ marginTop: 16 }}>
63+
<Button type="primary" onClick={handleSave}>
64+
Save
65+
</Button>
66+
</Space>
67+
</div>
68+
);
69+
}

src/hooks.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import axios from "axios";
22
import { useEffect, useState } from "react";
33
import { toast } from 'react-toastify';
44
import { reqWrapper } from "./utils/requests";
5+
import { getFromLocalStorage } from "./utils/storage"
6+
import { SettingItem } from './containers/Settings/'
57

68
const fhirpath = require('fhirpath');
79
const fhirpath_r4_model = require('fhirpath/fhir-context/r4');
@@ -17,12 +19,17 @@ export function useFHIRPathUI() {
1719
const isGetResourceActive = url !== '';
1820
const isExecuteActive = resource !== '' && expression !== '';
1921
const isShareActive = url !== '' && expression !== '';
22+
const isShareResultActive = result.length > 0
2023
const showError = (message: string) => toast.error(message)
2124
const showSuccess = (message: string) => toast.success(message)
25+
const authorizationHeader = getFromLocalStorage<Array<SettingItem>>("settings")?.find((settingItem) => settingItem.id === 'authorization_header')?.id
26+
const fetchHeaders = authorizationHeader
27+
? { headers: { Authorization: authorizationHeader } }
28+
: {};
2229

2330
const handleFetch = async (fetchUrl: string) => {
2431
setIsLoading(true);
25-
const result = await reqWrapper(axios.get(fetchUrl))
32+
const result = await reqWrapper(axios.get(fetchUrl, fetchHeaders))
2633
if (result.status === 'success') {
2734
setResource(JSON.stringify(result.data, null, 2))
2835
} else {
@@ -33,21 +40,37 @@ export function useFHIRPathUI() {
3340

3441
const handleExecute = async (executeResource: string, executeExpression: string) => {
3542
setIsLoading(true);
36-
setResult(fhirpath.evaluate(JSON.parse(executeResource), executeExpression, null, fhirpath_r4_model));
37-
setIsLoading(false);
43+
try {
44+
const parsed = JSON.parse(executeResource);
45+
const result = fhirpath.evaluate(parsed, executeExpression, null, fhirpath_r4_model);
46+
setResult(result);
47+
} catch (err: any) {
48+
showError(err?.message || String(err));
49+
console.error('Execution error:', err);
50+
} finally {
51+
setIsLoading(false);
52+
}
3853
};
3954

55+
const copyToClipboard = (toCopy: string, successMessage: string, errorMessage: string) => {
56+
navigator.clipboard.writeText(toCopy).then(() => {
57+
showSuccess(successMessage);
58+
}).catch(err => {
59+
showError(errorMessage + " " + err);
60+
});
61+
}
62+
4063
const handleShare = () => {
4164
const currentUrl = window.location.href.split('?')[0];
4265
const shareUrl = `${currentUrl}?url=${encodeURIComponent(url)}&expression=${encodeURIComponent(expression)}`;
4366
setShareLink(shareUrl);
44-
navigator.clipboard.writeText(shareUrl).then(() => {
45-
showSuccess('Link copied to clipboard!');
46-
}).catch(err => {
47-
showError('Could not copy text: ' + err);
48-
});
67+
copyToClipboard(shareUrl, "Link copied to clipboard!", "Could not copy text")
4968
};
5069

70+
const handleShareResult = () => {
71+
copyToClipboard(result.map((resItem) => JSON.stringify(resItem)).join(', '), "Result copied to clipboard!", "Could not copy result")
72+
}
73+
5174
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => setUrl(e.target.value);
5275

5376
const getUrlParams = (name: string): string | null => {
@@ -94,6 +117,8 @@ export function useFHIRPathUI() {
94117
isLoading,
95118
isExecuteActive,
96119
isGetResourceActive,
97-
isShareActive
120+
isShareActive,
121+
isShareResultActive,
122+
handleShareResult,
98123
};
99124
}

src/utils/storage.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const setToLocalStorage = <T>(key: string, value: T): void => {
2+
localStorage.setItem(key, JSON.stringify(value));
3+
};
4+
5+
export const getFromLocalStorage = <T>(key: string): T | null => {
6+
const item = localStorage.getItem(key);
7+
return item ? (JSON.parse(item) as T) : null;
8+
};
9+
10+
export const removeFromLocalStorage = (key: string): void => {
11+
localStorage.removeItem(key);
12+
};

src/version.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pkg from '../package.json';
2+
const version = pkg.version;
3+
export default version;

0 commit comments

Comments
 (0)