From 3099df5e941142ed07c3209b0ab76afc68f4fe2a Mon Sep 17 00:00:00 2001 From: Jordan Stephens Date: Wed, 8 Jan 2025 12:03:07 -0800 Subject: [PATCH] Revert "deduplicate samples from docs site in favour of mktg site" --- ...024-02-12-announcing-defang-public-beta.md | 2 +- blog/2024-07-31-july-product-updates-2.md | 13 +- docs/intro/features.mdx | 2 +- docs/samples.md | 15 + docusaurus.config.js | 19 +- package-lock.json | 25 -- package.json | 1 - scripts/prebuild.sh | 6 + scripts/prep-samples.js | 93 ++++++ sidebars.js | 5 +- src/components/Samples/index.tsx | 272 ++++++++++++++++++ src/components/Samples/samples.module.css | 7 + 12 files changed, 405 insertions(+), 55 deletions(-) create mode 100644 docs/samples.md create mode 100644 scripts/prep-samples.js create mode 100644 src/components/Samples/index.tsx create mode 100644 src/components/Samples/samples.module.css diff --git a/blog/2024-02-12-announcing-defang-public-beta.md b/blog/2024-02-12-announcing-defang-public-beta.md index d70328e46..a36fd8ee0 100644 --- a/blog/2024-02-12-announcing-defang-public-beta.md +++ b/blog/2024-02-12-announcing-defang-public-beta.md @@ -12,7 +12,7 @@ Ever since we shipped our Private Beta in the summer of 2023, we have been worki And so, today with our Public Beta, we are addressing this request. With today’s release of [Defang BYOC](https://docs.defang.io/docs/concepts/defang-byoc) (Bring-your-own-Cloud), you can now enjoy all the benefits of Defang **and** deploy applications to your own AWS account! Our Private Beta experience is still available as [Defang Playground](https://docs.defang.io/docs/concepts/defang-playground) for you to quickly and easily prototype applications and deploy them to our hosted environment. -You can learn more about Defang [here](https://docs.defang.io/docs/intro). Also check out our [tutorials](https://docs.defang.io/docs/category/tutorials), [samples](https://defang.io/#samples), and [FAQ](https://docs.defang.io/docs/category/faq) to know more. +You can learn more about Defang [here](https://docs.defang.io/docs/intro). Also check out our [tutorials](https://docs.defang.io/docs/category/tutorials), [samples](https://docs.defang.io/docs/samples), and [FAQ](https://docs.defang.io/docs/category/faq) to know more. **Try the Public Beta!** diff --git a/blog/2024-07-31-july-product-updates-2.md b/blog/2024-07-31-july-product-updates-2.md index ddac46bec..c767c1545 100644 --- a/blog/2024-07-31-july-product-updates-2.md +++ b/blog/2024-07-31-july-product-updates-2.md @@ -11,12 +11,12 @@ Hey folks! We can’t believe a month has gone by already, time flies when you 1. As our user-base grows, we wanted to make sure we’re able to scale our [Playground](https://docs.defang.io/docs/concepts/defang-playground) environment to be able to handle the load. This involved being able to shard the workload across multiple ALBs and being able to dynamically move some workloads across shards where possible. With these changes, we are now able handle a large number of concurrent users comfortably. The only noticeable change in behavior you would see is that Defang will now ask you to “`compose down`” your previous project before you are able to deploy a new project on Playground. -2. The major news this month was the introduction of our “`debug`” functionality. The motivation for this feature was that while the Defang experience is amazing when everything goes smoothly, we saw users (including our own interns who are helping write all those [samples](https://defang.io/#samples)) struggle when they hit an error. The underlying reason for the error could come from a variety of sources: an error in the developer’s application (including in their Dockerfile or Compose file), an issue in the way Defang is processing the application, or an issue in the underlying cloud platform (currently, AWS). To the developer, it is often not obvious what the issue is or how to fix it. That got us thinking how we could make this debugging experience “radically simpler” and thus the idea for `defang debug` was born. - +2. The major news this month was the introduction of our “`debug`” functionality. The motivation for this feature was that while the Defang experience is amazing when everything goes smoothly, we saw users (including our own interns who are helping write all those [samples](https://docs.defang.io/docs/samples)) struggle when they hit an error. The underlying reason for the error could come from a variety of sources: an error in the developer’s application (including in their Dockerfile or Compose file), an issue in the way Defang is processing the application, or an issue in the underlying cloud platform (currently, AWS). To the developer, it is often not obvious what the issue is or how to fix it. That got us thinking how we could make this debugging experience “radically simpler” and thus the idea for `defang debug` was born. + Now (with CLI v0.5.37 if your application encounters an error that leads to a failed deployment, a failed health-check, or a run-time error, Defang will automatically detect the issue. It will then offer to help you debug it by running the `defang debug` command. If you choose to proceed, Defang will apply an LLM model to try to determine the precise cause of the error, with the context of your application source, logs, error code etc. And it will try to come up with one or more actionable insights on how to fix the error. For an example, see the case below: - - - + + + Behind the scenes, Defang is having a conversation on your behalf with the LLM to narrow down to the cause of the error. We would love for you to try the `debug` feature and give us your feedback so we can improve it further. One future improvement already on our list is the ability to, with user consent, automatically apply a chosen fix and re-try. We are also looking for way to improve the range of failures we are able to diagnose successfully. ## Townhall @@ -24,7 +24,8 @@ Hey folks! We can’t believe a month has gone by already, time flies when you If you're excited about what's coming next and want to hear more about our vision for the future, join us for our Townhall on August 21st. We'll be sharing more about our roadmap and what we're working on next. We'll also be making sure to take time to answer any questions you have, hear your feedback, and learn more about what you want from Defang! **[Register here](https://lu.ma/rlj13eq5)** - + --- We’re excited to keep improving Defang to make it the easiest way for you to Develop, Deploy, and Debug cloud application. Stay tuned for more updates next month. + diff --git a/docs/intro/features.mdx b/docs/intro/features.mdx index ff13f3b50..74d0b1344 100644 --- a/docs/intro/features.mdx +++ b/docs/intro/features.mdx @@ -10,7 +10,7 @@ Defang provides a streamlined experience to develop, deploy, and debug your clou ### Wide Variety of Use Cases - Support for [various types of applications](/docs/intro/use-cases): web services and APIs, mobile app backends, ML services, hosting LLMs, etc... -- Support for your programming [language of choice](https://defang.io/#samples): Node.js, Python, Golang, or anything else you can package in a Dockerfile +- Support for your programming [language of choice](/docs/samples): Node.js, Python, Golang, or anything else you can package in a Dockerfile ### AI-Driven Features - Built-in AI agent to go [from natural language prompt to an outline project](/docs/tutorials/generate-new-code-using-ai) diff --git a/docs/samples.md b/docs/samples.md new file mode 100644 index 000000000..f01da6756 --- /dev/null +++ b/docs/samples.md @@ -0,0 +1,15 @@ +--- +title: Samples +description: Sample projects to help you launch services faster with Defang. +sidebar_position: 600 +--- + +import {Button, ButtonGroup, FormGroup, FormLabel} from "@mui/material" + +# Samples + +Check out our sample projects here to get some inspiration and get a sense of how Defang works. + +import Samples from "../src/components/Samples"; + + diff --git a/docusaurus.config.js b/docusaurus.config.js index aaf48b092..a944bfe3b 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,16 +1,9 @@ // @ts-check // Note: type annotations allow type checking and IDEs autocompletion -const { themes } = require('prism-react-renderer'); +const {themes} = require('prism-react-renderer'); const lightCodeTheme = themes.github; const darkCodeTheme = themes.dracula; -const redirects = [ - { - from: '/docs/samples', - to: 'https://defang.io/#samples', - }, -]; - /** @type {import('@docusaurus/types').DocusaurusConfig} */ const config = { scripts: [ @@ -163,15 +156,7 @@ const config = { darkTheme: darkCodeTheme, }, }, - plugins: [ - require.resolve('docusaurus-lunr-search'), - [ - '@docusaurus/plugin-client-redirects', - { - redirects, - }, - ], - ], + plugins: [require.resolve('docusaurus-lunr-search')], }; module.exports = config; diff --git a/package-lock.json b/package-lock.json index 50915cba7..deeab4ecb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0", "dependencies": { "@docusaurus/core": "3.0.0", - "@docusaurus/plugin-client-redirects": "3.0.0", "@docusaurus/preset-classic": "3.0.0", "@docusaurus/theme-common": "3.0.0", "@emotion/react": "^11.11.1", @@ -2347,30 +2346,6 @@ "react-dom": "*" } }, - "node_modules/@docusaurus/plugin-client-redirects": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.0.0.tgz", - "integrity": "sha512-JcZLod4lgPdbv/OpCbNwTc57u54d01dcWiDy/sBaxls/4HkDGdj6838oBPzbBdnCWrmasBIRz3JYLk+1GU0IOQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.0.0", - "@docusaurus/logger": "3.0.0", - "@docusaurus/utils": "3.0.0", - "@docusaurus/utils-common": "3.0.0", - "@docusaurus/utils-validation": "3.0.0", - "eta": "^2.2.0", - "fs-extra": "^11.1.1", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/@docusaurus/plugin-content-blog": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.0.0.tgz", diff --git a/package.json b/package.json index defe5ddd0..a274a601e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ }, "dependencies": { "@docusaurus/core": "3.0.0", - "@docusaurus/plugin-client-redirects": "3.0.0", "@docusaurus/preset-classic": "3.0.0", "@docusaurus/theme-common": "3.0.0", "@emotion/react": "^11.11.1", diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index e6b7a9eb2..6717004dc 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -14,7 +14,13 @@ if [ -d "../defang" ]; then else DEFANG_PATH=$(readlink -f ./defang) fi +if [ -d "../samples" ]; then + SAMPLES_PATH=$(readlink -f ../samples) +else + SAMPLES_PATH=$(readlink -f ./samples) +fi cd "$DEFANG_PATH/src/cmd/gendocs" && go run main.go "$CLI_DOCS_PATH" cd "$CWD" node scripts/prep-cli-docs.js +node scripts/prep-samples.js "$SAMPLES_PATH/samples" diff --git a/scripts/prep-samples.js b/scripts/prep-samples.js new file mode 100644 index 000000000..4efe8ad04 --- /dev/null +++ b/scripts/prep-samples.js @@ -0,0 +1,93 @@ +const fs = require('fs'); +const path = require('path'); +const YAML = require('yaml'); + +const samplesDir = process.argv[2]; + +// categories are directories in the current directory (i.e. we're running in samples/ and we might have a samples/ruby/ directory) +const directories = fs.readdirSync(samplesDir).filter(file => fs.statSync(path.join(samplesDir, file)).isDirectory()); + +let jsonArray = []; + +directories.forEach((sample) => { + const directoryName = sample; + console.log(`@@ Adding ${sample}`); + let readme; + try { + readme = fs.readFileSync(path.join(samplesDir, sample, 'README.md'), 'utf8'); + } catch (error) { + readme = `# ${sample}`; + } + + // The readme should contain lines that start with the following: + // Title: + // Short Description: + // Tags: + // Languages: + // + // We want to extract the title, short description, tags, and languages from the readme. Tags and languages are comma separated lists. + const title = readme.match(/Title: (.*)/)[1]; + const shortDescription = readme.match(/Short Description: (.*)/)[1]; + const tags = readme.match(/Tags: (.*)/)[1].split(',').map(tag => tag.trim()); + const languages = readme.match(/Languages: (.*)/)[1].split(',').map(language => language.trim()); + + let configs = []; + try { + composeFile = fs.readFileSync(path.join(samplesDir, sample, 'compose.yaml'), 'utf8'); + compose = YAML.parse(composeFile); + + for (var name in compose.services) { + service = compose.services[name] + if (Array.isArray(service.environment)) { + service.environment.forEach(env => { + if (!env.includes("=")) { + configs.push(env); + } + }); + } else { + for (var name in service.environment) { + value = service.environment[name]; + if (value === null || value === undefined || value === "") { + configs.push(name); + } + } + } + } + } catch (error) { + // Ignore if the sample doesn't have a compose file + if (error.code != 'ENOENT') { + console.log(`failed to parese compose for configs for sample`, sample, error); + } + } + + const sampleSummary = { + name: directoryName, + category: languages?.[0], + readme, + directoryName, + title, + shortDescription, + tags, + languages, + }; + if (configs.length > 0) { + sampleSummary.configs = configs; + } + jsonArray.push(sampleSummary); + + console.log(`@@ Added ${sample}`); +}); + +const stringified = JSON.stringify(jsonArray, null, 2); + +// fs.writeFileSync(path.join(__dirname, '..', 'samples.json'), stringified); + +// we're going to open up the ../docs/samples.md file and replce [] with the stringified JSON + +// const samplesMd = path.join(__dirname, '..', 'docs', 'samples.md'); +// let samplesMdContents = fs.readFileSync(samplesMd, 'utf8'); +// samplesMdContents += ``; +// fs.writeFileSync(samplesMd, samplesMdContents); + +// save the json to the samples.json file in static +fs.writeFileSync(path.join(__dirname, '..', 'static', 'samples-v2.json'), stringified); diff --git a/sidebars.js b/sidebars.js index e0df30dc6..6848dbd28 100644 --- a/sidebars.js +++ b/sidebars.js @@ -14,10 +14,7 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { // By default, Docusaurus generates a sidebar from the docs folder structure - docsSidebar: [ - {type: 'autogenerated', dirName: '.'}, - { type: 'link', label: 'Samples', href: 'https://defang.io/#samples' }, - ], + docsSidebar: [{type: 'autogenerated', dirName: '.'}], }; module.exports = sidebars; diff --git a/src/components/Samples/index.tsx b/src/components/Samples/index.tsx new file mode 100644 index 000000000..843944777 --- /dev/null +++ b/src/components/Samples/index.tsx @@ -0,0 +1,272 @@ + +import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, List, ListItemButton, ListItemText, Stack, TextField } from '@mui/material'; +import CodeBlock from '@theme/CodeBlock'; +import ExternalLink from '@theme/Icon/ExternalLink'; +import Fuse, { FuseResult } from 'fuse.js'; +import { Fragment, ReactNode, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; + +interface Sample { + name: string; + category: string; + readme: string; + title?: string; + shortDescription?: string; + languages?: string[]; + tags?: string[]; +} + +interface SamplesProps { + samples: Sample[]; +} + +const categoryColors = { + python: '#FFFFE0', + nodejs: '#90EE90', + typescript: '#cabbff', + golang: '#b8e4f3', + go: '#b8e4f3', + sql: '#ebaef4', + ruby: '#FF7F7F', + other: 'lightgray', +}; + +function highlightMatches(text: string, matches: FuseResult['matches']) { + let lastIndex = 0; + const [match] = matches; + const { indices } = match; + const parts = []; + for (const [start, end] of indices) { + parts.push(text.slice(lastIndex, start)); + parts.push({text.slice(start, end + 1)}); + lastIndex = end + 1; + } + parts.push(text.slice(lastIndex)); + return ( + <> + {parts.map((part, i) => + {part} + )} + + ); +} + +/** + * Returns react nodes with just the highlighted text and a few characters before and after. Each match is separated by an ellipsis. + */ +function getHighlightedTextWithContext(text: string, matches: FuseResult['matches']) { + let lastIndex = 0; + const [match] = matches; + const { indices } = match; + const parts = []; + for (const [start, end] of indices.slice(0, 3)) { + if (start > lastIndex) { + parts.push(text.slice(Math.max(0, lastIndex), Math.max(0, start - 5))); + parts.push('...'); + } + parts.push({text.slice(start, end + 1)}); + lastIndex = end + 1; + } + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex, Math.min(lastIndex + 5, text.length))); + parts.push('...'); + } + return ( + <> + {parts.map((part, i) => + {part} + )} + + ); +} + + + + +export default function Samples() { + const [samples, setSamples] = useState([]); + const [filter, setFilter] = useState(""); + const [selectedSample, setSelectedSample] = useState(null); + const [loading, setLoading] = useState(true); + + const fuse = useRef(new Fuse(samples, { + keys: ['title', 'category', 'shortDescription', 'tags', 'languages'], + includeMatches: true, + isCaseSensitive: false, + threshold: 0.3, + })).current; + + useEffect(() => { + const fetchSamples = async () => { + const response = await fetch('/samples-v2.json'); + const samples = await response.json(); + + fuse.setCollection(samples); + + setSamples(samples); + setLoading(false); + }; + fetchSamples(); + }, []); + + const deferredFilter = useDeferredValue(filter); + const results = useMemo((): FuseResult[] => { + if (!deferredFilter) { + return samples.map((item, i) => ({ + item, + score: 0, + refIndex: i, + matches: [], + })); + } + return fuse.search({ + $and: deferredFilter.split(/\s+/).map((word) => ({ + $or: [ + { title: word }, + { category: word }, + { shortDescription: word }, + { tags: word }, + { languages: word }, + ], + })), + }); + }, [deferredFilter, samples]); + + return ( + <> + setSelectedSample(null)} fullWidth maxWidth="md" scroll='paper' PaperProps={{ + sx: { + maxHeight: 'calc(100vh - 100px)', + } + }}> + {selectedSample && ( + <> + + + + {selectedSample.title} + + {(selectedSample.languages?.length ?? 0) > 0 && ( + selectedSample.languages?.map((language) => ( + + )) + )} + + + + + + + {selectedSample.readme} + + + + {/* */} + + + Use this sample (requires Defang CLI v0.5.21 or later) + + + {`defang generate ${selectedSample.name}`} + + + {/* */} + + + )} + + + + setFilter(e.target.value)} + variant='filled' + /> + {loading && ( +

+ Loading samples... +

+ )} +
+ + {results + .map((result) => { + const sample = result.item; + const { + matches + } = result; + + let title: ReactNode = sample.title; + const titleMatched = matches.find((match) => match.key === 'title'); + + let category: ReactNode = sample.category; + const categoryMatched = matches.find((match) => match.key === 'category'); + + let shortDescription: ReactNode = sample.shortDescription.slice(0, 80); + if (sample.shortDescription.length > 80) { + shortDescription += '...'; + } + const shortDescriptionMatched = matches.find((match) => match.key === 'shortDescription'); + + if (titleMatched) { + title = highlightMatches(sample.title, [titleMatched]); + } + if (categoryMatched) { + category = highlightMatches(sample.category, [categoryMatched]); + } + if (shortDescriptionMatched) { + shortDescription = getHighlightedTextWithContext(sample.shortDescription, [shortDescriptionMatched]); + } + + return ( + setSelectedSample(sample)} + > + + {category && } + {true && ( + <> +
+ {shortDescription} + + )} + + )} + /> +
+ ); + })} +
+
+ + ); +} diff --git a/src/components/Samples/samples.module.css b/src/components/Samples/samples.module.css new file mode 100644 index 000000000..a49c5cda0 --- /dev/null +++ b/src/components/Samples/samples.module.css @@ -0,0 +1,7 @@ +.highlight { + background-color: yellow; + color: black; + padding: 0.2em; + margin: 0 0.1em; + border-radius: 4px; +} \ No newline at end of file