diff --git a/.dockerignore b/.dockerignore index cd2e6702..289e7d21 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ /build /node_modules -/.git \ No newline at end of file +/.git +/.vscode +/.github \ No newline at end of file diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml new file mode 100644 index 00000000..af5fd9d6 --- /dev/null +++ b/.github/workflows/push-docker.yml @@ -0,0 +1,54 @@ +name: Build and Push Docker Image +permissions: + contents: read + packages: write + id-token: write + pages: write + +on: + push: + branches: + - main + release: + types: [published] + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 + + - name: Login to DockerHub + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ vars.DOCKERHUB_REPO }} + + - name: Build and push Docker image + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + with: + context: . + file: ./docker/Dockerfile + push: true + build-args: GITHUB_REF=${{ github.ref_name }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + + - uses: shrink/actions-docker-extract@f97780187d59e88f06a98b166a7e700c0f400314 + id: extract + with: + image: ${{ vars.DOCKERHUB_REPO }} + path: /usr/share/nginx/html/. + destination: dist \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..37d6eec4 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,35 @@ +# Use the official Node.js image as the base image +FROM node:20 + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the application code +COPY . . + +# Build the React app +RUN npm run build + +# Use the official Nginx image to serve the React app +FROM nginxinc/nginx-unprivileged:alpine + +# Copy only JS/CSS from the build output +COPY --from=0 /app/build/yasgui.min.js /usr/share/nginx/html/ +COPY --from=0 /app/build/yasgui.min.css /usr/share/nginx/html/ + +# Copy other static assets +COPY docker/src/ /usr/share/nginx/html/ + +USER root + +COPY docker/change-default-endpoint.sh /docker-entrypoint.d/ +RUN chmod +x /docker-entrypoint.d/change-default-endpoint.sh +RUN chown 1001:0 -R /usr/share/nginx/html/ + +USER 1001 \ No newline at end of file diff --git a/docker/change-default-endpoint.sh b/docker/change-default-endpoint.sh new file mode 100644 index 00000000..71fa5503 --- /dev/null +++ b/docker/change-default-endpoint.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# Set default value if the environment variable is not set +: ${YASGUI_DEFAULT_ENDPOINT:="https://dbpedia.org/sparql"} + +# Replace the default endpoint in the bundle.js file +echo "Switching YASGUI default endpoint to: $YASGUI_DEFAULT_ENDPOINT" +sed -i "s|https://change.to.default.endpoint/sparql|$(printf '%s' "$YASGUI_DEFAULT_ENDPOINT" | sed 's/[&/\]/\\&/g')|g" /usr/share/nginx/html/*.js + +# Execute the command passed to the entrypoint +exec "$@" + +# Execute the original Docker entrypoint script +exec /docker-entrypoint.sh \ No newline at end of file diff --git a/docker/src/favicon.png b/docker/src/favicon.png new file mode 100644 index 00000000..663009b9 Binary files /dev/null and b/docker/src/favicon.png differ diff --git a/docker/src/index.html b/docker/src/index.html new file mode 100644 index 00000000..8b188298 --- /dev/null +++ b/docker/src/index.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + Yasgui + + + + + + +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/docker/src/logo.svg b/docker/src/logo.svg new file mode 100644 index 00000000..62b2cc99 --- /dev/null +++ b/docker/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docker/src/main.css b/docker/src/main.css new file mode 100644 index 00000000..f1facb7f --- /dev/null +++ b/docker/src/main.css @@ -0,0 +1,128 @@ +/* Global styles */ +body { + margin: 0; + padding: 0; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* YASGUI container */ +#yasgui { + margin-top: 0px; + height: calc(100vh - 60px); +} + +/* Footer styles */ +#footer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: #f5f5f5; + padding: 8px 20px; + border-top: 1px solid #e0e0e0; + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + transition: transform 0.3s ease-in-out; +} + +.footer-left { + position: absolute; + left: 20px; + display: flex; + align-items: center; +} + +.footer-center { + text-align: center; +} + +#logo { + height: 35px; + width: auto; + transition: opacity 0.2s; +} + +#logo:hover { + opacity: 0.8; +} + +#footer a { + color: #0066cc; + text-decoration: none; +} + +#footer a:hover { + text-decoration: underline; +} + +/* Footer toggle button */ +#footer-toggle { + display: none; + position: fixed; + bottom: 20px; + right: 20px; + width: 50px; + height: 50px; + border-radius: 50%; + background-color: #0066cc; + color: white; + border: none; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + cursor: pointer; + z-index: 999; + font-size: 20px; + transition: all 0.3s ease; +} + +#footer-toggle:hover { + background-color: #0052a3; + transform: scale(1.1); +} + +#footer-toggle:active { + transform: scale(0.95); +} + +/* Responsive behavior for small screens */ +@media (max-width: 768px) { + #footer { + transform: translateY(100%); + } + + #footer.expanded { + transform: translateY(0); + } + + #footer-toggle { + display: flex; + align-items: center; + justify-content: center; + } + + #footer-toggle.hidden { + opacity: 0; + pointer-events: none; + transform: scale(0); + } + + .footer-left { + position: relative; + left: 0; + margin-bottom: 10px; + } + + #footer { + flex-direction: column; + padding: 15px 20px; + } + + .footer-center { + margin-top: 10px; + } +} diff --git a/docker/src/main.js b/docker/src/main.js new file mode 100644 index 00000000..ec244d00 --- /dev/null +++ b/docker/src/main.js @@ -0,0 +1,74 @@ +// Footer toggle functionality for small screens +(function () { + const footer = document.getElementById("footer"); + const footerToggle = document.getElementById("footer-toggle"); + + if (!footer || !footerToggle) return; + + // Toggle footer visibility + footerToggle.addEventListener("click", function () { + footer.classList.toggle("expanded"); + footerToggle.classList.toggle("hidden"); + }); + + // Close footer when clicking outside + document.addEventListener("click", function (event) { + const isClickInsideFooter = footer.contains(event.target); + const isClickOnToggle = footerToggle.contains(event.target); + + // Only close if footer is expanded, click is outside, and not on toggle button + if (footer.classList.contains("expanded") && !isClickInsideFooter && !isClickOnToggle) { + footer.classList.remove("expanded"); + footerToggle.classList.remove("hidden"); + } + }); +})(); + +// YASGUI is loaded via CDN in index.html and available as a global variable +if (typeof Yasgui !== "undefined") { + const yasgui = new Yasgui(document.getElementById("yasgui"), { + // Set the SPARQL endpoint + requestConfig: { + endpoint: "https://change.to.default.endpoint/sparql", + }, + + // Allow resizing of the Yasqe editor + resizeable: true, + + // Whether to autofocus on Yasqe on page load + autofocus: true, + + // Use the default endpoint when a new tab is opened + copyEndpointOnNewTab: true, + + // Configuring which endpoints appear in the endpoint catalogue list + endpointCatalogueOptions: { + getData: () => { + return [ + //List of objects should contain the endpoint field + //Feel free to include any other fields (e.g. a description or icon + //that you'd like to use when rendering) + { + endpoint: "https://data-interop.era.europa.eu/api/sparql", + }, + { + endpoint: "https://dbpedia.org/sparql", + }, + { + endpoint: "https://query.wikidata.org", + }, + // ... + ]; + }, + //Data object keys that are used for filtering. The 'endpoint' key already used by default + keys: [], + //Data argument contains a `value` property for the matched data object + //Source argument is the suggestion DOM element to append your rendered item to + renderItem: (data, source) => { + const contentDiv = document.createElement("div"); + contentDiv.innerText = data.value.endpoint; + source.appendChild(contentDiv); + }, + }, + }); +} diff --git a/docker/src/manifest.json b/docker/src/manifest.json new file mode 100644 index 00000000..9c89c9f9 --- /dev/null +++ b/docker/src/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Yasgui", + "name": "Yet Another SPARQL GUI", + "icons": [ + { + "src": "favicon.png", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/docker/src/robots.txt b/docker/src/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/docker/src/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: