diff --git a/README.md b/README.md index 2640dc89..fbe9ba98 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ curl -sSL https://raw.githubusercontent.com/czhu12/canine/refs/heads/main/instal Or run manually if you prefer: ```bash -git clone git@github.com:czhu12/canine.git +git clone https://github.com/czhu12/canine.git cd canine/install docker compose up -d ``` @@ -39,6 +39,9 @@ Canine Cloud offers additional features for small teams: For more information & pricing, take a look at our landing page [https://canine.sh](https://canine.sh). +## Repo Activity +![Alt](https://repobeats.axiom.co/api/embed/0af4ce8a75f4a12ec78973ddf7021c769b9a0051.svg "Repobeats analytics image") + ## License -[MIT License](https://github.com/czhu12/canine/blob/main/LICENSE) +[Apache 2.0 License](https://github.com/czhu12/canine/blob/main/LICENSE) diff --git a/TODO.md b/TODO.md index a2cbe7d5..a7482b60 100644 --- a/TODO.md +++ b/TODO.md @@ -3,18 +3,14 @@ - [ ] Change the loading state of an add on install to have a better UX - [ ] Show multi step install progress - [ ] Automatic DNS mapping for canineapp.run -- [ ] Support login without Github -- [ ] Support organization projects +- [ ] Migrate Login with Github to github apps + - [ ] Support organization projects on github - [ ] Add skeleton loader for processes page -- [ ] Migrate to goodjob to support scheduled jobs without a ton of separate gems -- [ ] Gray out deploy button if there are no services -- [ ] Make the metrics page look better + filter - [ ] Show ingress logs at the cluster level -- parse NGINX logs - [ ] Streaming logs for pods - [ ] Project groupings? -- [ ] Create a pricing calculator - [ ] Constantly refresh the processes page for readiness of pods -- [ ] Pre-deploy task +- [ ] Make the metrics page look better + filter - [ ] Metrics improvement: https://stackoverflow.com/questions/68058199/chartjs-need-help-on-drawing-a-vertical-line-when-hovering-cursor - [ ] Add metrics page for add ons -- [ ] improve helm chart previews \ No newline at end of file +- [ ] Web socket issues with deployment status's diff --git a/app/controllers/add_ons_controller.rb b/app/controllers/add_ons_controller.rb index 391ed7de..bfe72e13 100644 --- a/app/controllers/add_ons_controller.rb +++ b/app/controllers/add_ons_controller.rb @@ -112,6 +112,8 @@ class AddOnsController < ApplicationController @service = K8::Helm::Redis.new(@add_on) elsif @add_on.chart_type == "postgresql" @service = K8::Helm::Postgresql.new(@add_on) + elsif @add_on.chart_type == "clickhouse" + @service = K8::Helm::Clickhouse.new(@add_on) else @service = K8::Helm::Service.new(@add_on) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index edafea9c..96f9669f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -28,8 +28,8 @@ class ProjectsController < ApplicationController # GET /projects/new def new - selected_provider = params[:provider] || Provider::GITHUB_PROVIDER - @providers = current_user.providers.where(provider: selected_provider) + @selected_provider = params[:provider] || Provider::GITHUB_PROVIDER + @providers = current_user.providers.where(provider: @selected_provider) # Temporary hack @provider = @providers.first @project = Project.new diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index ea24b764..5800bf61 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -9,14 +9,6 @@ module Users redirect_to root_path, alert: "Something went wrong" end - def facebook - handle_auth "Facebook" - end - - def twitter - handle_auth "Twitter" - end - def github handle_auth "Github" end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 52ea44a3..3ab1cdca 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,6 +1,15 @@ class Users::RegistrationsController < Devise::RegistrationsController layout 'homepage', only: [ :new, :create ] + def create + ActiveRecord::Base.transaction do + super do |user| + account = Account.create!(name: "#{user.first_name}'s Account", owner: user) if user.persisted? + AccountUser.create!(account:, user:) + end + end + end + protected def update_resource(resource, params) if account_update_params[:password].blank? diff --git a/app/javascript/controllers/github_select_repository_controller.js b/app/javascript/controllers/github_select_repository_controller.js index 89e9e30e..29fa5255 100644 --- a/app/javascript/controllers/github_select_repository_controller.js +++ b/app/javascript/controllers/github_select_repository_controller.js @@ -17,7 +17,8 @@ export default class extends Controller { } async filterRepositories(e) { - this.searchFunc(e) + e.preventDefault(); + this.searchFunc(e); } closeModal() { diff --git a/app/javascript/controllers/helm_search_controller.js b/app/javascript/controllers/helm_search_controller.js index ee0dbc9a..d734c69f 100644 --- a/app/javascript/controllers/helm_search_controller.js +++ b/app/javascript/controllers/helm_search_controller.js @@ -1,5 +1,5 @@ import { Controller } from "@hotwired/stimulus" -import { renderHelmChartCard, getLogoImageUrl } from "../utils/helm_charts" +import { renderHelmChartCard, helmChartHeader } from "../utils/helm_charts" import { debounce } from "../utils" export default class extends Controller { @@ -9,10 +9,12 @@ export default class extends Controller { connect() { this.input = this.element.querySelector(`input[name="add_on[metadata][helm_chart][helm_chart.name]"]`) + // disable autocomplete + this.input.setAttribute('autocomplete', 'off') // Create and append dropdown this.dropdown = document.createElement('ul') - this.dropdown.className = 'hidden absolute z-10 w-full mt-1 menu bg-base-100 rounded-box shadow-lg' + this.dropdown.className = 'hidden absolute z-10 w-full mt-1 menu bg-base-200 block rounded-box shadow-lg max-h-[300px] overflow-y-auto' this.element.appendChild(this.dropdown) // Bind search handler with debounce @@ -39,15 +41,8 @@ export default class extends Controller { } this.dropdown.innerHTML = packages.map(pkg => ` -
  • -
    - ${pkg.name} logo -
    - ${pkg.name} -
    -
    ${pkg.description}
    -
    -
    +
  • + ${helmChartHeader(pkg)}
  • `).join('') diff --git a/app/javascript/controllers/pricing_controller.js b/app/javascript/controllers/pricing_controller.js index 9dd60ece..003a57b5 100644 --- a/app/javascript/controllers/pricing_controller.js +++ b/app/javascript/controllers/pricing_controller.js @@ -172,8 +172,10 @@ export default class extends Controller { cost(breakdown) { return breakdown.reduce((sum, b) => sum + (typeof b.cost === 'number' ? b.cost : 0), 0); } + render(service, breakdown) { const serviceName = this.pricesValue[service].name + const supportsCanine = this.pricesValue[service].canine if (breakdown.error) { return `
    @@ -188,7 +190,7 @@ export default class extends Controller { const header = `
    - ${serviceName} + ${serviceName}${supportsCanine ? '+' : ''}
    ${total == 0 ? 'FREE' : `$${total}.00`}
    diff --git a/app/javascript/controllers/processes_controller.js b/app/javascript/controllers/processes_controller.js index 6ec30dd0..fb9af2a6 100644 --- a/app/javascript/controllers/processes_controller.js +++ b/app/javascript/controllers/processes_controller.js @@ -15,8 +15,8 @@ export default class extends Controller { showConnectionInstructions(event) { event.preventDefault(); - const text = `KUBECONFIG=/path/to/kubeconfig.yml kubectl exec -it -n ${event.target.dataset.namespace} ${event.target.dataset.podName} -- /bin/bash` + const text = `KUBECONFIG=/path/to/${event.target.dataset.clusterName}-kubeconfig.yml kubectl exec -it -n ${event.target.dataset.namespace} ${event.target.dataset.podName} -- /bin/bash` this.commandTarget.textContent = text click_outside_modal.showModal() } -} \ No newline at end of file +} diff --git a/app/javascript/utils/helm_charts/index.js b/app/javascript/utils/helm_charts/index.js index cb4e48c2..83423a18 100644 --- a/app/javascript/utils/helm_charts/index.js +++ b/app/javascript/utils/helm_charts/index.js @@ -20,12 +20,9 @@ export async function getDefaultValues( return html; } -export function renderHelmChartCard(packageData) { +export function helmChartHeader(packageData) { const logoImageUrl = getLogoImageUrl(packageData); - // Create a temporary container to convert HTML string to DOM element - const tempContainer = document.createElement('div'); - tempContainer.innerHTML = ` -
    + return `
    ${packageData.name}
    @@ -61,6 +58,15 @@ export function renderHelmChartCard(packageData) {
    + ` +} + +export function renderHelmChartCard(packageData) { + // Create a temporary container to convert HTML string to DOM element + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = ` +
    + ${helmChartHeader(packageData)}
    `; diff --git a/app/models/build.rb b/app/models/build.rb index 2fb6852b..1c165dc2 100644 --- a/app/models/build.rb +++ b/app/models/build.rb @@ -35,6 +35,10 @@ class Build < ApplicationRecord } after_update_commit do + broadcast_build + end + + def broadcast_build if events.last broadcast_replace_later_to [ project, :events ], target: dom_id(self, :index), partial: "projects/deployments/event_build_row", locals: { project:, event: events.last } end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 05cc4d24..5a5f488d 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -21,4 +21,7 @@ class Deployment < ApplicationRecord belongs_to :build has_one :project, through: :build enum :status, { in_progress: 0, completed: 1, failed: 2 } + after_update_commit do + self.build.broadcast_build + end end diff --git a/app/services/k8/helm/clickhouse.rb b/app/services/k8/helm/clickhouse.rb new file mode 100644 index 00000000..d9410761 --- /dev/null +++ b/app/services/k8/helm/clickhouse.rb @@ -0,0 +1,35 @@ +class K8::Helm::Clickhouse < K8::Helm::Service + def name + add_on.name + end + + def username + "default" + end + + def password + output = K8::Kubectl.new(add_on.cluster.kubeconfig, Cli::RunAndReturnOutput.new).call("get secret --namespace #{add_on.name} #{service_name} -o jsonpath='{.data.admin-password}' | base64 -d") + output + end + + def database + "default" + end + + def port + 8123 + end + + def service_name + service = client.get_services(namespace: add_on.name).find do |service| + service.metadata.name.ends_with?("-clickhouse") + end + return nil if service.nil? + service.metadata.name + end + + def internal_url + return nil if service_name.nil? + "clickhouse://#{username}:#{password}@#{service_name}.#{add_on.name}.svc.cluster.local:#{port}/#{database}" + end +end diff --git a/app/services/random_name_generator.rb b/app/services/random_name_generator.rb deleted file mode 100644 index f7fa7256..00000000 --- a/app/services/random_name_generator.rb +++ /dev/null @@ -1,11 +0,0 @@ -class RandomNameGenerator - ADJECTIVES = %w[quick lazy happy brave clever quiet mighty kind shiny eager] - NOUNS = %w[fox bear wolf tiger lion eagle owl deer hare dolphin] - - def self.generate_name(prefix = nil) - adjective = ADJECTIVES.sample - noun = NOUNS.sample - name = "#{adjective}-#{noun}" - prefix ? "#{prefix}-#{name}" : name - end -end diff --git a/app/views/add_ons/_template_variables.html.erb b/app/views/add_ons/_template_variables.html.erb index 22d1e324..52c30d16 100644 --- a/app/views/add_ons/_template_variables.html.erb +++ b/app/views/add_ons/_template_variables.html.erb @@ -9,17 +9,39 @@ <% end %> <% else %>
    - <%= form.label variable['name'] %> <% if variable['type'] == 'string' %> + <%= form.label variable['name'] %> <%= form.text_field( variable['key'], name: "add_on[metadata][#{chart['name']}][template][#{variable['key']}]", class: "input input-bordered w-full max-w-md", value: form.object.metadata.dig('template', variable['key']) || variable['default'] ) %> + <% elsif variable['type'] == 'boolean' %> + + <% elsif variable['type'] == 'integer' %> + <%= form.label variable['name'] %> + <%= form.number_field( + variable['key'], + name: "add_on[metadata][#{chart['name']}][template][#{variable['key']}]", + class: "input input-bordered", + value: form.object.metadata.dig('template', variable['key']) || variable['default'] + ) %> <% elsif variable['type'] == 'enum' %> + <%= form.label variable['name'] %> <%= form.select "metadata][#{chart['name']}][template][#{variable['key']}", options_for_select(variable['options']), {}, class: "select select-bordered w-full max-w-md" %> <% elsif variable['type'] == 'size' %> + <%= form.label variable['name'] %> <%= form.hidden_field( "metadata][#{chart['name']}][template][#{variable['key']}][type", value: 'size' diff --git a/app/views/add_ons/new.html.erb b/app/views/add_ons/new.html.erb index a8e9d621..fbf05678 100644 --- a/app/views/add_ons/new.html.erb +++ b/app/views/add_ons/new.html.erb @@ -14,7 +14,7 @@ <%= form_with(model: @add_on) do |form| %>
    <%= form.label :name %> - <%= form.text_field :name, class: "input input-bordered", value: RandomNameGenerator.generate_name %> + <%= form.text_field :name, class: "input input-bordered", autofocus: true, required: true %>
    diff --git a/app/views/application/_error_messages.html.erb b/app/views/application/_error_messages.html.erb new file mode 100644 index 00000000..cfcb043f --- /dev/null +++ b/app/views/application/_error_messages.html.erb @@ -0,0 +1,18 @@ +<% if resource.errors.any? %> + +<% end %> \ No newline at end of file diff --git a/app/views/clusters/_form.html.erb b/app/views/clusters/_form.html.erb index 09d46cb6..a2a26921 100644 --- a/app/views/clusters/_form.html.erb +++ b/app/views/clusters/_form.html.erb @@ -3,7 +3,7 @@
    <%= form.label :name %> - <%= form.text_field :name, class: "input input-bordered", value: RandomNameGenerator.generate_name %> + <%= form.text_field :name, class: "input input-bordered", autofocus: true, required: true %>
    diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index bddef7c3..37f15ee9 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -1,9 +1,61 @@
    -
    - <%= render "devise/shared/links" %> +

    Sign up

    +
    + <%= link_to "Sign in", new_user_session_path, class: "underline" %> instead
    + + <% if @account_invitation %> +
    + <%= image_tag avatar_url_for(@account_invitation.account), class: "h-5 w-5 rounded-full inline-block mr-1" %> +
    + <%= @account_invitation.invited_by.name %> invited you to join <%= @account_invitation.account.name %> +
    +
    + <% end %> + + <%= form_with( + model: resource, + as: resource_name, + url: registration_path(resource_name, invite: params[:invite]), + html: { class: "space-y-4" }, + ) do |f| %> + <%= render "error_messages", resource: resource %> + +
    + <%= f.label :name, class: "label" do %> + Name + <% end %> + <%= f.text_field :name, autocomplete: "off", placeholder: true, class: "input input-bordered w-full" %> +
    + +
    + <%= f.label :email, class: "label" do %> + Email + <% end %> + <%= f.email_field :email, autocomplete: "email", placeholder: true, class: "input input-bordered w-full" %> +
    + +
    + <%= f.label :password, class: "label" do %> + Password + <% end %> + <%= f.password_field :password, autocomplete: "new-password", placeholder: true, class: "input input-bordered w-full" %> +
    + +
    + <%= f.submit "Sign in", class: "btn btn-primary w-full" %> +
    + <% end %> + +
    +
    + OR +
    +
    + + <%= render "devise/shared/links" %>
    diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 726c5a59..5fc2662e 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -2,9 +2,47 @@

    Log in

    -
    - <%= render "devise/shared/links" %> +
    + <%= link_to "Sign up", new_user_registration_path, class: "underline" %> instead
    + + <%= form_with(model: resource, as: resource_name, url: session_path(resource_name), html: { class: "space-y-4" }) do |f| %> + <%= render "error_messages", resource: resource %> + +
    + <%= f.label :email, class: "label" do %> + Email + <% end %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: true, class: "input input-bordered w-full" %> +
    + +
    + <%= f.label :password, class: "label" do %> + Password + <% end %> + <%= f.password_field :password, autocomplete: "current-password", placeholder: true, class: "input input-bordered w-full" %> +
    + + <% if devise_mapping.rememberable? -%> +
    + +
    + <% end %> + +
    + <%= f.submit "Sign in", class: "btn btn-primary w-full" %> +
    + <% end %> + +
    +
    + OR +
    +
    + <%= render "devise/shared/links" %>
    diff --git a/app/views/processes/_pods.html.erb b/app/views/processes/_pods.html.erb index 7d96091f..cd337150 100644 --- a/app/views/processes/_pods.html.erb +++ b/app/views/processes/_pods.html.erb @@ -73,6 +73,7 @@ data-action="click->processes#showConnectionInstructions" data-pod-name="<%= pod.metadata.name %>" data-namespace="<%= pod.metadata.namespace %>" + data-cluster-name="<%= parent.cluster.name %>" >Connect <% else %>