diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..97cda862 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Please complete the following information:** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Elixir Version [e.g. 1.16] + - Backpex Version [e.g. 0.5.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..c1dc2661 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Community Support + url: https://github.com/naymspace/backpex/discussions + about: Please ask and answer questions here. + - name: Feature Requests + url: https://github.com/naymspace/backpex/discussions/categories/ideas + about: Share ideas for new features here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a0140975..5e869c0f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -30,3 +30,9 @@ updates: reviewers: - 'krns' open-pull-requests-limit: 10 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + reviewers: + - 'krns' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60374113..4669b3ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,9 @@ name: CI on: workflow_dispatch: push: + branches: + - main + - develop pull_request: branches: - main @@ -12,7 +15,6 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME_TESTING: ${{ github.repository }}/testing IMAGE_NAME_RUNTIME: ${{ github.repository }}/runtime jobs: @@ -21,8 +23,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - elixir: ["1.16.2", "1.15.7"] - erlang: ["26.2.2", "25.3.2"] + elixir: ["1.17", "1.16"] + erlang: ["27.0", "26.2"] + exclude: + - elixir: "1.16" + erlang: "27.0" steps: - name: Checkout code @@ -105,21 +110,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Determine the elixir version - id: elixir_version - run: echo "version=$(grep -h elixir .tool-versions | awk '{ print $2 }' | awk -F - '{print $1}')" >> $GITHUB_OUTPUT - - - name: Determine the otp version - id: otp_version - run: echo "version=$(grep -h erlang .tool-versions | awk '{ print $2 }')" >> $GITHUB_OUTPUT - - - name: Use versions - run: echo "Using Elixir version ${{ steps.elixir_version.outputs.version }} and OTP version ${{ steps.otp_version.outputs.version }}" - - uses: erlef/setup-beam@v1 + id: beam with: - otp-version: ${{ steps.otp_version.outputs.version }} - elixir-version: ${{ steps.elixir_version.outputs.version }} + version-file: .tool-versions + version-type: strict - name: Install dependencies run: | @@ -132,153 +127,137 @@ jobs: HEX_API_KEY: ${{ secrets.HEX_API_KEY }} run: mix hex.publish --yes - build-testing: - name: "Build testing" + test-demo: + name: "Test (Demo)" runs-on: ubuntu-latest - permissions: - contents: read - packages: write + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - "5432:5432" steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the container registry - uses: docker/login-action@v3 + - name: Setup beam + uses: erlef/setup-beam@v1 + id: beam with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + version-file: .tool-versions + version-type: strict - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 + - name: Setup node + uses: actions/setup-node@v4 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_TESTING }} + node-version: 20 + cache: 'yarn' + cache-dependency-path: demo/yarn.lock - - name: Build and push container - id: docker - uses: docker/build-push-action@v5 + - name: Restore the deps cache + uses: actions/cache@v4 + id: deps-cache with: - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_TESTING }}:buildcache - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_TESTING }}:buildcache,mode=max - target: builder - build-args: | - MIX_ENV=test - outputs: - image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_TESTING }}@${{ steps.docker.outputs.digest }} + path: demo/deps + key: ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-deps-demo-mixlockhash-${{ hashFiles(format('{0}{1}', github.workspace, '/demo/mix.lock')) }} + restore-keys: | + ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-deps-demo- + + - name: Restore the _build cache + uses: actions/cache@v4 + id: build-cache + with: + path: demo/_build + key: ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-build-demo-mixlockhash-${{ hashFiles(format('{0}{1}', github.workspace, '/demo/mix.lock')) }} + restore-keys: | + ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-build-demo- + + - name: Install dependencies + if: steps.deps-cache.outputs.cache-hit != 'true' + working-directory: demo + run: | + mix local.hex --force --if-missing + mix local.rebar --force --if-missing + mix deps.get + + - name: Compile dependencies + if: steps.deps-cache.outputs.cache-hit != 'true' + working-directory: demo + run: mix deps.compile - lint-demo: - needs: build-testing - name: "Lint (Demo)" - runs-on: ubuntu-latest - container: - image: ${{ needs.build-testing.outputs.image }} + - name: Compile with warnings as errors + working-directory: demo + run: | + mix compile --warnings-as-errors --force + + - name: Install node dependencies + working-directory: demo + run: yarn install --pure-lockfile - steps: - name: lint:mix - working-directory: /opt/app/demo + working-directory: demo run: | yarn lint:mix - - name: Add tar that supports --posix option to be used by cache action - run: | - apk add --no-cache tar - - name: lint:credo - working-directory: /opt/app/demo + working-directory: demo run: | yarn lint:credo - name: lint:sobelow - working-directory: /opt/app/demo + working-directory: demo run: | yarn lint:sobelow - name: lint:style - working-directory: /opt/app/demo + working-directory: demo run: | yarn lint:style - name: lint:standard - working-directory: /opt/app/demo + working-directory: demo run: | yarn lint:standard - name: lint:deps-unused - working-directory: /opt/app/demo + working-directory: demo run: | yarn lint:deps-unused - name: lint:gettext - working-directory: /opt/app/demo + working-directory: demo run: | yarn lint:gettext - test-compile: - needs: build-testing - name: "Compile (Demo)" - runs-on: ubuntu-latest - container: - image: ${{ needs.build-testing.outputs.image }} - - steps: - - name: Compile with warnings as errors - working-directory: /opt/app/demo - run: | - mix compile --warnings-as-errors --force - - test-demo: - needs: build-testing - name: "Test (Demo)" - runs-on: ubuntu-latest - container: - image: ${{ needs.build-testing.outputs.image }} - env: - POSTGRES_DB: test - POSTGRES_PASSWORD: postgres - services: - postgres: - image: postgres:16-alpine - env: - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - name: Run test - working-directory: /opt/app/demo + working-directory: demo + env: + DB_HOSTNAME: localhost + DB_USERNAME: postgres + DB_PASSWORD: postgres + DB_DATABASE: test run: | yarn test - deps-audit: - needs: build-testing - name: "Deps Audit (Demo)" - runs-on: ubuntu-latest - container: - image: ${{ needs.build-testing.outputs.image }} - - steps: - name: Deps audit - working-directory: /opt/app/demo + working-directory: demo continue-on-error: true run: | mix deps.audit build-runtime: - name: "Build runtime" + name: "Build and push image (Demo)" runs-on: ubuntu-latest - needs: [lint-demo, test-compile, test-demo, deps-audit] + needs: [test-demo] + if: github.event_name == 'push' permissions: contents: read @@ -305,7 +284,7 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_RUNTIME }} - name: Build container - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: push: true tags: ${{ steps.meta.outputs.tags }} diff --git a/.tool-versions b/.tool-versions index dd1dbfca..ce0360e0 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -erlang 26.2.2 -elixir 1.16.2-otp-26 +erlang 27.0.1 +elixir 1.17.2-otp-27 diff --git a/Dockerfile b/Dockerfile index 191e9218..688dcc55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Stage: builder ######################################################################## -FROM hexpm/elixir:1.16.3-erlang-26.2.5-alpine-3.20.0 as builder +FROM hexpm/elixir:1.17.2-erlang-27.0.1-alpine-3.20.1 as builder ENV MIX_HOME=/opt/mix \ HEX_HOME=/opt/hex \ diff --git a/demo/assets/tailwind.config.js b/demo/assets/tailwind.config.js index f9e9742d..fd7370d6 100644 --- a/demo/assets/tailwind.config.js +++ b/demo/assets/tailwind.config.js @@ -12,6 +12,12 @@ module.exports = { 'primary-content': 'white', secondary: '#f39325', 'secondary-content': 'white' + }, + dark: { + ...require('daisyui/src/theming/themes').dark + }, + cyberpunk: { + ...require('daisyui/src/theming/themes').cyberpunk } } ] diff --git a/demo/config/runtime.exs b/demo/config/runtime.exs index a2a134b1..3e524640 100644 --- a/demo/config/runtime.exs +++ b/demo/config/runtime.exs @@ -26,11 +26,6 @@ config :demo, DemoWeb.Endpoint, signing_salt: fetch_env!("LIVE_VIEW_SIGNING_SALT") ] -config :demo, Demo.Newsletter.Brevo, - api_key: get_env("BREVO_API_KEY"), - include_list_ids: get_env("BREVO_INCLUDE_LIST_IDS", "0,42,1337") |> split(",") |> Enum.map(&to_integer/1), - template_id: get_env("BREVO_TEMPLATE_ID", "0") |> to_integer() - config :demo, DemoWeb.DashboardAuthPlug, enabled: get_env("DASHBOARD_AUTH_ENABLED", "false") |> to_existing_atom(), username: get_env("DASHBOARD_AUTH_USERNAME", "backpex"), diff --git a/demo/lib/demo/newsletter.ex b/demo/lib/demo/newsletter.ex deleted file mode 100644 index c2902b3d..00000000 --- a/demo/lib/demo/newsletter.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule Demo.Newsletter do - @moduledoc """ - Manage newsletter subscriptions. - """ - - alias Demo.Newsletter.Brevo - alias Demo.Newsletter.Contact - alias Ecto.Changeset - - @doc """ - Create changeset for contact with given attributes. - """ - def change_contact(contact_or_changeset, attrs \\ %{}) do - Contact.changeset(contact_or_changeset, attrs) - end - - @doc """ - Subscribe to the newsletter. - """ - def subscribe(params) do - changeset = - %Contact{} - |> change_contact(params) - |> Map.put(:action, :insert) - - if changeset.valid? do - case Brevo.double_optin_confirmation(changeset.changes.email) do - {:ok, %Tesla.Env{status: 201}} -> - {:ok, changeset} - - {:ok, %Tesla.Env{status: 204}} -> - {:ok, changeset} - - {:ok, %Tesla.Env{status: 400, body: %{"message" => message}}} -> - {:error, Changeset.add_error(changeset, :email, message)} - - {:error, _message} -> - {:error, Changeset.add_error(changeset, :email, "something went wrong")} - end - else - {:error, changeset} - end - end -end diff --git a/demo/lib/demo/newsletter/brevo.ex b/demo/lib/demo/newsletter/brevo.ex deleted file mode 100644 index ec1e2e10..00000000 --- a/demo/lib/demo/newsletter/brevo.ex +++ /dev/null @@ -1,37 +0,0 @@ -defmodule Demo.Newsletter.Brevo do - @moduledoc """ - Interact with the Brevo API. - """ - - @doc """ - Create Contact via DOI (Double-Opt-In) Flow. - - https://developers.brevo.com/reference/createdoicontact - """ - def double_optin_confirmation(email) do - client() - |> Tesla.post("/contacts/doubleOptinConfirmation", %{ - email: email, - includeListIds: config!(:include_list_ids), - templateId: config!(:template_id), - redirectionUrl: "https://backpex.live" - }) - end - - @doc """ - Create a Tesla client with dynamic runtime middlewares. - - https://hexdocs.pm/tesla/readme.html#runtime-middleware - """ - def client do - middleware = [ - {Tesla.Middleware.BaseUrl, "https://api.brevo.com/v3"}, - Tesla.Middleware.JSON, - {Tesla.Middleware.Headers, [{"api-key", config!(:api_key)}]} - ] - - Tesla.client(middleware) - end - - defp config!(key), do: Application.fetch_env!(:demo, __MODULE__)[key] -end diff --git a/demo/lib/demo/newsletter/contact.ex b/demo/lib/demo/newsletter/contact.ex deleted file mode 100644 index cc271d48..00000000 --- a/demo/lib/demo/newsletter/contact.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Demo.Newsletter.Contact do - @moduledoc false - - use Ecto.Schema - import Ecto.Changeset - - schema "contact" do - field :email, :string - end - - @required_fields ~w[email]a - - def changeset(change, attrs) do - change - |> cast(attrs, @required_fields) - |> validate_required(@required_fields) - end -end diff --git a/demo/lib/demo_web/components/layouts/admin.html.heex b/demo/lib/demo_web/components/layouts/admin.html.heex index c2c705d1..beb7884c 100644 --- a/demo/lib/demo_web/components/layouts/admin.html.heex +++ b/demo/lib/demo_web/components/layouts/admin.html.heex @@ -9,7 +9,7 @@
  • - <.link navigate="/" class="flex justify-between text-red-600 hover:bg-gray-100"> + <.link navigate="/" class="flex justify-between text-error hover:bg-base-200">

    Logout

    diff --git a/demo/lib/demo_web/endpoint.ex b/demo/lib/demo_web/endpoint.ex index 78cb5685..428a6c6d 100644 --- a/demo/lib/demo_web/endpoint.ex +++ b/demo/lib/demo_web/endpoint.ex @@ -10,7 +10,10 @@ defmodule DemoWeb.Endpoint do signing_salt: "szYoVHQC" ] - socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] + socket "/live", Phoenix.LiveView.Socket, + websocket: [ + connect_info: [:peer_data, :uri, :user_agent, session: @session_options] + ] # Serve at "/" the static files from "priv/static" directory. # diff --git a/demo/lib/demo_web/live/home_live/index.ex b/demo/lib/demo_web/live/home_live/index.ex index 88c39bba..5347f9d8 100644 --- a/demo/lib/demo_web/live/home_live/index.ex +++ b/demo/lib/demo_web/live/home_live/index.ex @@ -2,45 +2,6 @@ defmodule DemoWeb.HomeLive.Index do @moduledoc false use DemoWeb, :live_view - alias Demo.Newsletter - alias Demo.Newsletter.Contact - - @impl Phoenix.LiveView - def mount(_params, _session, socket) do - changeset = Newsletter.change_contact(%Contact{}) - - socket = - socket - |> assign(:form_hidden?, form_hidden?()) - |> assign(:subscribed?, false) - |> assign(:form, to_form(changeset)) - - {:ok, socket} - end - - defp form_hidden? do - Application.fetch_env!(:demo, Demo.Newsletter.Brevo)[:api_key] - |> Kernel.is_nil() - end - - @impl Phoenix.LiveView - def handle_event("validate", %{"contact" => params}, socket) do - form = - %Contact{} - |> Newsletter.change_contact(params) - |> Map.put(:action, :insert) - |> to_form() - - {:noreply, assign(socket, form: to_form(form))} - end - - @impl Phoenix.LiveView - def handle_event("subscribe", %{"contact" => params}, socket) do - case Newsletter.subscribe(params) do - {:ok, _contact} -> {:noreply, assign(socket, :subscribed?, true)} - {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} - end - end attr :class, :string, default: "" slot(:inner_block, required: true) @@ -56,7 +17,7 @@ defmodule DemoWeb.HomeLive.Index do def bg_pattern(assigns) do ~H"""
    -

    +

    <%= feature.title %>

    -
    +
    <%= render_slot(feature) %>
    diff --git a/demo/lib/demo_web/live/home_live/index.html.heex b/demo/lib/demo_web/live/home_live/index.html.heex index 8c29d1fb..39c2f42e 100644 --- a/demo/lib/demo_web/live/home_live/index.html.heex +++ b/demo/lib/demo_web/live/home_live/index.html.heex @@ -8,7 +8,7 @@ Beta version available! -

    +

    Phoenix Admin Panel built with <%= String.at(tech, 0) %>

    -

    +

    Backpex is a highly customizable administration panel for Phoenix LiveView applications. Quickly create beautiful CRUD views and more for your existing data via configurable LiveResources. @@ -31,37 +31,18 @@ <.link href="https://github.com/naymspace/backpex" - class="text-sm font-semibold leading-6 text-gray-900 hover:underline" + class="text-sm font-semibold leading-6 text-base-content hover:underline" > GitHub - - -

    -

    - Get notified when we're launching. -

    - <.form :if={not @subscribed?} class="mt-4 flex gap-x-4" for={@form} phx-change="validate" phx-submit="subscribe"> - - - - -
    - -

    - You successfully subscribed to the Backpex newsletter. Please check your inbox to confirm your email address! -

    -
    -

    - No spam. Just infos about Backpex related stuff about once a month. -

    +
    <.link navigate="/admin/users" - class="ring-gray-900/10 bg-gray-900/5 relative block overflow-hidden rounded-xl ring-1 transition hover:ring-4 lg:-m-4 lg:rounded-2xl" + class="ring-base-content/10 bg-base-content/5 relative block overflow-hidden rounded-box ring-1 transition hover:ring-4 lg:-m-4" >