mirror of
https://github.com/mirego/accent.git
synced 2024-08-17 06:50:31 +03:00
Add AWS S3 integration to push strings
This commit is contained in:
parent
388c07a227
commit
857176fdbf
53
lib/accent/integrations/execute/aws_s3.ex
Normal file
53
lib/accent/integrations/execute/aws_s3.ex
Normal file
@ -0,0 +1,53 @@
|
||||
defmodule Accent.IntegrationManager.Execute.AWSS3 do
|
||||
@moduledoc false
|
||||
|
||||
alias Accent.Hook
|
||||
|
||||
def upload_translations(integration, user, params) do
|
||||
{uploads, version_tag} = Accent.IntegrationManager.Execute.UploadDocuments.all(integration, params)
|
||||
base_url = "https://s3-#{integration.data.aws_s3_region}.amazonaws.com/#{integration.data.aws_s3_bucket}"
|
||||
|
||||
url =
|
||||
Path.join([
|
||||
base_url,
|
||||
integration.data.aws_s3_path_prefix
|
||||
])
|
||||
|
||||
uri = URI.parse(url)
|
||||
|
||||
document_urls =
|
||||
for upload <- uploads do
|
||||
{url, document_name} = Accent.IntegrationManager.Execute.UploadDocuments.url(upload, uri, version_tag)
|
||||
|
||||
headers =
|
||||
:aws_signature.sign_v4(
|
||||
integration.data.aws_s3_access_key_id,
|
||||
integration.data.aws_s3_secret_access_key,
|
||||
integration.data.aws_s3_region,
|
||||
"s3",
|
||||
:calendar.universal_time(),
|
||||
"put",
|
||||
url,
|
||||
[{"host", uri.authority}],
|
||||
upload.render,
|
||||
uri_encode_path: false
|
||||
)
|
||||
|
||||
{:ok, %{status_code: 200}} = HTTPoison.put(url, {:file, upload.file}, headers)
|
||||
|
||||
%{name: document_name, url: url}
|
||||
end
|
||||
|
||||
Hook.outbound(%Hook.Context{
|
||||
event: "integration_execute_azure_storage_container",
|
||||
project_id: integration.project_id,
|
||||
user_id: user.id,
|
||||
payload: %{
|
||||
version_tag: version_tag,
|
||||
document_urls: document_urls
|
||||
}
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
@ -1,82 +1,24 @@
|
||||
defmodule Accent.IntegrationManager.Execute.AzureStorageContainer do
|
||||
@moduledoc false
|
||||
|
||||
alias Accent.Document
|
||||
alias Accent.Hook
|
||||
alias Accent.Repo
|
||||
alias Accent.Revision
|
||||
alias Accent.Scopes.Document, as: DocumentScope
|
||||
alias Accent.Scopes.Revision, as: RevisionScope
|
||||
alias Accent.Scopes.Translation, as: TranslationScope
|
||||
alias Accent.Scopes.Version, as: VersionScope
|
||||
alias Accent.Translation
|
||||
alias Accent.Version
|
||||
|
||||
def upload_translations(integration, user, params) do
|
||||
project = Repo.one!(Ecto.assoc(integration, :project))
|
||||
version = fetch_version(project, params)
|
||||
documents = fetch_documents(project)
|
||||
revisions = fetch_revisions(project)
|
||||
|
||||
master_revision =
|
||||
Repo.preload(Repo.one!(RevisionScope.master(Ecto.assoc(project, :revisions))), :language)
|
||||
|
||||
uploads =
|
||||
Enum.flat_map(documents, fn document ->
|
||||
Enum.flat_map(revisions, fn revision ->
|
||||
translations = fetch_translations(document, revision, version)
|
||||
|
||||
if Enum.any?(translations) do
|
||||
render_options = %{
|
||||
translations: translations,
|
||||
master_language: Revision.language(master_revision),
|
||||
language: Revision.language(revision),
|
||||
document: document
|
||||
}
|
||||
|
||||
%{render: render} = Accent.TranslationsRenderer.render_translations(render_options)
|
||||
|
||||
[
|
||||
%{
|
||||
document: %{document | render: render},
|
||||
language: Accent.Revision.language(revision)
|
||||
}
|
||||
]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end)
|
||||
{uploads, version_tag} = Accent.IntegrationManager.Execute.UploadDocuments.all(integration, params)
|
||||
|
||||
uri = URI.parse(integration.data.azure_storage_container_sas)
|
||||
|
||||
version_tag = (version && version.tag) || "latest"
|
||||
|
||||
document_urls =
|
||||
for upload <- uploads do
|
||||
file = Path.join([System.tmp_dir(), Accent.Utils.SecureRandom.urlsafe_base64(16)])
|
||||
:ok = File.write(file, upload.document.render)
|
||||
|
||||
extension = Accent.DocumentFormat.extension_by_format(upload.document.format)
|
||||
document_name = upload.document.path <> "." <> extension
|
||||
|
||||
path =
|
||||
Path.join([
|
||||
uri.path || "/",
|
||||
version_tag,
|
||||
upload.language.slug,
|
||||
document_name
|
||||
])
|
||||
|
||||
url = URI.to_string(%{uri | path: path})
|
||||
HTTPoison.put(url, {:file, file}, [{"x-ms-blob-type", "BlockBlob"}])
|
||||
{url, document_name} = Accent.IntegrationManager.Execute.UploadDocuments.url(upload, uri, version_tag)
|
||||
HTTPoison.put(url, {:file, upload.file}, [{"x-ms-blob-type", "BlockBlob"}])
|
||||
|
||||
%{name: document_name, url: url}
|
||||
end
|
||||
|
||||
Hook.outbound(%Hook.Context{
|
||||
event: "integration_execute_azure_storage_container",
|
||||
project_id: project.id,
|
||||
project_id: integration.project_id,
|
||||
user_id: user.id,
|
||||
payload: %{
|
||||
version_tag: version_tag,
|
||||
@ -86,37 +28,4 @@ defmodule Accent.IntegrationManager.Execute.AzureStorageContainer do
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp fetch_version(project, %{target_version: :specific, tag: tag}) do
|
||||
Version
|
||||
|> VersionScope.from_project(project.id)
|
||||
|> VersionScope.from_tag(tag)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
defp fetch_version(_, _) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp fetch_documents(project) do
|
||||
Document
|
||||
|> DocumentScope.from_project(project.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
defp fetch_revisions(project) do
|
||||
Revision
|
||||
|> RevisionScope.from_project(project.id)
|
||||
|> Repo.all()
|
||||
|> Repo.preload(:language)
|
||||
end
|
||||
|
||||
defp fetch_translations(document, revision, version) do
|
||||
Translation
|
||||
|> TranslationScope.active()
|
||||
|> TranslationScope.from_document(document.id)
|
||||
|> TranslationScope.from_revision(revision.id)
|
||||
|> TranslationScope.from_version(version && version.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
end
|
||||
|
106
lib/accent/integrations/execute/upload_documents.ex
Normal file
106
lib/accent/integrations/execute/upload_documents.ex
Normal file
@ -0,0 +1,106 @@
|
||||
defmodule Accent.IntegrationManager.Execute.UploadDocuments do
|
||||
@moduledoc false
|
||||
|
||||
alias Accent.Document
|
||||
alias Accent.Repo
|
||||
alias Accent.Revision
|
||||
alias Accent.Scopes.Document, as: DocumentScope
|
||||
alias Accent.Scopes.Revision, as: RevisionScope
|
||||
alias Accent.Scopes.Translation, as: TranslationScope
|
||||
alias Accent.Scopes.Version, as: VersionScope
|
||||
alias Accent.Translation
|
||||
alias Accent.Version
|
||||
|
||||
def url(upload, uri, version_tag) do
|
||||
extension = Accent.DocumentFormat.extension_by_format(upload.document.format)
|
||||
document_name = upload.document.path <> "." <> extension
|
||||
|
||||
path =
|
||||
Path.join([
|
||||
uri.path || "/",
|
||||
version_tag,
|
||||
upload.language.slug,
|
||||
document_name
|
||||
])
|
||||
|
||||
{URI.to_string(%{uri | path: path}), document_name}
|
||||
end
|
||||
|
||||
def all(integration, params) do
|
||||
project = Repo.one!(Ecto.assoc(integration, :project))
|
||||
version = fetch_version(project, params)
|
||||
documents = fetch_documents(project)
|
||||
revisions = fetch_revisions(project)
|
||||
|
||||
master_revision =
|
||||
Repo.preload(Repo.one!(RevisionScope.master(Ecto.assoc(project, :revisions))), :language)
|
||||
|
||||
version_tag = (version && version.tag) || "latest"
|
||||
|
||||
uploads =
|
||||
Enum.flat_map(documents, fn document ->
|
||||
Enum.flat_map(revisions, fn revision ->
|
||||
translations = fetch_translations(document, revision, version)
|
||||
|
||||
if Enum.any?(translations) do
|
||||
render_options = %{
|
||||
translations: translations,
|
||||
master_language: Revision.language(master_revision),
|
||||
language: Revision.language(revision),
|
||||
document: document
|
||||
}
|
||||
|
||||
%{render: render} = Accent.TranslationsRenderer.render_translations(render_options)
|
||||
file = Path.join([System.tmp_dir(), Accent.Utils.SecureRandom.urlsafe_base64(16)])
|
||||
:ok = File.write(file, render)
|
||||
|
||||
[
|
||||
%{
|
||||
file: file,
|
||||
render: render,
|
||||
document: document,
|
||||
language: Accent.Revision.language(revision)
|
||||
}
|
||||
]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
{uploads, version_tag}
|
||||
end
|
||||
|
||||
defp fetch_version(project, %{target_version: :specific, tag: tag}) do
|
||||
Version
|
||||
|> VersionScope.from_project(project.id)
|
||||
|> VersionScope.from_tag(tag)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
defp fetch_version(_, _) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp fetch_documents(project) do
|
||||
Document
|
||||
|> DocumentScope.from_project(project.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
defp fetch_revisions(project) do
|
||||
Revision
|
||||
|> RevisionScope.from_project(project.id)
|
||||
|> Repo.all()
|
||||
|> Repo.preload(:language)
|
||||
end
|
||||
|
||||
defp fetch_translations(document, revision, version) do
|
||||
Translation
|
||||
|> TranslationScope.active()
|
||||
|> TranslationScope.from_document(document.id)
|
||||
|> TranslationScope.from_revision(revision.id)
|
||||
|> TranslationScope.from_version(version && version.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
end
|
@ -45,7 +45,7 @@ defmodule Accent.IntegrationManager do
|
||||
defp changeset(model, params) do
|
||||
model
|
||||
|> cast(params, [:project_id, :user_id, :service, :events])
|
||||
|> validate_inclusion(:service, ~w(slack github discord azure_storage_container))
|
||||
|> validate_inclusion(:service, ~w(slack github discord azure_storage_container aws_s3))
|
||||
|> cast_embed(:data, with: changeset_data(params[:service] || model.service))
|
||||
|> foreign_key_constraint(:project_id)
|
||||
|> validate_required([:service, :data])
|
||||
@ -61,6 +61,16 @@ defmodule Accent.IntegrationManager do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp execute_integration(%{service: "aws_s3"} = integration, user, params) do
|
||||
Accent.IntegrationManager.Execute.AWSS3.upload_translations(
|
||||
integration,
|
||||
user,
|
||||
params[:aws_s3]
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp execute_integration(_integration, _user, _params) do
|
||||
:noop
|
||||
end
|
||||
@ -89,6 +99,26 @@ defmodule Accent.IntegrationManager do
|
||||
end
|
||||
end
|
||||
|
||||
defp changeset_data("aws_s3") do
|
||||
fn model, params ->
|
||||
model
|
||||
|> cast(params, [
|
||||
:aws_s3_bucket,
|
||||
:aws_s3_path_prefix,
|
||||
:aws_s3_region,
|
||||
:aws_s3_access_key_id,
|
||||
:aws_s3_secret_access_key
|
||||
])
|
||||
|> validate_required([
|
||||
:aws_s3_bucket,
|
||||
:aws_s3_path_prefix,
|
||||
:aws_s3_region,
|
||||
:aws_s3_access_key_id,
|
||||
:aws_s3_secret_access_key
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
defp changeset_data(_) do
|
||||
fn model, params -> cast(model, params, []) end
|
||||
end
|
||||
|
@ -10,6 +10,11 @@ defmodule Accent.Integration do
|
||||
embeds_one(:data, IntegrationData, on_replace: :update) do
|
||||
field(:url)
|
||||
field(:azure_storage_container_sas)
|
||||
field(:aws_s3_bucket)
|
||||
field(:aws_s3_path_prefix)
|
||||
field(:aws_s3_region)
|
||||
field(:aws_s3_access_key_id)
|
||||
field(:aws_s3_secret_access_key)
|
||||
end
|
||||
|
||||
belongs_to(:project, Accent.Project)
|
||||
|
@ -12,15 +12,30 @@ defmodule Accent.GraphQL.Mutations.Integration do
|
||||
value(:latest)
|
||||
end
|
||||
|
||||
enum :project_integration_execute_aws_s3_target_version do
|
||||
value(:specific)
|
||||
value(:latest)
|
||||
end
|
||||
|
||||
input_object :project_integration_execute_azure_storage_container_input do
|
||||
field(:target_version, :project_integration_execute_azure_storage_container_target_version)
|
||||
field(:tag, :string)
|
||||
end
|
||||
|
||||
input_object :project_integration_execute_aws_s3_input do
|
||||
field(:target_version, :project_integration_execute_aws_s3_target_version)
|
||||
field(:tag, :string)
|
||||
end
|
||||
|
||||
input_object :project_integration_data_input do
|
||||
field(:id, :id)
|
||||
field(:url, :string)
|
||||
field(:azure_storage_container_sas, :string)
|
||||
field(:aws_s3_region, :string)
|
||||
field(:aws_s3_bucket, :string)
|
||||
field(:aws_s3_path_prefix, :string)
|
||||
field(:aws_s3_access_key_id, :string)
|
||||
field(:aws_s3_secret_access_key, :string)
|
||||
end
|
||||
|
||||
payload_object(:project_integration_payload, :project_integration)
|
||||
@ -39,6 +54,7 @@ defmodule Accent.GraphQL.Mutations.Integration do
|
||||
field :execute_project_integration, :project_integration_payload do
|
||||
arg(:id, non_null(:id))
|
||||
arg(:azure_storage_container, :project_integration_execute_azure_storage_container_input)
|
||||
arg(:aws_s3, :project_integration_execute_aws_s3_input)
|
||||
|
||||
resolve(integration_authorize(:execute_project_integration, &IntegrationResolver.execute/3))
|
||||
middleware(&build_payload/2)
|
||||
|
@ -6,6 +6,10 @@ defmodule Accent.GraphQL.Resolvers.Integration do
|
||||
alias Accent.IntegrationManager
|
||||
alias Accent.Plugs.GraphQLContext
|
||||
alias Accent.Project
|
||||
alias Accent.Repo
|
||||
alias Accent.Scopes.Integration, as: IntegrationScope
|
||||
|
||||
require Ecto.Query
|
||||
|
||||
@typep integration_operation :: Accent.GraphQL.Response.t()
|
||||
|
||||
@ -38,4 +42,13 @@ defmodule Accent.GraphQL.Resolvers.Integration do
|
||||
|> IntegrationManager.delete()
|
||||
|> build()
|
||||
end
|
||||
|
||||
@spec list_project(Project.t(), map(), GraphQLContext.t()) :: {:ok, [Integration.t()]}
|
||||
def list_project(project, _args, _) do
|
||||
Integration
|
||||
|> IntegrationScope.from_project(project.id)
|
||||
|> Ecto.Query.order_by(desc: :inserted_at)
|
||||
|> Repo.all()
|
||||
|> then(&{:ok, &1})
|
||||
end
|
||||
end
|
||||
|
@ -7,6 +7,7 @@ defmodule Accent.GraphQL.Types.Integration do
|
||||
value(:discord, as: "discord")
|
||||
value(:github, as: "github")
|
||||
value(:azure_storage_container, as: "azure_storage_container")
|
||||
value(:aws_s3, as: "aws_s3")
|
||||
end
|
||||
|
||||
enum :project_integration_event do
|
||||
@ -16,6 +17,7 @@ defmodule Accent.GraphQL.Types.Integration do
|
||||
value(:create_collaborator, as: "create_collaborator")
|
||||
value(:create_comment, as: "create_comment")
|
||||
value(:integration_execute_azure_storage_container, as: "integration_execute_azure_storage_container")
|
||||
value(:integration_execute_aws_s3, as: "integration_execute_aws_s3")
|
||||
end
|
||||
|
||||
interface :project_integration do
|
||||
@ -27,6 +29,7 @@ defmodule Accent.GraphQL.Types.Integration do
|
||||
%{service: "slack"}, _ -> :project_integration_slack
|
||||
%{service: "github"}, _ -> :project_integration_github
|
||||
%{service: "azure_storage_container"}, _ -> :project_integration_azure_storage_container
|
||||
%{service: "aws_s3"}, _ -> :project_integration_aws_s3
|
||||
end)
|
||||
end
|
||||
|
||||
@ -61,7 +64,16 @@ defmodule Accent.GraphQL.Types.Integration do
|
||||
field(:id, non_null(:id))
|
||||
field(:service, non_null(:project_integration_service))
|
||||
field(:last_executed_at, :datetime)
|
||||
field(:data, non_null(:project_integration_azure_data))
|
||||
field(:data, non_null(:project_integration_azure_storage_container_data))
|
||||
|
||||
interfaces([:project_integration])
|
||||
end
|
||||
|
||||
object :project_integration_aws_s3 do
|
||||
field(:id, non_null(:id))
|
||||
field(:service, non_null(:project_integration_service))
|
||||
field(:last_executed_at, :datetime)
|
||||
field(:data, non_null(:project_integration_aws_s3_data))
|
||||
|
||||
interfaces([:project_integration])
|
||||
end
|
||||
@ -75,7 +87,7 @@ defmodule Accent.GraphQL.Types.Integration do
|
||||
field(:id, non_null(:id))
|
||||
end
|
||||
|
||||
object :project_integration_azure_data do
|
||||
object :project_integration_azure_storage_container_data do
|
||||
field(:id, non_null(:id))
|
||||
|
||||
field(:sas_base_url, non_null(:string),
|
||||
@ -86,4 +98,12 @@ defmodule Accent.GraphQL.Types.Integration do
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
object :project_integration_aws_s3_data do
|
||||
field(:id, non_null(:id))
|
||||
field(:bucket, non_null(:string), resolve: fn data, _, _ -> {:ok, data.aws_s3_bucket} end)
|
||||
field(:path_prefix, non_null(:string), resolve: fn data, _, _ -> {:ok, data.aws_s3_path_prefix} end)
|
||||
field(:region, non_null(:string), resolve: fn data, _, _ -> {:ok, data.aws_s3_region} end)
|
||||
field(:access_key_id, non_null(:string), resolve: fn data, _, _ -> {:ok, data.aws_s3_access_key_id} end)
|
||||
end
|
||||
end
|
||||
|
@ -125,7 +125,10 @@ defmodule Accent.GraphQL.Types.Project do
|
||||
end
|
||||
|
||||
field(:language, :language, resolve: dataloader(Accent.Language))
|
||||
field(:integrations, list_of(:project_integration), resolve: dataloader(Accent.Integration))
|
||||
|
||||
field :integrations, list_of(non_null(:project_integration)) do
|
||||
resolve(project_authorize(:index_project_integrations, &Accent.GraphQL.Resolvers.Integration.list_project/3))
|
||||
end
|
||||
|
||||
field :document, :document do
|
||||
arg(:id, non_null(:id))
|
||||
|
1
mix.exs
1
mix.exs
@ -82,6 +82,7 @@ defmodule Accent.Mixfile do
|
||||
{:jason, "~> 1.2", override: true},
|
||||
{:erlsom, "~> 1.5"},
|
||||
{:xml_builder, "~> 2.0"},
|
||||
{:aws_signature, "~> 0.3"},
|
||||
|
||||
# Auth
|
||||
{:ueberauth, "~> 0.10"},
|
||||
|
1
mix.lock
1
mix.lock
@ -2,6 +2,7 @@
|
||||
"absinthe": {:hex, :absinthe, "1.7.6", "0b897365f98d068cfcb4533c0200a8e58825a4aeeae6ec33633ebed6de11773b", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7626951ca5eec627da960615b51009f3a774765406ff02722b1d818f17e5778"},
|
||||
"absinthe_error_payload": {:hex, :absinthe_error_payload, "1.1.4", "502ff239148c8deaac028ddb600d6502d5be68d24fece0c93f4c3cf7e74c1a4d", [:make, :mix], [{:absinthe, "~> 1.3", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "9e262ef2fd4a2c644075e0cdde2573b1f713c0676ab905c8640eaa8a882b2aca"},
|
||||
"absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"},
|
||||
"aws_signature": {:hex, :aws_signature, "0.3.2", "adf33bc4af00b2089b7708bf20e3246f09c639a905a619b3689f0a0a22c3ef8f", [:rebar3], [], "hexpm", "b0daf61feb4250a8ab0adea60db3e336af732ff71dd3fb22e45ae3dcbd071e44"},
|
||||
"bamboo": {:hex, :bamboo, "2.3.0", "d2392a2cabe91edf488553d3c70638b532e8db7b76b84b0a39e3dfe492ffd6fc", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dd0037e68e108fd04d0e8773921512c940e35d981e097b5793543e3b2f9cd3f6"},
|
||||
"bamboo_phoenix": {:hex, :bamboo_phoenix, "1.0.0", "f3cc591ffb163ed0bf935d256f1f4645cd870cf436545601215745fb9cc9953f", [:mix], [{:bamboo, ">= 2.0.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.3.0", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "6db88fbb26019c84a47994bb2bd879c0887c29ce6c559bc6385fd54eb8b37dee"},
|
||||
"bamboo_smtp": {:hex, :bamboo_smtp, "4.2.2", "e9f57a2300df9cb496c48751bd7668a86a2b89aa2e79ccaa34e0c46a5f64c3ae", [:mix], [{:bamboo, "~> 2.2.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 1.2.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "28cac2ec8adaae02aed663bf68163992891a3b44cfd7ada0bebe3e09bed7207f"},
|
||||
|
@ -77,7 +77,8 @@ defmodule AccentTest.GraphQL.Resolvers.Integration do
|
||||
|
||||
assert integration.errors == [
|
||||
service:
|
||||
{"is invalid", [validation: :inclusion, enum: ["slack", "github", "discord", "azure_storage_container"]]}
|
||||
{"is invalid",
|
||||
[validation: :inclusion, enum: ["slack", "github", "discord", "azure_storage_container", "aws_s3"]]}
|
||||
]
|
||||
|
||||
assert Repo.all(Integration) == []
|
||||
|
@ -0,0 +1,65 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
|
||||
interface Args {
|
||||
errors: any;
|
||||
project: any;
|
||||
onChangeBucket: (value: string) => void;
|
||||
onChangePathPrefix: (value: string) => void;
|
||||
onChangeRegion: (value: string) => void;
|
||||
onChangeAccessKeyId: (value: string) => void;
|
||||
onChangeSecretAccessKey: (value: string) => void;
|
||||
}
|
||||
|
||||
export default class AwsS3 extends Component<Args> {
|
||||
get policyContent() {
|
||||
const bucket = this.args.bucket || '-';
|
||||
const pathPrefix = this.args.pathPrefix || '/';
|
||||
|
||||
return `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:PutObject"],
|
||||
"Resource": "arn:aws:s3:::${bucket}${pathPrefix}*"
|
||||
}
|
||||
]
|
||||
}`;
|
||||
}
|
||||
|
||||
@action
|
||||
changeBucket(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
this.args.onChangeBucket(target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
changePathPrefix(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
this.args.onChangePathPrefix(target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
changeRegion(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
this.args.onChangeRegion(target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
changeAccessKeyId(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
this.args.onChangeAccessKeyId(target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
changeSecretAccessKey(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
this.args.onChangeSecretAccessKey(target.value);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const LOGOS = {
|
||||
AZURE_STORAGE_CONTAINER: 'assets/services/azure.svg',
|
||||
AWS_S3: 'assets/services/aws-s3.svg',
|
||||
DISCORD: 'assets/services/discord.svg',
|
||||
GITHUB: 'assets/services/github.svg',
|
||||
SLACK: 'assets/services/slack.svg'
|
||||
@ -27,6 +28,11 @@ interface Args {
|
||||
data: {
|
||||
url: string;
|
||||
azureStorageContainerSas: string;
|
||||
awsS3Bucket: string;
|
||||
awsS3PathPrefix: string;
|
||||
awsS3Region: string;
|
||||
awsS3AccessKeyId: string;
|
||||
awsS3SecretAccessKey: string;
|
||||
};
|
||||
}) => Promise<{errors: any}>;
|
||||
onCancel: () => void;
|
||||
@ -62,7 +68,22 @@ export default class IntegrationsForm extends Component<Args> {
|
||||
@tracked
|
||||
azureStorageContainerSasBaseUrl: string;
|
||||
|
||||
services = ['AZURE_STORAGE_CONTAINER', 'SLACK', 'DISCORD'];
|
||||
@tracked
|
||||
awsS3Bucket: string;
|
||||
|
||||
@tracked
|
||||
awsS3PathPrefix: string;
|
||||
|
||||
@tracked
|
||||
awsS3Region: string;
|
||||
|
||||
@tracked
|
||||
awsS3AccessKeyId: string;
|
||||
|
||||
@tracked
|
||||
awsS3SecretAccessKey: string;
|
||||
|
||||
services = ['AWS_S3', 'AZURE_STORAGE_CONTAINER', 'SLACK', 'DISCORD'];
|
||||
|
||||
@not('url')
|
||||
emptyUrl: boolean;
|
||||
@ -109,6 +130,10 @@ export default class IntegrationsForm extends Component<Args> {
|
||||
this.url = this.integration.data.url;
|
||||
this.events = this.integration.events;
|
||||
this.azureStorageContainerSasBaseUrl = this.integration.data.sasBaseUrl;
|
||||
this.awsS3Bucket = this.integration.data.bucket;
|
||||
this.awsS3PathPrefix = this.integration.data.pathPrefix;
|
||||
this.awsS3Region = this.integration.data.region;
|
||||
this.awsS3AccessKeyId = this.integration.data.accessKeyId;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -132,8 +157,33 @@ export default class IntegrationsForm extends Component<Args> {
|
||||
}
|
||||
|
||||
@action
|
||||
setAzureStorageContainerSas(sas: string) {
|
||||
this.azureStorageContainerSas = sas;
|
||||
setAzureStorageContainerSas(value: string) {
|
||||
this.azureStorageContainerSas = value;
|
||||
}
|
||||
|
||||
@action
|
||||
setAwsS3Bucket(value: string) {
|
||||
this.awsS3Bucket = value;
|
||||
}
|
||||
|
||||
@action
|
||||
setAwsS3PathPrefix(value: string) {
|
||||
this.awsS3PathPrefix = value;
|
||||
}
|
||||
|
||||
@action
|
||||
setAwsS3Region(value: string) {
|
||||
this.awsS3Region = value;
|
||||
}
|
||||
|
||||
@action
|
||||
setAwsS3AccessKeyId(value: string) {
|
||||
this.awsS3AccessKeyId = value;
|
||||
}
|
||||
|
||||
@action
|
||||
setAwsS3SecretAccessKey(value: string) {
|
||||
this.awsS3SecretAccessKey = value;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -146,7 +196,12 @@ export default class IntegrationsForm extends Component<Args> {
|
||||
integration: this.integration.newRecord ? null : this.integration,
|
||||
data: {
|
||||
url: this.url,
|
||||
azureStorageContainerSas: this.azureStorageContainerSas
|
||||
azureStorageContainerSas: this.azureStorageContainerSas,
|
||||
awsS3Bucket: this.awsS3Bucket,
|
||||
awsS3PathPrefix: this.awsS3PathPrefix,
|
||||
awsS3Region: this.awsS3Region,
|
||||
awsS3AccessKeyId: this.awsS3AccessKeyId,
|
||||
awsS3SecretAccessKey: this.awsS3SecretAccessKey
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -4,12 +4,13 @@ import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const LOGOS = {
|
||||
AZURE_STORAGE_CONTAINER: 'assets/services/azure.svg',
|
||||
AWS_S3: 'assets/services/aws-s3.svg',
|
||||
DISCORD: 'assets/services/discord.svg',
|
||||
GITHUB: 'assets/services/github.svg',
|
||||
SLACK: 'assets/services/slack.svg'
|
||||
};
|
||||
|
||||
const EXECUTABLE_SERVICES = ['AZURE_STORAGE_CONTAINER'];
|
||||
const EXECUTABLE_SERVICES = ['AZURE_STORAGE_CONTAINER', 'AWS_S3'];
|
||||
|
||||
interface Args {
|
||||
project: any;
|
||||
|
@ -0,0 +1,110 @@
|
||||
import {inject as service} from '@ember/service';
|
||||
import {action} from '@ember/object';
|
||||
import {readOnly} from '@ember/object/computed';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
import Component from '@glimmer/component';
|
||||
import IntlService from 'ember-intl/services/intl';
|
||||
import FlashMessages from 'ember-cli-flash/services/flash-messages';
|
||||
import ApolloMutate from 'accent-webapp/services/apollo-mutate';
|
||||
import executeIntegration from 'accent-webapp/queries/execute-integration';
|
||||
|
||||
const FLASH_MESSAGE_CREATE_SUCCESS =
|
||||
'pods.versions.new.flash_messages.create_success';
|
||||
const FLASH_MESSAGE_CREATE_ERROR =
|
||||
'pods.versions.new.flash_messages.create_error';
|
||||
|
||||
interface Args {
|
||||
close: () => void;
|
||||
integration: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default class IntegrationExecuteAwsS3 extends Component<Args> {
|
||||
@service('intl')
|
||||
intl: IntlService;
|
||||
|
||||
@service('apollo-mutate')
|
||||
apolloMutate: ApolloMutate;
|
||||
|
||||
@service('flash-messages')
|
||||
flashMessages: FlashMessages;
|
||||
|
||||
@readOnly('model.projectModel.project')
|
||||
project: any;
|
||||
|
||||
@tracked
|
||||
error = false;
|
||||
|
||||
allTargetVersions = [
|
||||
{
|
||||
value: 'LATEST',
|
||||
label:
|
||||
'components.project_settings.integrations.execute.aws_s3.target_version.options.latest'
|
||||
},
|
||||
{
|
||||
value: 'SPECIFIC',
|
||||
label:
|
||||
'components.project_settings.integrations.execute.aws_s3.target_version.options.specific'
|
||||
}
|
||||
];
|
||||
|
||||
@tracked
|
||||
targetVersion = this.allTargetVersions[0].value;
|
||||
|
||||
@tracked
|
||||
tag: string | null = null;
|
||||
|
||||
@tracked
|
||||
isSubmitting = false;
|
||||
|
||||
@action
|
||||
setTargetVersion(targetVersion: string) {
|
||||
this.tag = null;
|
||||
this.targetVersion = targetVersion;
|
||||
}
|
||||
|
||||
@action
|
||||
setTag(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
this.tag = target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
autofocus(input: HTMLInputElement) {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
@action
|
||||
async submit() {
|
||||
const confirmMessage = this.intl.t(
|
||||
'components.project_settings.integrations.execute.aws_s3.submit_confirm'
|
||||
);
|
||||
/* eslint-disable-next-line no-alert */
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.apolloMutate.mutate({
|
||||
mutation: executeIntegration,
|
||||
refetchQueries: ['ProjectServiceIntegrations'],
|
||||
variables: {
|
||||
integrationId: this.args.integration.id,
|
||||
azureStorageContainer: {
|
||||
tag: this.tag,
|
||||
targetVersion: this.targetVersion
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (response.errors) {
|
||||
this.flashMessages.error(this.intl.t(FLASH_MESSAGE_CREATE_ERROR));
|
||||
} else {
|
||||
this.args.close();
|
||||
this.flashMessages.success(this.intl.t(FLASH_MESSAGE_CREATE_SUCCESS));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
@ -487,6 +487,7 @@
|
||||
"developer_text": "Can make file operations: Sync, add translations and preview those operations in the UI.",
|
||||
"owner_text": "With the same roles as the admin, the owners are people who the project belongs to.",
|
||||
"reviewer_text": "Can do every tasks except those listed in the above roles. Review, update strings, comments, etc.",
|
||||
"translator_text": "Translate strings and make updates on documents. The translator is not included in the review process.",
|
||||
"title": "Collaborators"
|
||||
},
|
||||
"collaborators_item": {
|
||||
@ -509,9 +510,18 @@
|
||||
"last_executed_at": "Last executed at:",
|
||||
"data": {
|
||||
"azure_storage_container_sas": "SAS URL",
|
||||
"aws_s3_bucket": "Bucket name",
|
||||
"aws_s3_path_prefix": "Path prefix",
|
||||
"aws_s3_region": "Region",
|
||||
"aws_s3_access_key_id": "Access Key ID",
|
||||
"aws_s3_secret_access_key": "Secret Access Key",
|
||||
"url": "URL"
|
||||
},
|
||||
"empty_description": {
|
||||
"aws_s3": {
|
||||
"title": "AWS S3",
|
||||
"text": "Upload your files in Amazon Web Services S3 to serve them directly in your application"
|
||||
},
|
||||
"azure_storage_container": {
|
||||
"title": "Azure Storage Container",
|
||||
"text": "Upload your files in Azure Storage Container to serve them directly in your application"
|
||||
@ -525,11 +535,6 @@
|
||||
"text": "Post your project’s notifications in Slack"
|
||||
}
|
||||
},
|
||||
"token_how": "How?",
|
||||
"github_webhook_url_how": "How?",
|
||||
"github_webhook_accent_cli_1": "You need to have a valid <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/mirego/accent/tree/master/cli\">accent-cli</a> setup for the hook to work.",
|
||||
"github_webhook_accent_cli_2": "The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/mirego/accent/blob/master/cli/examples/react/accent.json\">accent.json</a> file at the root of your project will be used.",
|
||||
"github_webhook_url": "Make sure to add the webhook in your project’s settings",
|
||||
"webhook_url_how": "How to obtain a webhook URL?",
|
||||
"events": {
|
||||
"title": "Which events would you like to trigger this webhook?",
|
||||
@ -541,6 +546,22 @@
|
||||
}
|
||||
},
|
||||
"execute": {
|
||||
"aws_s3": {
|
||||
"title": "Upload files to AWS S3",
|
||||
"cancel_button": "Cancel",
|
||||
"error": "Error while pushing to your bucket. Check your credentials and try again.",
|
||||
"push_button": "Upload",
|
||||
"bucket": "Bucket",
|
||||
"path_prefix": "Path prefix",
|
||||
"submit_confirm": "Uploading files to AWS S3 can override existing files if you upload for the latest or an already uploaded version tag.",
|
||||
"target_version": {
|
||||
"label": "Target Version",
|
||||
"options": {
|
||||
"latest": "Latest Version",
|
||||
"specific": "Specific Version"
|
||||
}
|
||||
}
|
||||
},
|
||||
"azure_storage_container": {
|
||||
"title": "Upload files to Azure Storage Container",
|
||||
"cancel_button": "Cancel",
|
||||
@ -1020,7 +1041,8 @@
|
||||
"integration_services": {
|
||||
"DISCORD": "Discord",
|
||||
"SLACK": "Slack",
|
||||
"AZURE_STORAGE_CONTAINER": "Azure Storage Container"
|
||||
"AZURE_STORAGE_CONTAINER": "Azure Storage Container",
|
||||
"AWS_S3": "AWS S3"
|
||||
},
|
||||
"search_input_placeholder_text": "Search for a string…"
|
||||
},
|
||||
|
@ -503,6 +503,7 @@
|
||||
"developer_text": "Peut effectuer des opérations sur les fichiers : synchroniser, ajouter des traductions et prévisualiser ces opérations dans l’interface utilisateur.",
|
||||
"owner_text": "Avec les mêmes rôles que l’administrateur, les propriétaires sont les personnes à qui appartient le projet.",
|
||||
"reviewer_text": "Peut effectuer toutes les tâches sauf celles énumérées dans les rôles ci-dessus. Réviser, mettre à jour les chaînes, les commentaires, etc.",
|
||||
"translator_text": "Traduire des chaînes et effectuez des mises à jour sur les fichiers. Le traducteur n’est pas inclus dans le processus de révision.",
|
||||
"title": "Collaborateurs"
|
||||
},
|
||||
"collaborators_item": {
|
||||
@ -525,9 +526,18 @@
|
||||
"delete": "Supprimer",
|
||||
"data": {
|
||||
"azure_storage_container_sas": "SAS URL",
|
||||
"aws_s3_bucket": "Nom du bucket",
|
||||
"aws_s3_path_prefix": "Préfix",
|
||||
"aws_s3_region": "Région",
|
||||
"aws_s3_access_key_id": "Access Key ID",
|
||||
"aws_s3_secret_access_key": "Secret Access Key",
|
||||
"url": "URL"
|
||||
},
|
||||
"empty_description": {
|
||||
"aws_s3": {
|
||||
"title": "AWS S3",
|
||||
"text": "Téléchargez vos fichiers dans Amazon Web Service S3 pour les servir directement dans votre application"
|
||||
},
|
||||
"azure_storage_container": {
|
||||
"title": "Azure Storage Container",
|
||||
"text": "Téléchargez vos fichiers dans Azure Storage Container pour les servir directement dans votre application"
|
||||
@ -541,11 +551,6 @@
|
||||
"text": "Publiez les notifications de votre projet dans Slack"
|
||||
}
|
||||
},
|
||||
"token_how": "Comment?",
|
||||
"github_webhook_url_how": "Comment?",
|
||||
"github_webhook_accent_cli_1": "Vous devez avoir une configuration <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/mirego/accent/tree/master/cli\">accent-cli</a> valide pour le crochet au travail.",
|
||||
"github_webhook_accent_cli_2": "Le <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/mirego/accent/blob/master/cli/examples/react/accent.json\">accent.json</a > fichier à la racine de votre projet sera utilisé.",
|
||||
"github_webhook_url": "Assurez-vous d’ajouter le webhook dans les paramètres de votre projet",
|
||||
"webhook_url_how": "Comment obtenir une URL de webhook ?",
|
||||
"events": {
|
||||
"title": "Quels événements souhaitez-vous déclencher ce webhook ?",
|
||||
@ -557,6 +562,22 @@
|
||||
}
|
||||
},
|
||||
"execute": {
|
||||
"aws_s3": {
|
||||
"title": "Publier sur AWS S3",
|
||||
"cancel_button": "Annuler",
|
||||
"error": "Une erreur s’est produite lors de la publication sur Azure. Veuillez vérifier vos informations d’identification et réessayer.",
|
||||
"push_button": "Publier",
|
||||
"bucket": "Nom du bucket",
|
||||
"path_prefix": "Préfix",
|
||||
"submit_confirm": "La publication de fichiers vers AWS S3 peut remplacer les fichiers existants si vous publiez la dernière version ou une version déjà publiée.",
|
||||
"target_version": {
|
||||
"label": "Version Cible",
|
||||
"options": {
|
||||
"latest": "Dernière version",
|
||||
"specific": "Version spécifique"
|
||||
}
|
||||
}
|
||||
},
|
||||
"azure_storage_container": {
|
||||
"title": "Publier sur Azure Storage Container",
|
||||
"cancel_button": "Annuler",
|
||||
@ -1020,7 +1041,8 @@
|
||||
"integration_services": {
|
||||
"DISCORD": "Discord",
|
||||
"SLACK": "Slack",
|
||||
"AZURE_STORAGE_CONTAINER": "Azure Storage Container"
|
||||
"AZURE_STORAGE_CONTAINER": "Azure Storage Container",
|
||||
"AWS_S3": "AWS S3"
|
||||
},
|
||||
"search_input_placeholder_text": "Rechercher une chaîne…"
|
||||
},
|
||||
|
@ -4,10 +4,12 @@ export default gql`
|
||||
mutation IntegrationExecute(
|
||||
$integrationId: ID!
|
||||
$azureStorageContainer: ProjectIntegrationExecuteAzureStorageContainerInput
|
||||
$awsS3: ProjectIntegrationExecuteAwsS3Input
|
||||
) {
|
||||
executeProjectIntegration(
|
||||
id: $integrationId
|
||||
azureStorageContainer: $azureStorageContainer
|
||||
awsS3: $awsS3
|
||||
) {
|
||||
projectIntegration: result {
|
||||
id
|
||||
|
@ -27,6 +27,17 @@ export default gql`
|
||||
}
|
||||
}
|
||||
|
||||
... on ProjectIntegrationAwsS3 {
|
||||
lastExecutedAt
|
||||
data {
|
||||
id
|
||||
bucket
|
||||
pathPrefix
|
||||
region
|
||||
accessKeyId
|
||||
}
|
||||
}
|
||||
|
||||
... on ProjectIntegrationAzureStorageContainer {
|
||||
lastExecutedAt
|
||||
data {
|
||||
|
@ -24,13 +24,13 @@
|
||||
}
|
||||
|
||||
.prompt-button-quick-access[data-rtl] {
|
||||
transform: translateX(36px);
|
||||
transform: translateX(40px);
|
||||
}
|
||||
|
||||
.prompt-button-quick-access {
|
||||
opacity: 1;
|
||||
transform: translateX(-36px);
|
||||
padding: 0 10px;
|
||||
transform: translateX(-40px);
|
||||
padding: 0 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,7 @@
|
||||
background: var(--input-background);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
border-radius: var(--border-radius);
|
||||
right: 0;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
|
@ -40,16 +40,16 @@
|
||||
}
|
||||
|
||||
.project {
|
||||
padding: 20px 0 20px 16px;
|
||||
padding: 20px 10px 20px 16px;
|
||||
margin-left: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 18px;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--color-black);
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.project-logo {
|
||||
@ -59,13 +59,11 @@
|
||||
top: 2px;
|
||||
font-size: 22px;
|
||||
margin-right: 10px;
|
||||
line-height: 1.1;
|
||||
line-height: 1.2;
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
||||
circle {
|
||||
fill: var(--logo-background);
|
||||
|
@ -28,8 +28,15 @@
|
||||
border-left: 1px solid var(--background-light-highlight);
|
||||
}
|
||||
|
||||
.rolesList-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.rolesList-title {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding-bottom: 5px;
|
||||
margin: 15px 0 0;
|
||||
color: var(--color-primary);
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
.empty-description {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
@ -73,7 +73,7 @@
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
@media (max-width: 940px) {
|
||||
@media (max-width: 1140px) {
|
||||
.empty-description {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
|
@ -0,0 +1,32 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.policy {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.policy-title {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.policy-render {
|
||||
background: var(--content-background);
|
||||
border: 1px solid var(--background-light-highlight);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
.instructions {
|
||||
border-top: 1px solid var(--background-light-highlight);
|
||||
padding-top: 10px;
|
||||
margin: 20px 0 10px;
|
||||
}
|
||||
|
||||
.instructions-text {
|
||||
margin-top: 7px;
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
|
||||
a {
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-control {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.data-title {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.data-title-help {
|
||||
margin-left: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
color: var(--color-primary);
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.textInput {
|
||||
@extend %textInput;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
padding: 8px 10px;
|
||||
margin-right: 10px;
|
||||
background: #fafafa;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 11px;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
.data-control {
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.data-title {
|
||||
|
@ -0,0 +1,103 @@
|
||||
.aws-push-form {
|
||||
padding: 20px;
|
||||
background: var(--content-background);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
font-size: 27px;
|
||||
font-weight: 300;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--background-light-highlight);
|
||||
font-size: 13px;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
font-family: var(--font-monospace);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 13px;
|
||||
margin-bottom: 20px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.textInput {
|
||||
@extend %textInput;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
padding: 10px;
|
||||
min-width: 250px;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-primary);
|
||||
}
|
||||
|
||||
.errors {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-bottom: 5px;
|
||||
color: var(--color-error);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.formActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.data-control {
|
||||
.radio {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
border: 1px solid var(--background-light-highlight);
|
||||
padding: 4px 6px;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--input-background);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: background;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--background-light);
|
||||
}
|
||||
|
||||
input {
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-title {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
@ -48,27 +48,24 @@
|
||||
</div>
|
||||
|
||||
{{#if @translation.isConflicted}}
|
||||
{{#if (get @permissions 'update_translation')}}
|
||||
<AsyncButton @loading={{this.isUpdateLoading}} tabindex='-1' class='button button--borderLess button--iconOnly button--grey' @onClick={{fn this.updateConflict}}>
|
||||
{{inline-svg '/assets/pencil.svg' class='button-icon'}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
{{#if (get @permissions 'correct_translation')}}
|
||||
<AsyncButton @loading={{this.isCorrectLoading}} class='button button--iconOnly button--borderLess button--green' @onClick={{fn this.correctConflict}}>
|
||||
{{inline-svg '/assets/check.svg' class='button-icon'}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (get @permissions 'update_translation')}}
|
||||
{{else if (get @permissions 'update_translation')}}
|
||||
<AsyncButton @loading={{this.isUpdateLoading}} tabindex='-1' class='button button--borderLess button--iconOnly button--grey' @onClick={{fn this.updateConflict}}>
|
||||
{{inline-svg '/assets/pencil.svg' class='button-icon'}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
{{#if (get @permissions 'uncorrect_translation')}}
|
||||
<AsyncButton @loading={{this.isUncorrectLoading}} class='button button--borderLess button--iconOnly button--red' @onClick={{fn this.uncorrectConflict}}>
|
||||
{{inline-svg '/assets/revert.svg' class='button-icon'}}
|
||||
</AsyncButton>
|
||||
{{else if (get @permissions 'update_translation')}}
|
||||
<AsyncButton @loading={{this.isUpdateLoading}} tabindex='-1' class='button button--borderLess button--iconOnly button--grey' @onClick={{fn this.updateConflict}}>
|
||||
{{inline-svg '/assets/pencil.svg' class='button-icon'}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
<div local-class='columns-item rolesList'>
|
||||
<span local-class='rolesList-title'>
|
||||
{{inline-svg 'assets/tool.svg' local-class='rolesList-icon'}}
|
||||
{{t 'general.roles.OWNER'}}
|
||||
</span>
|
||||
<p local-class='rolesList-text'>
|
||||
@ -24,6 +25,7 @@
|
||||
</p>
|
||||
|
||||
<span local-class='rolesList-title'>
|
||||
{{inline-svg 'assets/users.svg' local-class='rolesList-icon'}}
|
||||
{{t 'general.roles.ADMIN'}}
|
||||
</span>
|
||||
<p local-class='rolesList-text'>
|
||||
@ -31,6 +33,7 @@
|
||||
</p>
|
||||
|
||||
<span local-class='rolesList-title'>
|
||||
{{inline-svg 'assets/code.svg' local-class='rolesList-icon'}}
|
||||
{{t 'general.roles.DEVELOPER'}}
|
||||
</span>
|
||||
<p local-class='rolesList-text'>
|
||||
@ -38,11 +41,20 @@
|
||||
</p>
|
||||
|
||||
<span local-class='rolesList-title'>
|
||||
{{inline-svg 'assets/check.svg' local-class='rolesList-icon'}}
|
||||
{{t 'general.roles.REVIEWER'}}
|
||||
</span>
|
||||
<p local-class='rolesList-text'>
|
||||
{{t 'components.project_settings.collaborators.reviewer_text'}}
|
||||
</p>
|
||||
|
||||
<span local-class='rolesList-title'>
|
||||
{{inline-svg 'assets/machine-translations.svg' local-class='rolesList-icon'}}
|
||||
{{t 'general.roles.TRANSLATOR'}}
|
||||
</span>
|
||||
<p local-class='rolesList-text'>
|
||||
{{t 'components.project_settings.collaborators.translator_text'}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -38,6 +38,12 @@
|
||||
<p>{{t 'components.project_settings.integrations.empty_description.azure_storage_container.text'}}</p>
|
||||
</button>
|
||||
|
||||
<button local-class='empty-description-item' {{on 'click' (fn this.toggleCreateForm 'AWS_S3')}}>
|
||||
{{inline-svg 'assets/services/aws-s3.svg' local-class='empty-description-icon'}}
|
||||
<strong>{{t 'components.project_settings.integrations.empty_description.aws_s3.title'}}</strong>
|
||||
<p>{{t 'components.project_settings.integrations.empty_description.aws_s3.text'}}</p>
|
||||
</button>
|
||||
|
||||
<button local-class='empty-description-item' {{on 'click' (fn this.toggleCreateForm 'SLACK')}}>
|
||||
{{inline-svg 'assets/services/slack.svg' local-class='empty-description-icon'}}
|
||||
<strong>{{t 'components.project_settings.integrations.empty_description.slack.title'}}</strong>
|
||||
|
@ -24,10 +24,19 @@
|
||||
this.dataFormComponent
|
||||
errors=this.errors
|
||||
url=this.url
|
||||
bucket=this.awsS3Bucket
|
||||
pathPrefix=this.awsS3PathPrefix
|
||||
region=this.awsS3Region
|
||||
accessKeyId=this.awsS3AccessKeyId
|
||||
project=@project
|
||||
events=this.events
|
||||
onChangeUrl=(fn this.setUrl)
|
||||
onChangeSas=(fn this.setAzureStorageContainerSas)
|
||||
onChangeBucket=(fn this.setAwsS3Bucket)
|
||||
onChangePathPrefix=(fn this.setAwsS3PathPrefix)
|
||||
onChangeRegion=(fn this.setAwsS3Region)
|
||||
onChangeAccessKeyId=(fn this.setAwsS3AccessKeyId)
|
||||
onChangeSecretAccessKey=(fn this.setAwsS3SecretAccessKey)
|
||||
onChangeEventsChecked=(fn this.setEventsChecked)
|
||||
}}
|
||||
</div>
|
||||
|
@ -0,0 +1,49 @@
|
||||
<div local-class='container'>
|
||||
<div local-class='form'>
|
||||
|
||||
<ProjectSettings::Integrations::Form::DataControlText
|
||||
@error={{field-error @errors 'data.awsS3Bucket'}}
|
||||
@label={{t 'components.project_settings.integrations.data.aws_s3_bucket'}}
|
||||
@value={{@bucket}}
|
||||
@onChange={{this.changeBucket}}
|
||||
/>
|
||||
|
||||
<ProjectSettings::Integrations::Form::DataControlText
|
||||
@error={{field-error @errors 'data.awsS3PathPrefix'}}
|
||||
@label={{t 'components.project_settings.integrations.data.aws_s3_path_prefix'}}
|
||||
@value={{@pathPrefix}}
|
||||
@placeholder='/'
|
||||
@onChange={{this.changePathPrefix}}
|
||||
/>
|
||||
|
||||
<ProjectSettings::Integrations::Form::DataControlText
|
||||
@error={{field-error @errors 'data.awsS3Region'}}
|
||||
@label={{t 'components.project_settings.integrations.data.aws_s3_region'}}
|
||||
@value={{@region}}
|
||||
@placeholder='us-east-1'
|
||||
@onChange={{this.changeRegion}}
|
||||
/>
|
||||
|
||||
<ProjectSettings::Integrations::Form::DataControlText
|
||||
@error={{field-error @errors 'data.awsS3AccessKeyId'}}
|
||||
@label={{t 'components.project_settings.integrations.data.aws_s3_access_key_id'}}
|
||||
@value={{@accessKeyId}}
|
||||
@onChange={{this.changeAccessKeyId}}
|
||||
/>
|
||||
|
||||
<ProjectSettings::Integrations::Form::DataControlText
|
||||
@error={{field-error @errors 'data.awsS3SecretAccessKey'}}
|
||||
@label={{t 'components.project_settings.integrations.data.aws_s3_secret_access_key'}}
|
||||
@onChange={{this.changeSecretAccessKey}}
|
||||
/>
|
||||
</div>
|
||||
<div local-class='policy'>
|
||||
<label local-class='policy-title'>
|
||||
Minimum policy
|
||||
</label>
|
||||
|
||||
<div local-class='policy-render'>
|
||||
<HighlightRender @content={{this.policyContent}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -18,6 +18,9 @@
|
||||
<span local-class='details-preview'>
|
||||
{{@integration.data.url}}
|
||||
{{@integration.data.sasBaseUrl}}
|
||||
{{@integration.data.accessKeyId}}
|
||||
{{@integration.data.bucket}}
|
||||
{{@integration.data.pathPrefix}}
|
||||
|
||||
{{#if @integration.lastExecutedAt}}
|
||||
<span local-class='details-last-executed-at'>{{t 'components.project_settings.integrations.last_executed_at'}} {{time-ago-in-words @integration.lastExecutedAt}}</span>
|
||||
|
@ -0,0 +1,50 @@
|
||||
<div local-class='aws-push-form'>
|
||||
<h1 local-class='title'>
|
||||
{{t 'components.project_settings.integrations.execute.aws_s3.title'}}
|
||||
</h1>
|
||||
|
||||
<div local-class='info'>
|
||||
<div>
|
||||
<strong>{{t 'components.project_settings.integrations.execute.aws_s3.bucket'}}</strong>
|
||||
<span>{{@integration.data.bucket}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{t 'components.project_settings.integrations.execute.aws_s3.path_prefix'}}</strong>
|
||||
<span>{{@integration.data.pathPrefix}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.error}}
|
||||
<div local-class='errors'>
|
||||
<div local-class='error'>
|
||||
{{t 'components.project_settings.integrations.execute.aws_s3.error'}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div local-class='formItem'>
|
||||
<div local-class='data-control'>
|
||||
<h3 local-class='data-title'>
|
||||
{{t 'components.project_settings.integrations.execute.aws_s3.target_version.label'}}
|
||||
</h3>
|
||||
|
||||
{{#each this.allTargetVersions as |target|}}
|
||||
<label local-class='radio'>
|
||||
<input type='radio' checked={{eq this.targetVersion target.value}} name='target_version' {{on 'change' (fn this.setTargetVersion target.value)}} required />
|
||||
{{t target.label}}
|
||||
</label>
|
||||
{{/each}}
|
||||
|
||||
{{#if (eq this.targetVersion 'SPECIFIC')}}
|
||||
<ProjectSettings::Integrations::Form::DataControlText @placeholder='1.0.0' @value={{this.tag}} @onChange={{this.setTag}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div local-class='formActions'>
|
||||
<AsyncButton {{did-insert (fn this.autofocus)}} class='button button--filled' @loading={{this.isSubmitting}} @onClick={{this.submit}}>
|
||||
{{inline-svg '/assets/arrow-up-right.svg' class='button-icon'}}
|
||||
{{t 'components.project_settings.integrations.execute.aws_s3.push_button'}}
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</div>
|
43
webapp/public/assets/services/aws-s3.svg
Normal file
43
webapp/public/assets/services/aws-s3.svg
Normal file
@ -0,0 +1,43 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 428 512">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #e25444;
|
||||
}
|
||||
.cls-1,
|
||||
.cls-2,
|
||||
.cls-3 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
.cls-2 {
|
||||
fill: #7b1d13;
|
||||
}
|
||||
.cls-3 {
|
||||
fill: #58150d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path d="m378 99-83 158 83 158 34-19V118Z" class="cls-1" />
|
||||
<path d="m378 99-166 19-84.5 139L212 396l166 19z" class="cls-2" />
|
||||
<path d="m43 99-27 12v292l27 12 169-158Z" class="cls-3" />
|
||||
<path
|
||||
d="m42.637 98.667 169.587 47.111v226.666L42.637 415.111z"
|
||||
class="cls-1"
|
||||
/>
|
||||
<path
|
||||
d="m212.313 170.667-72.008-11.556 72.008-81.778 71.83 81.778Z"
|
||||
class="cls-3"
|
||||
/>
|
||||
<path
|
||||
d="m284.143 159.111-71.919 11.733-71.919-11.733V77.333M212.313 342.222l-72.008 13.334 72.008 70.222 71.83-70.222Z"
|
||||
class="cls-3"
|
||||
/>
|
||||
<path
|
||||
d="m212 16-72 38v105l72.224-20.333ZM212.224 196.444l-71.919 7.823v104.838l71.919 8.228zM212.224 373.333 140.305 355.3v103.063L212.224 496z"
|
||||
class="cls-2"
|
||||
/>
|
||||
<path
|
||||
d="m284.143 355.3-71.919 18.038V496l71.919-37.637zM212.224 196.444l71.919 7.823v104.838l-71.919 8.228zM212 16l72 38v105l-72-20z"
|
||||
class="cls-1"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
14
webapp/public/assets/tool.svg
Normal file
14
webapp/public/assets/tool.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
class="feather feather-tool"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 386 B |
Loading…
Reference in New Issue
Block a user