mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 11:51:30 +00:00
Merge pull request #6583 from naman-bruno/add/collection-docs
add: collection-docs
This commit is contained in:
@@ -38,6 +38,15 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.generate-docs {
|
||||
background-color: ${(props) => rgba(props.theme.accents.primary, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.accents.primary, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.accents.primary};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { IconFolder, IconWorld, IconApi, IconShare, IconBook } from '@tabler/icons';
|
||||
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
|
||||
import { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
import GenerateDocumentation from 'components/Sidebar/Collections/Collection/GenerateDocumentation';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -15,6 +16,7 @@ const Info = ({ collection }) => {
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
|
||||
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
|
||||
const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false);
|
||||
|
||||
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
|
||||
|
||||
@@ -111,6 +113,19 @@ const Info = ({ collection }) => {
|
||||
</div>
|
||||
</div>
|
||||
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
|
||||
<div className="flex items-start group cursor-pointer" onClick={() => setShowGenerateDocumentationModal(true)}>
|
||||
<div className="icon-box generate-docs flex-shrink-0 p-3 rounded-lg">
|
||||
<IconBook className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4 h-full flex flex-col justify-start">
|
||||
<div className="font-medium h-fit my-auto">Documentation</div>
|
||||
<div className="group-hover:underline text-link">
|
||||
Generate Docs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showGenerateDocumentationModal && <GenerateDocumentation collectionUid={collection.uid} onClose={() => setShowGenerateDocumentationModal(false)} />}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.info-card {
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
background-color: ${(props) => rgba(props.theme.accents.primary, 0.06)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.accents.primary, 0.2)};
|
||||
|
||||
.info-icon {
|
||||
color: ${(props) => props.theme.accents.primary};
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.info-description {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
background-color: ${(props) => props.theme.background.base};
|
||||
border: 1px solid ${(props) => props.theme.border.border0};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
.feature-icon {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
}
|
||||
|
||||
.note-section {
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.06)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.colors.text.warning, 0.2)};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.warning};
|
||||
line-height: 1.5;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,181 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconCheck, IconInfoCircle, IconAlertTriangle, IconLoader2 } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { findCollectionByUid, areItemsLoading } from 'utils/collections/index';
|
||||
import { brunoToOpenCollection } from '@usebruno/converters';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import * as FileSaver from 'file-saver';
|
||||
import jsyaml from 'js-yaml';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useApp } from 'providers/App';
|
||||
import { escapeHtml } from 'utils/response';
|
||||
|
||||
const GenerateDocumentation = ({ onClose, collectionUid }) => {
|
||||
const { version } = useApp();
|
||||
const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const isCollectionLoading = collection ? areItemsLoading(collection) : false;
|
||||
|
||||
if (!collection) {
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
title="Generate Documentation"
|
||||
confirmText="Close"
|
||||
handleConfirm={onClose}
|
||||
hideCancel={true}
|
||||
>
|
||||
<StyledWrapper className="w-[500px]">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="note-section flex items-start gap-2 p-3">
|
||||
<IconAlertTriangle size={16} className="shrink-0 mt-px" />
|
||||
<span>Collection not found. It may have been deleted or is no longer available.</span>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const generateHtmlDocumentation = () => {
|
||||
try {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const transformedCollection = transformCollectionToSaveToExportAsFile(collectionCopy);
|
||||
const openCollection = brunoToOpenCollection(transformedCollection);
|
||||
|
||||
if (!openCollection.extensions) {
|
||||
openCollection.extensions = {};
|
||||
}
|
||||
if (!openCollection.extensions.bruno) {
|
||||
openCollection.extensions.bruno = {};
|
||||
}
|
||||
openCollection.extensions.bruno.exportedAt = new Date().toISOString();
|
||||
openCollection.extensions.bruno.exportedUsing = version ? `Bruno/${version}` : 'Bruno';
|
||||
|
||||
const yamlContent = jsyaml.dump(openCollection, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
noRefs: true,
|
||||
sortKeys: false
|
||||
});
|
||||
|
||||
const escapedYaml = yamlContent
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\$/g, '\\$');
|
||||
|
||||
const escapedCollectionName = escapeHtml(collection.name);
|
||||
|
||||
const htmlContent = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${escapedCollectionName} - API Documentation</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#opencollection-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdn.opencollection.com/core.css">
|
||||
<script src="https://cdn.opencollection.com/playground.umd.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="opencollection-container"></div>
|
||||
<script>
|
||||
const collectionData = \`${escapedYaml}\`;
|
||||
const container = document.getElementById('opencollection-container');
|
||||
new window.OpenCollection({
|
||||
target: container,
|
||||
opencollection: collectionData,
|
||||
theme: 'light'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const sanitizedName = sanitizeName(collection.name);
|
||||
const fileName = `${sanitizedName}-documentation.html`;
|
||||
const fileBlob = new Blob([htmlContent], { type: 'text/html' });
|
||||
|
||||
FileSaver.saveAs(fileBlob, fileName);
|
||||
toast.success('Documentation generated successfully');
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error generating documentation:', error);
|
||||
toast.error('Failed to generate documentation');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
title="Generate Documentation"
|
||||
confirmText={isCollectionLoading ? 'Loading...' : 'Generate'}
|
||||
cancelText="Cancel"
|
||||
handleConfirm={isCollectionLoading ? undefined : generateHtmlDocumentation}
|
||||
handleCancel={onClose}
|
||||
confirmDisabled={isCollectionLoading}
|
||||
>
|
||||
<StyledWrapper className="w-[500px]">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="info-card flex items-start p-4 gap-3">
|
||||
<div className="info-icon shrink-0">
|
||||
{isCollectionLoading ? (
|
||||
<IconLoader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
<IconInfoCircle size={20} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="info-title mb-1">
|
||||
{isCollectionLoading ? 'Loading collection...' : 'Interactive API Documentation'}
|
||||
</div>
|
||||
<div className="info-description">
|
||||
{isCollectionLoading
|
||||
? 'Please wait while the collection is being loaded.'
|
||||
: 'Generate a standalone HTML file containing interactive documentation for your API collection. This file can be hosted anywhere or shared with your team.'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCollectionLoading && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<div className="feature-item flex items-center gap-2 py-2 px-3">
|
||||
<IconCheck size={16} className="feature-icon shrink-0" />
|
||||
<span>Standalone HTML file - no server required</span>
|
||||
</div>
|
||||
<div className="feature-item flex items-center gap-2 py-2 px-3">
|
||||
<IconCheck size={16} className="feature-icon shrink-0" />
|
||||
<span>Interactive API playground</span>
|
||||
</div>
|
||||
<div className="feature-item flex items-center gap-2 py-2 px-3">
|
||||
<IconCheck size={16} className="feature-icon shrink-0" />
|
||||
<span>Host on any static file server</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="note-section flex items-start gap-2 p-3">
|
||||
<IconAlertTriangle size={16} className="shrink-0 mt-px" />
|
||||
<span>
|
||||
The generated file uses OpenCollection CDN for rendering. An internet connection is required when viewing the documentation.
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateDocumentation;
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
IconX,
|
||||
IconSettings,
|
||||
IconTerminal2,
|
||||
IconFolder
|
||||
IconFolder,
|
||||
IconBook
|
||||
} from '@tabler/icons';
|
||||
import { toggleCollection, collapseFullCollection } from 'providers/ReduxStore/slices/collections';
|
||||
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop, pasteItem, showInFolder, saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -40,6 +41,7 @@ import CloneCollection from './CloneCollection';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
import GenerateDocumentation from './GenerateDocumentation';
|
||||
import { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index';
|
||||
import { sortByNameThenSequence } from 'utils/common/index';
|
||||
import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';
|
||||
@@ -54,6 +56,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
|
||||
const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);
|
||||
const [showShareCollectionModal, setShowShareCollectionModal] = useState(false);
|
||||
const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [dropType, setDropType] = useState(null);
|
||||
const [isKeyboardFocused, setIsKeyboardFocused] = useState(false);
|
||||
@@ -339,6 +342,15 @@ const Collection = ({ collection, searchText }) => {
|
||||
setShowShareCollectionModal(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'generate-docs',
|
||||
leftSection: IconBook,
|
||||
label: 'Generate Docs',
|
||||
onClick: () => {
|
||||
ensureCollectionIsMounted();
|
||||
setShowGenerateDocumentationModal(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'collapse',
|
||||
leftSection: IconFoldDown,
|
||||
@@ -393,6 +405,9 @@ const Collection = ({ collection, searchText }) => {
|
||||
{showShareCollectionModal && (
|
||||
<ShareCollection collectionUid={collection.uid} onClose={() => setShowShareCollectionModal(false)} />
|
||||
)}
|
||||
{showGenerateDocumentationModal && (
|
||||
<GenerateDocumentation collectionUid={collection.uid} onClose={() => setShowGenerateDocumentationModal(false)} />
|
||||
)}
|
||||
{showCloneCollectionModalOpen && (
|
||||
<CloneCollection collectionUid={collection.uid} onClose={() => setShowCloneCollectionModalOpen(false)} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user